main
jsx 163 lines 10 KB
Raw
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 }