feat: initial public release (MAESTRO)

This commit is contained in:
oss-sync
2026-06-03 05:08:00 +00:00
commit f5c7666f6b
823 changed files with 184150 additions and 0 deletions
@@ -0,0 +1,535 @@
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { MarkdownText } from '../../lib/markdown-text';
interface Subscription {
consumer_user_id: string;
publisher_user_id: string;
folder: string;
mode: 'search' | 'inject';
enabled: number;
}
interface DiscoverRow {
owner_id: string;
folder: string;
file_name: string;
title: string | null;
visibility: string;
mode_hint: string | null;
updated_at: number;
}
interface InjectItem {
owner_id: string;
folder: string;
file_name: string;
size_kb: number;
}
interface InjectPreview {
items: InjectItem[];
total_kb: number;
budget_kb: number;
per_note_max_kb: number;
}
function NotesListExpanded({
ownerId,
folder,
onSelectNote,
}: {
ownerId: string;
folder: string;
onSelectNote: (fileName: string) => void;
}) {
const list = useQuery<{ rows: DiscoverRow[] }>({
queryKey: ['notes-folder-list', ownerId, folder],
queryFn: async () => {
const r = await fetch(
`/api/notes/discover?owner_id=${encodeURIComponent(ownerId)}&folder=${encodeURIComponent(folder)}&limit=200`,
{ credentials: 'include' },
);
if (!r.ok) throw new Error(`${r.status}`);
return r.json();
},
staleTime: 30_000,
});
if (list.isLoading) return <p className="text-2xs text-slate-400 pl-3 py-1">Loading</p>;
if (list.isError) return <p className="text-2xs text-red-500 pl-3 py-1">Failed to load notes.</p>;
const rows = list.data?.rows ?? [];
if (rows.length === 0) return <p className="text-2xs text-slate-400 pl-3 py-1">()</p>;
return (
<ul className="pl-3 pt-1 pb-1 space-y-0.5">
{rows.map((n) => (
<li key={n.file_name}>
<button
type="button"
onClick={() => onSelectNote(n.file_name)}
className="w-full text-left flex items-center gap-2 px-2 py-1 rounded text-2xs hover:bg-surface-2/60 transition-colors"
title={`${n.owner_id}/${n.folder}/${n.file_name}`}
>
<span className="text-slate-400 flex-shrink-0">📄</span>
<span className="flex-1 min-w-0 truncate text-slate-700">
{n.title || <span className="font-mono text-slate-500">{n.file_name}</span>}
</span>
{n.mode_hint && (
<span className="text-slate-400 text-[10px] font-mono">{n.mode_hint}</span>
)}
</button>
</li>
))}
</ul>
);
}
function NoteContentModal({
ownerId,
folder,
fileName,
onClose,
}: {
ownerId: string;
folder: string;
fileName: string;
onClose: () => void;
}) {
const note = useQuery<{ fm: Record<string, unknown>; body: string; content: string }>({
queryKey: ['notes-cross-user-file', ownerId, folder, fileName],
queryFn: async () => {
const r = await fetch(
`/api/notes/file?owner_id=${encodeURIComponent(ownerId)}&folder=${encodeURIComponent(folder)}&file_name=${encodeURIComponent(fileName)}`,
{ credentials: 'include' },
);
if (!r.ok) throw new Error(`${r.status}`);
return r.json();
},
staleTime: 30_000,
});
const title = (note.data?.fm.title as string | undefined) || fileName;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/30 p-4"
onClick={onClose}
>
<div
className="bg-white rounded-md shadow-lg w-full max-w-3xl max-h-[85vh] flex flex-col overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
<header className="flex items-center justify-between border-b border-hairline px-4 py-3 flex-shrink-0">
<div className="min-w-0">
<h3 className="text-[13px] font-semibold text-slate-900 truncate">{title}</h3>
<p className="text-2xs text-slate-500 font-mono truncate">
{ownerId}/{folder}/{fileName}
</p>
</div>
<button
type="button"
onClick={onClose}
aria-label="閉じる"
className="px-2 py-1 text-slate-500 hover:text-slate-800 rounded hover:bg-surface-2"
>
×
</button>
</header>
<div className="flex-1 min-h-0 overflow-y-auto px-5 py-4">
{note.isLoading && <p className="text-[13px] text-slate-400">Loading</p>}
{note.isError && <p className="text-[13px] text-red-500"></p>}
{note.data && note.data.body
? <MarkdownText text={note.data.body} />
: note.data && <p className="text-[13px] text-slate-400 italic">()</p>}
</div>
</div>
</div>
);
}
function ModeSelect({
mode,
onChange,
}: {
mode: 'search' | 'inject';
onChange: (m: 'search' | 'inject') => void;
}) {
return (
<select
className="border border-hairline rounded text-2xs px-1 py-0.5 bg-white focus:outline-none focus:ring-1 focus:ring-accent"
value={mode}
onChange={(e) => onChange(e.target.value as 'search' | 'inject')}
>
<option value="search">search</option>
<option value="inject">inject</option>
</select>
);
}
export function SubscriptionsPanel({ currentUserId }: { currentUserId: string }) {
const qc = useQueryClient();
const [q, setQ] = useState('');
// Track which (owner, folder) rows are expanded to show the notes list inline.
const [expanded, setExpanded] = useState<Set<string>>(new Set());
const toggleExpanded = (key: string) =>
setExpanded((prev) => {
const next = new Set(prev);
if (next.has(key)) next.delete(key);
else next.add(key);
return next;
});
// Open-modal target for note content preview.
const [openNote, setOpenNote] = useState<{ ownerId: string; folder: string; fileName: string } | null>(null);
const subs = useQuery<{ rows: Subscription[] }>({
queryKey: ['notes-subscriptions'],
queryFn: async () => {
const r = await fetch('/api/notes/subscriptions', { credentials: 'include' });
if (!r.ok) throw new Error(`${r.status}`);
return r.json();
},
staleTime: 30_000,
});
const discover = useQuery<{ rows: DiscoverRow[] }>({
queryKey: ['notes-discover', q],
queryFn: async () => {
const r = await fetch(`/api/notes/discover?q=${encodeURIComponent(q)}`, {
credentials: 'include',
});
if (!r.ok) throw new Error(`${r.status}`);
return r.json();
},
staleTime: 15_000,
});
const preview = useQuery<InjectPreview>({
queryKey: ['notes-inject-preview'],
queryFn: async () => {
const r = await fetch('/api/notes/inject-preview', { credentials: 'include' });
if (!r.ok) throw new Error(`${r.status}`);
return r.json();
},
staleTime: 30_000,
});
const subscribe = useMutation({
mutationFn: async ({
publisher,
folder,
mode,
}: {
publisher: string;
folder: string;
mode: 'search' | 'inject';
}) => {
const r = await fetch('/api/notes/subscriptions', {
method: 'PUT',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ publisher_user_id: publisher, folder, mode, enabled: true }),
});
if (!r.ok) {
const j = await r.json().catch(() => ({ error: 'failed' }));
throw new Error((j as { error?: string }).error ?? 'failed');
}
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['notes-subscriptions'] });
qc.invalidateQueries({ queryKey: ['notes-inject-preview'] });
qc.invalidateQueries({ queryKey: ['notes-discover'] });
},
});
const unsubscribe = useMutation({
mutationFn: async ({ publisher, folder }: { publisher: string; folder: string }) => {
const r = await fetch(
`/api/notes/subscriptions?publisher_user_id=${encodeURIComponent(publisher)}&folder=${encodeURIComponent(folder)}`,
{ method: 'DELETE', credentials: 'include' },
);
if (!r.ok) throw new Error(`${r.status}`);
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['notes-subscriptions'] });
qc.invalidateQueries({ queryKey: ['notes-inject-preview'] });
},
});
// Group discover rows by (owner_id, folder)
const folderGroups = new Map<
string,
{ owner_id: string; folder: string; count: number; visibility: string }
>();
for (const row of discover.data?.rows ?? []) {
const key = `${row.owner_id}/${row.folder}`;
const existing = folderGroups.get(key);
if (existing) {
existing.count++;
} else {
folderGroups.set(key, {
owner_id: row.owner_id,
folder: row.folder,
count: 1,
visibility: row.visibility,
});
}
}
const myFolders = (subs.data?.rows ?? []).filter(
(s) => s.publisher_user_id === currentUserId,
);
const otherSubs = (subs.data?.rows ?? []).filter(
(s) => s.publisher_user_id !== currentUserId,
);
return (
<div className="h-full overflow-y-auto">
<div className="max-w-2xl mx-auto px-6 py-6 space-y-8">
{/* My Folders */}
<section>
<h3 className="text-[13px] font-semibold text-slate-800 mb-2">
My Folders{' '}
<span className="text-slate-400 font-normal">({myFolders.length})</span>
</h3>
{subs.isLoading && (
<p className="text-[13px] text-slate-400">Loading</p>
)}
{subs.isError && (
<p className="text-[13px] text-red-500">Failed to load subscriptions.</p>
)}
{!subs.isLoading && !subs.isError && myFolders.length === 0 && (
<p className="text-[13px] text-slate-400"> folder </p>
)}
<ul className="space-y-1">
{myFolders.map((s) => {
const key = `${s.publisher_user_id}/${s.folder}`;
const isOpen = expanded.has(key);
return (
<li
key={key}
className="rounded-md bg-surface-2/40 border border-hairline overflow-hidden"
>
<div className="flex items-center gap-2 px-3 py-2">
<button
type="button"
onClick={() => toggleExpanded(key)}
aria-label={isOpen ? '折りたたむ' : '展開'}
className="text-slate-500 hover:text-slate-800 text-2xs w-4"
>
{isOpen ? '▼' : '▶'}
</button>
<span className="flex-1 text-[13px] font-mono text-slate-700">{s.folder}</span>
<ModeSelect
mode={s.mode}
onChange={(m) =>
subscribe.mutate({ publisher: s.publisher_user_id, folder: s.folder, mode: m })
}
/>
</div>
{isOpen && (
<NotesListExpanded
ownerId={s.publisher_user_id}
folder={s.folder}
onSelectNote={(fileName) =>
setOpenNote({ ownerId: s.publisher_user_id, folder: s.folder, fileName })
}
/>
)}
</li>
);
})}
</ul>
</section>
{/* My Subscriptions */}
<section>
<h3 className="text-[13px] font-semibold text-slate-800 mb-2">
My Subscriptions{' '}
<span className="text-slate-400 font-normal">({otherSubs.length})</span>
</h3>
{!subs.isLoading && !subs.isError && otherSubs.length === 0 && (
<p className="text-[13px] text-slate-400"> folder </p>
)}
<ul className="space-y-1">
{otherSubs.map((s) => {
const key = `${s.publisher_user_id}/${s.folder}`;
const isOpen = expanded.has(key);
return (
<li
key={key}
className="rounded-md bg-surface-2/40 border border-hairline overflow-hidden"
>
<div className="flex items-center gap-2 px-3 py-2">
<button
type="button"
onClick={() => toggleExpanded(key)}
aria-label={isOpen ? '折りたたむ' : '展開'}
className="text-slate-500 hover:text-slate-800 text-2xs w-4"
>
{isOpen ? '▼' : '▶'}
</button>
<span className="flex-1 text-[13px] font-mono text-slate-700">
{s.publisher_user_id}/{s.folder}
</span>
<ModeSelect
mode={s.mode}
onChange={(m) =>
subscribe.mutate({ publisher: s.publisher_user_id, folder: s.folder, mode: m })
}
/>
<button
type="button"
className="text-2xs text-red-600 hover:text-red-800 font-medium px-2 py-0.5 rounded hover:bg-red-50 transition-colors"
onClick={() =>
unsubscribe.mutate({ publisher: s.publisher_user_id, folder: s.folder })
}
disabled={unsubscribe.isPending}
>
Unsubscribe
</button>
</div>
{isOpen && (
<NotesListExpanded
ownerId={s.publisher_user_id}
folder={s.folder}
onSelectNote={(fileName) =>
setOpenNote({ ownerId: s.publisher_user_id, folder: s.folder, fileName })
}
/>
)}
</li>
);
})}
</ul>
{unsubscribe.isError && (
<p className="mt-1 text-2xs text-red-600">{(unsubscribe.error as Error).message}</p>
)}
</section>
{/* Discover */}
<section>
<h3 className="text-[13px] font-semibold text-slate-800 mb-2">Discover</h3>
<input
className="border border-hairline rounded px-2 py-1.5 mb-3 w-full text-[13px] bg-white focus:outline-none focus:ring-1 focus:ring-accent placeholder:text-slate-400"
placeholder="Search by title, tag, body…"
value={q}
onChange={(e) => setQ(e.target.value)}
/>
{discover.isLoading && (
<p className="text-[13px] text-slate-400">Searching</p>
)}
{discover.isError && (
<p className="text-[13px] text-red-500">Search failed.</p>
)}
{!discover.isLoading && !discover.isError && folderGroups.size === 0 && (
<p className="text-[13px] text-slate-400">
{q ? '結果なし' : '検索してフォルダーを探してください'}
</p>
)}
<ul className="space-y-2">
{Array.from(folderGroups.values()).map((g) => {
const alreadySubbed = (subs.data?.rows ?? []).some(
(s) => s.publisher_user_id === g.owner_id && s.folder === g.folder,
);
return (
<li
key={`${g.owner_id}/${g.folder}`}
className="border border-hairline rounded-md px-3 py-2 bg-surface-2/30"
>
<div className="flex items-center gap-2 mb-1.5">
<span className="flex-1 text-[13px] font-mono text-slate-700">
{g.owner_id}/{g.folder}
</span>
<span className="text-2xs text-slate-400">
{g.visibility} · {g.count} notes
</span>
</div>
{alreadySubbed ? (
<span className="text-2xs text-slate-400 italic"></span>
) : (
<div className="flex gap-2">
<button
type="button"
className="text-2xs bg-surface-2 border border-hairline px-2 py-0.5 rounded hover:bg-slate-100 transition-colors disabled:opacity-50"
disabled={subscribe.isPending}
onClick={() =>
subscribe.mutate({ publisher: g.owner_id, folder: g.folder, mode: 'search' })
}
>
Subscribe (search)
</button>
<button
type="button"
className="text-2xs bg-surface-2 border border-hairline px-2 py-0.5 rounded hover:bg-slate-100 transition-colors disabled:opacity-50"
disabled={subscribe.isPending}
onClick={() =>
subscribe.mutate({ publisher: g.owner_id, folder: g.folder, mode: 'inject' })
}
>
Subscribe (inject)
</button>
</div>
)}
</li>
);
})}
</ul>
{subscribe.isError && (
<p className="mt-1 text-2xs text-red-600">{(subscribe.error as Error).message}</p>
)}
</section>
{/* Inject Preview */}
<section>
<h3 className="text-[13px] font-semibold text-slate-800 mb-1">Inject Preview</h3>
<p className="text-2xs text-slate-500 mb-2">
inject LLM
</p>
{preview.isLoading && (
<p className="text-[13px] text-slate-400">Loading</p>
)}
{preview.isError && (
<p className="text-[13px] text-red-500">Failed to load inject preview.</p>
)}
{!preview.isLoading && !preview.isError && (
<>
{(preview.data?.items ?? []).length === 0 ? (
<p className="text-[13px] text-slate-400">inject </p>
) : (
<ul className="space-y-0.5 mb-2">
{(preview.data?.items ?? []).map((it) => (
<li
key={`${it.owner_id}/${it.folder}/${it.file_name}`}
className="flex items-center gap-2 text-[13px] font-mono text-slate-700"
>
<span className="flex-1">{it.owner_id}/{it.folder}/{it.file_name}</span>
<span className="text-slate-400 text-2xs">{it.size_kb} KB</span>
</li>
))}
</ul>
)}
<div className="text-2xs text-slate-500 pt-1 border-t border-hairline">
Total:{' '}
<span className="font-semibold text-slate-700">{preview.data?.total_kb ?? 0} KB</span>
{' '}/{' '}
<span className="font-semibold text-slate-700">{preview.data?.budget_kb ?? 0} KB</span>
{' '}budget
</div>
</>
)}
</section>
</div>
{openNote && (
<NoteContentModal
ownerId={openNote.ownerId}
folder={openNote.folder}
fileName={openNote.fileName}
onClose={() => setOpenNote(null)}
/>
)}
</div>
);
}