"use client"; import { DISCIPLINE_LABELS, RESEARCH_MODE_LABELS } from "@autoscholar/schemas"; import { useRouter } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; import { useToast } from "@/components/Toast"; import { type ApiError, api } from "@/lib/client/api"; type Validation = { ok: boolean; sha256: string; byteSize: number; errors: string[]; warnings: string[]; headings: string[]; sections: { present: string[]; missing: string[] }; frontmatter: Record | null; }; type Props = { projects: Array<{ id: string; title: string; discipline: string }>; templates: Array<{ slug: string; name: string; mode: string; content: string }>; initialTemplate?: string; initialProject?: string; defaults: { defaultBudget: number; defaultRuntime: number; model: string }; }; const MAX = 256 * 1024; const HUMAN_REVIEW = new Set(["medicine", "biology", "law", "psychology"]); export function NewResearchForm({ projects, templates, initialTemplate, initialProject, defaults }: Props) { const router = useRouter(); const toast = useToast(); const tpl = templates.find((t) => t.slug === initialTemplate); const [content, setContent] = useState(tpl?.content ?? ""); const [filename, setFilename] = useState("CLAUDE.md"); const [projectId, setProjectId] = useState(initialProject ?? ""); const [newProject, setNewProject] = useState({ title: "", discipline: "economics", description: "" }); const [meta, setMeta] = useState({ title: "", discipline: "economics", researchMode: tpl?.mode ?? "empirical", secondaryModes: [] as string[], computeProfile: "standard", maxBudgetUsd: defaults.defaultBudget, maxRuntimeMinutes: defaults.defaultRuntime, maxAttempts: 2, autoPublish: false, }); const [uploaded, setUploaded] = useState<{ id: string; version: number; validation: Validation; projectId: string; } | null>(null); const [busy, setBusy] = useState(null); const [idem, setIdem] = useState(""); useEffect(() => { const k = sessionStorage.getItem("as-launch-idem") ?? crypto.randomUUID(); sessionStorage.setItem("as-launch-idem", k); setIdem(k); }, []); useEffect(() => { const p = projects.find((x) => x.id === projectId); if (p) setMeta((m) => ({ ...m, discipline: p.discipline, title: m.title || p.title })); }, [projectId, projects]); const bytes = useMemo(() => new TextEncoder().encode(content).length, [content]); const fm = useMemo(() => { const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); const out: Record = {}; if (m) for (const line of (m[1] ?? "").split("\n")) { const kv = line.match(/^(\w+):\s*(.+)$/); if (kv) out[kv[1]!] = kv[2]!.trim(); } return out; }, [content]); useEffect(() => { if (fm.mode && fm.mode in RESEARCH_MODE_LABELS) setMeta((m) => ({ ...m, researchMode: fm.mode! })); if (fm.title) setMeta((m) => ({ ...m, title: m.title || fm.title! })); if (fm.discipline && fm.discipline in DISCIPLINE_LABELS) setMeta((m) => ({ ...m, discipline: fm.discipline! })); }, [fm.mode, fm.title, fm.discipline]); async function onFile(f: File | null) { if (!f) return; if (!/\.(md|txt)$/i.test(f.name)) return toast.push("error", "Only .md or .txt files are accepted."); if (f.size > MAX) return toast.push("error", `File exceeds ${MAX / 1024} KB.`); const text = await f.text(); setFilename(f.name); setContent(text); setUploaded(null); } async function ensureProject(): Promise { if (projectId) return projectId; const r = await api<{ project: { id: string } }>("/api/admin/projects", { json: { title: newProject.title || meta.title, discipline: newProject.discipline, description: newProject.description, }, }); return r.project.id; } async function validateAndFreeze() { setBusy("validate"); try { const pid = await ensureProject(); setProjectId(pid); const r = await api<{ id: string; version: number; validation: Validation; deduplicated: boolean }>( `/api/admin/projects/${pid}/instructions`, { json: { filename, content } }, ); setUploaded({ id: r.id, version: r.version, validation: r.validation, projectId: pid }); toast.push( "success", r.deduplicated ? `Identical content already stored as v${r.version}.` : `Frozen as instruction v${r.version} (${r.validation.sha256.slice(0, 12)}).`, ); } catch (e) { const ae = e as ApiError; toast.push( "error", ae.message + (Array.isArray((ae.details as { errors?: string[] })?.errors) ? `: ${(ae.details as { errors: string[] }).errors.join("; ")}` : ""), ); } finally { setBusy(null); } } async function launch() { if (!uploaded) return; setBusy("launch"); try { const r = await api<{ run: { id: string }; created: boolean }>("/api/admin/runs", { json: { projectId: uploaded.projectId, instructionVersionId: uploaded.id, idempotencyKey: idem, title: meta.title, researchMode: meta.researchMode, secondaryModes: meta.secondaryModes, discipline: meta.discipline, computeProfile: meta.computeProfile, maxBudgetUsd: meta.maxBudgetUsd, maxRuntimeMinutes: meta.maxRuntimeMinutes, maxAttempts: meta.maxAttempts, autoPublish: meta.autoPublish, }, }); sessionStorage.removeItem("as-launch-idem"); toast.push( "success", r.created ? "Run launched." : "This launch was already recorded; opening the existing run.", ); router.push(`/admin/runs/${r.run.id}`); } catch (e) { toast.push("error", (e as ApiError).message); setBusy(null); } } const humanReview = HUMAN_REVIEW.has(meta.discipline) || meta.researchMode === "experimental"; const v = uploaded?.validation; return (

1. Research instruction (CLAUDE.md)

MAX ? "text-[var(--color-danger-500)]" : ""}`} > {bytes.toLocaleString()} / {MAX.toLocaleString()} bytes