"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { CornerDownLeftIcon, DumbbellIcon } from "lucide-react"; import { GraphView } from "@/components/graph-view"; import { MinimalHome } from "@/components/minimal-home"; import { SetupDialog } from "@/components/setup-dialog"; import { useStrongKeyless, useZeroKeys, ZeroKeysPill } from "@/components/setup-cta"; import { DictationBar, joinDictation, useDictation } from "@/components/dictation"; import { SidebarToggle } from "@/components/sidebar"; import { StatsBar } from "@/components/stats-bar"; import type { TurnUsage } from "@/src/core"; /** * Home — the knowledge graph is the main view, full-bleed edge to edge * (owner, graph-view round 1): the canvas owns the whole column, running * under the header and the new-session dock, both of which float over it * with no gradient scrims. Sessions live in the toggleable sidebar. */ export function Home({ initialConfigured, rendering = true, }: { initialConfigured: boolean; /** GRAPH RENDERING off (owner, 2026-10-06): the home's main view is * the chains view, not the canvas. Read server-side per navigation — * changing the setting applies on the next visit home. */ rendering?: boolean; }) { const router = useRouter(); const [topic, setTopic] = useState(""); /* The topic MIRROR ("nothing gets lost" round, 2026-10-01 — same * mechanics as the chat composer, owner's consistency ruling): a * dictation flush lands via setState, invisible to a closure that * waited through `await finish()`. Kept current at every write. */ const topicRef = useRef(""); // First-run gate (model-slots wave, owner 2026-09-20): chats cannot // start until the user has picked all three slots — no defaults for // 33 providers. The dock shows the CTA while unconfigured; submitting // a topic opens the setup dialog WITH THE DRAFT PRESERVED (Save // stores the slots and the queued message sends). const [configured, setConfigured] = useState(initialConfigured); // The zero-keys truth (zero-keys wave): no provider key = the pill // takes precedence over the models CTA (keys come first in the chain). const zeroKeys = useZeroKeys(); // The strong slot's keyless truth (pre-send key guards, owner interlude // 2026-09-25): keys may exist generally while the STRONG slot's pinned // provider has none — the same skip-everything treatment on send. const strongKeyless = useStrongKeyless(); const [setupOpen, setSetupOpen] = useState(false); // Voice dictation (STT wave, 2026-09-17): finals append at the end of the // topic draft; the strip lives in the stats bar's middle slot. const dictation = useDictation({ // Ref-first: the join reads the mirror so the send resuming after // `await finish()` sees the dictated tail. append: (t) => { const v = joinDictation(topicRef.current, t); topicRef.current = v; setTopic(v); }, }); const [busy, setBusy] = 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. */ const topicInputRef = useRef(null); const dictationWasActiveRef = useRef(false); useEffect(() => { if (dictation.active) { dictationWasActiveRef.current = true; } else if (dictationWasActiveRef.current) { dictationWasActiveRef.current = false; topicInputRef.current?.focus(); } }, [dictation.active]); /* PRACTICE MODE (owner, 2026-10-04): the graph composer's teach | * practice switch — practice creates a kind:"practice" session and * may carry proposition picks from the chain table (the chip above * the composer opens the picker; the picks seed the opening prompt). * Not enforcing any input format — the picks are a convenience. */ const [mode, setMode] = useState<"teach" | "practice">("teach"); const [practiceProps, setPracticeProps] = useState<{ id: string; name: string; statement: string }[]>([]); const [chainSelectOpen, setChainSelectOpen] = useState(false); // The chain table is CONTROLLED off this id set (the lifted pick). const pickedIds = useMemo(() => new Set(practiceProps.map((p) => p.id)), [practiceProps]); /* FINISH DICTATION FIRST (owner, 2026-10-01): Enter may arrive while a * run is live with the topic still deferred in the run's buffer — stop * the run, let the last finals land, THEN read the REF mirror (not the * state closure) and run. Applies before the setup/key guards too, so * an intercepted draft is the complete one. */ const start = (force = false) => { if (!dictation.active) { void begin(force); return; } void dictation.finish().then(() => begin(force)); }; const begin = async (force = false) => { const t = topicRef.current.trim(); if (!t || busy) return; // Unconfigured: the send INTERCEPTS into the setup flow (the draft // stays in the textarea; Save completes and this re-runs). `force` // is the onDone path — the state this closure captured is still // the pre-save `configured`, the state update hasn't landed yet. if (!configured && !force) { setSetupOpen(true); return; } // The pre-send KEY guards (owner interlude, no-orphan-chats): the // models case guards BEFORE creation; the key cases must too — a // refused first send would otherwise leave an orphan error-shell // chat in the sidebar (the 503 lands only after POST /api/chats // already created the record). Same skip-everything-and-open-setup // treatment; `force` (setup's onDone replay) bypasses, and the // server's 503 gate remains the belt for /chat/[id] sessions. if (!force && (zeroKeys || strongKeyless)) { setSetupOpen(true); return; } setBusy(true); 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. router.push(`/chat/${id}?prompt=${encodeURIComponent(t)}`); } catch { setBusy(false); } }; return (
{/* Window-drag strip (merged chrome, owner 2026-09-17): a dedicated mask element, deliberately FIRST in the DOM. Electron's app-region mask resolves by DOM order — a no-drag element subtracts only drag regions that came EARLIER in the tree — not by z-index, so the header's old inline drag region (after
) re-added itself over the graph chrome row's carve-out and the buttons dragged the window. Placed first, every carved control (chrome row, node detail card, the toggle) comes later and subtracts correctly. Pure mask: it never needs to receive events — the mask routes the pixels. */}