"use client"; import dynamic from "next/dynamic"; import { useCallback, useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { useTranslation } from "react-i18next"; import { ArrowRight, Bot, ChevronDown, ChevronRight, ExternalLink, Loader2, MessageSquare, NotebookPen, Pencil, Plus, Search, Trash2, } from "lucide-react"; import SpaceSectionHeader from "@/components/space/SpaceSectionHeader"; import { createNotebook, deleteNotebook, getNotebook, listNotebooks, } from "@/lib/notebook-api"; const MarkdownRenderer = dynamic( () => import("@/components/common/MarkdownRenderer"), { ssr: false }, ); interface NotebookInfo { id: string; name: string; description?: string; record_count?: number; color?: string; icon?: string; updated_at?: number; } interface NotebookRecord { id: string; type: string; title: string; summary?: string; user_query?: string; output: string; metadata?: Record; created_at?: number; } interface NotebookDetail extends NotebookInfo { records: NotebookRecord[]; } export default function NotebooksSection() { const { t } = useTranslation(); const router = useRouter(); const [notebooks, setNotebooks] = useState([]); const [loading, setLoading] = useState(true); const [newName, setNewName] = useState(""); const [newDescription, setNewDescription] = useState(""); const [selectedId, setSelectedId] = useState(null); const [selected, setSelected] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const [expandedRecordId, setExpandedRecordId] = useState(null); const loadDetail = useCallback( async (notebookId: string, list?: NotebookInfo[]) => { setSelectedId(notebookId); setExpandedRecordId(null); setDetailLoading(true); try { const info = (list ?? notebooks).find((n) => n.id === notebookId); const data = await getNotebook(notebookId); const records: NotebookRecord[] = (data.records || []).map((rec) => ({ id: String(rec.id), type: String(rec.type), title: rec.title, summary: rec.summary, user_query: rec.user_query, output: rec.output, metadata: rec.metadata, created_at: rec.created_at, })); setSelected({ id: notebookId, name: data.name ?? info?.name ?? "", description: data.description ?? info?.description, record_count: records.length, color: data.color ?? info?.color, icon: data.icon ?? info?.icon, updated_at: data.updated_at ?? info?.updated_at, records, }); } catch { setSelected(null); } finally { setDetailLoading(false); } }, [notebooks], ); const load = useCallback(async () => { setLoading(true); try { const nbs = await listNotebooks(); const next: NotebookInfo[] = nbs.map((nb) => ({ id: String(nb.id), name: nb.name, description: nb.description, record_count: nb.record_count ?? 0, color: nb.color, icon: nb.icon, updated_at: nb.updated_at, })); setNotebooks(next); if (selectedId && next.some((n) => n.id === selectedId)) { void loadDetail(selectedId, next); } else if (!selectedId && next.length > 0) { void loadDetail(next[0].id, next); } else if (selectedId && !next.some((n) => n.id === selectedId)) { setSelectedId(null); setSelected(null); } } finally { setLoading(false); } }, [loadDetail, selectedId]); useEffect(() => { void load(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleCreate = async () => { if (!newName.trim()) return; await createNotebook({ name: newName.trim(), description: newDescription.trim(), }); setNewName(""); setNewDescription(""); await load(); }; const handleDelete = async (notebookId: string, name: string) => { if (!window.confirm(t('Delete notebook "{{name}}"?', { name }))) return; await deleteNotebook(notebookId); if (selectedId === notebookId) { setSelectedId(null); setSelected(null); } await load(); }; const openRecord = (record: NotebookRecord) => { const sessionId = String(record.metadata?.session_id || ""); if (!sessionId) return; if (record.type === "chat") { router.push(`/?session=${encodeURIComponent(sessionId)}`); } }; const formatTimestamp = (value?: number) => { if (!value) return t("Unknown time"); return new Date(value * 1000).toLocaleString(); }; const getRecordBadge = (record: NotebookRecord) => { switch (record.type) { case "chat": return { label: t("Chat"), color: "bg-sky-100 text-sky-700 dark:bg-sky-900/40 dark:text-sky-300", icon: MessageSquare, }; case "tutorbot": { // Partner conversations carry the partner's name in metadata so the // badge reads as that partner's own category, not a generic "Chat". const partnerName = typeof record.metadata?.partner_name === "string" ? record.metadata.partner_name.trim() : ""; return { label: partnerName || t("Partner"), color: "bg-violet-100 text-violet-700 dark:bg-violet-900/40 dark:text-violet-300", icon: Bot, }; } case "research": return { label: t("Research"), color: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300", icon: Search, }; case "co_writer": return { label: t("Co-Writer"), color: "bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300", icon: Pencil, }; default: return { label: record.type, color: "bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-400", icon: NotebookPen, }; } }; if (loading) { return (
); } return (
{notebooks.length} {t("notebooks.count.suffix")} } /> {/* Create notebook */}

{t("Create notebook")}

{t("Give it a name and short description.")}
setNewName(event.target.value)} placeholder={t("Notebook name")} className="rounded-lg border border-[var(--border)] bg-[var(--background)] px-3 py-2 text-[13px] text-[var(--foreground)] outline-none transition-colors focus:border-[var(--foreground)]/25" /> setNewDescription(event.target.value)} placeholder={t("Description")} className="rounded-lg border border-[var(--border)] bg-[var(--background)] px-3 py-2 text-[13px] text-[var(--foreground)] outline-none transition-colors focus:border-[var(--foreground)]/25" />
{/* Notebook list */}

{t("Your notebooks")}

{notebooks.length}
{t("Click a notebook to inspect its records.")}
{notebooks.map((notebook) => { const active = selectedId === notebook.id; return (
); })} {!notebooks.length && (
{t("No notebooks yet. Create one to organize outputs.")}
)}
{detailLoading ? (
) : selected ? (

{selected.name}

{selected.description && ( — {selected.description} )}
{selected.records?.length || 0} {t("records")}
{selected.records?.map((record) => { const badge = getRecordBadge(record); const BadgeIcon = badge.icon; const expanded = expandedRecordId === record.id; const canOpenSession = record.type === "chat" && Boolean(record.metadata?.session_id); const sessionLabel = t("Open chat session"); return (
{expanded && (
{record.summary && (

{record.summary}

)} {record.type !== "chat" && record.user_query && (
{t("Query:")} {record.user_query}
)} {canOpenSession && ( )}
)}
); })} {!selected.records?.length && (
{t("This notebook is empty for now.")}
)}
) : (
{t("Select a notebook to inspect its saved records.")}
)}
); }