"use client"; /** Research workspace panel rendered above the editor of a research page (status, venue, deadline, repo, datasets + tabs). */ import { useEffect, useRef, useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { ChevronDown, ChevronRight, FlaskConical, GitBranch, Database, CalendarClock, BookMarked, FileText, CheckSquare, Sparkles, X, Plus, LayoutGrid } from "lucide-react"; import Link from "next/link"; import { api } from "@/lib/client/api"; import { Tabs, TabsList, TabsTrigger, TabsContent, Badge } from "@/components/ui/primitives"; import { Input } from "@/components/ui/input"; import { toast } from "@/components/ui/toast"; import { Select, PAPER_STATUSES, STATUS_TONE } from "./ui"; import { ReferencesTab, refsKey, attKey } from "./ReferencesTab"; import { FilesTab } from "./FilesTab"; import { ExperimentsTab, expKey } from "./ExperimentsTab"; import { TasksTab, taskKey } from "./TasksTab"; import { AITab } from "./AITab"; import { LatexSnippets } from "./LatexSnippets"; import { cn } from "@/lib/utils"; type Meta = { status?: string; venue?: string; deadline?: string; repoUrl?: string; datasets?: string[]; [k: string]: unknown }; export function ResearchPanel({ pageId }: { pageId: string }) { const qc = useQueryClient(); const { data: page } = useQuery({ queryKey: ["research-page", pageId], queryFn: () => api(`/api/pages/${pageId}`), staleTime: 30_000 }); const { data: refs = [] } = useQuery({ queryKey: refsKey(pageId), queryFn: () => api(`/api/research/references?pageId=${pageId}`) }); const { data: files = [] } = useQuery({ queryKey: attKey(pageId), queryFn: () => api(`/api/attachments?pageId=${pageId}`) }); const { data: exps = [] } = useQuery({ queryKey: expKey(pageId), queryFn: () => api(`/api/research/experiments?pageId=${pageId}`) }); const { data: tasks = [] } = useQuery({ queryKey: taskKey(pageId), queryFn: () => api(`/api/research/tasks?pageId=${pageId}`) }); const [open, setOpen] = useState(true); const [tab, setTab] = useState("references"); const [meta, setMeta] = useState({}); const [dsDraft, setDsDraft] = useState(""); const loaded = useRef(false); useEffect(() => { try { const o = localStorage.getItem("research-panel-open"); if (o != null) setOpen(o === "1"); const t = localStorage.getItem(`research-tab-${pageId}`); if (t) setTab(t); } catch {} }, [pageId]); useEffect(() => { if (page && !loaded.current) { setMeta((page.properties ?? {}) as Meta); loaded.current = true; } }, [page]); const timer = useRef(null); /** Merge into the freshest server copy of properties so other editors of `properties` (tags) are not clobbered. */ async function persist(patch: Partial) { const next = { ...meta, ...patch }; setMeta(next); clearTimeout(timer.current); timer.current = setTimeout(async () => { try { const fresh = await api(`/api/pages/${pageId}`); const merged = { ...(fresh.properties ?? {}), ...next }; await api(`/api/pages/${pageId}`, { method: "PATCH", json: { properties: merged } }); qc.setQueryData(["research-page", pageId], { ...fresh, properties: merged }); qc.invalidateQueries({ queryKey: ["research-overview"] }); } catch (e: any) { toast("Could not save research properties", { description: e.message, tone: "danger" }); } }, 500); } const openTasks = tasks.filter((t) => t.status !== "done").length; const overdue = tasks.filter((t) => t.status !== "done" && t.dueAt && new Date(t.dueAt) < new Date()).length; const deadlineSoon = meta.deadline && (new Date(meta.deadline).getTime() - Date.now()) / 864e5 < 14; const datasets = Array.isArray(meta.datasets) ? meta.datasets : []; return (
{!open && {meta.status && {meta.status}}{refs.length} refs· {files.length} files· {exps.length} exp.· {openTasks} tasks{overdue ? ` (${overdue} overdue)` : ""}{meta.venue && · {meta.venue}}{meta.deadline && · due {meta.deadline}}} Overview
{open && (
Datasets
{datasets.map((d) => {d})} setDsDraft(e.target.value)} onKeyDown={(e) => { if ((e.key === "Enter" || e.key === ",") && dsDraft.trim()) { e.preventDefault(); persist({ datasets: [...datasets, dsDraft.trim()] }); setDsDraft(""); } }} onBlur={() => { if (dsDraft.trim()) { persist({ datasets: [...datasets, dsDraft.trim()] }); setDsDraft(""); } }} placeholder={datasets.length ? "" : "name or URL, Enter"} className="h-6 min-w-[6rem] flex-1 bg-transparent text-[12px] outline-none placeholder:text-fg-faint" />
{ setTab(v); try { localStorage.setItem(`research-tab-${pageId}`, v); } catch {} }} className="mt-3">
References {refs.length} Files {files.length} Experiments {exps.length} Tasks {openTasks} AI
)}
); }