Spaces:
Running
Running
| // C0 β Blueprint workspace store: one live state object, mutated in the browser. | |
| // | |
| // `useBlueprintWorkspace` is the single source of truth for the Details page. It generates the | |
| // initial blueprint and applies chat instructions entirely via `blueprint-engine` (no network), | |
| // so the workspace is instant and works fully offline. Two optional enhancements layer on top: | |
| // β’ C4 persistence β the state autosaves to localStorage and rehydrates on reload (no server). | |
| // β’ C3 AI refine β one bounded, fail-open call may improve the explanation prose afterward. | |
| import { useCallback, useEffect, useRef, useState } from "react"; | |
| import * as engine from "@/lib/blueprint-engine"; | |
| import { refineWithAI } from "@/lib/ai-refine"; | |
| import { loadWorkspace, saveWorkspace, workspaceKey } from "@/lib/blueprint-persistence"; | |
| import type { BlueprintDetailsData, ChatMessage } from "@/types/blueprint-state"; | |
| const now = () => new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); | |
| function messagesFrom(data: BlueprintDetailsData): ChatMessage[] { | |
| return (data.chat_history ?? []).map((m, i) => ({ id: m.id || `h-${i}`, role: m.role, content: m.content })); | |
| } | |
| export interface BlueprintWorkspace { | |
| data: BlueprintDetailsData; | |
| messages: ChatMessage[]; | |
| dirty: boolean; | |
| busy: boolean; | |
| /** Apply a chat instruction locally (instant, no network) and patch the affected sections. */ | |
| applyInstruction: (text: string) => void; | |
| /** Replace state with a refined version (optional AI/server enhancement). */ | |
| setData: (next: BlueprintDetailsData) => void; | |
| markSaved: () => void; | |
| } | |
| export function useBlueprintWorkspace(candidateId: string, idea: string): BlueprintWorkspace { | |
| const [data, setDataState] = useState<BlueprintDetailsData>(() => engine.generate(candidateId, idea)); | |
| const [messages, setMessages] = useState<ChatMessage[]>([]); | |
| const [dirty, setDirty] = useState(false); | |
| const [busy, setBusy] = useState(false); | |
| const dataRef = useRef(data); // latest state, so applyInstruction never reads a stale closure | |
| dataRef.current = data; | |
| const keyRef = useRef(workspaceKey(idea, candidateId)); | |
| // C4 β rehydrate from localStorage if present, else generate locally. No fetch on the render path. | |
| useEffect(() => { | |
| const key = workspaceKey(idea, candidateId); | |
| keyRef.current = key; | |
| const saved = loadWorkspace(key); | |
| const next = saved ?? engine.generate(candidateId, idea); | |
| dataRef.current = next; | |
| setDataState(next); | |
| setMessages(saved ? messagesFrom(saved) : []); | |
| setDirty(false); | |
| setBusy(false); | |
| }, [candidateId, idea]); | |
| const setData = useCallback((next: BlueprintDetailsData) => { | |
| dataRef.current = next; | |
| setDataState(next); | |
| setMessages(messagesFrom(next)); | |
| saveWorkspace(keyRef.current, next); | |
| }, []); | |
| const applyInstruction = useCallback((text: string) => { | |
| const clean = text.trim(); | |
| if (!clean || busy) return; | |
| setMessages((m) => [...m, { id: `u-${Date.now()}`, role: "user", content: clean, time: now() }]); | |
| setBusy(true); | |
| // Defer a tick so the "thinking" affordance can paint β still zero network for the result. | |
| window.setTimeout(() => { | |
| const res = engine.apply(dataRef.current, clean); | |
| const stamp = new Date().toISOString(); | |
| res.data.chat_history = [ | |
| ...(dataRef.current.chat_history ?? []), | |
| { id: `uh-${Date.now()}`, role: "user", content: clean, timestamp: stamp }, | |
| { id: `bh-${Date.now()}`, role: "blueprint", content: res.reply, timestamp: stamp }, | |
| ]; | |
| dataRef.current = res.data; | |
| setDataState(res.data); | |
| setMessages((m) => [...m, { id: `b-${Date.now()}`, role: "blueprint", content: res.reply, time: now() }]); | |
| setDirty(true); | |
| setBusy(false); | |
| saveWorkspace(keyRef.current, res.data); // C4 autosave | |
| // C3 β optional, bounded, fail-open AI refinement of the explanation only. | |
| void refineWithAI(res.data, clean) | |
| .then((ref) => { | |
| if (!ref) return; | |
| const patched: BlueprintDetailsData = { | |
| ...dataRef.current, | |
| overview: ref.overview ?? dataRef.current.overview, | |
| design_brain: ref.designBrain ?? dataRef.current.design_brain, | |
| }; | |
| dataRef.current = patched; | |
| setDataState(patched); | |
| saveWorkspace(keyRef.current, patched); | |
| if (ref.reply) setMessages((m) => [...m, { id: `r-${Date.now()}`, role: "blueprint", content: `β¦ ${ref.reply}`, time: now() }]); | |
| }) | |
| .catch(() => undefined); | |
| }, 140); | |
| }, [busy]); | |
| const markSaved = useCallback(() => setDirty(false), []); | |
| return { data, messages, dirty, busy, applyInstruction, setData, markSaved }; | |
| } | |