fix: finish new-session opening-message handoff

This commit is contained in:
amosblomqvist
2026-10-27 18:19:38 +02:00
parent 8ca291526c
commit f24fda926a
11 changed files with 689 additions and 107 deletions
+17 -19
View File
@@ -12,7 +12,8 @@ import { DictationBar, joinDictation, useDictation } from "@/components/dictatio
import { useFieldCycle } from "@/components/field-cycle";
import { SidebarToggle } from "@/components/sidebar";
import { StatsBar } from "@/components/stats-bar";
import { stashOpeningPrompt } from "@/components/opening-prompt";
import { OPENING_PROMPT_FAILURE } from "@/components/opening-prompt";
import { SessionStarter } from "@/components/start-session";
import type { TurnUsage } from "@/src/core";
/**
@@ -57,6 +58,9 @@ export function Home({
},
});
const [busy, setBusy] = useState(false);
const startingRef = useRef(false);
const starterRef = useRef(new SessionStarter());
const [startError, setStartError] = useState(false);
/* Run end focuses the topic input (owner, 2026-10-01): the hands are on
* the keyboard from the stop. Falling edge only; this surface's only
* dictation target IS this textarea, so no case-split is needed. */
@@ -106,7 +110,7 @@ export function Home({
};
const begin = async () => {
const t = topicRef.current.trim();
if (!t || busy) return;
if (!t || startingRef.current) return;
// Unconfigured: the send INTERCEPTS into the setup flow (the draft
// stays in the textarea; the learner sends it after Save).
if (!configured) {
@@ -129,32 +133,25 @@ export function Home({
openModelPicker();
return; // Preserve the draft and never create an orphan session.
}
startingRef.current = true;
setBusy(true);
setStartError(false);
try {
// The map context (owner, 2026-10-04): picks in EITHER mode ride
// the CREATION as graphContext — the server writes a
// graph-context marker as message[0] (a separate custom message
// before the learner's first user message, propositions + the
// chains among them); the prompt stays the learner's own words.
const res = await fetch("/api/chats", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: t,
...(mode === "practice" ? { kind: "practice" } : {}),
...(practiceProps.length > 0 ? { graphContext: { propositions: practiceProps } } : {}),
}),
});
const { id } = (await res.json()) as { id: string };
// No settings PUT on create (per-session models, owner
// 2026-09-28): the SERVER stamps the newborn from the strong
// slot — the same default this surface's model control edits.
// The opening text stays out of the URL (opening-prompt.ts):
// a long paste as ?prompt= is a 431 and a blank window.
stashOpeningPrompt(id, t);
router.push(`/chat/${id}`);
const target = await starterRef.current.start({
title: t,
...(mode === "practice" ? { kind: "practice" } : {}),
...(practiceProps.length > 0 ? { graphContext: { propositions: practiceProps } } : {}),
}, fetch, JSON.stringify(catalog?.slots.strong));
router.push(target);
} catch {
startingRef.current = false;
setBusy(false);
setStartError(true);
}
};
@@ -243,6 +240,7 @@ export function Home({
)
)}
/>
{startError && <p role="alert" className="mb-2 text-sm text-destructive">{OPENING_PROMPT_FAILURE}</p>}
<form
className="flex items-end gap-1.5 rounded-2xl border bg-card p-1.5 shadow-lg"
onSubmit={(e) => {