import { useCallback, useEffect, useRef, useState } from "react"; import { Viewer } from "./components/Viewer"; import { runJscad, parseParams, type Param } from "./engine"; interface Msg { role: "user" | "assistant"; content: string; } const MAX_REPAIRS = 3; interface GenResponse { script?: string; summary?: string; error?: string; } export default function App() { const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [script, setScript] = useState(""); const [params, setParams] = useState([]); const [stl, setStl] = useState(null); const [status, setStatus] = useState(""); const [busy, setBusy] = useState(false); const scrollRef = useRef(null); useEffect(() => { scrollRef.current?.scrollTo(0, scrollRef.current.scrollHeight); }, [messages, status]); const paramValues = useCallback( (list: Param[]) => Object.fromEntries(list.map((p) => [p.name, p.value])), [] ); // Run a script through the worker; on failure, ask the server to repair and // retry up to MAX_REPAIRS. Returns the script that actually executed. const executeWithRepair = useCallback( async (initialScript: string, values: Record): Promise => { let current = initialScript; for (let attempt = 0; attempt <= MAX_REPAIRS; attempt++) { try { const buf = await runJscad(current, values); setStl(buf); return current; } catch (err) { if (attempt === MAX_REPAIRS) throw err; setStatus(`Fixing a geometry error (attempt ${attempt + 1}/${MAX_REPAIRS})…`); const res = await fetch("/api/repair", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ script: current, error: (err as Error).message }), }); const data = (await res.json()) as GenResponse; if (data.error) throw new Error(data.error); current = data.script!; } } return current; }, [] ); const send = useCallback(async () => { const text = input.trim(); if (!text || busy) return; setInput(""); setBusy(true); const nextMessages = [...messages, { role: "user" as const, content: text }]; setMessages(nextMessages); setStatus("Designing…"); try { const res = await fetch("/api/generate", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ messages: nextMessages, currentScript: script }), }); const data = (await res.json()) as GenResponse; if (data.error) throw new Error(data.error); const newParams = parseParams(data.script!); setStatus("Building geometry…"); const finalScript = await executeWithRepair(data.script!, paramValues(newParams)); setScript(finalScript); setParams(parseParams(finalScript)); setMessages((m) => [...m, { role: "assistant", content: data.summary! }]); setStatus(""); } catch (err) { setMessages((m) => [...m, { role: "assistant", content: `⚠️ ${(err as Error).message}` }]); setStatus(""); } finally { setBusy(false); } }, [input, busy, messages, script, executeWithRepair, paramValues]); // Live slider edits: re-run locally, no API call. const onParam = useCallback( (name: string, value: number) => { const updated = params.map((p) => (p.name === name ? { ...p, value } : p)); setParams(updated); runJscad(script, paramValues(updated)).then(setStl).catch(() => {}); }, [params, script, paramValues] ); const download = useCallback(() => { if (!stl) return; const blob = new Blob([stl], { type: "model/stl" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "chisel-model.stl"; a.click(); URL.revokeObjectURL(url); }, [stl]); return (