@akshayram1/omnibrowser-agent 0.2.28 → 0.2.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.html DELETED
@@ -1,1448 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>OmniBrowser Agent — Local-first Browser AI</title>
7
- <meta name="description" content="OmniBrowser Agent — local-first browser AI operator. No API keys. No cloud. Runs entirely in the browser via WebLLM + WebGPU." />
8
- <link rel="preconnect" href="https://fonts.googleapis.com">
9
- <link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Quantico:wght@400;700&display=swap" rel="stylesheet">
10
- <link rel="stylesheet" href="./styles.css" />
11
- </head>
12
- <body>
13
-
14
- <div class="orb orb-1"></div>
15
- <div class="orb orb-2"></div>
16
- <div class="orb orb-3"></div>
17
-
18
- <header class="header">
19
- <div class="wrap header-row">
20
- <a class="brand" href="#home">omnibrowser-agent</a>
21
- <nav class="nav">
22
- <a href="#home">Home</a>
23
- <a href="#whats-new">What's New</a>
24
- <a href="#docs">Docs</a>
25
- <a href="#architecture">Architecture</a>
26
- <a href="#embedding">Embedding</a>
27
- <a href="#roadmap">Roadmap</a>
28
- <a href="#contact">Contact</a>
29
- </nav>
30
- </div>
31
- </header>
32
-
33
- <main>
34
-
35
- <!-- ── HOME ── -->
36
- <section id="home" class="section hero">
37
- <div class="wrap">
38
- <div class="badge-pill fade-up">omnibrowser-agent v0.2.8</div>
39
- <h1 class="fade-up delay-1">Local-first browser AI automation library</h1>
40
- <p class="fade-up delay-2">
41
- OmniBrowser Agent plans and executes DOM actions entirely in the browser — no API keys, no cloud costs, no data leaving your machine.
42
- Wire in a WebLLM model and it reasons, remembers, and acts on any webpage.
43
- </p>
44
- <div class="chips fade-up delay-2">
45
- <span>Privacy-first</span>
46
- <span>WebLLM + WebGPU</span>
47
- <span>Reflection loop</span>
48
- <span>Human-approved mode</span>
49
- <span>Custom system prompt</span>
50
- <span>Embeddable API</span>
51
- </div>
52
- <div class="actions fade-up delay-3">
53
- <a class="btn primary" href="./examples/">🚀 Live Demo</a>
54
- <a class="btn" href="https://www.npmjs.com/package/@akshayram1/omnibrowser-agent" target="_blank" rel="noreferrer">NPM Package</a>
55
- <a class="btn" href="https://github.com/akshayram1/omnibrowser-agent" target="_blank" rel="noreferrer">GitHub</a>
56
- </div>
57
- <div class="stats fade-up delay-3" aria-label="project stats">
58
- <div class="stat"><strong>2</strong><span>Agent Modes</span></div>
59
- <div class="stat"><strong>2</strong><span>Planner Modes</span></div>
60
- <div class="stat"><strong>8</strong><span>Action Types</span></div>
61
- <div class="stat"><strong>MIT</strong><span>License</span></div>
62
- </div>
63
- <div class="home-grid fade-up delay-4">
64
- <article class="card">
65
- <h3>Use Cases</h3>
66
- <ul>
67
- <li>CRM profile lookup automation</li>
68
- <li>Guided form-filling workflows</li>
69
- <li>Assisted data extraction flows</li>
70
- <li>Multi-step task automation</li>
71
- </ul>
72
- </article>
73
- <article class="card">
74
- <h3>Core Engine</h3>
75
- <ul>
76
- <li><strong>Observer:</strong> DOM snapshot + candidate elements</li>
77
- <li><strong>Planner:</strong> reflection → next action</li>
78
- <li><strong>Safety:</strong> safe / review / blocked gating</li>
79
- <li><strong>Executor:</strong> DOM actions with framework compat</li>
80
- </ul>
81
- </article>
82
- <article class="card">
83
- <h3>Project Links</h3>
84
- <ul>
85
- <li><a href="https://www.npmjs.com/package/@akshayram1/omnibrowser-agent" target="_blank" rel="noreferrer">NPM package</a></li>
86
- <li><a href="https://github.com/akshayram1/omnibrowser-agent" target="_blank" rel="noreferrer">GitHub repository</a></li>
87
- <li><a href="./examples/">Live Examples</a></li>
88
- </ul>
89
- </article>
90
- </div>
91
- </div>
92
- </section>
93
-
94
- <!-- ── WHAT'S NEW ── -->
95
- <section id="whats-new" class="section">
96
- <div class="wrap">
97
- <div class="section-label fade-up">what's new</div>
98
- <div class="surface fade-up delay-1">
99
- <h2>What's New in v0.2.8</h2>
100
- <p>This release implements the <strong>reflection-before-action pattern</strong> — the same loop used by leading browser agents — plus a new <code>systemPrompt</code> option so you can shape agent behaviour without rewriting the bridge.</p>
101
-
102
- <h3>Reflection Loop <span class="badge new">New</span></h3>
103
- <p>Before every action the agent now goes through a 4-step inner loop:</p>
104
- <div class="docs-grid">
105
- <article class="doc-card">
106
- <h4>1 · Evaluate</h4>
107
- <p>What happened in the previous step? Did it succeed? What changed on the page?</p>
108
- </article>
109
- <article class="doc-card">
110
- <h4>2 · Remember</h4>
111
- <p>What key facts should be carried into the next step? Selector mappings, field values, task state.</p>
112
- </article>
113
- <article class="doc-card">
114
- <h4>3 · Plan</h4>
115
- <p>State the next goal in plain English before choosing an action.</p>
116
- </article>
117
- <article class="doc-card">
118
- <h4>4 · Act</h4>
119
- <p>Output the specific DOM action: click, type, navigate, scroll, etc.</p>
120
- </article>
121
- </div>
122
-
123
- <p>The WebLLM bridge now returns the full reflection object:</p>
124
- <pre><code>{
125
- "evaluation": "The name field was filled successfully.",
126
- "memory": "Name=#name done. Next: fill email at #email.",
127
- "next_goal": "Type the email address into #email",
128
- "action": { "type": "type", "selector": "#email", "text": "jane@example.com", "clearFirst": true }
129
- }</code></pre>
130
-
131
- <p>The <code>nextGoal</code> field is surfaced in the live demo as a <strong>💭 thought bubble</strong> before each action, so you can follow the agent's reasoning in real time.</p>
132
-
133
- <h3>Working Memory Across Steps <span class="badge new">New</span></h3>
134
- <p>The agent's <code>memory</code> string is automatically carried forward from one tick to the next inside <code>AgentSession</code>. The planner receives it as <code>input.memory</code> and can update it each step — giving the agent a scratchpad across the whole task.</p>
135
-
136
- <h3>Custom System Prompt <span class="badge new">New</span></h3>
137
- <p>Pass your own system prompt directly in the planner config — no need to rewrite the bridge:</p>
138
- <pre><code>const agent = createBrowserAgent({
139
- goal: "Fill the checkout form",
140
- planner: {
141
- kind: "webllm",
142
- systemPrompt: "You are a careful checkout assistant. Never submit before all required fields are filled."
143
- }
144
- });</code></pre>
145
-
146
- <h3>New Exports <span class="badge new">New</span></h3>
147
- <ul>
148
- <li><code>parsePlannerResult(raw)</code> — parse the full reflection+action JSON from raw LLM output, with fallback to bare AgentAction for backward compatibility.</li>
149
- <li><code>PlannerResult</code> type — <code>{ action, evaluation?, memory?, nextGoal? }</code></li>
150
- </ul>
151
- <pre><code>import { parsePlannerResult } from "@akshayram1/omnibrowser-agent";
152
-
153
- const result = parsePlannerResult(llmRawOutput);
154
- // result.action → AgentAction
155
- // result.evaluation → string | undefined
156
- // result.memory → string | undefined
157
- // result.nextGoal → string | undefined</code></pre>
158
-
159
- <h3>Backward Compatible</h3>
160
- <p>Existing bridges that return a bare <code>AgentAction</code> object still work without any changes. The library normalises both formats automatically.</p>
161
- </div>
162
- </div>
163
- </section>
164
-
165
- <!-- ── DOCS ── -->
166
- <section id="docs" class="section">
167
- <div class="wrap">
168
- <div class="section-label fade-up">docs</div>
169
- <div class="surface fade-up delay-1">
170
- <h2>Docs</h2>
171
- <p>Everything you need to install, initialise, and run your first browser agent.</p>
172
-
173
- <h3>Installation</h3>
174
- <pre><code>npm install @akshayram1/omnibrowser-agent</code></pre>
175
-
176
- <h3>Quick Start</h3>
177
- <pre><code>import { createBrowserAgent } from "@akshayram1/omnibrowser-agent";
178
-
179
- const agent = createBrowserAgent(
180
- {
181
- goal: "Open CRM and find customer John Smith",
182
- mode: "human-approved", // or "autonomous"
183
- planner: { kind: "heuristic" } // or "webllm"
184
- },
185
- {
186
- onStep: (result, session) => console.log(result.message),
187
- onApprovalRequired:(action, session) => console.log("Needs approval:", action),
188
- onDone: (result, session) => console.log("Done:", result.message),
189
- onError: (err, session) => console.error(err),
190
- onMaxStepsReached: (session) => console.log("Max steps hit"),
191
- }
192
- );
193
-
194
- await agent.start();
195
-
196
- // Resume after an approval prompt:
197
- await agent.resume();
198
-
199
- // Inspect state at any time:
200
- console.log(agent.isRunning, agent.hasPendingAction);
201
-
202
- // Stop:
203
- agent.stop();</code></pre>
204
-
205
- <h3>AbortSignal Support</h3>
206
- <pre><code>const controller = new AbortController();
207
- const agent = createBrowserAgent({ goal: "...", signal: controller.signal });
208
- agent.start();
209
-
210
- controller.abort(); // cancel from outside</code></pre>
211
-
212
- <h3>Reading Reflection Fields</h3>
213
- <p>Every <code>onStep</code> result now includes optional reflection data from the planner:</p>
214
- <pre><code>onStep(result, session) {
215
- if (result.reflection?.nextGoal) {
216
- console.log("Agent thinking:", result.reflection.nextGoal);
217
- }
218
- if (result.reflection?.memory) {
219
- console.log("Agent memory:", result.reflection.memory);
220
- }
221
- console.log("Action:", result.message);
222
- }</code></pre>
223
-
224
- <h3>Agent Modes</h3>
225
- <div class="docs-grid">
226
- <article class="doc-card">
227
- <h4>human-approved</h4>
228
- <p>Pauses on review-rated actions and fires <code>onApprovalRequired</code>. Call <code>agent.resume()</code> to continue. Recommended for CRM, finance, and admin flows.</p>
229
- </article>
230
- <article class="doc-card">
231
- <h4>autonomous</h4>
232
- <p>Executes all safe and review actions without pausing. Best for rapid prototyping and demos.</p>
233
- </article>
234
- </div>
235
-
236
- <h3>Planner Modes</h3>
237
- <div class="docs-grid">
238
- <article class="doc-card">
239
- <h4>heuristic</h4>
240
- <p>Zero-dependency regex planner. Works fully offline. Best for simple, predictable goals: navigate, fill a field, click a button.</p>
241
- </article>
242
- <article class="doc-card">
243
- <h4>webllm</h4>
244
- <p>On-device LLM via WebGPU through <code>window.__browserAgentWebLLM</code>. Fully private. Supports the reflection loop and custom system prompts.</p>
245
- </article>
246
- </div>
247
-
248
- <h3>Supported Actions</h3>
249
- <table>
250
- <thead>
251
- <tr><th>Action</th><th>Description</th><th>Risk level</th></tr>
252
- </thead>
253
- <tbody>
254
- <tr><td><code>navigate</code></td><td>Navigate to a URL (http/https only)</td><td>safe</td></tr>
255
- <tr><td><code>click</code></td><td>Click an element by CSS selector</td><td>safe / review</td></tr>
256
- <tr><td><code>type</code></td><td>Type text into an input or textarea</td><td>safe / review</td></tr>
257
- <tr><td><code>scroll</code></td><td>Scroll a container or the page</td><td>safe</td></tr>
258
- <tr><td><code>focus</code></td><td>Focus an element (useful for dropdowns)</td><td>safe</td></tr>
259
- <tr><td><code>wait</code></td><td>Pause for N milliseconds</td><td>safe</td></tr>
260
- <tr><td><code>extract</code></td><td>Extract text from an element</td><td>review</td></tr>
261
- <tr><td><code>done</code></td><td>Signal task completion</td><td>safe</td></tr>
262
- </tbody>
263
- </table>
264
-
265
- <h3>Safety Model</h3>
266
- <ul>
267
- <li><strong>safe</strong> — executes immediately in all modes.</li>
268
- <li><strong>review</strong> — pauses in <code>human-approved</code> mode; executes in <code>autonomous</code>. Triggered by actions on labels matching delete / submit / pay / confirm / transfer.</li>
269
- <li><strong>blocked</strong> — never executes. Triggered by <code>javascript:</code>, <code>file:</code>, or malformed URLs.</li>
270
- </ul>
271
- </div>
272
- </div>
273
- </section>
274
-
275
- <!-- ── ARCHITECTURE ── -->
276
- <section id="architecture" class="section">
277
- <div class="wrap">
278
- <div class="section-label fade-up">architecture</div>
279
-
280
- <div class="section-label fade-up delay-1">01 — delivery</div>
281
- <div class="delivery-grid fade-up delay-1">
282
- <div class="card card-ext">
283
- <span class="card-icon">🧩</span>
284
- <div class="card-title">Chrome Extension</div>
285
- <div class="card-sub">popup + background worker</div>
286
- <div class="card-desc">Load the <code>dist/</code> folder as an unpacked extension. Enter a goal in the popup, pick a mode, and hit Start. The background service worker drives the tick loop across tabs.</div>
287
- <span class="tag tag-purple">MV3</span>
288
- <span class="tag tag-purple">background worker</span>
289
- </div>
290
- <div class="card card-lib">
291
- <span class="card-icon">📦</span>
292
- <div class="card-title">npm Library</div>
293
- <div class="card-sub">createBrowserAgent()</div>
294
- <div class="card-desc">Embed the agent directly into any web app. Import, configure, and wire up event callbacks. The same core engine powers both — zero duplication.</div>
295
- <span class="tag tag-green">@akshayram1/omnibrowser-agent</span>
296
- </div>
297
- </div>
298
-
299
- <div class="connector fade-up delay-2">
300
- <div class="connector-line"></div>
301
- <span class="connector-label">both share the same core engine</span>
302
- </div>
303
-
304
- <div class="section-label fade-up delay-2">02 — agent tick loop</div>
305
- <div class="flow-section fade-up delay-2">
306
- <div class="flow-header">
307
- <div class="flow-header-dot"></div>
308
- <div class="flow-header-title">one tick = observe → plan → assess risk → execute</div>
309
- </div>
310
- <div class="flow-steps">
311
- <div class="step">
312
- <div class="step-num step-num-1">01</div>
313
- <div class="step-title">Observe</div>
314
- <div class="step-file">observer.ts</div>
315
- <div class="step-desc">Scans the live DOM. Filters invisible elements. Prioritises in-viewport candidates. Resolves ARIA labels. Returns a PageSnapshot.</div>
316
- </div>
317
- <div class="step">
318
- <div class="step-num step-num-2">02</div>
319
- <div class="step-title">Plan</div>
320
- <div class="step-file">planner.ts</div>
321
- <div class="step-desc">Takes the goal, snapshot, and history. Returns the next AgentAction — plus optional reflection (evaluation, memory, nextGoal).</div>
322
- </div>
323
- <div class="step">
324
- <div class="step-num step-num-3">03</div>
325
- <div class="step-title">Assess risk</div>
326
- <div class="step-file">safety.ts</div>
327
- <div class="step-desc">Every action gets a risk level: safe, review, or blocked. Risky actions pause for human approval in human-approved mode.</div>
328
- </div>
329
- <div class="step">
330
- <div class="step-num step-num-4">04</div>
331
- <div class="step-title">Execute</div>
332
- <div class="step-file">executor.ts</div>
333
- <div class="step-desc">Performs the DOM action. Dispatches proper InputEvents for framework compat. Verifies success. Feeds errors back to the planner.</div>
334
- </div>
335
- </div>
336
- </div>
337
-
338
- <div class="connector fade-up delay-3">
339
- <div class="connector-line"></div>
340
- </div>
341
-
342
- <div class="section-label fade-up delay-3">03 — core modules (src/core/)</div>
343
- <div class="core-grid fade-up delay-3">
344
- <div class="module-card mc-blue">
345
- <div class="module-name">planner.ts</div>
346
- <div class="module-fn">planNextAction(config, input)</div>
347
- <ul class="module-items">
348
- <li>URL, fill, search, click regex patterns</li>
349
- <li>Fallback: first input → first button → done</li>
350
- <li>WebLLM bridge via window.__browserAgentWebLLM</li>
351
- <li>Returns PlannerResult with reflection fields</li>
352
- <li>lastError fed back on retry</li>
353
- </ul>
354
- </div>
355
- <div class="module-card mc-green">
356
- <div class="module-name">observer.ts</div>
357
- <div class="module-fn">collectSnapshot()</div>
358
- <ul class="module-items">
359
- <li>Queries a, button, input, textarea, select…</li>
360
- <li>Filters hidden &amp; zero-dimension elements</li>
361
- <li>In-viewport elements listed first</li>
362
- <li>Resolves label via aria-labelledby, for/id, aria-label</li>
363
- <li>Max 60 candidates, 1500-char text preview</li>
364
- </ul>
365
- </div>
366
- <div class="module-card mc-amber">
367
- <div class="module-name">executor.ts</div>
368
- <div class="module-fn">executeAction(action)</div>
369
- <ul class="module-items">
370
- <li>click — disabled-check before firing</li>
371
- <li>type — InputEvent + change dispatch (React/Vue safe)</li>
372
- <li>navigate — sets window.location.href</li>
373
- <li>extract — verifies non-empty text</li>
374
- <li>scroll, focus, wait actions</li>
375
- </ul>
376
- </div>
377
- </div>
378
-
379
- <div class="section-label fade-up delay-4">04 — safety &amp; planner modes</div>
380
- <div class="shared-grid fade-up delay-4">
381
- <div class="risk-panel">
382
- <div class="risk-title">// safety.ts — assessRisk()</div>
383
- <div class="risk-row">
384
- <div class="risk-dot" style="background:var(--accent)"></div>
385
- <div class="risk-label" style="color:var(--accent)">safe</div>
386
- <div class="risk-bar-wrap"><div class="risk-bar" style="width:90%;background:var(--accent)"></div></div>
387
- </div>
388
- <div class="risk-note" style="margin-bottom:12px;padding-left:18px">navigate (http/s), click, scroll, wait, focus, done</div>
389
- <div class="risk-row">
390
- <div class="risk-dot" style="background:var(--amber)"></div>
391
- <div class="risk-label" style="color:var(--amber)">review</div>
392
- <div class="risk-bar-wrap"><div class="risk-bar" style="width:55%;background:var(--amber)"></div></div>
393
- </div>
394
- <div class="risk-note" style="margin-bottom:12px;padding-left:18px">extract, click/type on delete · pay · submit · transfer</div>
395
- <div class="risk-row">
396
- <div class="risk-dot" style="background:var(--accent3)"></div>
397
- <div class="risk-label" style="color:var(--accent3)">blocked</div>
398
- <div class="risk-bar-wrap"><div class="risk-bar" style="width:20%;background:var(--accent3)"></div></div>
399
- </div>
400
- <div class="risk-note" style="padding-left:18px">javascript: · file: · malformed URLs</div>
401
- </div>
402
-
403
- <div class="planner-panel">
404
- <div class="risk-title" style="margin-bottom:14px">// planner modes</div>
405
- <div class="planner-tabs">
406
- <button class="ptab active-h" onclick="switchPlanner('h')">heuristic</button>
407
- <button class="ptab" onclick="switchPlanner('w')">webllm</button>
408
- </div>
409
- <div class="pmode active" id="pm-h">
410
- <div class="pmode-title">Heuristic — zero deps, works offline</div>
411
- <div class="pmode-desc">Pure regex matching against your goal string. Handles navigate, fill, search, and click patterns. Falls back to the first visible input or button.</div>
412
- <code class="code-chip"><span class="kw">const</span> agent = createBrowserAgent({<br>&nbsp;&nbsp;goal: <span class="str">"search for John Smith"</span>,<br>&nbsp;&nbsp;planner: { kind: <span class="str">"heuristic"</span> }<br>});</code>
413
- </div>
414
- <div class="pmode" id="pm-w">
415
- <div class="pmode-title">WebLLM — on-device via WebGPU</div>
416
- <div class="pmode-desc">Delegates to <code>window.__browserAgentWebLLM</code>. Fully private — no API calls, runs local inference via WebGPU. Wire in your own bridge implementation.</div>
417
- <code class="code-chip"><span class="kw">const</span> agent = createBrowserAgent({<br>&nbsp;&nbsp;goal: <span class="str">"open CRM and find customer"</span>,<br>&nbsp;&nbsp;planner: { kind: <span class="str">"webllm"</span>, modelId: <span class="str">"Llama-3"</span> }<br>}); <span class="cm">// needs bridge on window</span></code>
418
- </div>
419
- </div>
420
- </div>
421
-
422
- <div class="section-label fade-up delay-5">05 — action outcomes</div>
423
- <div class="outcomes-grid fade-up delay-5">
424
- <div class="outcome-card oc-safe">
425
- <span class="outcome-icon">✅</span>
426
- <div class="outcome-title" style="color:var(--accent)">Executed</div>
427
- <div class="outcome-desc">Action is safe. Runs immediately. Result message appended to session history. Next tick begins.</div>
428
- <span class="tag tag-green">status: "executed"</span>
429
- </div>
430
- <div class="outcome-card oc-review">
431
- <span class="outcome-icon">⏸</span>
432
- <div class="outcome-title" style="color:var(--amber)">Needs approval</div>
433
- <div class="outcome-desc">Risk level is "review" in human-approved mode. Agent pauses, fires onApprovalRequired. Resume with agent.resume().</div>
434
- <span class="tag tag-amber">status: "needs_approval"</span>
435
- </div>
436
- <div class="outcome-card oc-block">
437
- <span class="outcome-icon">🚫</span>
438
- <div class="outcome-title" style="color:var(--accent3)">Blocked</div>
439
- <div class="outcome-desc">Dangerous protocol or malformed URL. Action is rejected entirely. Session stops without executing anything.</div>
440
- <span class="tag tag-red">status: "blocked"</span>
441
- </div>
442
- </div>
443
-
444
- <div class="surface fade-up delay-5">
445
- <h3 style="margin-top:0">Data Flow — One Tick</h3>
446
- <pre><code>goal + history + memory
447
-
448
-
449
- observer.collectSnapshot() → PageSnapshot (url, title, candidates[])
450
-
451
-
452
- planner.planNextAction() → PlannerResult
453
- { action, evaluation?, memory?, nextGoal? }
454
-
455
-
456
- safety.assessRisk(action) → safe | review | blocked
457
-
458
- ┌────┴──────────────────────────┐
459
- blocked review (human-approved mode)
460
- │ │
461
- stop pause → user approves → resume()
462
-
463
- safe / approved
464
-
465
-
466
- executor.executeAction(action) → result string
467
-
468
-
469
- session.history.push(result)
470
- session.memory = plannerResult.memory
471
- → next tick</code></pre>
472
-
473
- <h3>WebLLM Bridge Contract</h3>
474
- <p>Attach an object to <code>window.__browserAgentWebLLM</code> before starting the agent. The bridge can return either the new <code>PlannerResult</code> format or a bare <code>AgentAction</code> (backward compatible).</p>
475
- <pre><code>window.__browserAgentWebLLM = {
476
- async plan(input, modelId) {
477
- // input.goal, input.snapshot, input.history,
478
- // input.lastError, input.memory, input.systemPrompt
479
- return {
480
- evaluation: "Previous step succeeded.",
481
- memory: "Name field is #name.",
482
- next_goal: "Fill the email field.",
483
- action: { "type": "type", "selector": "#email", "text": "jane@example.com", "clearFirst": true }
484
- };
485
- }
486
- };</code></pre>
487
-
488
- <h3>Current Limitations</h3>
489
- <ul>
490
- <li>No persistent long-term memory (IndexedDB) yet</li>
491
- <li>No goal decomposition / multi-step task graphs yet</li>
492
- <li>Risk scoring is keyword-based, not semantic</li>
493
- <li>No selector healing or fallback strategy yet</li>
494
- </ul>
495
- </div>
496
-
497
- </div>
498
- </section>
499
-
500
- <!-- ── EMBEDDING ── -->
501
- <section id="embedding" class="section">
502
- <div class="wrap">
503
- <div class="section-label fade-up">embedding guide</div>
504
-
505
- <div class="section-label fade-up delay-1">06 — get started</div>
506
-
507
- <div class="install-strip fade-up delay-1">
508
- <div class="install-left">
509
- <div class="install-pkg">
510
- <span class="install-scope">@akshayram1/</span><span class="install-name">omnibrowser-agent</span>
511
- </div>
512
- <div class="install-meta">
513
- <span class="tag tag-green" style="margin:0">v0.2.8</span>
514
- <span class="tag tag-purple" style="margin:0">MIT</span>
515
- <span class="tag tag-amber" style="margin:0">TypeScript</span>
516
- </div>
517
- </div>
518
- <div class="install-cmd-wrap">
519
- <span class="install-prompt">$</span>
520
- <span class="install-cmd" id="install-cmd-text">npm install @akshayram1/omnibrowser-agent</span>
521
- <button class="copy-btn" onclick="copyInstall()" title="Copy">
522
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
523
- <span id="copy-label">copy</span>
524
- </button>
525
- </div>
526
- </div>
527
-
528
- <div class="snippet-panel fade-up delay-2">
529
- <div class="snippet-tabs">
530
- <button class="stab stab-active" onclick="switchSnippet('basic')">basic usage</button>
531
- <button class="stab" onclick="switchSnippet('approved')">human-approved</button>
532
- <button class="stab" onclick="switchSnippet('events')">event callbacks</button>
533
- <button class="stab" onclick="switchSnippet('abort')">abort signal</button>
534
- <button class="stab" onclick="switchSnippet('webllm')">webllm bridge</button>
535
- </div>
536
- <div class="snippet-body">
537
- <div class="snippet active" id="sn-basic">
538
- <div class="snippet-comment">// Minimal setup — heuristic planner, autonomous mode</div>
539
- <div class="snippet-line"><span class="kw">import</span> { createBrowserAgent } <span class="kw">from</span> <span class="str">'@akshayram1/omnibrowser-agent'</span>;</div>
540
- <div class="snippet-line">&nbsp;</div>
541
- <div class="snippet-line"><span class="kw">const</span> agent = createBrowserAgent({</div>
542
- <div class="snippet-line">&nbsp;&nbsp;goal: <span class="str">"search for contact John Smith in CRM"</span>,</div>
543
- <div class="snippet-line">&nbsp;&nbsp;mode: <span class="str">"autonomous"</span>,</div>
544
- <div class="snippet-line">&nbsp;&nbsp;planner: { kind: <span class="str">"heuristic"</span> }</div>
545
- <div class="snippet-line">});</div>
546
- <div class="snippet-line">&nbsp;</div>
547
- <div class="snippet-line"><span class="kw">await</span> agent.start();</div>
548
- </div>
549
- <div class="snippet" id="sn-approved">
550
- <div class="snippet-comment">// Human-approved mode — agent pauses on risky actions</div>
551
- <div class="snippet-line"><span class="kw">import</span> { createBrowserAgent } <span class="kw">from</span> <span class="str">'@akshayram1/omnibrowser-agent'</span>;</div>
552
- <div class="snippet-line">&nbsp;</div>
553
- <div class="snippet-line"><span class="kw">const</span> agent = createBrowserAgent({</div>
554
- <div class="snippet-line">&nbsp;&nbsp;goal: <span class="str">"fill out the payment form"</span>,</div>
555
- <div class="snippet-line">&nbsp;&nbsp;mode: <span class="str">"human-approved"</span>, <span class="cm">// pauses on delete / submit / pay</span></div>
556
- <div class="snippet-line">&nbsp;&nbsp;planner: { kind: <span class="str">"heuristic"</span> },</div>
557
- <div class="snippet-line">&nbsp;&nbsp;maxSteps: <span class="num">20</span>,</div>
558
- <div class="snippet-line">&nbsp;&nbsp;stepDelayMs: <span class="num">500</span></div>
559
- <div class="snippet-line">});</div>
560
- <div class="snippet-line">&nbsp;</div>
561
- <div class="snippet-line"><span class="kw">await</span> agent.start();</div>
562
- <div class="snippet-line">&nbsp;</div>
563
- <div class="snippet-comment">// Later — after user reviews the pending action:</div>
564
- <div class="snippet-line"><span class="kw">await</span> agent.resume();</div>
565
- </div>
566
- <div class="snippet" id="sn-events">
567
- <div class="snippet-comment">// Full event callback API</div>
568
- <div class="snippet-line"><span class="kw">import</span> { createBrowserAgent } <span class="kw">from</span> <span class="str">'@akshayram1/omnibrowser-agent'</span>;</div>
569
- <div class="snippet-line">&nbsp;</div>
570
- <div class="snippet-line"><span class="kw">const</span> agent = createBrowserAgent(</div>
571
- <div class="snippet-line">&nbsp;&nbsp;{ goal: <span class="str">"open CRM and find customer"</span>, mode: <span class="str">"human-approved"</span> },</div>
572
- <div class="snippet-line">&nbsp;&nbsp;{</div>
573
- <div class="snippet-line">&nbsp;&nbsp;&nbsp;&nbsp;onStart: (session) =&gt; console.log(<span class="str">'started'</span>, session.id),</div>
574
- <div class="snippet-line">&nbsp;&nbsp;&nbsp;&nbsp;onStep: (result, session) =&gt; console.log(result.message),</div>
575
- <div class="snippet-line">&nbsp;&nbsp;&nbsp;&nbsp;onApprovalRequired: (action, session) =&gt; {</div>
576
- <div class="snippet-line">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;console.log(<span class="str">'Review:'</span>, action);</div>
577
- <div class="snippet-line">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="cm">// call agent.resume() after user confirms</span></div>
578
- <div class="snippet-line">&nbsp;&nbsp;&nbsp;&nbsp;},</div>
579
- <div class="snippet-line">&nbsp;&nbsp;&nbsp;&nbsp;onDone: (result) =&gt; console.log(<span class="str">'Done:'</span>, result.message),</div>
580
- <div class="snippet-line">&nbsp;&nbsp;&nbsp;&nbsp;onError: (err) =&gt; console.error(err),</div>
581
- <div class="snippet-line">&nbsp;&nbsp;&nbsp;&nbsp;onMaxStepsReached: (session) =&gt; console.warn(<span class="str">'max steps'</span>, session)</div>
582
- <div class="snippet-line">&nbsp;&nbsp;}</div>
583
- <div class="snippet-line">);</div>
584
- <div class="snippet-line">&nbsp;</div>
585
- <div class="snippet-line"><span class="kw">await</span> agent.start();</div>
586
- </div>
587
- <div class="snippet" id="sn-abort">
588
- <div class="snippet-comment">// AbortSignal — cancel from outside at any time</div>
589
- <div class="snippet-line"><span class="kw">import</span> { createBrowserAgent } <span class="kw">from</span> <span class="str">'@akshayram1/omnibrowser-agent'</span>;</div>
590
- <div class="snippet-line">&nbsp;</div>
591
- <div class="snippet-line"><span class="kw">const</span> controller = <span class="kw">new</span> AbortController();</div>
592
- <div class="snippet-line">&nbsp;</div>
593
- <div class="snippet-line"><span class="kw">const</span> agent = createBrowserAgent({</div>
594
- <div class="snippet-line">&nbsp;&nbsp;goal: <span class="str">"extract all product prices"</span>,</div>
595
- <div class="snippet-line">&nbsp;&nbsp;signal: controller.signal <span class="cm">// wire in the abort signal</span></div>
596
- <div class="snippet-line">});</div>
597
- <div class="snippet-line">&nbsp;</div>
598
- <div class="snippet-line">agent.start();</div>
599
- <div class="snippet-line">&nbsp;</div>
600
- <div class="snippet-comment">// Cancel externally (e.g. button click, timeout, unmount)</div>
601
- <div class="snippet-line">setTimeout(() =&gt; controller.abort(), <span class="num">5000</span>);</div>
602
- <div class="snippet-line">&nbsp;</div>
603
- <div class="snippet-comment">// Or call stop() directly:</div>
604
- <div class="snippet-line">agent.stop();</div>
605
- </div>
606
- <div class="snippet" id="sn-webllm">
607
- <div class="snippet-comment">// WebLLM bridge with reflection loop</div>
608
- <div class="snippet-line"><span class="kw">import</span> * <span class="kw">as</span> webllm <span class="kw">from</span> <span class="str">"@mlc-ai/web-llm"</span>;</div>
609
- <div class="snippet-line"><span class="kw">import</span> { createBrowserAgent, parsePlannerResult } <span class="kw">from</span> <span class="str">"@akshayram1/omnibrowser-agent"</span>;</div>
610
- <div class="snippet-line">&nbsp;</div>
611
- <div class="snippet-line"><span class="kw">const</span> engine = <span class="kw">await</span> webllm.CreateMLCEngine(<span class="str">"Llama-3.2-3B-Instruct-q4f16_1-MLC"</span>);</div>
612
- <div class="snippet-line">&nbsp;</div>
613
- <div class="snippet-line">window.__browserAgentWebLLM = {</div>
614
- <div class="snippet-line">&nbsp;&nbsp;<span class="kw">async</span> plan(input, modelId) {</div>
615
- <div class="snippet-line">&nbsp;&nbsp;&nbsp;&nbsp;<span class="kw">const</span> resp = <span class="kw">await</span> engine.chat.completions.create({</div>
616
- <div class="snippet-line">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;messages: [{ role: <span class="str">"user"</span>, content: `Goal: ${input.goal}` }],</div>
617
- <div class="snippet-line">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;temperature: <span class="num">0</span>, max_tokens: <span class="num">200</span></div>
618
- <div class="snippet-line">&nbsp;&nbsp;&nbsp;&nbsp;});</div>
619
- <div class="snippet-line">&nbsp;&nbsp;&nbsp;&nbsp;<span class="kw">return</span> parsePlannerResult(resp.choices[<span class="num">0</span>].message.content);</div>
620
- <div class="snippet-line">&nbsp;&nbsp;}</div>
621
- <div class="snippet-line">};</div>
622
- </div>
623
- </div>
624
- <div class="exports-row">
625
- <div class="exports-label">exports</div>
626
- <div class="exports-chips">
627
- <span class="exp-chip exp-fn">createBrowserAgent()</span>
628
- <span class="exp-chip exp-fn">parseAction()</span>
629
- <span class="exp-chip exp-fn">parsePlannerResult()</span>
630
- <span class="exp-chip exp-type">AgentAction</span>
631
- <span class="exp-chip exp-type">AgentMode</span>
632
- <span class="exp-chip exp-type">AgentSession</span>
633
- <span class="exp-chip exp-type">PlannerConfig</span>
634
- <span class="exp-chip exp-type">ContentResult</span>
635
- <span class="exp-chip exp-type">RiskLevel</span>
636
- </div>
637
- </div>
638
- </div>
639
-
640
- <div class="surface fade-up delay-3">
641
- <h2>Embedding Guide</h2>
642
- <p>Embed OmniBrowser Agent as a library in any web application. Full reference in <a href="https://github.com/akshayram1/omnibrowser-agent/blob/main/docs/EMBEDDING.md" target="_blank" rel="noreferrer">docs/EMBEDDING.md</a>.</p>
643
-
644
- <h3>Heuristic Planner (zero setup)</h3>
645
- <pre><code>import { createBrowserAgent } from "@akshayram1/omnibrowser-agent";
646
-
647
- const agent = createBrowserAgent(
648
- {
649
- goal: "Search contact Jane Doe and open profile",
650
- mode: "human-approved",
651
- planner: { kind: "heuristic" },
652
- maxSteps: 15,
653
- stepDelayMs: 400
654
- },
655
- {
656
- onStep: (result) => console.log("step", result),
657
- onApprovalRequired: (action) => showApprovalModal(action),
658
- onDone: (result) => console.log("done", result),
659
- onError: (error) => console.error(error)
660
- }
661
- );
662
-
663
- await agent.start();
664
-
665
- // Approve a paused action:
666
- await agent.approvePendingAction();
667
-
668
- // Stop at any time:
669
- agent.stop();</code></pre>
670
-
671
- <h3>WebLLM Planner with Reflection</h3>
672
- <p>Load a WebLLM engine, wire the bridge, then start the agent. The bridge receives the full reflection input and should return the reflection+action object:</p>
673
- <pre><code>import * as webllm from "@mlc-ai/web-llm";
674
- import { createBrowserAgent, parsePlannerResult } from "@akshayram1/omnibrowser-agent";
675
-
676
- const engine = await webllm.CreateMLCEngine("Llama-3.2-3B-Instruct-q4f16_1-MLC");
677
-
678
- window.__browserAgentWebLLM = {
679
- async plan(input, modelId) {
680
- const { goal, history, lastError, memory, systemPrompt } = input;
681
-
682
- const defaultSystem = `You are a browser automation agent.
683
- Output ONLY a JSON object in this format:
684
- {"evaluation":"...","memory":"...","next_goal":"...","action":{...}}`;
685
-
686
- const resp = await engine.chat.completions.create({
687
- messages: [
688
- { role: "system", content: systemPrompt || defaultSystem },
689
- { role: "user", content: `Goal: "${goal}"\nHistory: ${history.slice(-4).join(" → ")}${memory ? "\nMemory: " + memory : ""}${lastError ? "\nLast error: " + lastError : ""}` }
690
- ],
691
- temperature: 0,
692
- max_tokens: 200
693
- });
694
-
695
- return parsePlannerResult(resp.choices[0].message.content);
696
- }
697
- };
698
-
699
- const agent = createBrowserAgent({
700
- goal: "Fill the checkout form with my details",
701
- planner: { kind: "webllm" }
702
- }, {
703
- onStep(result) {
704
- if (result.reflection?.nextGoal) console.log("💭", result.reflection.nextGoal);
705
- console.log("✅", result.message);
706
- }
707
- });
708
-
709
- await agent.start();</code></pre>
710
-
711
- <h3>Custom System Prompt</h3>
712
- <p>Shape the agent's personality or constraints without touching the bridge:</p>
713
- <pre><code>const agent = createBrowserAgent({
714
- goal: "Book a meeting room for tomorrow",
715
- planner: {
716
- kind: "webllm",
717
- systemPrompt: `You are a careful meeting room booking assistant.
718
- Always confirm the room is available before clicking Book.
719
- Never navigate away from the booking portal.`
720
- }
721
- });</code></pre>
722
-
723
- <h3>Notes</h3>
724
- <ul>
725
- <li>The WebLLM bridge is not bundled — bring your own engine and attach it to <code>window.__browserAgentWebLLM</code>.</li>
726
- <li>Use <code>human-approved</code> mode for CRM, finance, and admin actions.</li>
727
- <li>Bridges returning a bare <code>AgentAction</code> still work — backward compatible.</li>
728
- <li>For production apps, mount inside an authenticated shell and add your own permission checks.</li>
729
- </ul>
730
- </div>
731
- </div>
732
- </section>
733
-
734
- <!-- ── ROADMAP ── -->
735
- <section id="roadmap" class="section">
736
- <div class="wrap">
737
- <div class="section-label fade-up">roadmap</div>
738
- <div class="surface fade-up delay-1">
739
- <h2>Roadmap</h2>
740
- <p>Full roadmap in <a href="https://github.com/akshayram1/omnibrowser-agent/blob/main/docs/ROADMAP.md" target="_blank" rel="noreferrer">docs/ROADMAP.md</a>.</p>
741
-
742
- <h3>v0.1</h3>
743
- <ul>
744
- <li>Extension runtime loop</li>
745
- <li>Shared action contracts</li>
746
- <li>Heuristic + WebLLM planner switch</li>
747
- <li>Human-approved mode</li>
748
- </ul>
749
-
750
- <h3>v0.2 <span class="badge stable">stable</span></h3>
751
- <ul>
752
- <li>New actions: <code>scroll</code>, <code>focus</code></li>
753
- <li>Improved heuristic planner with regex goal patterns</li>
754
- <li>Better page observation (visibility filtering, up to 60 candidates)</li>
755
- <li>Library API: <code>resume()</code>, <code>isRunning</code>, <code>hasPendingAction</code>, <code>AbortSignal</code>, <code>onMaxStepsReached</code></li>
756
- <li>CI pipeline with auto version bump on push to main</li>
757
- </ul>
758
-
759
- <h3>v0.2.8 <span class="badge new">current</span></h3>
760
- <ul>
761
- <li>Reflection-before-action pattern (<code>evaluation → memory → next_goal → act</code>)</li>
762
- <li>Working memory carried across ticks via <code>AgentSession.memory</code></li>
763
- <li><code>parsePlannerResult()</code> exported from library</li>
764
- <li><code>systemPrompt</code> option in <code>PlannerConfig</code></li>
765
- <li>Thought bubble (💭) messages in live demo</li>
766
- <li>Chatbot UI redesign: tabs, typing indicator, right-aligned messages</li>
767
- <li>Doc Viewer example with hidden tabs + side chat</li>
768
- <li>Live Examples hub at <code>/examples</code></li>
769
- </ul>
770
-
771
- <h3>v0.3</h3>
772
- <ul>
773
- <li>Expanded WebLLM model catalog (new 7B/8B options + compatibility matrix)</li>
774
- <li>Improved model loading UX (recommended presets by speed/quality and device memory)</li>
775
- <li>Enhanced default system prompts for safer, clearer multi-step planning</li>
776
- <li>Prompt presets for common workflows (docs navigation, CRM form fill, task automation)</li>
777
- </ul>
778
-
779
- <h3>v1.0</h3>
780
- <ul>
781
- <li>Advanced prompt orchestration (goal-aware system prompt routing and contextual guardrails)</li>
782
- <li>Functionality expansion: richer action toolkit and stronger extraction/navigation reliability</li>
783
- <li>Adaptive planner behaviour (model-aware retries, fallback strategies, and recovery flows)</li>
784
- <li>Evaluation suite for prompt and model quality across benchmark browser tasks</li>
785
- </ul>
786
- </div>
787
- </div>
788
- </section>
789
-
790
- <!-- ── CONTACT ── -->
791
- <section id="contact" class="section">
792
- <div class="wrap">
793
- <div class="section-label fade-up">contact</div>
794
- <div class="surface fade-up delay-1">
795
- <h2>Contact</h2>
796
- <p>Maintainer: Akshay Chame</p>
797
- <ul>
798
- <li>Email: <a href="mailto:akshaychame2@gmail.com">akshaychame2@gmail.com</a></li>
799
- <li>GitHub: <a href="https://github.com/akshayram1" target="_blank" rel="noreferrer">@akshayram1</a></li>
800
- <li>Package: <a href="https://www.npmjs.com/package/@akshayram1/omnibrowser-agent" target="_blank" rel="noreferrer">@akshayram1/omnibrowser-agent</a></li>
801
- </ul>
802
- <p class="contact-note">For feature requests or bugs, please open an issue on GitHub with reproduction steps.</p>
803
- </div>
804
-
805
- <div class="cta-row fade-up delay-2">
806
- <a href="https://github.com/akshayram1/omnibrowser-agent" class="btn primary">↗ View on GitHub</a>
807
- <a href="https://www.npmjs.com/package/@akshayram1/omnibrowser-agent" class="btn">npm package</a>
808
- <a href="./examples/" class="btn">live examples</a>
809
- </div>
810
- </div>
811
- </section>
812
-
813
- </main>
814
-
815
- <footer class="footer">
816
- <div class="wrap">
817
- <p>© 2026 OmniBrowser Agent · MIT License · <a href="https://github.com/akshayram1/omnibrowser-agent" target="_blank" rel="noreferrer">GitHub</a> · <a href="https://www.npmjs.com/package/@akshayram1/omnibrowser-agent" target="_blank" rel="noreferrer">npm</a></p>
818
- </div>
819
- </footer>
820
-
821
- <!-- ── DOCS NAVIGATION CHATBOT WIDGET ── -->
822
- <style>
823
- #chatbot-toggle {
824
- position: fixed; bottom: 28px; right: 28px; z-index: 9000;
825
- width: 52px; height: 52px; border-radius: 50%;
826
- background: var(--accent); border: none; cursor: pointer;
827
- display: flex; align-items: center; justify-content: center;
828
- box-shadow: 0 4px 24px rgba(0,229,160,0.35);
829
- transition: transform .2s, box-shadow .2s; font-size: 22px;
830
- }
831
- #chatbot-toggle:hover { transform: scale(1.1); box-shadow: 0 6px 30px rgba(0,229,160,0.45); }
832
- #chatbot-toggle.open { background: var(--surface2); box-shadow: 0 4px 20px rgba(0,0,0,0.4); }
833
-
834
- #chatbot-panel {
835
- position: fixed; bottom: 92px; right: 28px; z-index: 9000;
836
- width: 350px; max-height: 560px;
837
- background: var(--surface); border: 1px solid var(--border-bright);
838
- border-radius: 20px; display: flex; flex-direction: column;
839
- box-shadow: 0 20px 60px rgba(0,0,0,0.55), 0 0 0 1px rgba(0,229,160,0.06);
840
- transform: translateY(12px) scale(0.97);
841
- opacity: 0; pointer-events: none;
842
- transition: transform .22s cubic-bezier(.34,1.56,.64,1), opacity .18s ease;
843
- overflow: hidden;
844
- }
845
- #chatbot-panel.visible { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }
846
-
847
- /* Header */
848
- .cb-header {
849
- padding: 13px 16px 11px; border-bottom: 1px solid var(--border);
850
- display: flex; align-items: center; gap: 10px; flex-shrink: 0;
851
- }
852
- .cb-icon {
853
- width: 30px; height: 30px; border-radius: 9px;
854
- background: rgba(0,229,160,0.12); border: 1px solid rgba(0,229,160,0.2);
855
- display: flex; align-items: center; justify-content: center; font-size: 14px;
856
- }
857
- .cb-title { font-family: var(--sans); font-size: 13px; font-weight: 600; color: var(--text); }
858
- .cb-subtitle { font-family: var(--mono); font-size: 10px; color: var(--muted); letter-spacing: 0.04em; }
859
- .cb-status-pill {
860
- margin-left: auto; font-family: var(--mono); font-size: 10px;
861
- border-radius: 999px; padding: 2px 8px; letter-spacing: 0.04em;
862
- white-space: nowrap; display: flex; align-items: center; gap: 5px;
863
- border: 1px solid var(--border); color: var(--muted); background: transparent;
864
- transition: all .2s;
865
- }
866
- .cb-status-pill.ready { color: var(--accent); border-color: rgba(0,229,160,0.28); background: rgba(0,229,160,0.07); }
867
- .cb-status-pill.loading{ color: var(--amber); border-color: rgba(245,158,11,0.3); background: rgba(245,158,11,0.07); }
868
- .cb-sdot {
869
- width: 5px; height: 5px; border-radius: 50%; background: var(--muted); flex-shrink: 0;
870
- }
871
- .cb-sdot.ready { background: var(--accent); }
872
- .cb-sdot.loading { background: var(--amber); animation: cbPulse 1s ease-in-out infinite; }
873
- @keyframes cbPulse { 0%,100%{opacity:1} 50%{opacity:.3} }
874
-
875
- /* Model loader bar */
876
- .cb-model-bar {
877
- padding: 9px 14px; border-bottom: 1px solid var(--border);
878
- flex-shrink: 0; display: flex; flex-direction: column; gap: 7px;
879
- }
880
- .cb-model-row { display: flex; gap: 7px; align-items: center; }
881
- .cb-model-select {
882
- flex: 1; background: var(--surface2); border: 1px solid var(--border);
883
- border-radius: 8px; padding: 5px 8px; color: var(--text);
884
- font-family: var(--mono); font-size: 10.5px; outline: none; cursor: pointer;
885
- transition: border-color .15s;
886
- }
887
- .cb-model-select:focus { border-color: rgba(0,229,160,0.35); }
888
- .cb-model-select option { background: #0d1117; }
889
- .cb-load-btn {
890
- padding: 5px 13px; border-radius: 8px; border: none; cursor: pointer;
891
- background: var(--accent); color: #000; font-family: var(--mono);
892
- font-size: 10.5px; font-weight: 700; white-space: nowrap;
893
- transition: opacity .15s, transform .1s;
894
- }
895
- .cb-load-btn:hover:not(:disabled) { opacity: .88; transform: translateY(-1px); }
896
- .cb-load-btn:disabled { opacity: .4; cursor: not-allowed; transform: none; }
897
- .cb-load-btn.loaded {
898
- background: transparent; border: 1px solid rgba(0,229,160,0.28);
899
- color: var(--accent);
900
- }
901
-
902
- .cb-progress-wrap { display: none; }
903
- .cb-progress-wrap.visible { display: flex; flex-direction: column; gap: 3px; }
904
- .cb-progress-track {
905
- height: 2px; background: var(--border); border-radius: 999px; overflow: hidden;
906
- }
907
- .cb-progress-fill {
908
- height: 100%; background: linear-gradient(90deg, var(--accent), #00ffc2);
909
- border-radius: 999px; width: 0%; transition: width .4s ease;
910
- }
911
- .cb-progress-text { font-family: var(--mono); font-size: 10px; color: var(--muted); }
912
- .cb-webgpu-warn {
913
- font-family: var(--mono); font-size: 10px; color: var(--amber);
914
- background: rgba(245,158,11,0.07); border: 1px solid rgba(245,158,11,0.2);
915
- border-radius: 6px; padding: 5px 8px; display: none; line-height: 1.5;
916
- }
917
- .cb-webgpu-warn.visible { display: block; }
918
-
919
- /* Messages */
920
- .cb-messages {
921
- flex: 1; overflow-y: auto; padding: 12px 14px;
922
- display: flex; flex-direction: column; gap: 8px; scroll-behavior: smooth;
923
- scrollbar-width: thin; scrollbar-color: var(--border) transparent;
924
- min-height: 0;
925
- }
926
- .cb-messages::-webkit-scrollbar { width: 4px; }
927
- .cb-messages::-webkit-scrollbar-thumb { background: var(--border-bright); border-radius: 4px; }
928
-
929
- .cb-msg { display: flex; gap: 8px; align-items: flex-end; animation: cbMsgIn .18s ease; }
930
- @keyframes cbMsgIn { from{opacity:0;transform:translateY(4px)} to{opacity:1;transform:translateY(0)} }
931
- .cb-msg.user { flex-direction: row-reverse; }
932
- .cb-bubble { max-width: 82%; padding: 8px 12px; font-size: 12.5px; line-height: 1.55; border-radius: 14px; }
933
- .cb-msg.bot .cb-bubble { background: var(--surface2); border: 1px solid var(--border); color: var(--text); border-radius: 4px 14px 14px 14px; }
934
- .cb-msg.bot.thought .cb-bubble {
935
- max-width: 88%; padding: 7px 10px; font-size: 11px;
936
- color: var(--muted); background: rgba(0,229,160,0.06);
937
- border: 1px solid rgba(0,229,160,0.16);
938
- }
939
- .cb-msg.user .cb-bubble { background: rgba(0,229,160,0.12); border: 1px solid rgba(0,229,160,0.22); color: var(--text); border-radius: 14px 14px 4px 14px; }
940
- .cb-msg.bot .cb-bubble a { color: var(--accent); text-decoration: none; }
941
- .cb-msg.bot .cb-bubble a:hover { text-decoration: underline; }
942
-
943
- /* Typing indicator */
944
- .cb-typing { display: flex; gap: 8px; align-items: flex-end; animation: cbMsgIn .18s ease; }
945
- .cb-typing-dots {
946
- background: var(--surface2); border: 1px solid var(--border);
947
- border-radius: 4px 14px 14px 14px; padding: 10px 14px;
948
- display: flex; gap: 4px; align-items: center;
949
- }
950
- .cb-tdot { width: 5px; height: 5px; border-radius: 50%; background: var(--muted); animation: cbTdot 1.2s ease-in-out infinite; }
951
- .cb-tdot:nth-child(2) { animation-delay: .15s; }
952
- .cb-tdot:nth-child(3) { animation-delay: .3s; }
953
- @keyframes cbTdot { 0%,60%,100%{transform:translateY(0)} 30%{transform:translateY(-4px)} }
954
-
955
- /* Suggestions */
956
- .cb-suggestions { padding: 8px 14px 0; display: flex; flex-wrap: wrap; gap: 6px; flex-shrink: 0; }
957
- .cb-chip {
958
- font-family: var(--mono); font-size: 10px; color: var(--muted);
959
- background: var(--surface2); border: 1px solid var(--border);
960
- border-radius: 999px; padding: 3px 10px; cursor: pointer;
961
- transition: all .15s; white-space: nowrap; letter-spacing: 0.02em;
962
- }
963
- .cb-chip:hover { color: var(--accent); border-color: rgba(0,229,160,0.3); }
964
-
965
- /* Input */
966
- .cb-input-area {
967
- padding: 11px 14px; border-top: 1px solid var(--border);
968
- display: flex; gap: 8px; flex-shrink: 0;
969
- }
970
- .cb-input {
971
- flex: 1; background: var(--surface2); border: 1px solid var(--border);
972
- border-radius: 10px; padding: 8px 12px; color: var(--text);
973
- font-size: 12.5px; font-family: var(--sans); outline: none; transition: border-color .15s;
974
- }
975
- .cb-input::placeholder { color: var(--muted); }
976
- .cb-input:focus { border-color: rgba(0,229,160,0.35); }
977
- .cb-send {
978
- width: 34px; height: 34px; border-radius: 9px;
979
- background: var(--accent); border: none; cursor: pointer;
980
- display: flex; align-items: center; justify-content: center;
981
- font-size: 14px; flex-shrink: 0; transition: opacity .15s, transform .1s;
982
- }
983
- .cb-send:hover { opacity: .85; transform: scale(1.05); }
984
- .cb-send:disabled { opacity: .4; cursor: not-allowed; transform: none; }
985
- </style>
986
-
987
- <button id="chatbot-toggle" title="Ask about the docs" aria-label="Open docs assistant">💬</button>
988
-
989
- <div id="chatbot-panel" role="dialog" aria-label="Docs navigation assistant">
990
-
991
- <div class="cb-header">
992
- <div class="cb-icon">🤖</div>
993
- <div>
994
- <div class="cb-title">Docs Assistant</div>
995
- <div class="cb-subtitle">powered by omnibrowser-agent</div>
996
- </div>
997
- <div class="cb-status-pill" id="cb-status-pill">
998
- <span class="cb-sdot" id="cb-sdot"></span>
999
- <span id="cb-status-text">heuristic</span>
1000
- </div>
1001
- </div>
1002
-
1003
- <!-- Model loader -->
1004
- <div class="cb-model-bar">
1005
- <div class="cb-model-row">
1006
- <select class="cb-model-select" id="cb-model-select">
1007
- <option value="Phi-3.5-mini-instruct-q4f16_1-MLC" selected>Phi-3.5 Mini — quality (~2 GB)</option>
1008
- <option value="Llama-3.2-3B-Instruct-q4f16_1-MLC">Llama 3.2 3B — balanced (~1.5 GB)</option>
1009
- <option value="Llama-3.2-1B-Instruct-q4f16_1-MLC">Llama 3.2 1B — fast (~600 MB)</option>
1010
- <option value="Mistral-7B-Instruct-v0.3-q4f16_1-MLC">Mistral 7B v0.3 — balanced (~4.1 GB)</option>
1011
- <option value="Qwen2.5-7B-Instruct-q4f16_1-MLC">Qwen2.5 7B — strongest (~4.3 GB)</option>
1012
- <option value="Llama-3.1-8B-Instruct-q4f16_1-MLC">Llama 3.1 8B — strong (~4.8 GB)</option>
1013
- </select>
1014
- <button class="cb-load-btn" id="cb-load-btn">Load AI</button>
1015
- </div>
1016
- <div class="cb-progress-wrap" id="cb-progress-wrap">
1017
- <div class="cb-progress-track"><div class="cb-progress-fill" id="cb-progress-fill"></div></div>
1018
- <div class="cb-progress-text" id="cb-progress-text">Initialising…</div>
1019
- </div>
1020
- <div class="cb-webgpu-warn" id="cb-webgpu-warn">
1021
- ⚠️ WebGPU unavailable — try Chrome 113+. Running in heuristic mode.
1022
- </div>
1023
- </div>
1024
-
1025
- <div class="cb-messages" id="cb-messages"></div>
1026
-
1027
- <div class="cb-suggestions" id="cb-suggestions">
1028
- <button class="cb-chip" data-q="How do I install?">install</button>
1029
- <button class="cb-chip" data-q="Show me the examples">examples</button>
1030
- <button class="cb-chip" data-q="How does the architecture work?">architecture</button>
1031
- <button class="cb-chip" data-q="What's new in v0.2?">what's new</button>
1032
- <button class="cb-chip" data-q="Embedding guide">embedding</button>
1033
- </div>
1034
-
1035
- <div class="cb-input-area">
1036
- <input class="cb-input" id="cb-input" type="text" placeholder="Ask me anything about the docs…" autocomplete="off" />
1037
- <button class="cb-send" id="cb-send" aria-label="Send">↑</button>
1038
- </div>
1039
- </div>
1040
-
1041
- <script type="module">
1042
-
1043
- /* ── Navigation map ── */
1044
- const NAV = [
1045
- { match: /all.?docs|show.?docs|docs.?section|documentation|all.?sections/i, id: '#docs', reply: 'Taking you to the <strong>Docs</strong> section overview.' },
1046
- { match: /install|npm|package|get.?start|quick.?start|setup/i, id: '#docs', reply: 'Scrolling to <strong>Installation & Quick Start</strong>.' },
1047
- { match: /what.?s.?new|release|v0\.|changelog|update|new.?feature/i, id: '#whats-new', reply: 'Here are the <strong>latest features</strong> — reflection loop, working memory, custom system prompts.' },
1048
- { match: /architect|system|design|how.?it.?work|internals|engine|tick/i, id: '#architecture', reply: 'Navigating to <strong>Architecture</strong> — agent tick loop, safety model, core modules.' },
1049
- { match: /embed|integrat|api|librar|wire|drop.?in|snippet/i, id: '#embedding', reply: 'Taking you to the <strong>Embedding Guide</strong> with heuristic & WebLLM code snippets.' },
1050
- { match: /roadmap|future|plan|coming|v1|next/i, id: '#roadmap', reply: 'Here\'s the <strong>Roadmap</strong> — v0.3 through v1.0.' },
1051
- { match: /contact|author|maintainer|who|person/i, id: '#contact', reply: 'Navigating to <strong>Contact</strong> info.' },
1052
- { match: /home|top|start|begin|back/i, id: '#home', reply: 'Back to the <strong>top</strong>.' },
1053
- { match: /example|demo|live|chatbot|simple|doc.?viewer/i, href: './examples/', reply: 'Opening the <strong>Live Examples</strong> page.' },
1054
- { match: /github|source|repo|code/i, href: 'https://github.com/akshayram1/omnibrowser-agent', reply: 'Opening the <strong>GitHub repository</strong>.' },
1055
- ];
1056
-
1057
- const FALLBACK_REPLIES = [
1058
- 'Try asking about <strong>installation</strong>, <strong>architecture</strong>, <strong>embedding</strong>, or <strong>examples</strong>.',
1059
- 'I can navigate to any section — e.g. "show me docs" or "how does the architecture work?"',
1060
- 'I know about: Install, What\'s New, Architecture, Embedding, Roadmap, Contact, and Examples.',
1061
- ];
1062
- let fallbackIdx = 0;
1063
-
1064
- /* ── DOM refs ── */
1065
- const toggleBtn = document.getElementById('chatbot-toggle');
1066
- const panel = document.getElementById('chatbot-panel');
1067
- const messagesEl = document.getElementById('cb-messages');
1068
- const inputEl = document.getElementById('cb-input');
1069
- const sendBtn = document.getElementById('cb-send');
1070
- const suggsEl = document.getElementById('cb-suggestions');
1071
- const modelSelect = document.getElementById('cb-model-select');
1072
- const loadBtn = document.getElementById('cb-load-btn');
1073
- const progressWrap = document.getElementById('cb-progress-wrap');
1074
- const progressFill = document.getElementById('cb-progress-fill');
1075
- const progressText = document.getElementById('cb-progress-text');
1076
- const webgpuWarn = document.getElementById('cb-webgpu-warn');
1077
- const statusPill = document.getElementById('cb-status-pill');
1078
- const statusDot = document.getElementById('cb-sdot');
1079
- const statusText = document.getElementById('cb-status-text');
1080
-
1081
- let isOpen = false;
1082
- let loadedEngine = null; // WebLLM engine when loaded
1083
-
1084
- /* ── Model loader ── */
1085
- loadBtn.addEventListener('click', loadModel);
1086
-
1087
- function getPlannerTimeoutMs() {
1088
- const modelId = modelSelect.value;
1089
- if (/1B/i.test(modelId)) return 18000;
1090
- if (/3B|Phi-3\.5-mini/i.test(modelId)) return 30000;
1091
- if (/7B|8B/i.test(modelId)) return 45000;
1092
- return 30000;
1093
- }
1094
-
1095
- function contentToText(content) {
1096
- if (typeof content === 'string') {
1097
- return content;
1098
- }
1099
- if (Array.isArray(content)) {
1100
- return content
1101
- .map(part => {
1102
- if (typeof part === 'string') return part;
1103
- if (part && typeof part === 'object' && typeof part.text === 'string') return part.text;
1104
- return '';
1105
- })
1106
- .join('\n');
1107
- }
1108
- return '';
1109
- }
1110
-
1111
- function extractFirstJsonObject(text) {
1112
- const start = text.indexOf('{');
1113
- if (start === -1) return null;
1114
- let depth = 0;
1115
- for (let i = start; i < text.length; i += 1) {
1116
- const ch = text[i];
1117
- if (ch === '{') depth += 1;
1118
- if (ch === '}') {
1119
- depth -= 1;
1120
- if (depth === 0) return text.slice(start, i + 1);
1121
- }
1122
- }
1123
- return null;
1124
- }
1125
-
1126
- async function loadModel() {
1127
- if (!navigator.gpu) {
1128
- webgpuWarn.classList.add('visible');
1129
- setStatus('heuristic', 'idle');
1130
- return;
1131
- }
1132
-
1133
- const modelId = modelSelect.value;
1134
- loadBtn.disabled = true;
1135
- modelSelect.disabled = true;
1136
- progressWrap.classList.add('visible');
1137
- setStatus('loading', 'loading');
1138
- addBotMsg(`Loading <strong>${modelSelect.options[modelSelect.selectedIndex].text.split('—')[0].trim()}</strong>… this may take a moment on first run.`);
1139
-
1140
- try {
1141
- const webllm = await import('https://esm.run/@mlc-ai/web-llm');
1142
- loadedEngine = await webllm.CreateMLCEngine(modelId, {
1143
- initProgressCallback({ progress, text }) {
1144
- progressFill.style.width = `${Math.round(progress * 100)}%`;
1145
- progressText.textContent = text || `${Math.round(progress * 100)}%`;
1146
- }
1147
- });
1148
-
1149
- progressText.textContent = 'Warming up first response…';
1150
- await loadedEngine.chat.completions.create({
1151
- messages: [{ role: 'user', content: 'Reply with exactly: ready' }],
1152
- temperature: 0,
1153
- max_tokens: 4
1154
- });
1155
-
1156
- /* Install WebLLM bridge for the library */
1157
- window.__browserAgentWebLLM = {
1158
- async plan(input) {
1159
- const sectionList = NAV.filter(n => n.id).map(n => {
1160
- const keywords = n.match.source.replace(/\\/g, '').replace(/\|/g, ', ');
1161
- return `${n.id} (matches: ${keywords})`;
1162
- }).join('\n');
1163
- const prompt = [
1164
- 'You are a docs navigation assistant. Your ONLY job is to pick which section to scroll to.',
1165
- 'Available sections:',
1166
- sectionList,
1167
- '',
1168
- 'IMPORTANT RULES:',
1169
- '- You MUST always set "section" to one of the IDs above. NEVER set it to null.',
1170
- '- If the query mentions install, setup, getting started → use "#docs"',
1171
- '- If the query mentions home, top, start → use "#home"',
1172
- '- If unsure, pick the closest match. Do NOT tell the user to "visit" a section — you must navigate them there.',
1173
- '- Reply with ONLY a JSON object: {"section":"#id","reply":"short confirmation","reasoning":"brief reason"}',
1174
- '',
1175
- `User query: ${input.goal}`
1176
- ].join('\n');
1177
-
1178
- const resp = await loadedEngine.chat.completions.create({
1179
- messages: [
1180
- { role: 'system', content: 'Return only one JSON object. No markdown or extra prose.' },
1181
- { role: 'user', content: prompt }
1182
- ],
1183
- temperature: 0,
1184
- max_tokens: 160
1185
- });
1186
-
1187
- try {
1188
- const raw = contentToText(resp.choices?.[0]?.message?.content ?? '').trim();
1189
- const jsonText = extractFirstJsonObject(raw);
1190
- if (!jsonText) {
1191
- return { type: 'done', reason: 'Could not parse model response.' };
1192
- }
1193
- const parsed = JSON.parse(jsonText);
1194
- if (parsed.section) {
1195
- return {
1196
- type: 'scroll',
1197
- selector: parsed.section,
1198
- _reply: parsed.reply,
1199
- _reasoning: typeof parsed.reasoning === 'string' ? parsed.reasoning : ''
1200
- };
1201
- }
1202
- /* Model didn't return a section — try keyword fallback before giving up */
1203
- const kwFallback = NAV.find(n => n.match.test(input.goal));
1204
- if (kwFallback) {
1205
- return {
1206
- type: 'scroll',
1207
- selector: kwFallback.id || null,
1208
- _reply: kwFallback.reply,
1209
- _href: kwFallback.href || null,
1210
- _reasoning: typeof parsed.reasoning === 'string' ? parsed.reasoning : 'Matched via keyword fallback.'
1211
- };
1212
- }
1213
- return {
1214
- type: 'done',
1215
- reason: parsed.reply || 'No matching section.',
1216
- _reasoning: typeof parsed.reasoning === 'string' ? parsed.reasoning : ''
1217
- };
1218
- } catch {
1219
- return { type: 'done', reason: 'Could not parse model response.' };
1220
- }
1221
- }
1222
- };
1223
-
1224
- progressWrap.classList.remove('visible');
1225
- loadBtn.textContent = 'Reload';
1226
- loadBtn.classList.add('loaded');
1227
- loadBtn.disabled = false;
1228
- modelSelect.disabled = false;
1229
- setStatus('ready', 'AI ready');
1230
- addBotMsg('✅ Model loaded! I can now answer questions about the docs and show a brief <strong>reasoning summary</strong>.');
1231
- } catch (err) {
1232
- loadedEngine = null;
1233
- progressWrap.classList.remove('visible');
1234
- loadBtn.disabled = false;
1235
- modelSelect.disabled = false;
1236
- setStatus('idle', 'heuristic');
1237
- addBotMsg('⚠️ Model failed to load — falling back to heuristic mode. Check WebGPU support.');
1238
- }
1239
- }
1240
-
1241
- function setStatus(dotClass, label) {
1242
- statusDot.className = `cb-sdot${dotClass === 'idle' ? '' : ' ' + dotClass}`;
1243
- statusText.textContent = label;
1244
- statusPill.className = `cb-status-pill${dotClass === 'ready' ? ' ready' : dotClass === 'loading' ? ' loading' : ''}`;
1245
- }
1246
-
1247
- /* ── Toggle ── */
1248
- toggleBtn.addEventListener('click', () => {
1249
- isOpen = !isOpen;
1250
- panel.classList.toggle('visible', isOpen);
1251
- toggleBtn.classList.toggle('open', isOpen);
1252
- toggleBtn.textContent = isOpen ? '✕' : '💬';
1253
- if (isOpen) {
1254
- if (messagesEl.children.length === 0) {
1255
- addBotMsg('Hi! I\'m your <strong>docs assistant</strong>. Load an AI model above for smarter answers, or ask me about any section right now.');
1256
- }
1257
- inputEl.focus();
1258
- }
1259
- });
1260
-
1261
- /* ── Suggestions ── */
1262
- suggsEl.querySelectorAll('.cb-chip').forEach(chip => {
1263
- chip.addEventListener('click', () => handleQuery(chip.dataset.q));
1264
- });
1265
-
1266
- /* ── Send ── */
1267
- sendBtn.addEventListener('click', send);
1268
- inputEl.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } });
1269
-
1270
- function send() {
1271
- const q = inputEl.value.trim();
1272
- if (!q) return;
1273
- addUserMsg(q);
1274
- inputEl.value = '';
1275
- handleQuery(q);
1276
- }
1277
-
1278
- /* ── Greetings — answer instantly, no model needed ── */
1279
- const GREETINGS = /^(hi|hey|hello|sup|yo|hiya|howdy|what's up|whats up|greetings)[!?.]*$/i;
1280
- const GREETING_REPLIES = [
1281
- 'Hey! Ask me about any section — <strong>install</strong>, <strong>architecture</strong>, <strong>embedding</strong>, <strong>examples</strong>…',
1282
- 'Hi there! What part of the docs can I take you to?',
1283
- 'Hello! I can navigate to any section. What are you looking for?',
1284
- ];
1285
- let greetIdx = 0;
1286
-
1287
- function withTimeout(promise, timeoutMs = 30000) {
1288
- let timeoutId;
1289
- const timeoutPromise = new Promise((_, reject) => {
1290
- timeoutId = setTimeout(() => reject(new Error('planner_timeout')), timeoutMs);
1291
- });
1292
- return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
1293
- }
1294
-
1295
- /* ── Route query ── */
1296
- async function handleQuery(q) {
1297
- sendBtn.disabled = true;
1298
- inputEl.disabled = true;
1299
-
1300
- /* 1. Instant greeting reply — never hits the model */
1301
- if (GREETINGS.test(q.trim())) {
1302
- addBotMsg(GREETING_REPLIES[greetIdx % GREETING_REPLIES.length]);
1303
- greetIdx++;
1304
- sendBtn.disabled = false;
1305
- inputEl.disabled = false;
1306
- inputEl.focus();
1307
- return;
1308
- }
1309
-
1310
- /* 2. AI-first routing — when model is loaded, always route via LLM */
1311
- if (loadedEngine && window.__browserAgentWebLLM) {
1312
- setStatus('loading', 'AI thinking');
1313
- const typingEl = addTyping();
1314
- try {
1315
- const result = await withTimeout(window.__browserAgentWebLLM.plan({ goal: q }), getPlannerTimeoutMs());
1316
- typingEl.remove();
1317
- if (result._reasoning) addBotThought(result._reasoning);
1318
- const replyText = result._reply || (result.reason ?? 'Done.');
1319
- addBotMsg(replyText);
1320
- if (result._href) {
1321
- setTimeout(() => window.open(result._href, '_blank', 'noopener'), 400);
1322
- } else {
1323
- const targetId = result.selector || null;
1324
- if (targetId) {
1325
- const target = document.querySelector(targetId);
1326
- if (target) setTimeout(() => target.scrollIntoView({ behavior: 'smooth', block: 'start' }), 200);
1327
- }
1328
- }
1329
- } catch (err) {
1330
- typingEl.remove();
1331
- const kwFallback = NAV.find(n => n.match.test(q));
1332
- if (kwFallback) {
1333
- addBotThought('Using the keyword fallback because the model was slow.');
1334
- addBotMsg(kwFallback.reply);
1335
- if (kwFallback.href) {
1336
- setTimeout(() => window.open(kwFallback.href, '_blank', 'noopener'), 400);
1337
- } else {
1338
- const target = document.querySelector(kwFallback.id);
1339
- if (target) setTimeout(() => target.scrollIntoView({ behavior: 'smooth', block: 'start' }), 200);
1340
- }
1341
- } else {
1342
- const isTimeout = err instanceof Error && err.message === 'planner_timeout';
1343
- addBotMsg(
1344
- isTimeout
1345
- ? 'The AI model is still thinking and hit the current wait limit. Try a more direct query, switch to <strong>Llama 3.2 1B</strong> for speed, or retry after the first prompt warm-up.'
1346
- : 'The AI model failed to answer this request. Try again, or use a direct query like <strong>install</strong>, <strong>architecture</strong>, <strong>embedding</strong>, or <strong>docs</strong>.'
1347
- );
1348
- }
1349
- } finally {
1350
- setStatus('ready', 'AI ready');
1351
- }
1352
- } else {
1353
- const kwMatch = NAV.find(n => n.match.test(q));
1354
- if (kwMatch) {
1355
- addBotMsg(kwMatch.reply);
1356
- if (kwMatch.href) {
1357
- setTimeout(() => window.open(kwMatch.href, '_blank', 'noopener'), 400);
1358
- } else {
1359
- const target = document.querySelector(kwMatch.id);
1360
- if (target) setTimeout(() => target.scrollIntoView({ behavior: 'smooth', block: 'start' }), 200);
1361
- }
1362
- } else {
1363
- addBotMsg(FALLBACK_REPLIES[fallbackIdx % FALLBACK_REPLIES.length]);
1364
- fallbackIdx++;
1365
- }
1366
- }
1367
-
1368
- sendBtn.disabled = false;
1369
- inputEl.disabled = false;
1370
- inputEl.focus();
1371
- }
1372
-
1373
- /* ── Helpers ── */
1374
- function addBotMsg(html) {
1375
- const msg = document.createElement('div');
1376
- msg.className = 'cb-msg bot';
1377
- msg.innerHTML = `<div class="cb-bubble">${html}</div>`;
1378
- messagesEl.appendChild(msg);
1379
- messagesEl.scrollTop = messagesEl.scrollHeight;
1380
- }
1381
-
1382
- function addBotThought(text) {
1383
- const msg = document.createElement('div');
1384
- msg.className = 'cb-msg bot thought';
1385
- msg.innerHTML = `<div class="cb-bubble">💭 ${text}</div>`;
1386
- messagesEl.appendChild(msg);
1387
- messagesEl.scrollTop = messagesEl.scrollHeight;
1388
- }
1389
-
1390
- function addUserMsg(text) {
1391
- const msg = document.createElement('div');
1392
- msg.className = 'cb-msg user';
1393
- msg.innerHTML = `<div class="cb-bubble">${text}</div>`;
1394
- messagesEl.appendChild(msg);
1395
- messagesEl.scrollTop = messagesEl.scrollHeight;
1396
- }
1397
-
1398
- function addTyping() {
1399
- const el = document.createElement('div');
1400
- el.className = 'cb-typing';
1401
- el.innerHTML = `<div class="cb-typing-dots"><div class="cb-tdot"></div><div class="cb-tdot"></div><div class="cb-tdot"></div></div>`;
1402
- messagesEl.appendChild(el);
1403
- messagesEl.scrollTop = messagesEl.scrollHeight;
1404
- return el;
1405
- }
1406
- </script>
1407
-
1408
- <script>
1409
- function switchSnippet(id) {
1410
- document.querySelectorAll('.snippet').forEach(s => s.classList.remove('active'));
1411
- document.querySelectorAll('.stab').forEach(t => t.classList.remove('stab-active'));
1412
- document.getElementById('sn-' + id).classList.add('active');
1413
- event.target.classList.add('stab-active');
1414
- }
1415
-
1416
- function copyInstall() {
1417
- const txt = document.getElementById('install-cmd-text').textContent;
1418
- navigator.clipboard.writeText(txt).then(() => {
1419
- const lbl = document.getElementById('copy-label');
1420
- lbl.textContent = 'copied!';
1421
- setTimeout(() => lbl.textContent = 'copy', 1800);
1422
- });
1423
- }
1424
-
1425
- function switchPlanner(mode) {
1426
- document.getElementById('pm-h').classList.toggle('active', mode === 'h');
1427
- document.getElementById('pm-w').classList.toggle('active', mode === 'w');
1428
- document.querySelectorAll('.ptab').forEach((t, i) => {
1429
- t.classList.remove('active-h', 'active-w');
1430
- if (i === 0 && mode === 'h') t.classList.add('active-h');
1431
- if (i === 1 && mode === 'w') t.classList.add('active-w');
1432
- });
1433
- }
1434
-
1435
- const observer = new IntersectionObserver((entries) => {
1436
- entries.forEach(e => {
1437
- if (e.isIntersecting) e.target.style.animationPlayState = 'running';
1438
- });
1439
- }, { threshold: 0.08 });
1440
-
1441
- document.querySelectorAll('.fade-up').forEach(el => {
1442
- el.style.animationPlayState = 'paused';
1443
- observer.observe(el);
1444
- });
1445
- </script>
1446
-
1447
- </body>
1448
- </html>