@akshayram1/omnibrowser-agent 0.2.6 → 0.2.26
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/README.md +219 -110
- package/dist/background.js +24 -5
- package/dist/background.js.map +2 -2
- package/dist/content.js +120 -4
- package/dist/content.js.map +3 -3
- package/dist/lib.js +264 -58
- package/dist/lib.js.map +3 -3
- package/dist/popup.html +7 -1
- package/dist/popup.js +19 -1
- package/dist/popup.js.map +2 -2
- package/dist/types/core/prompt.d.ts +3 -0
- package/dist/types/core/webllm-bridge.d.ts +33 -0
- package/dist/types/lib/index.d.ts +2 -0
- package/dist/types/shared/contracts.d.ts +4 -0
- package/dist/types/shared/parse-action.d.ts +2 -1
- package/docs/EMBEDDING.md +3 -14
- package/docs/ROADMAP.md +8 -13
- package/docs/arch.md +220 -0
- package/index.html +1204 -198
- package/package.json +1 -1
- package/plan.md +114 -0
- package/styles.css +654 -293
- package/vercel.json +7 -2
package/index.html
CHANGED
|
@@ -3,19 +3,24 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>OmniBrowser Agent</title>
|
|
7
|
-
<meta
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
/>
|
|
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">
|
|
11
10
|
<link rel="stylesheet" href="./styles.css" />
|
|
12
11
|
</head>
|
|
13
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
|
+
|
|
14
18
|
<header class="header">
|
|
15
19
|
<div class="wrap header-row">
|
|
16
|
-
<a class="brand" href="#home">
|
|
20
|
+
<a class="brand" href="#home">omnibrowser-agent</a>
|
|
17
21
|
<nav class="nav">
|
|
18
22
|
<a href="#home">Home</a>
|
|
23
|
+
<a href="#whats-new">What's New</a>
|
|
19
24
|
<a href="#docs">Docs</a>
|
|
20
25
|
<a href="#architecture">Architecture</a>
|
|
21
26
|
<a href="#embedding">Embedding</a>
|
|
@@ -26,57 +31,52 @@
|
|
|
26
31
|
</header>
|
|
27
32
|
|
|
28
33
|
<main>
|
|
29
|
-
|
|
34
|
+
|
|
35
|
+
<!-- ── HOME ── -->
|
|
30
36
|
<section id="home" class="section hero">
|
|
31
37
|
<div class="wrap">
|
|
32
|
-
<
|
|
33
|
-
<h1>Local-first browser AI automation library</h1>
|
|
34
|
-
<p>
|
|
35
|
-
OmniBrowser Agent
|
|
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.
|
|
36
43
|
</p>
|
|
37
|
-
<div class="chips">
|
|
44
|
+
<div class="chips fade-up delay-2">
|
|
38
45
|
<span>Privacy-first</span>
|
|
39
|
-
<span>WebLLM
|
|
46
|
+
<span>WebLLM + WebGPU</span>
|
|
47
|
+
<span>Reflection loop</span>
|
|
40
48
|
<span>Human-approved mode</span>
|
|
49
|
+
<span>Custom system prompt</span>
|
|
41
50
|
<span>Embeddable API</span>
|
|
42
51
|
</div>
|
|
43
|
-
<div class="actions">
|
|
44
|
-
<a class="btn primary" href="./examples/
|
|
45
|
-
<a
|
|
46
|
-
|
|
47
|
-
href="https://www.npmjs.com/package/@akshayram1/omnibrowser-agent"
|
|
48
|
-
target="_blank"
|
|
49
|
-
rel="noreferrer"
|
|
50
|
-
>NPM Package</a
|
|
51
|
-
>
|
|
52
|
-
<a
|
|
53
|
-
class="btn"
|
|
54
|
-
href="https://github.com/akshayram1/omnibrowser-agent"
|
|
55
|
-
target="_blank"
|
|
56
|
-
rel="noreferrer"
|
|
57
|
-
>GitHub Repo</a
|
|
58
|
-
>
|
|
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>
|
|
59
56
|
</div>
|
|
60
|
-
<div class="stats" aria-label="project stats">
|
|
61
|
-
<div class="stat"><strong>2</strong><span>
|
|
62
|
-
<div class="stat"><strong>
|
|
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>
|
|
63
61
|
<div class="stat"><strong>MIT</strong><span>License</span></div>
|
|
64
62
|
</div>
|
|
65
|
-
<div class="home-grid">
|
|
63
|
+
<div class="home-grid fade-up delay-4">
|
|
66
64
|
<article class="card">
|
|
67
65
|
<h3>Use Cases</h3>
|
|
68
66
|
<ul>
|
|
69
67
|
<li>CRM profile lookup automation</li>
|
|
70
|
-
<li>Guided
|
|
68
|
+
<li>Guided form-filling workflows</li>
|
|
71
69
|
<li>Assisted data extraction flows</li>
|
|
70
|
+
<li>Multi-step task automation</li>
|
|
72
71
|
</ul>
|
|
73
72
|
</article>
|
|
74
73
|
<article class="card">
|
|
75
|
-
<h3>Core
|
|
74
|
+
<h3>Core Engine</h3>
|
|
76
75
|
<ul>
|
|
77
|
-
<li><strong>Observer:</strong>
|
|
78
|
-
<li><strong>Planner:</strong> next
|
|
79
|
-
<li><strong>
|
|
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
80
|
</ul>
|
|
81
81
|
</article>
|
|
82
82
|
<article class="card">
|
|
@@ -84,19 +84,91 @@
|
|
|
84
84
|
<ul>
|
|
85
85
|
<li><a href="https://www.npmjs.com/package/@akshayram1/omnibrowser-agent" target="_blank" rel="noreferrer">NPM package</a></li>
|
|
86
86
|
<li><a href="https://github.com/akshayram1/omnibrowser-agent" target="_blank" rel="noreferrer">GitHub repository</a></li>
|
|
87
|
-
<li><a href="./
|
|
87
|
+
<li><a href="./examples/">Live Examples</a></li>
|
|
88
88
|
</ul>
|
|
89
89
|
</article>
|
|
90
90
|
</div>
|
|
91
91
|
</div>
|
|
92
92
|
</section>
|
|
93
93
|
|
|
94
|
-
<!--
|
|
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 ── -->
|
|
95
166
|
<section id="docs" class="section">
|
|
96
167
|
<div class="wrap">
|
|
97
|
-
<div class="
|
|
168
|
+
<div class="section-label fade-up">docs</div>
|
|
169
|
+
<div class="surface fade-up delay-1">
|
|
98
170
|
<h2>Docs</h2>
|
|
99
|
-
<p>Everything you need to install,
|
|
171
|
+
<p>Everything you need to install, initialise, and run your first browser agent.</p>
|
|
100
172
|
|
|
101
173
|
<h3>Installation</h3>
|
|
102
174
|
<pre><code>npm install @akshayram1/omnibrowser-agent</code></pre>
|
|
@@ -107,180 +179,469 @@
|
|
|
107
179
|
const agent = createBrowserAgent(
|
|
108
180
|
{
|
|
109
181
|
goal: "Open CRM and find customer John Smith",
|
|
110
|
-
mode: "human-approved",
|
|
111
|
-
planner: { kind: "heuristic" }
|
|
182
|
+
mode: "human-approved", // or "autonomous"
|
|
183
|
+
planner: { kind: "heuristic" } // or "webllm"
|
|
112
184
|
},
|
|
113
185
|
{
|
|
114
|
-
onStep:
|
|
115
|
-
onApprovalRequired:
|
|
116
|
-
onDone:
|
|
117
|
-
|
|
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"),
|
|
118
191
|
}
|
|
119
192
|
);
|
|
120
193
|
|
|
121
194
|
await agent.start();
|
|
122
195
|
|
|
123
|
-
// Resume after approval:
|
|
196
|
+
// Resume after an approval prompt:
|
|
124
197
|
await agent.resume();
|
|
125
198
|
|
|
126
|
-
// Inspect state:
|
|
199
|
+
// Inspect state at any time:
|
|
127
200
|
console.log(agent.isRunning, agent.hasPendingAction);
|
|
128
201
|
|
|
129
202
|
// Stop:
|
|
130
203
|
agent.stop();</code></pre>
|
|
131
204
|
|
|
132
|
-
<h3>AbortSignal
|
|
205
|
+
<h3>AbortSignal Support</h3>
|
|
133
206
|
<pre><code>const controller = new AbortController();
|
|
134
207
|
const agent = createBrowserAgent({ goal: "...", signal: controller.signal });
|
|
135
208
|
agent.start();
|
|
136
209
|
|
|
137
|
-
//
|
|
138
|
-
|
|
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>
|
|
139
223
|
|
|
140
|
-
<h3>
|
|
224
|
+
<h3>Agent Modes</h3>
|
|
141
225
|
<div class="docs-grid">
|
|
142
226
|
<article class="doc-card">
|
|
143
227
|
<h4>human-approved</h4>
|
|
144
|
-
<p>
|
|
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>
|
|
145
229
|
</article>
|
|
146
230
|
<article class="doc-card">
|
|
147
231
|
<h4>autonomous</h4>
|
|
148
|
-
<p>
|
|
232
|
+
<p>Executes all safe and review actions without pausing. Best for rapid prototyping and demos.</p>
|
|
149
233
|
</article>
|
|
150
234
|
</div>
|
|
151
235
|
|
|
152
|
-
<h3>Planner
|
|
236
|
+
<h3>Planner Modes</h3>
|
|
153
237
|
<div class="docs-grid">
|
|
154
238
|
<article class="doc-card">
|
|
155
239
|
<h4>heuristic</h4>
|
|
156
|
-
<p>Zero-dependency regex
|
|
240
|
+
<p>Zero-dependency regex planner. Works fully offline. Best for simple, predictable goals: navigate, fill a field, click a button.</p>
|
|
157
241
|
</article>
|
|
158
242
|
<article class="doc-card">
|
|
159
243
|
<h4>webllm</h4>
|
|
160
|
-
<p>
|
|
244
|
+
<p>On-device LLM via WebGPU through <code>window.__browserAgentWebLLM</code>. Fully private. Supports the reflection loop and custom system prompts.</p>
|
|
161
245
|
</article>
|
|
162
|
-
|
|
163
246
|
</div>
|
|
164
247
|
|
|
165
248
|
<h3>Supported Actions</h3>
|
|
166
249
|
<table>
|
|
167
250
|
<thead>
|
|
168
|
-
<tr><th>Action</th><th>Description</th></tr>
|
|
251
|
+
<tr><th>Action</th><th>Description</th><th>Risk level</th></tr>
|
|
169
252
|
</thead>
|
|
170
253
|
<tbody>
|
|
171
|
-
<tr><td><code>
|
|
172
|
-
<tr><td><code>
|
|
173
|
-
<tr><td><code>
|
|
174
|
-
<tr><td><code>
|
|
175
|
-
<tr><td><code>
|
|
176
|
-
<tr><td><code>
|
|
177
|
-
<tr><td><code>
|
|
178
|
-
<tr><td><code>done</code></td><td>Signal task completion</td></tr>
|
|
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>
|
|
179
262
|
</tbody>
|
|
180
263
|
</table>
|
|
181
264
|
|
|
182
|
-
<h3>Safety
|
|
265
|
+
<h3>Safety Model</h3>
|
|
183
266
|
<ul>
|
|
184
|
-
<li>
|
|
185
|
-
<li>
|
|
186
|
-
<li>
|
|
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>
|
|
187
270
|
</ul>
|
|
188
271
|
</div>
|
|
189
272
|
</div>
|
|
190
273
|
</section>
|
|
191
274
|
|
|
192
|
-
<!-- ARCHITECTURE -->
|
|
275
|
+
<!-- ── ARCHITECTURE ── -->
|
|
193
276
|
<section id="architecture" class="section">
|
|
194
277
|
<div class="wrap">
|
|
195
|
-
<div class="
|
|
196
|
-
<h2>Architecture</h2>
|
|
197
|
-
<p>How OmniBrowser Agent is structured internally and how its components interact.</p>
|
|
278
|
+
<div class="section-label fade-up">architecture</div>
|
|
198
279
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
<
|
|
203
|
-
<
|
|
204
|
-
<
|
|
205
|
-
|
|
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>
|
|
206
298
|
|
|
207
|
-
|
|
208
|
-
<div class="
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
</
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
<
|
|
221
|
-
<
|
|
222
|
-
<
|
|
223
|
-
|
|
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>
|
|
224
335
|
</div>
|
|
336
|
+
</div>
|
|
225
337
|
|
|
226
|
-
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
<li><code>click</code> — click element by CSS selector</li>
|
|
230
|
-
<li><code>type</code> — type text into input/textarea</li>
|
|
231
|
-
<li><code>navigate</code> — navigate to URL</li>
|
|
232
|
-
<li><code>extract</code> — extract text from element</li>
|
|
233
|
-
<li><code>scroll</code> — scroll container or page</li>
|
|
234
|
-
<li><code>focus</code> — focus an element</li>
|
|
235
|
-
<li><code>wait</code> — pause for N milliseconds</li>
|
|
236
|
-
<li><code>done</code> — signal task completion</li>
|
|
237
|
-
</ul>
|
|
338
|
+
<div class="connector fade-up delay-3">
|
|
339
|
+
<div class="connector-line"></div>
|
|
340
|
+
</div>
|
|
238
341
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
<
|
|
243
|
-
<
|
|
244
|
-
|
|
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 & 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>
|
|
245
378
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
379
|
+
<div class="section-label fade-up delay-4">04 — safety & 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>
|
|
252
402
|
|
|
253
|
-
<
|
|
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> goal: <span class="str">"search for John Smith"</span>,<br> 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> goal: <span class="str">"open CRM and find customer"</span>,<br> 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>
|
|
254
475
|
<pre><code>window.__browserAgentWebLLM = {
|
|
255
476
|
async plan(input, modelId) {
|
|
256
|
-
//
|
|
257
|
-
|
|
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
|
+
};
|
|
258
485
|
}
|
|
259
486
|
};</code></pre>
|
|
260
487
|
|
|
261
|
-
|
|
262
488
|
<h3>Current Limitations</h3>
|
|
263
489
|
<ul>
|
|
264
|
-
<li>No persistent long-term memory yet</li>
|
|
265
|
-
<li>No
|
|
266
|
-
<li>Risk scoring is
|
|
267
|
-
<li>No
|
|
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>
|
|
268
494
|
</ul>
|
|
269
495
|
</div>
|
|
496
|
+
|
|
270
497
|
</div>
|
|
271
498
|
</section>
|
|
272
499
|
|
|
273
|
-
<!-- EMBEDDING -->
|
|
500
|
+
<!-- ── EMBEDDING ── -->
|
|
274
501
|
<section id="embedding" class="section">
|
|
275
502
|
<div class="wrap">
|
|
276
|
-
<div class="
|
|
277
|
-
<h2>Embedding Guide</h2>
|
|
278
|
-
<p>How to embed OmniBrowser Agent as a library inside your own web application.</p>
|
|
503
|
+
<div class="section-label fade-up">embedding guide</div>
|
|
279
504
|
|
|
280
|
-
|
|
281
|
-
|
|
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"> </div>
|
|
541
|
+
<div class="snippet-line"><span class="kw">const</span> agent = createBrowserAgent({</div>
|
|
542
|
+
<div class="snippet-line"> goal: <span class="str">"search for contact John Smith in CRM"</span>,</div>
|
|
543
|
+
<div class="snippet-line"> mode: <span class="str">"autonomous"</span>,</div>
|
|
544
|
+
<div class="snippet-line"> planner: { kind: <span class="str">"heuristic"</span> }</div>
|
|
545
|
+
<div class="snippet-line">});</div>
|
|
546
|
+
<div class="snippet-line"> </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"> </div>
|
|
553
|
+
<div class="snippet-line"><span class="kw">const</span> agent = createBrowserAgent({</div>
|
|
554
|
+
<div class="snippet-line"> goal: <span class="str">"fill out the payment form"</span>,</div>
|
|
555
|
+
<div class="snippet-line"> mode: <span class="str">"human-approved"</span>, <span class="cm">// pauses on delete / submit / pay</span></div>
|
|
556
|
+
<div class="snippet-line"> planner: { kind: <span class="str">"heuristic"</span> },</div>
|
|
557
|
+
<div class="snippet-line"> maxSteps: <span class="num">20</span>,</div>
|
|
558
|
+
<div class="snippet-line"> stepDelayMs: <span class="num">500</span></div>
|
|
559
|
+
<div class="snippet-line">});</div>
|
|
560
|
+
<div class="snippet-line"> </div>
|
|
561
|
+
<div class="snippet-line"><span class="kw">await</span> agent.start();</div>
|
|
562
|
+
<div class="snippet-line"> </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"> </div>
|
|
570
|
+
<div class="snippet-line"><span class="kw">const</span> agent = createBrowserAgent(</div>
|
|
571
|
+
<div class="snippet-line"> { goal: <span class="str">"open CRM and find customer"</span>, mode: <span class="str">"human-approved"</span> },</div>
|
|
572
|
+
<div class="snippet-line"> {</div>
|
|
573
|
+
<div class="snippet-line"> onStart: (session) => console.log(<span class="str">'started'</span>, session.id),</div>
|
|
574
|
+
<div class="snippet-line"> onStep: (result, session) => console.log(result.message),</div>
|
|
575
|
+
<div class="snippet-line"> onApprovalRequired: (action, session) => {</div>
|
|
576
|
+
<div class="snippet-line"> console.log(<span class="str">'Review:'</span>, action);</div>
|
|
577
|
+
<div class="snippet-line"> <span class="cm">// call agent.resume() after user confirms</span></div>
|
|
578
|
+
<div class="snippet-line"> },</div>
|
|
579
|
+
<div class="snippet-line"> onDone: (result) => console.log(<span class="str">'Done:'</span>, result.message),</div>
|
|
580
|
+
<div class="snippet-line"> onError: (err) => console.error(err),</div>
|
|
581
|
+
<div class="snippet-line"> onMaxStepsReached: (session) => console.warn(<span class="str">'max steps'</span>, session)</div>
|
|
582
|
+
<div class="snippet-line"> }</div>
|
|
583
|
+
<div class="snippet-line">);</div>
|
|
584
|
+
<div class="snippet-line"> </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"> </div>
|
|
591
|
+
<div class="snippet-line"><span class="kw">const</span> controller = <span class="kw">new</span> AbortController();</div>
|
|
592
|
+
<div class="snippet-line"> </div>
|
|
593
|
+
<div class="snippet-line"><span class="kw">const</span> agent = createBrowserAgent({</div>
|
|
594
|
+
<div class="snippet-line"> goal: <span class="str">"extract all product prices"</span>,</div>
|
|
595
|
+
<div class="snippet-line"> signal: controller.signal <span class="cm">// wire in the abort signal</span></div>
|
|
596
|
+
<div class="snippet-line">});</div>
|
|
597
|
+
<div class="snippet-line"> </div>
|
|
598
|
+
<div class="snippet-line">agent.start();</div>
|
|
599
|
+
<div class="snippet-line"> </div>
|
|
600
|
+
<div class="snippet-comment">// Cancel externally (e.g. button click, timeout, unmount)</div>
|
|
601
|
+
<div class="snippet-line">setTimeout(() => controller.abort(), <span class="num">5000</span>);</div>
|
|
602
|
+
<div class="snippet-line"> </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"> </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"> </div>
|
|
613
|
+
<div class="snippet-line">window.__browserAgentWebLLM = {</div>
|
|
614
|
+
<div class="snippet-line"> <span class="kw">async</span> plan(input, modelId) {</div>
|
|
615
|
+
<div class="snippet-line"> <span class="kw">const</span> resp = <span class="kw">await</span> engine.chat.completions.create({</div>
|
|
616
|
+
<div class="snippet-line"> messages: [{ role: <span class="str">"user"</span>, content: `Goal: ${input.goal}` }],</div>
|
|
617
|
+
<div class="snippet-line"> temperature: <span class="num">0</span>, max_tokens: <span class="num">200</span></div>
|
|
618
|
+
<div class="snippet-line"> });</div>
|
|
619
|
+
<div class="snippet-line"> <span class="kw">return</span> parsePlannerResult(resp.choices[<span class="num">0</span>].message.content);</div>
|
|
620
|
+
<div class="snippet-line"> }</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>
|
|
282
639
|
|
|
283
|
-
|
|
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>
|
|
284
645
|
<pre><code>import { createBrowserAgent } from "@akshayram1/omnibrowser-agent";
|
|
285
646
|
|
|
286
647
|
const agent = createBrowserAgent(
|
|
@@ -292,51 +653,91 @@ const agent = createBrowserAgent(
|
|
|
292
653
|
stepDelayMs: 400
|
|
293
654
|
},
|
|
294
655
|
{
|
|
295
|
-
onStep:
|
|
296
|
-
onApprovalRequired: (action) =>
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
},
|
|
300
|
-
onDone: (result) => console.log("done", result),
|
|
301
|
-
onError: (error) => console.error(error)
|
|
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)
|
|
302
660
|
}
|
|
303
661
|
);
|
|
304
662
|
|
|
305
|
-
await agent.start()
|
|
663
|
+
await agent.start();
|
|
306
664
|
|
|
307
|
-
|
|
308
|
-
|
|
665
|
+
// Approve a paused action:
|
|
666
|
+
await agent.approvePendingAction();
|
|
309
667
|
|
|
310
|
-
|
|
311
|
-
|
|
668
|
+
// Stop at any time:
|
|
669
|
+
agent.stop();</code></pre>
|
|
312
670
|
|
|
313
|
-
<h3>WebLLM
|
|
314
|
-
<p>
|
|
315
|
-
<pre><code>
|
|
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 = {
|
|
316
679
|
async plan(input, modelId) {
|
|
317
|
-
|
|
318
|
-
|
|
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);
|
|
319
696
|
}
|
|
320
697
|
};
|
|
321
698
|
|
|
322
|
-
|
|
323
|
-
|
|
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>
|
|
324
722
|
|
|
325
723
|
<h3>Notes</h3>
|
|
326
724
|
<ul>
|
|
327
|
-
<li>For production, mount this inside an authenticated app shell and add your own permission checks.</li>
|
|
328
|
-
<li><code>human-approved</code> mode is recommended for CRM, finance, and admin actions.</li>
|
|
329
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>
|
|
330
729
|
</ul>
|
|
331
730
|
</div>
|
|
332
731
|
</div>
|
|
333
732
|
</section>
|
|
334
733
|
|
|
335
|
-
<!-- ROADMAP -->
|
|
734
|
+
<!-- ── ROADMAP ── -->
|
|
336
735
|
<section id="roadmap" class="section">
|
|
337
736
|
<div class="wrap">
|
|
338
|
-
<div class="
|
|
737
|
+
<div class="section-label fade-up">roadmap</div>
|
|
738
|
+
<div class="surface fade-up delay-1">
|
|
339
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>
|
|
340
741
|
|
|
341
742
|
<h3>v0.1</h3>
|
|
342
743
|
<ul>
|
|
@@ -346,72 +747,677 @@ planner: { kind: "webllm", modelId: "Llama-3.2-1B-Instruct-q4f16_1-MLC" }</code>
|
|
|
346
747
|
<li>Human-approved mode</li>
|
|
347
748
|
</ul>
|
|
348
749
|
|
|
349
|
-
<h3>v0.2 <span class="badge">
|
|
750
|
+
<h3>v0.2 <span class="badge stable">stable</span></h3>
|
|
350
751
|
<ul>
|
|
351
752
|
<li>New actions: <code>scroll</code>, <code>focus</code></li>
|
|
352
753
|
<li>Improved heuristic planner with regex goal patterns</li>
|
|
353
|
-
<li>Better page observation (visibility filtering,
|
|
754
|
+
<li>Better page observation (visibility filtering, up to 60 candidates)</li>
|
|
354
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>
|
|
355
769
|
</ul>
|
|
356
770
|
|
|
357
771
|
<h3>v0.3</h3>
|
|
358
772
|
<ul>
|
|
359
|
-
<li>
|
|
360
|
-
<li>
|
|
361
|
-
<li>
|
|
362
|
-
<li>
|
|
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>
|
|
363
777
|
</ul>
|
|
364
778
|
|
|
365
779
|
<h3>v1.0</h3>
|
|
366
780
|
<ul>
|
|
367
|
-
<li>
|
|
368
|
-
<li>
|
|
369
|
-
<li>
|
|
370
|
-
<li>
|
|
371
|
-
<li>Validation/eval harness with benchmark tasks</li>
|
|
372
|
-
<li>Cross-browser packaging (Chromium + Firefox)</li>
|
|
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>
|
|
373
785
|
</ul>
|
|
374
786
|
</div>
|
|
375
787
|
</div>
|
|
376
788
|
</section>
|
|
377
789
|
|
|
378
|
-
<!-- CONTACT -->
|
|
790
|
+
<!-- ── CONTACT ── -->
|
|
379
791
|
<section id="contact" class="section">
|
|
380
792
|
<div class="wrap">
|
|
381
|
-
<div class="
|
|
793
|
+
<div class="section-label fade-up">contact</div>
|
|
794
|
+
<div class="surface fade-up delay-1">
|
|
382
795
|
<h2>Contact</h2>
|
|
383
796
|
<p>Maintainer: Akshay Chame</p>
|
|
384
797
|
<ul>
|
|
385
|
-
<li>
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
</li>
|
|
389
|
-
<li>
|
|
390
|
-
GitHub:
|
|
391
|
-
<a href="https://github.com/akshayram1" target="_blank" rel="noreferrer">@akshayram1</a>
|
|
392
|
-
</li>
|
|
393
|
-
<li>
|
|
394
|
-
Package:
|
|
395
|
-
<a
|
|
396
|
-
href="https://www.npmjs.com/package/@akshayram1/omnibrowser-agent"
|
|
397
|
-
target="_blank"
|
|
398
|
-
rel="noreferrer"
|
|
399
|
-
>@akshayram1/omnibrowser-agent</a
|
|
400
|
-
>
|
|
401
|
-
</li>
|
|
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>
|
|
402
801
|
</ul>
|
|
403
|
-
<p class="contact-note">
|
|
404
|
-
|
|
405
|
-
|
|
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>
|
|
406
809
|
</div>
|
|
407
810
|
</div>
|
|
408
811
|
</section>
|
|
812
|
+
|
|
409
813
|
</main>
|
|
410
814
|
|
|
411
815
|
<footer class="footer">
|
|
412
816
|
<div class="wrap">
|
|
413
|
-
<p>© 2026 OmniBrowser Agent · MIT License</p>
|
|
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>
|
|
414
818
|
</div>
|
|
415
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 sections = NAV.filter(n => n.id).map(n => `${n.id}`).join(', ');
|
|
1160
|
+
const prompt = [
|
|
1161
|
+
'You are a docs navigation assistant. Given the user query, decide which page section to navigate to.',
|
|
1162
|
+
`Available sections: ${sections}`,
|
|
1163
|
+
'Reply with ONLY a JSON object containing: "section", "reply", and "reasoning". Use a "#section-id" string for "section", or null if nothing matches.',
|
|
1164
|
+
'The "reasoning" field must be a concise user-visible summary, not hidden chain-of-thought.',
|
|
1165
|
+
`User query: ${input.goal}`
|
|
1166
|
+
].join('\n');
|
|
1167
|
+
|
|
1168
|
+
const resp = await loadedEngine.chat.completions.create({
|
|
1169
|
+
messages: [
|
|
1170
|
+
{ role: 'system', content: 'Return only one JSON object. No markdown or extra prose.' },
|
|
1171
|
+
{ role: 'user', content: prompt }
|
|
1172
|
+
],
|
|
1173
|
+
temperature: 0,
|
|
1174
|
+
max_tokens: 160
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
try {
|
|
1178
|
+
const raw = contentToText(resp.choices?.[0]?.message?.content ?? '').trim();
|
|
1179
|
+
const jsonText = extractFirstJsonObject(raw);
|
|
1180
|
+
if (!jsonText) {
|
|
1181
|
+
return { type: 'done', reason: 'Could not parse model response.' };
|
|
1182
|
+
}
|
|
1183
|
+
const parsed = JSON.parse(jsonText);
|
|
1184
|
+
if (parsed.section) {
|
|
1185
|
+
return {
|
|
1186
|
+
type: 'scroll',
|
|
1187
|
+
selector: parsed.section,
|
|
1188
|
+
_reply: parsed.reply,
|
|
1189
|
+
_reasoning: typeof parsed.reasoning === 'string' ? parsed.reasoning : ''
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
return {
|
|
1193
|
+
type: 'done',
|
|
1194
|
+
reason: parsed.reply || 'No matching section.',
|
|
1195
|
+
_reasoning: typeof parsed.reasoning === 'string' ? parsed.reasoning : ''
|
|
1196
|
+
};
|
|
1197
|
+
} catch {
|
|
1198
|
+
return { type: 'done', reason: 'Could not parse model response.' };
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1203
|
+
progressWrap.classList.remove('visible');
|
|
1204
|
+
loadBtn.textContent = 'Reload';
|
|
1205
|
+
loadBtn.classList.add('loaded');
|
|
1206
|
+
loadBtn.disabled = false;
|
|
1207
|
+
modelSelect.disabled = false;
|
|
1208
|
+
setStatus('ready', 'AI ready');
|
|
1209
|
+
addBotMsg('✅ Model loaded! I can now answer questions about the docs and show a brief <strong>reasoning summary</strong>.');
|
|
1210
|
+
} catch (err) {
|
|
1211
|
+
loadedEngine = null;
|
|
1212
|
+
progressWrap.classList.remove('visible');
|
|
1213
|
+
loadBtn.disabled = false;
|
|
1214
|
+
modelSelect.disabled = false;
|
|
1215
|
+
setStatus('idle', 'heuristic');
|
|
1216
|
+
addBotMsg('⚠️ Model failed to load — falling back to heuristic mode. Check WebGPU support.');
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function setStatus(dotClass, label) {
|
|
1221
|
+
statusDot.className = `cb-sdot${dotClass === 'idle' ? '' : ' ' + dotClass}`;
|
|
1222
|
+
statusText.textContent = label;
|
|
1223
|
+
statusPill.className = `cb-status-pill${dotClass === 'ready' ? ' ready' : dotClass === 'loading' ? ' loading' : ''}`;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/* ── Toggle ── */
|
|
1227
|
+
toggleBtn.addEventListener('click', () => {
|
|
1228
|
+
isOpen = !isOpen;
|
|
1229
|
+
panel.classList.toggle('visible', isOpen);
|
|
1230
|
+
toggleBtn.classList.toggle('open', isOpen);
|
|
1231
|
+
toggleBtn.textContent = isOpen ? '✕' : '💬';
|
|
1232
|
+
if (isOpen) {
|
|
1233
|
+
if (messagesEl.children.length === 0) {
|
|
1234
|
+
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.');
|
|
1235
|
+
}
|
|
1236
|
+
inputEl.focus();
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
/* ── Suggestions ── */
|
|
1241
|
+
suggsEl.querySelectorAll('.cb-chip').forEach(chip => {
|
|
1242
|
+
chip.addEventListener('click', () => handleQuery(chip.dataset.q));
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
/* ── Send ── */
|
|
1246
|
+
sendBtn.addEventListener('click', send);
|
|
1247
|
+
inputEl.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } });
|
|
1248
|
+
|
|
1249
|
+
function send() {
|
|
1250
|
+
const q = inputEl.value.trim();
|
|
1251
|
+
if (!q) return;
|
|
1252
|
+
addUserMsg(q);
|
|
1253
|
+
inputEl.value = '';
|
|
1254
|
+
handleQuery(q);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/* ── Greetings — answer instantly, no model needed ── */
|
|
1258
|
+
const GREETINGS = /^(hi|hey|hello|sup|yo|hiya|howdy|what's up|whats up|greetings)[!?.]*$/i;
|
|
1259
|
+
const GREETING_REPLIES = [
|
|
1260
|
+
'Hey! Ask me about any section — <strong>install</strong>, <strong>architecture</strong>, <strong>embedding</strong>, <strong>examples</strong>…',
|
|
1261
|
+
'Hi there! What part of the docs can I take you to?',
|
|
1262
|
+
'Hello! I can navigate to any section. What are you looking for?',
|
|
1263
|
+
];
|
|
1264
|
+
let greetIdx = 0;
|
|
1265
|
+
|
|
1266
|
+
function withTimeout(promise, timeoutMs = 30000) {
|
|
1267
|
+
let timeoutId;
|
|
1268
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1269
|
+
timeoutId = setTimeout(() => reject(new Error('planner_timeout')), timeoutMs);
|
|
1270
|
+
});
|
|
1271
|
+
return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/* ── Route query ── */
|
|
1275
|
+
async function handleQuery(q) {
|
|
1276
|
+
sendBtn.disabled = true;
|
|
1277
|
+
inputEl.disabled = true;
|
|
1278
|
+
|
|
1279
|
+
/* 1. Instant greeting reply — never hits the model */
|
|
1280
|
+
if (GREETINGS.test(q.trim())) {
|
|
1281
|
+
addBotMsg(GREETING_REPLIES[greetIdx % GREETING_REPLIES.length]);
|
|
1282
|
+
greetIdx++;
|
|
1283
|
+
sendBtn.disabled = false;
|
|
1284
|
+
inputEl.disabled = false;
|
|
1285
|
+
inputEl.focus();
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/* 2. AI-first routing — when model is loaded, always route via LLM */
|
|
1290
|
+
if (loadedEngine && window.__browserAgentWebLLM) {
|
|
1291
|
+
setStatus('loading', 'AI thinking');
|
|
1292
|
+
const typingEl = addTyping();
|
|
1293
|
+
try {
|
|
1294
|
+
const result = await withTimeout(window.__browserAgentWebLLM.plan({ goal: q }), getPlannerTimeoutMs());
|
|
1295
|
+
typingEl.remove();
|
|
1296
|
+
if (result._reasoning) addBotThought(result._reasoning);
|
|
1297
|
+
const replyText = result._reply || (result.reason ?? 'Done.');
|
|
1298
|
+
addBotMsg(replyText);
|
|
1299
|
+
const targetId = result.selector || null;
|
|
1300
|
+
if (targetId) {
|
|
1301
|
+
const target = document.querySelector(targetId);
|
|
1302
|
+
if (target) setTimeout(() => target.scrollIntoView({ behavior: 'smooth', block: 'start' }), 200);
|
|
1303
|
+
}
|
|
1304
|
+
} catch (err) {
|
|
1305
|
+
typingEl.remove();
|
|
1306
|
+
const kwFallback = NAV.find(n => n.match.test(q));
|
|
1307
|
+
if (kwFallback) {
|
|
1308
|
+
addBotThought('Using the keyword fallback because the model was slow.');
|
|
1309
|
+
addBotMsg(kwFallback.reply);
|
|
1310
|
+
if (kwFallback.href) {
|
|
1311
|
+
setTimeout(() => window.open(kwFallback.href, '_blank', 'noopener'), 400);
|
|
1312
|
+
} else {
|
|
1313
|
+
const target = document.querySelector(kwFallback.id);
|
|
1314
|
+
if (target) setTimeout(() => target.scrollIntoView({ behavior: 'smooth', block: 'start' }), 200);
|
|
1315
|
+
}
|
|
1316
|
+
} else {
|
|
1317
|
+
const isTimeout = err instanceof Error && err.message === 'planner_timeout';
|
|
1318
|
+
addBotMsg(
|
|
1319
|
+
isTimeout
|
|
1320
|
+
? '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.'
|
|
1321
|
+
: '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>.'
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
} finally {
|
|
1325
|
+
setStatus('ready', 'AI ready');
|
|
1326
|
+
}
|
|
1327
|
+
} else {
|
|
1328
|
+
const kwMatch = NAV.find(n => n.match.test(q));
|
|
1329
|
+
if (kwMatch) {
|
|
1330
|
+
addBotMsg(kwMatch.reply);
|
|
1331
|
+
if (kwMatch.href) {
|
|
1332
|
+
setTimeout(() => window.open(kwMatch.href, '_blank', 'noopener'), 400);
|
|
1333
|
+
} else {
|
|
1334
|
+
const target = document.querySelector(kwMatch.id);
|
|
1335
|
+
if (target) setTimeout(() => target.scrollIntoView({ behavior: 'smooth', block: 'start' }), 200);
|
|
1336
|
+
}
|
|
1337
|
+
} else {
|
|
1338
|
+
addBotMsg(FALLBACK_REPLIES[fallbackIdx % FALLBACK_REPLIES.length]);
|
|
1339
|
+
fallbackIdx++;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
sendBtn.disabled = false;
|
|
1344
|
+
inputEl.disabled = false;
|
|
1345
|
+
inputEl.focus();
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/* ── Helpers ── */
|
|
1349
|
+
function addBotMsg(html) {
|
|
1350
|
+
const msg = document.createElement('div');
|
|
1351
|
+
msg.className = 'cb-msg bot';
|
|
1352
|
+
msg.innerHTML = `<div class="cb-bubble">${html}</div>`;
|
|
1353
|
+
messagesEl.appendChild(msg);
|
|
1354
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function addBotThought(text) {
|
|
1358
|
+
const msg = document.createElement('div');
|
|
1359
|
+
msg.className = 'cb-msg bot thought';
|
|
1360
|
+
msg.innerHTML = `<div class="cb-bubble">💭 ${text}</div>`;
|
|
1361
|
+
messagesEl.appendChild(msg);
|
|
1362
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function addUserMsg(text) {
|
|
1366
|
+
const msg = document.createElement('div');
|
|
1367
|
+
msg.className = 'cb-msg user';
|
|
1368
|
+
msg.innerHTML = `<div class="cb-bubble">${text}</div>`;
|
|
1369
|
+
messagesEl.appendChild(msg);
|
|
1370
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
function addTyping() {
|
|
1374
|
+
const el = document.createElement('div');
|
|
1375
|
+
el.className = 'cb-typing';
|
|
1376
|
+
el.innerHTML = `<div class="cb-typing-dots"><div class="cb-tdot"></div><div class="cb-tdot"></div><div class="cb-tdot"></div></div>`;
|
|
1377
|
+
messagesEl.appendChild(el);
|
|
1378
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1379
|
+
return el;
|
|
1380
|
+
}
|
|
1381
|
+
</script>
|
|
1382
|
+
|
|
1383
|
+
<script>
|
|
1384
|
+
function switchSnippet(id) {
|
|
1385
|
+
document.querySelectorAll('.snippet').forEach(s => s.classList.remove('active'));
|
|
1386
|
+
document.querySelectorAll('.stab').forEach(t => t.classList.remove('stab-active'));
|
|
1387
|
+
document.getElementById('sn-' + id).classList.add('active');
|
|
1388
|
+
event.target.classList.add('stab-active');
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function copyInstall() {
|
|
1392
|
+
const txt = document.getElementById('install-cmd-text').textContent;
|
|
1393
|
+
navigator.clipboard.writeText(txt).then(() => {
|
|
1394
|
+
const lbl = document.getElementById('copy-label');
|
|
1395
|
+
lbl.textContent = 'copied!';
|
|
1396
|
+
setTimeout(() => lbl.textContent = 'copy', 1800);
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
function switchPlanner(mode) {
|
|
1401
|
+
document.getElementById('pm-h').classList.toggle('active', mode === 'h');
|
|
1402
|
+
document.getElementById('pm-w').classList.toggle('active', mode === 'w');
|
|
1403
|
+
document.querySelectorAll('.ptab').forEach((t, i) => {
|
|
1404
|
+
t.classList.remove('active-h', 'active-w');
|
|
1405
|
+
if (i === 0 && mode === 'h') t.classList.add('active-h');
|
|
1406
|
+
if (i === 1 && mode === 'w') t.classList.add('active-w');
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
const observer = new IntersectionObserver((entries) => {
|
|
1411
|
+
entries.forEach(e => {
|
|
1412
|
+
if (e.isIntersecting) e.target.style.animationPlayState = 'running';
|
|
1413
|
+
});
|
|
1414
|
+
}, { threshold: 0.08 });
|
|
1415
|
+
|
|
1416
|
+
document.querySelectorAll('.fade-up').forEach(el => {
|
|
1417
|
+
el.style.animationPlayState = 'paused';
|
|
1418
|
+
observer.observe(el);
|
|
1419
|
+
});
|
|
1420
|
+
</script>
|
|
1421
|
+
|
|
416
1422
|
</body>
|
|
417
1423
|
</html>
|