// Live seed interview screen. // // Layout retained from the M1 port: emerging-seed rail (left) + Socrates // conversation (right). The rail now reflects the live `draft` extracted // from the conversation; the right side is a real chat with the LM Studio // (or Anthropic) gateway via /api/seed/turn. When Socrates flags ready (or // the user clicks "Generate") we POST /api/seed/finalize, which generates // the SysMLModel + creates the project, then we router-push to the editor. "use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { Sigil } from "../socrates/Sigil"; interface InterviewTurn { role: "socrates" | "user"; text: string; pending?: boolean; } interface SeedDraft { title: string; problem: string; targetUser: string; desiredOutcome: string; initialHypothesis?: string; constraints?: string[]; } const EMPTY_DRAFT: SeedDraft = { title: "", problem: "", targetUser: "", desiredOutcome: "", }; export function SeedScreen() { const router = useRouter(); const [history, setHistory] = useState([]); const [draft, setDraft] = useState(EMPTY_DRAFT); const [confidence, setConfidence] = useState(0); const [ready, setReady] = useState(false); const [input, setInput] = useState(""); const [sending, setSending] = useState(false); const [generating, setGenerating] = useState(false); const [error, setError] = useState(null); const [meta, setMeta] = useState<{ provider?: string; model?: string }>({}); const threadEndRef = useRef(null); const openedRef = useRef(false); // On mount, get Socrates' opening question. useEffect(() => { if (openedRef.current) return; openedRef.current = true; void sendImpl("", true); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Auto-scroll useEffect(() => { threadEndRef.current?.scrollIntoView({ behavior: "smooth", block: "end" }); }, [history]); const sendImpl = useCallback( async (text: string, isOpening = false) => { setSending(true); setError(null); const historyToSend: InterviewTurn[] = [...history]; const userTurn: InterviewTurn | null = isOpening ? null : { role: "user", text }; const pendingTurn: InterviewTurn = { role: "socrates", text: "thinking…", pending: true, }; setHistory(curr => [...curr, ...(userTurn ? [userTurn] : []), pendingTurn]); try { const res = await fetch("/api/seed/turn", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ history: historyToSend, userText: text, draft, }), }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error ?? `${res.status}`); } const data = (await res.json()) as { assistant: { text: string }; draft: SeedDraft; confidence: number; ready: boolean; meta?: { provider?: string; model?: string }; }; setHistory(curr => curr.map(t => t === pendingTurn ? { role: "socrates", text: data.assistant.text } : t ) ); setDraft(data.draft); setConfidence(data.confidence); setReady(data.ready); if (data.meta) setMeta({ provider: data.meta.provider, model: data.meta.model }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); setError(msg); setHistory(curr => curr.map(t => t === pendingTurn ? { role: "socrates", text: `⚠ ${msg}`, pending: false } : t ) ); } finally { setSending(false); } }, [history, draft] ); const onSubmit = useCallback(async () => { const text = input.trim(); if (!text || sending) return; setInput(""); await sendImpl(text); }, [input, sending, sendImpl]); const generate = useCallback(async () => { if (generating) return; setGenerating(true); setError(null); try { const res = await fetch("/api/seed/finalize", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ draft }), }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error ?? `${res.status}`); } const data = (await res.json()) as { projectId: string }; router.push(`/editor/${data.projectId}`); } catch (err) { const msg = err instanceof Error ? err.message : String(err); setError(msg); setGenerating(false); } }, [draft, generating, router]); const filledFieldsCount = [draft.problem, draft.targetUser, draft.desiredOutcome].filter(Boolean).length + (draft.initialHypothesis ? 1 : 0) + ((draft.constraints?.length ?? 0) > 0 ? 1 : 0); const canGenerate = !!draft.problem && !!draft.targetUser && !!draft.desiredOutcome; const confidencePct = Math.round(confidence * 100); return (
Socrata · Seed · {ready ? "ready" : "forming"} {meta.model && ( via {meta.provider} · {meta.model.split("/").pop()} )}
Emerging seed
{draft.initialHypothesis && } {draft.constraints && draft.constraints.length > 0 && (
Constraints{draft.constraints.length}
    {draft.constraints.map((c, i) =>
  • {c}
  • )}
)}
Draft confidence {confidencePct}%
{ready ? "Socrates says you're ready — click Generate to create the project." : `${filledFieldsCount} of 5 fields filled · keep answering to firm up the draft.`}
{error && (
⚠ {error}
)}
{history.map((m, i) => (
{m.role === "socrates" && (
)}
{m.role === "socrates" ? "Socrates" : "You"}
{m.text}
))}
{ e.preventDefault(); void onSubmit(); }} >
setInput(e.target.value)} placeholder={sending ? "Socrates is thinking…" : ready ? "Want to keep refining? Ask again." : "Reply to Socrates…"} disabled={sending || generating} autoFocus />
); } function Field({ label, value, inferred }: { label: string; value: string; inferred?: boolean }) { if (!value) { return (
{label}
); } return (
{label}
{value}
); }