"use client"; import { useState } from "react"; import { useRouter } from "next/navigation"; import { CornerDownLeftIcon } from "lucide-react"; import { GraphView } from "@/components/graph-view"; import { SetupDialog } from "@/components/setup-dialog"; import { useStrongKeyless, useZeroKeys, ZeroKeysPill } from "@/components/setup-cta"; import { useDefaultSettings } from "@/components/default-settings"; import { DictationBar, useDictation } from "@/components/dictation"; import { SidebarToggle } from "@/components/sidebar"; import { StatsBar } from "@/components/stats-bar"; import type { ChatSettings, 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({ initialSettings, initialConfigured }: { initialSettings: ChatSettings | null; initialConfigured: boolean }) { const router = useRouter(); const [topic, setTopic] = useState(""); // 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({ append: (t) => setTopic((v) => (v ? v.replace(/\s+$/, "") + " " + t : t)), }); const [busy, setBusy] = useState(false); // THE default settings (owner, 2026-09-18): this topic dock IS the // settings page for the default model — no baked-in default exists; the // user's last choice (server-rendered in via initialSettings, shared // with /chat/new through data/default-settings.json) IS the default. // Still carried to the chat POST then PUT-on-create (the model picker // defers its save when chatId is empty; see components/model-picker). const { settings, save: saveDraft } = useDefaultSettings(initialSettings); const start = async (force = false) => { const t = topic.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 { const res = await fetch("/api/chats", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: t }), }); const { id } = (await res.json()) as { id: string }; if (settings) { // Best-effort persist — the selection was made pre-chat. await fetch(`/api/chats/${id}/settings`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(settings), }).catch(() => {}); } 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. */}