| 1 | import { useEffect, useMemo, useRef, useState } from "react"; |
| 2 | |
| 3 | const WORKERS = [ |
| 4 | { id: "claw-v1", color: "#00E5A0", specialty: "Matter, security, commissioning" }, |
| 5 | { id: "claw-v2", color: "#00B8FF", specialty: "BLE, GATT, mobile links" }, |
| 6 | { id: "claw-v3", color: "#7C6AFF", specialty: "Thread and border routers" }, |
| 7 | { id: "claw-v4", color: "#FF6B6B", specialty: "Zigbee, Z-Wave, KNX" }, |
| 8 | { id: "claw-v5", color: "#FFB347", specialty: "Examples, sensors, training" }, |
| 9 | { id: "claw-v6", color: "#FF4FA3", specialty: "Wi-Fi and SiWx91x" }, |
| 10 | { id: "claw-v7", color: "#50C8FF", specialty: "MCU peripherals and drivers" }, |
| 11 | { id: "claw-v8", color: "#A0FF60", specialty: "TinyML and LoRaWAN" }, |
| 12 | { id: "claw-v9", color: "#FFE040", specialty: "Zephyr, MCUboot, RTOS" }, |
| 13 | { id: "claw-v10", color: "#E040FB", specialty: "Docs, CI, Renode, Docker" }, |
| 14 | { id: "claw-v11", color: "#FF9500", specialty: "KONNWEI OBD, GATT discovery, bridges" }, |
| 15 | { id: "claw-v12", color: "#FF4D94", specialty: "Dashboards, HUD projection, rclone, Atlas claw" } |
| 16 | ]; |
| 17 | |
| 18 | const PROMPTS = { |
| 19 | "claw-v11": "You are claw-v11. Give realistic KONNWEI KDIAG/MAXOBD and ELM327 guidance: Bluetooth full-system scans, live data, DTC read/clear, and app-supported bidirectional tests only. Explain custom BLE GATT discovery. Include safety notes. Do not claim this browser can flash an ECU.", |
| 20 | "claw-v12": "You are claw-v12. Help build web dashboards from exported OBD logs or a local WebSocket bridge, phone-to-HUD projection, rclone sync of diagnostic logs, and Atlas Copco claw couplings as workshop air fittings." |
| 21 | }; |
| 22 | |
| 23 | const QUICK = [ |
| 24 | "GATT discovery steps for a BLE OBD adapter", |
| 25 | "KONNWEI KDIAG live-data workflow", |
| 26 | "Local ELM327 to WebSocket bridge outline", |
| 27 | "Self-expanding OBD dashboard layout", |
| 28 | "AndrOBD to HUD projection setup", |
| 29 | "rclone sync for diagnostic logs", |
| 30 | "Atlas Copco claw coupling workshop use" |
| 31 | ]; |
| 32 | |
| 33 | const FEEDS = [ |
| 34 | ["KONNWEI KDIAG / MAXOBD", "Bluetooth 5.x full-system scanner. Live graphing and DTC read/clear happen in the official app. Export logs, then bridge them."], |
| 35 | ["GATT", "Most BLE OBD adapters use a custom service, commonly the FFE0 family, with a write characteristic for AT/PID commands and a notify characteristic for replies."], |
| 36 | ["Bridge", "Phone app or ELM327 serial to a local script to WebSocket JSON. This hosted UI does not talk to the car directly."], |
| 37 | ["Dashboard", "Gauges expand from numeric keys in the JSON feed: RPM, SPEED, COOLANT_TEMP, ENGINE_LOAD."], |
| 38 | ["HUD", "AndrOBD or a dedicated OBD/GPS projector. Phone mirroring is the realistic path from this app."], |
| 39 | ["Atlas Copco claw", "Drop-forged zinc-plated air coupling for workshop pneumatic tools. Physical fitting, not a software API."] |
| 40 | ]; |
| 41 | |
| 42 | function route(text) { |
| 43 | const t = text.toLowerCase(); |
| 44 | if (/gatt|konnwei|kdiag|maxobd|obd|elm327|dtc|can|bridge/.test(t)) return "claw-v11"; |
| 45 | if (/dashboard|hud|gauge|rclone|atlas|projection|feed/.test(t)) return "claw-v12"; |
| 46 | if (/ble|bluetooth/.test(t)) return "claw-v2"; |
| 47 | if (/thread/.test(t)) return "claw-v3"; |
| 48 | if (/zigbee|zwave|z-wave/.test(t)) return "claw-v4"; |
| 49 | if (/wifi|wi-fi|siwx/.test(t)) return "claw-v6"; |
| 50 | return "claw-v1"; |
| 51 | } |
| 52 | |
| 53 | function localAnswer(workerId) { |
| 54 | const worker = WORKERS.find((w) => w.id === workerId); |
| 55 | if (workerId === "claw-v11") { |
| 56 | return "claw-v11 local brief\n\nKONNWEI KDIAG/MAXOBD path:\n1. Plug the scanner into the OBD-II port and pair Bluetooth in the KDIAG or MAXOBD app.\n2. Read a full-system report, then open live data and record RPM, speed, coolant, and load.\n3. Export the log. This site cannot open the car bus by itself.\n\nGATT discovery:\n- Filter for service 0000ffe0-0000-1000-8000-00805f9b34fb when the adapter advertises it.\n- Discover characteristics. Write AT commands to the write characteristic. Enable notifications on the notify characteristic.\n- Queue one command at a time and wait for the reply. Retry discovery after a disconnect.\n\nBidirectional actions stay inside the official app and only on supported modules."; |
| 57 | } |
| 58 | if (workerId === "claw-v12") { |
| 59 | return "claw-v12 local brief\n\nDashboard: consume JSON { rpm, speed, coolant } over ws://localhost:8765 and add a gauge for every new numeric key.\nHUD: run AndrOBD on the phone, then mirror that screen to a projector or a dedicated OBD windshield HUD.\nrclone: rclone sync ./obd_logs remote:car_diagnostics --progress\nAtlas Copco claw coupling: same coupling head across hose sizes for workshop air tools. It is a pneumatic fitting, not a data feed."; |
| 60 | } |
| 61 | return worker.id + " local brief\n\n" + worker.specialty + ". Set ANTHROPIC_API_KEY on Netlify to enable live model replies."; |
| 62 | } |
| 63 | |
| 64 | export default function App() { |
| 65 | const [messages, setMessages] = useState([]); |
| 66 | const [input, setInput] = useState(""); |
| 67 | const [autoRoute, setAutoRoute] = useState(true); |
| 68 | const [selected, setSelected] = useState("claw-v11"); |
| 69 | const [busy, setBusy] = useState(false); |
| 70 | const [tab, setTab] = useState("workers"); |
| 71 | const [menu, setMenu] = useState(false); |
| 72 | const [proxy, setProxy] = useState("checking"); |
| 73 | const endRef = useRef(null); |
| 74 | |
| 75 | useEffect(() => { |
| 76 | setMessages([{ |
| 77 | id: "boot", |
| 78 | role: "assistant", |
| 79 | workerId: "claw-v1", |
| 80 | content: "KLAW.CHAT v3.9 is live.\n\nFeeds loaded: KONNWEI KDIAG/MAXOBD, GATT discovery, local bridge, dashboard, HUD projection, rclone, Atlas Copco claw couplings.\n\nThis host does not connect to a vehicle. Use a scanner app or a local bridge, then ask here for the workflow." |
| 81 | }]); |
| 82 | fetch("/api/chat", { method: "POST", headers: { "content-type": "application/json" }, body: "{}" }) |
| 83 | .then((r) => setProxy(r.status === 503 ? "key-missing" : r.ok ? "ready" : "offline")) |
| 84 | .catch(() => setProxy("offline")); |
| 85 | }, []); |
| 86 | |
| 87 | useEffect(() => { endRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages, busy]); |
| 88 | const active = useMemo(() => WORKERS.find((w) => w.id === selected), [selected]); |
| 89 | |
| 90 | async function send(text) { |
| 91 | const content = (text || input).trim(); |
| 92 | if (!content || busy) return; |
| 93 | const workerId = autoRoute ? route(content) : selected; |
| 94 | setSelected(workerId); |
| 95 | setInput(""); |
| 96 | setMenu(false); |
| 97 | const user = { id: crypto.randomUUID(), role: "user", content }; |
| 98 | const pending = { id: crypto.randomUUID(), role: "assistant", workerId, content: "" }; |
| 99 | setMessages((prev) => [...prev, user, pending]); |
| 100 | setBusy(true); |
| 101 | try { |
| 102 | const history = [...messages, user].filter((m) => m.role === "user" || m.role === "assistant").slice(-8).map((m) => ({ role: m.role, content: m.content })); |
| 103 | const resp = await fetch("/api/chat", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ messages: history, system: PROMPTS[workerId] || PROMPTS["claw-v11"], workerId }) }); |
| 104 | const data = await resp.json(); |
| 105 | const modelText = data?.content?.[0]?.text; |
| 106 | const answer = modelText || localAnswer(workerId) + (data?.error ? "\n\nProxy: " + data.error : ""); |
| 107 | setMessages((prev) => prev.map((m) => m.id === pending.id ? { ...m, content: answer } : m)); |
| 108 | setProxy(resp.ok ? "ready" : "key-missing"); |
| 109 | } catch (error) { |
| 110 | setMessages((prev) => prev.map((m) => m.id === pending.id ? { ...m, content: localAnswer(workerId) + "\n\nLocal fallback: " + error.message } : m)); |
| 111 | setProxy("offline"); |
| 112 | } finally { |
| 113 | setBusy(false); |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | return ( |
| 118 | <div className="app"> |
| 119 | <header className="top"> |
| 120 | <button className="ghost" onClick={() => setMenu((v) => !v)}>MENU</button> |
| 121 | <div className="mark">K</div> |
| 122 | <div><div className="title">KLAW.CHAT</div><div className="sub">v3.9 · feeds persistent</div></div> |
| 123 | <div className="grow"> |
| 124 | <button className={"chip " + (autoRoute ? "on" : "")} onClick={() => setAutoRoute((v) => !v)}>{autoRoute ? "AUTO" : "MANUAL"}</button> |
| 125 | <button className="ghost" onClick={() => setMessages([])}>CLEAR</button> |
| 126 | </div> |
| 127 | </header> |
| 128 | <div className="body"> |
| 129 | <aside className={"side " + (menu ? "open" : "")}> |
| 130 | <div className="tabs">{["workers", "feeds"].map((name) => <button key={name} className={tab === name ? "on" : ""} onClick={() => setTab(name)}>{name.toUpperCase()}</button>)}</div> |
| 131 | <div className="list"> |
| 132 | {tab === "workers" && WORKERS.map((w) => ( |
| 133 | <button key={w.id} className={"worker " + (selected === w.id ? "on" : "")} onClick={() => { setSelected(w.id); setAutoRoute(false); setMenu(false); }}> |
| 134 | <b style={{ color: w.color }}>{w.id}</b><span>{w.specialty}</span> |
| 135 | </button> |
| 136 | ))} |
| 137 | {tab === "feeds" && FEEDS.map(([title, body]) => <div className="card" key={title}><b>{title}</b>{body}</div>)} |
| 138 | </div> |
| 139 | </aside> |
| 140 | <main className="main"> |
| 141 | <div className="head"><span className="dot" style={{ background: active.color }} /><b style={{ color: active.color }}>{active.id}</b><span className="sub">{active.specialty}</span></div> |
| 142 | <div className="msgs"> |
| 143 | {messages.map((m) => ( |
| 144 | <article key={m.id} className={"msg " + m.role}> |
| 145 | <div className="meta">{m.role === "user" ? "OPERATOR" : (m.workerId || "claw").toUpperCase()}</div> |
| 146 | <div className="bubble">{m.content || (busy ? "…" : "")}</div> |
| 147 | </article> |
| 148 | ))} |
| 149 | <div ref={endRef} /> |
| 150 | </div> |
| 151 | <div className="composer"> |
| 152 | <div className="quick">{QUICK.map((q) => <button key={q} onClick={() => send(q)}>{q}</button>)}</div> |
| 153 | <div className="row"> |
| 154 | <textarea value={input} placeholder={"Route to " + selected + (autoRoute ? " (auto)" : "")} onChange={(e) => setInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }} /> |
| 155 | <button className="send" disabled={busy || !input.trim()} onClick={() => send()}>↑</button> |
| 156 | </div> |
| 157 | </div> |
| 158 | </main> |
| 159 | </div> |
| 160 | <footer className="status"><span>12 WORKERS</span><span>PROXY {proxy.toUpperCase()}</span><span>{busy ? "STREAMING" : "IDLE"}</span><span style={{ marginLeft: "auto", color: active.color }}>{selected}</span></footer> |
| 161 | </div> |
| 162 | ); |
| 163 | } |