-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnodeagent-v1.html
More file actions
374 lines (351 loc) · 25.2 KB
/
Copy pathnodeagent-v1.html
File metadata and controls
374 lines (351 loc) · 25.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>NodeAgent — a cross-collaborative agent, built on assistant-ui</title>
<meta name="description" content="An assistant-ui chat where the agent gathers live room context, finds the right document for the right answer, updates the model as a versioned delta, and writes the memo — each step rendered inline as a tool UI. No build, no keys." />
<meta property="og:type" content="website" />
<meta property="og:title" content="NodeAgent — the room asks, the agent answers" />
<meta property="og:description" content="An assistant-ui chat with inline tool UIs: live context, grounded search, versioned spreadsheet, notebook memo." />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%23151413'/%3E%3Ccircle cx='16' cy='16' r='6' fill='%23d97757'/%3E%3C/svg%3E" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
<style>
/* ===========================================================================
NodeAgent v1 — a vanilla mirror of the assistant-ui chat (src/app uses the
real @assistant-ui/react). Design DNA from NodeBench home-v4 / home-v5.
=========================================================================== */
:root{
--bg:#151413; --paper:#1c1b1a; --panel:#232221; --surface:#2a2928;
--ink:#e8e4df; --ink-muted:#8a857e; --ink-faint:#5a5650;
--accent:#d97757; --accent-dim:rgba(217,119,87,.15); --accent-ghost:rgba(217,119,87,.10);
--green:#7bd089; --blue:#5b9bf5; --purple:#a78bfa;
--line:rgba(255,255,255,.07); --line-faint:rgba(255,255,255,.04); --glass:rgba(255,255,255,.02);
--fs-display:clamp(28px,4.4vw,42px); --fs-h2:clamp(19px,2.6vw,25px);
--r:14px; --r-sm:9px; --r-pill:999px;
--m-fast:120ms; --ease:cubic-bezier(.22,.61,.36,1); --maxw:840px;
}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
body{margin:0;background:var(--bg);color:var(--ink);font-family:'Manrope',system-ui,sans-serif;font-size:14px;line-height:1.55;-webkit-font-smoothing:antialiased;
background-image:radial-gradient(900px 480px at 50% -10%, rgba(217,119,87,.08), transparent 60%);}
.mono{font-family:'JetBrains Mono',ui-monospace,monospace}
a{color:var(--accent);text-decoration:none}
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:6px}
.skip{position:absolute;left:-999px;top:8px;z-index:99;background:var(--accent);color:#1a1009;padding:8px 14px;border-radius:8px;font-weight:600}
.skip:focus{left:12px}
.wrap{max-width:var(--maxw);margin:0 auto;padding:0 18px}
/* topbar */
.topbar{position:sticky;top:0;z-index:50;backdrop-filter:saturate(140%) blur(14px);
background:linear-gradient(180deg,rgba(21,20,19,.92),rgba(21,20,19,.72));border-bottom:1px solid var(--line)}
.topbar .row{display:flex;align-items:center;gap:12px;height:54px;max-width:var(--maxw);margin:0 auto;padding:0 18px}
.brand{display:flex;align-items:center;gap:8px;font-weight:800;letter-spacing:-.02em}
.dot{width:10px;height:10px;border-radius:50%;background:var(--accent);box-shadow:0 0 0 0 rgba(217,119,87,.55);animation:breathe 3.4s var(--ease) infinite}
@keyframes breathe{0%,100%{transform:scale(1);box-shadow:0 0 0 0 rgba(217,119,87,.5)}50%{transform:scale(1.18);box-shadow:0 0 0 7px rgba(217,119,87,0)}}
.brand .ver{color:var(--ink-faint);font-size:11px;font-weight:500;font-family:'JetBrains Mono',monospace}
.lineage{color:var(--ink-faint);font-size:11px;font-family:'JetBrains Mono',monospace}
.lineage .now{color:var(--accent)}
.spacer{flex:1}
.btn{display:inline-flex;align-items:center;gap:7px;padding:8px 14px;border-radius:var(--r-pill);font-weight:600;font-size:12px;transition:transform var(--m-fast) var(--ease),background var(--m-fast)}
.btn:active{transform:translateY(1px)}
.btn-primary{background:var(--accent);color:#1a1009}
.btn-primary:hover{background:#e0855f}
.btn-ghost{border:1px solid var(--line);color:var(--ink-muted)}
.btn-ghost:hover{border-color:var(--accent);color:var(--ink)}
.link{font-size:12px;color:var(--ink-muted)}.link:hover{color:var(--accent)}
@media(max-width:640px){.lineage,.nav-doc{display:none}}
/* hero */
.hero{padding:40px 0 8px}
.eyebrow{font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--accent);font-weight:600;font-family:'JetBrains Mono',monospace}
.hero h1{font-size:var(--fs-display);line-height:1.1;letter-spacing:-.025em;font-weight:800;margin:12px 0 0;max-width:20ch}
.hero h1 .g{background:linear-gradient(100deg,var(--accent),#e8a07f);-webkit-background-clip:text;background-clip:text;color:transparent}
.hero p.lead{margin:14px 0 0;color:var(--ink-muted);max-width:64ch}
.hero .built{margin-top:14px;font-size:12px;color:var(--ink-faint)}
.hero .built b{color:var(--ink-muted)}
/* ============ assistant-ui-style chat ============ */
.chat{margin:18px auto 8px;border:1px solid var(--line);border-radius:var(--r);background:var(--paper);overflow:hidden;display:flex;flex-direction:column;min-height:520px}
.chat-h{display:flex;align-items:center;gap:9px;padding:11px 14px;border-bottom:1px solid var(--line-faint);background:var(--glass);font-size:12px;color:var(--ink-faint)}
.chat-h .av{width:22px;height:22px;border-radius:50%;display:grid;place-items:center;background:var(--accent-dim);color:var(--accent);font-size:12px}
.chat-h b{color:var(--ink-muted);font-weight:600}
.chat-h .reset{margin-left:auto;color:var(--ink-faint);font-size:11px}
.chat-h .reset:hover{color:var(--accent)}
.thread{flex:1;overflow-y:auto;padding:18px 16px;display:flex;flex-direction:column;gap:16px;max-height:560px}
.empty{margin:auto;text-align:center;max-width:50ch}
.empty .av{width:46px;height:46px;border-radius:50%;display:grid;place-items:center;margin:0 auto 12px;background:var(--accent-dim);color:var(--accent);font-size:22px}
.empty h2{font-size:20px;font-weight:800;letter-spacing:-.02em;margin:0 0 6px}
.empty p{color:var(--ink-muted);margin:0 auto 16px}
.suggestions{display:flex;gap:8px;flex-wrap:wrap;justify-content:center}
.chip{border:1px solid var(--line);background:var(--glass);color:var(--ink-muted);border-radius:var(--r-pill);padding:9px 14px;font-size:12px;font-weight:600;transition:border-color var(--m-fast),color var(--m-fast)}
.chip:hover{border-color:var(--accent);color:var(--ink)}
.msg{display:flex;gap:10px;animation:rise var(--m-fast) var(--ease)}
@keyframes rise{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
.msg.user{justify-content:flex-end}
.msg .av{width:26px;height:26px;border-radius:50%;flex:0 0 26px;display:grid;place-items:center;background:var(--accent-dim);color:var(--accent);font-size:13px;margin-top:2px}
.bubble{max-width:88%;border-radius:16px;padding:11px 14px;font-size:14px;line-height:1.6}
.bubble.u{background:var(--accent);color:#1a1009;border-bottom-right-radius:5px;font-weight:500}
.bubble.a{background:var(--bg);border:1px solid var(--line);border-bottom-left-radius:5px;flex:1;min-width:0}
/* tool cards */
.tool{border:1px solid var(--line);border-radius:var(--r-sm);background:var(--paper);margin:9px 0;overflow:hidden}
.tool[data-running="true"]{border-style:dashed}
.tool-h{display:flex;align-items:center;gap:9px;padding:9px 12px;background:var(--glass)}
.tool-i{width:22px;height:22px;border-radius:6px;display:grid;place-items:center;font-size:12px;background:var(--surface)}
.tool-label{font-weight:700;font-size:13px}
.tool-status{margin-left:auto;font-size:10px;font-family:'JetBrains Mono',monospace}
.tool-status.done{color:var(--green)}
.tool-status.run{color:var(--accent);display:inline-flex;align-items:center;gap:6px}
.spin{width:10px;height:10px;border:2px solid var(--accent-dim);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.tool-b{padding:11px 12px}
.ctx{display:flex;gap:9px;align-items:baseline;padding:4px 0;font-size:12px;border-top:1px solid var(--line-faint)}
.ctx:first-child{border-top:none}
.ctx .rel{color:var(--accent);font-size:11px;font-family:'JetBrains Mono',monospace}
.ctx .kind{color:var(--ink-faint);font-size:10px;text-transform:uppercase;letter-spacing:.06em;flex:0 0 64px}
.ctx .txt{color:var(--ink-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.src{display:flex;gap:9px;align-items:center;padding:8px 9px;border:1px solid var(--line-faint);border-radius:7px;margin-bottom:6px}
.src.win{border-color:var(--accent);background:var(--accent-ghost)}
.src .badge{font-size:9px;padding:2px 6px;border-radius:5px;background:var(--surface);color:var(--ink-muted);font-weight:600;font-family:'JetBrains Mono',monospace}
.src .t{font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}
.src .g{margin-left:auto;font-size:11px;color:var(--ink-muted);font-family:'JetBrains Mono',monospace}
.src.win .g{color:var(--accent)}
.answer{margin-top:9px;padding:11px;border:1px solid var(--line);border-radius:7px;background:var(--paper);font-size:13px;line-height:1.6}
.cite{font-family:'JetBrains Mono',monospace;font-size:9px;color:var(--accent);background:var(--accent-dim);border-radius:4px;padding:0 4px;vertical-align:super;font-weight:600}
table.sheet{width:100%;border-collapse:collapse;font-size:12px;font-family:'JetBrains Mono',monospace}
table.sheet td{border:1px solid var(--line-faint);padding:6px 9px;text-align:right}
table.sheet td:first-child{text-align:left;color:var(--ink-muted)}
table.sheet td.delta{color:var(--accent);font-weight:700;background:var(--accent-ghost)}
.reason{margin-top:8px;font-size:10px;color:var(--ink-faint);font-family:'JetBrains Mono',monospace}
.memo h4{margin:0 0 6px;font-size:14px}.memo p{margin:6px 0}
.claim{border-left:2px solid var(--accent);background:var(--accent-ghost);padding:9px 11px;border-radius:0 7px 7px 0;margin:7px 0}
.claim .ct{font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--accent);font-weight:700;font-family:'JetBrains Mono',monospace;margin-bottom:4px}
.cite-line{color:var(--ink-muted);font-size:12px}
.composer{display:flex;align-items:flex-end;gap:8px;border-top:1px solid var(--line-faint);padding:10px 12px;background:var(--glass)}
.composer .field{flex:1;border:1px solid var(--line);background:var(--bg);border-radius:14px;padding:9px 13px;color:var(--ink);font-size:14px}
.composer .field::placeholder{color:var(--ink-faint)}
.composer .field:focus{outline:none;border-color:var(--accent)}
.composer .send{flex:0 0 auto;width:34px;height:34px;border-radius:50%;background:var(--accent);color:#1a1009;font-size:16px;font-weight:700;display:grid;place-items:center}
.disclaimer{text-align:center;font-size:11px;color:var(--ink-faint);padding:8px 0 0}
/* evolution + architecture */
.evolution{padding:46px 0 28px;border-top:1px solid var(--line-faint);margin-top:34px}
.sec-head{text-align:center;max-width:60ch;margin:0 auto 26px}
.sec-head .eyebrow{display:block}
.sec-head h2{font-size:var(--fs-h2);font-weight:800;letter-spacing:-.02em;margin:10px 0 0}
.sec-head p{color:var(--ink-muted);margin:10px 0 0}
.evo-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}
@media(max-width:760px){.evo-grid{grid-template-columns:1fr}}
.evo{border:1px solid var(--line);border-radius:var(--r);padding:18px;background:var(--glass);position:relative;overflow:hidden}
.evo:before{content:"";position:absolute;inset:0 0 auto 0;height:2px;background:var(--accent);opacity:.55}
.evo .era{font-size:11px;font-family:'JetBrains Mono',monospace;color:var(--ink-faint);text-transform:uppercase;letter-spacing:.1em}
.evo h3{font-size:15px;font-weight:700;margin:8px 0 0}
.evo .arrow{color:var(--accent);font-weight:700;margin:10px 0;font-size:12px}
.evo .maps{font-size:13px;color:var(--ink-muted);line-height:1.6}.evo .maps b{color:var(--ink)}
.evo .sysref{margin-top:12px;font-size:11px;font-family:'JetBrains Mono',monospace;color:var(--accent);padding-top:10px;border-top:1px solid var(--line-faint)}
.arch{padding:24px 0 50px}
.arch-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
@media(max-width:760px){.arch-grid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:460px){.arch-grid{grid-template-columns:1fr}}
.arch-card{border:1px solid var(--line);border-radius:var(--r-sm);padding:14px;background:var(--paper)}
.arch-card .ac-k{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint);font-weight:600}
.arch-card h4{font-size:13px;font-weight:700;margin:6px 0 8px}
.arch-card .files{font-size:11px;font-family:'JetBrains Mono',monospace;color:var(--ink-muted);line-height:1.8}
.arch-card .files span{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
footer{border-top:1px solid var(--line);padding:24px 0 48px;color:var(--ink-faint);font-size:13px}
footer .row{display:flex;flex-wrap:wrap;gap:14px;align-items:center;justify-content:space-between;max-width:var(--maxw);margin:0 auto;padding:0 18px}
footer a{color:var(--ink-muted)}footer a:hover{color:var(--accent)}
.reveal{opacity:0;transform:translateY(14px);transition:opacity .42s var(--ease),transform .42s var(--ease)}
.reveal.in{opacity:1;transform:none}
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important;scroll-behavior:auto!important}.reveal{opacity:1;transform:none}}
</style>
</head>
<body>
<a class="skip" href="#chat">Skip to the chat</a>
<header class="topbar">
<div class="row">
<a class="brand" href="#top"><span class="dot" aria-hidden="true"></span>NodeAgent<span class="ver mono">on assistant-ui</span></a>
<span class="lineage">home-v4 · home-v5 · <span class="now">NodeAgent</span></span>
<span class="spacer"></span>
<a class="link nav-doc" href="#evolution">The story</a>
<a class="link" href="https://github.com/HomenShum/NodeAgent">GitHub ↗</a>
<button class="btn btn-primary" onclick="runAgent()">▶ Run the agent</button>
</div>
</header>
<main id="top">
<section class="hero">
<div class="wrap">
<span class="eyebrow">Cross-collaborative agent · built on assistant-ui</span>
<h1>The room asks. The agent answers — <span class="g">inline, with tool UIs.</span></h1>
<p class="lead">
NodeAgent is an <a href="https://github.com/assistant-ui/assistant-ui">assistant-ui</a> chat.
Ask the room a question and watch the agent gather context, find the right document for the
right answer, update the model as a versioned delta, and write the memo — each step rendered
as a tool UI inside the conversation.
</p>
<p class="built">Built on <b>@assistant-ui/react</b> · <code class="mono">useLocalRuntime</code> + <code class="mono">makeAssistantToolUI</code>. This page is the no-build mirror; <code class="mono">src/app</code> uses the real library.</p>
</div>
</section>
<section id="chat" class="wrap">
<div class="chat">
<div class="chat-h">
<span class="av" aria-hidden="true">◆</span>
<b>NodeAgent</b> — live room · <span class="mono">/r/acme-dd</span>
<button class="reset" onclick="resetChat()">↺ Reset</button>
</div>
<div class="thread" id="thread" aria-live="polite"></div>
<div class="composer">
<input class="field" id="composer" placeholder="Ask the room anything…" aria-label="Ask the room"
onkeydown="if(event.key==='Enter')sendComposer()"/>
<button class="send" onclick="sendComposer()" aria-label="Send">↑</button>
</div>
</div>
<p class="disclaimer">Deterministic demo — no keys. The agent runs the real ported modules over a fixed scenario.</p>
</section>
<section class="evolution" id="evolution">
<div class="wrap">
<div class="sec-head">
<span class="eyebrow">Why this shape</span>
<h2>NodeAgent is a career, compiled into four tool UIs</h2>
<p>Each capability is the production form of a problem I already spent years solving — and assistant-ui renders each one inline as the agent works.</p>
</div>
<div class="evo-grid">
<div class="evo reveal"><span class="era">Banking · Finance</span><h3>Pressure-testing models</h3><div class="arrow">↓ becomes</div>
<div class="maps">A wrong assumption had to be a tracked, defensible change — never a silent overwrite — so the <b>Update model</b> tool UI shows a versioned delta with an audit line.</div>
<div class="sysref">→ spreadsheet/applySpreadsheetDelta.ts</div></div>
<div class="evo reveal"><span class="era">Data Engineering</span><h3>Context → structure</h3><div class="arrow">↓ becomes</div>
<div class="maps">Messy multi-source inputs into structured truth → the <b>Gather context</b> and <b>Search & synthesize</b> tool UIs: the right doc, with grounding scores and a winner.</div>
<div class="sysref">→ chat/contextCollector.ts · search/searchAndSynthesize.ts</div></div>
<div class="evo reveal"><span class="era">Agentic AI</span><h3>Orchestration & eval</h3><div class="arrow">↓ becomes</div>
<div class="maps">Tool orchestration and eval discipline → the <b>local-runtime adapter</b> that streams the loop, plus the <b>Write memo</b> tool UI — bounded, honest, traceable.</div>
<div class="sysref">→ runtime/nodeAgentChatAdapter.ts</div></div>
</div>
</div>
</section>
<section class="arch">
<div class="wrap">
<div class="sec-head">
<span class="eyebrow">Not vapor</span>
<h2>Four real modules, rendered as assistant-ui tool UIs</h2>
<p>The loop runs in a <code class="mono">ChatModelAdapter</code>; each capability is a <code class="mono">makeAssistantToolUI</code> renderer. Distilled from NodeBench AI into clean, tested TypeScript.</p>
</div>
<div class="arch-grid">
<div class="arch-card"><div class="ac-k">collect_context</div><h4>Gather room context</h4><div class="files"><span>chat/contextCollector.ts</span><span>presence-TTL, bounded</span></div></div>
<div class="arch-card"><div class="ac-k">search_synthesize</div><h4>Search & synthesize</h4><div class="files"><span>search/searchAndSynthesize.ts</span><span>4-layer grounding</span></div></div>
<div class="arch-card"><div class="ac-k">apply_spreadsheet_delta</div><h4>Update model</h4><div class="files"><span>spreadsheet/applySpreadsheetDelta.ts</span><span>versioned · optimistic</span></div></div>
<div class="arch-card"><div class="ac-k">write_memo</div><h4>Notebook memo</h4><div class="files"><span>notebook/notebookEditor.ts</span><span>claim · citation</span></div></div>
</div>
</div>
</section>
</main>
<footer>
<div class="row">
<span>NodeAgent — built by <a href="https://github.com/HomenShum">Homen Shum</a> on <a href="https://github.com/assistant-ui/assistant-ui">assistant-ui</a>. Distilled from NodeBench AI. MIT.</span>
<span class="mono" style="font-size:11px">context → structure → execution</span>
</div>
</footer>
<script>
"use strict";
const RM = window.matchMedia && window.matchMedia('(prefers-reduced-motion:reduce)').matches;
const wait = (ms)=>new Promise(r=>setTimeout(r, RM?0:ms));
const $ = (s,r=document)=>r.querySelector(s);
let busy=false;
/* ---- deterministic demo (mirrors src/features/node-agent/demoScenario.ts) ---- */
const QUESTION = "Does our wedge hold versus Acme, and does the runway model survive 18 months?";
const CONTEXT = {
items:[
{rel:0.20, kind:'document', txt:'room://benchmark — retrieval-latency benchmark doc (Priya)'},
{rel:0.19, kind:'message', txt:'Acme dropped a competing teardown of our wedge. Investor call in 40.'},
], active:2
};
const SOURCES=[
{badge:'RAG',win:true, cite:1,g:0.44,t:'room://benchmark — wedge holds vs Acme on latency'},
{badge:'DOC',win:false, cite:2,g:0.56,t:'finance://burn — runway model'},
{badge:'DOC',win:false, cite:3,g:0.44,t:'diligence://wedge — competitive note'},
{badge:'WEB',win:false, cite:4,g:0.11,t:'Acme teardown blog — Why context rooms lose'},
];
const ANSWER = `Best-grounded source <span class="cite">1</span> (RAG): defends the wedge — NodeAgent retrieval latency p95 <b>211ms</b> vs Acme 540ms on the shared eval set; grounded answer rate 0.93. Corroborated by <span class="cite">2</span> finance://burn and <span class="cite">3</span> diligence://wedge. Model holds: base burn $420k against $7.56M clears an <b>18.0-month</b> runway.`;
const MODEL={version:2, cells:[
{label:'Net burn / mo ($k)', v:'420', delta:true},
{label:'Cash on hand ($k)', v:'7,560', delta:false},
{label:'Runway (months)', v:'18.0', delta:true},
], reason:'Correct base burn to the finance-audited 420k (was stress figure 510k).'};
/* ---- card renderers (the four tool UIs) ---- */
function toolShell(icon,label,sub,running,body){
return `<div class="tool" data-running="${running}">
<div class="tool-h"><span class="tool-i">${icon}</span><span class="tool-label">${label}</span>
${running?`<span class="tool-status run"><i class="spin"></i> working…</span>`:`<span class="tool-status done">${sub}</span>`}</div>
${running?'':`<div class="tool-b">${body}</div>`}</div>`;
}
const ctxBody=()=>CONTEXT.items.map(i=>`<div class="ctx"><span class="rel">${i.rel.toFixed(2)}</span><span class="kind">${i.kind}</span><span class="txt">${i.txt}</span></div>`).join('');
const searchBody=()=>SOURCES.map(s=>`<div class="src ${s.win?'win':''}"><span class="badge">${s.badge}</span><span class="t">[${s.cite}] ${s.t}</span><span class="g">${s.g.toFixed(2)}</span></div>`).join('')+`<div class="answer">${ANSWER}</div>`;
const modelBody=()=>`<table class="sheet"><tbody>${MODEL.cells.map(c=>`<tr><td>${c.label}</td><td class="${c.delta?'delta':''}">${c.v}</td></tr>`).join('')}</tbody></table><div class="reason">↳ ${MODEL.reason}</div>`;
const memoBody=()=>`<div class="memo"><h4>Acme — diligence memo</h4>
<p>The room asked: "${QUESTION}"</p>
<div class="claim"><div class="ct">◆ Claim · grounded 3/4</div>${ANSWER}</div>
<p class="cite-line">⌗ room://benchmark — wedge holds vs Acme on latency <span class="cite">1</span></p>
<p>Model updated to v2: runway now 18.0 months.</p></div>`;
const STEPS=[
{icon:'💬',label:'Gather room context',sub:`${CONTEXT.items.length} items · ${CONTEXT.active} active`,body:ctxBody},
{icon:'🔎',label:'Search & synthesize',sub:'high · grounded 3/4',body:searchBody},
{icon:'▦',label:'Update model',sub:`v${MODEL.version} · 2 cells`,body:modelBody},
{icon:'✎',label:'Write memo',sub:'5 blocks',body:memoBody},
];
/* ---- chat plumbing ---- */
function renderEmpty(){
$('#thread').innerHTML=`<div class="empty">
<div class="av">◆</div><h2>Ask the room</h2>
<p>NodeAgent gathers context, finds the right document, updates the model as a versioned delta, and writes the cited memo — each step rendered inline as a tool UI.</p>
<div class="suggestions">
<button class="chip" onclick="runAgent()">Does our wedge hold + survive 18 months? →</button>
<button class="chip" onclick="runAgent('What changed since the last investor update?')">What changed since last update? →</button>
</div></div>`;
}
function scroll(){const t=$('#thread');t.scrollTop=t.scrollHeight;}
function addUser(text){
const t=$('#thread');
t.insertAdjacentHTML('beforeend',`<div class="msg user"><div class="bubble u">${text}</div></div>`);
scroll();
}
function addAssistant(){
const t=$('#thread');
const id='a'+t.querySelectorAll('.msg.assistant').length;
t.insertAdjacentHTML('beforeend',`<div class="msg assistant"><span class="av">◆</span><div class="bubble a" id="${id}"><div class="lead-line">On it — pulling the room context, then searching, updating the model, and writing the memo.</div><div class="cards"></div><div class="closing"></div></div></div>`);
scroll();
return $('#'+id);
}
async function runAgent(custom){
if(busy) return; busy=true;
if($('#thread .empty')) $('#thread').innerHTML='';
const q = custom || QUESTION;
addUser(q);
await wait(220);
const bubble = addAssistant();
const cards = $('.cards', bubble);
await wait(260);
for(const step of STEPS){
// running
cards.insertAdjacentHTML('beforeend', toolShell(step.icon,step.label,step.sub,true,''));
const el = cards.lastElementChild; scroll();
await wait(360);
// done
el.outerHTML = toolShell(step.icon,step.label,step.sub,false,step.body());
scroll();
await wait(170);
}
$('.closing',bubble).innerHTML=`<p style="margin:8px 0 0;color:var(--green)">✓ Done — wedge defended on high-confidence grounding (3/4), model clears 18.0 months, cited memo ready.</p>`;
scroll(); busy=false;
}
function sendComposer(){
const i=$('#composer'); const v=(i.value||'').trim(); if(!v||busy) return;
i.value=''; runAgent(v);
}
function resetChat(){ if(busy) return; renderEmpty(); }
/* reveal + init */
const io=new IntersectionObserver(es=>es.forEach(e=>{if(e.isIntersecting)e.target.classList.add('in')}),{threshold:.15});
document.querySelectorAll('.reveal').forEach(el=>io.observe(el));
document.addEventListener('keydown',e=>{ if(e.target.tagName==='INPUT')return; if(e.key==='r'||e.key==='R')runAgent(); });
renderEmpty();
</script>
</body>
</html>