"use client"; /** Small shared bits for the research workspace (select, field, key-value editor, badges, insert helpers). */ import * as React from "react"; import { Badge } from "@/components/ui/primitives"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; import { Plus, X } from "lucide-react"; import { api } from "@/lib/client/api"; import { toast } from "@/components/ui/toast"; export const PAPER_STATUSES = ["planning", "research", "development", "writing", "submitted", "published"] as const; export const STATUS_TONE: Record = { planning: "neutral", research: "accent", development: "ai", writing: "warning", submitted: "warning", published: "success" }; export const EXP_TONE: Record = { planned: "neutral", running: "accent", done: "success", failed: "danger" }; export function Select({ className, children, ...p }: React.SelectHTMLAttributes) { return ; } export function Field({ label, children, className, hint }: { label: React.ReactNode; children: React.ReactNode; className?: string; hint?: string }) { return ; } export function VerifiedBadge({ v }: { v: string }) { return v === "doi_verified" ? DOI verified : v === "ai_suggested" ? AI suggested : Manual; } export function AILabel({ model }: { model?: string | null }) { return AI-generated{model ? ` · ${model}` : ""}; } /** Key/value editor over a flat object (numbers are kept numeric). */ export function KVEditor({ value, onChange, placeholderKey = "key", placeholderValue = "value" }: { value: Record; onChange: (v: Record) => void; placeholderKey?: string; placeholderValue?: string }) { const rows = Object.entries(value ?? {}); const [draft, setDraft] = React.useState<{ k: string; v: string }>({ k: "", v: "" }); const commit = (k: string, v: string, oldKey?: string) => { const n: Record = {}; for (const [kk, vv] of rows) { if (kk === oldKey) continue; n[kk] = vv; } if (k.trim()) n[k.trim()] = v.trim() !== "" && !isNaN(Number(v)) && /^-?\d+(\.\d+)?(e-?\d+)?$/i.test(v.trim()) ? Number(v) : v; onChange(n); }; return (
{rows.map(([k, v]) =>
{ if (e.target.value !== k) commit(e.target.value, String(v ?? ""), k); }} /> { if (e.target.value !== String(v ?? "")) commit(k, e.target.value, k); }} />
)}
setDraft({ ...draft, k: e.target.value })} onKeyDown={(e) => { if (e.key === "Enter" && draft.k.trim()) { commit(draft.k, draft.v); setDraft({ k: "", v: "" }); } }} /> setDraft({ ...draft, v: e.target.value })} onKeyDown={(e) => { if (e.key === "Enter" && draft.k.trim()) { commit(draft.k, draft.v); setDraft({ k: "", v: "" }); } }} />
); } /** Insert Markdown into the open page editor as an AI block (PageView listens for this event). */ export function insertMarkdown(md: string) { window.dispatchEvent(new CustomEvent("note:insert-markdown", { detail: md })); } /** Save Markdown as a new note (wrapped in an AI block so provenance stays visible). */ export async function saveAsNote(md: string, title: string, model?: string | null) { const { markdownToDoc } = await import("@/lib/editor/markdown-client"); const p = await api("/api/pages", { method: "POST", json: { title: title.slice(0, 120) || "AI research note", section: "research", content: { type: "doc", content: [{ type: "aiBlock", attrs: { model: model ?? "assistant", at: new Date().toISOString() }, content: markdownToDoc(md) }] } } }); toast("Saved as a note", { tone: "success" }); return p as { id: string }; } export const copy = (s: string, msg = "Copied") => { navigator.clipboard.writeText(s).then(() => toast(msg)).catch(() => toast("Copy failed", { tone: "danger" })); }; export const fmtBytes = (n: number) => (n < 1024 ? `${n} B` : n < 1048576 ? `${(n / 1024).toFixed(0)} KB` : `${(n / 1048576).toFixed(1)} MB`); export const shortAuthors = (a: string[] | null | undefined) => { const ln = (a ?? []).map((x) => x.trim().split(/\s+/).pop() ?? ""); return ln.length === 0 ? "—" : ln.length === 1 ? ln[0] : ln.length === 2 ? `${ln[0]} & ${ln[1]}` : `${ln[0]} et al.`; }; export const dateInput = (d: string | Date | null | undefined) => (d ? new Date(d).toISOString().slice(0, 10) : "");