@unnoodle/browser 0.1.0
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 +403 -0
- package/dist/index.cjs +2639 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +517 -0
- package/dist/index.d.ts +517 -0
- package/dist/index.js +2639 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
package/README.md
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
# @unnoodle/browser
|
|
2
|
+
|
|
3
|
+
> Embed AI context assistance into any web application.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@unnoodle/browser)
|
|
6
|
+
[](https://bundlephobia.com/package/@unnoodle/browser)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## What is it?
|
|
12
|
+
|
|
13
|
+
`@unnoodle/browser` is a lightweight, framework-agnostic SDK that adds a floating AI assistant button to your web application. It automatically collects runtime context (current route, page title, environment) and sends it to the Unnoodle API to surface intelligent suggestions.
|
|
14
|
+
|
|
15
|
+
- Zero React / framework dependency
|
|
16
|
+
- Shadow DOM — completely isolated from your app's CSS
|
|
17
|
+
- < 50kb minified
|
|
18
|
+
- Works with Next.js, React, Vue, Svelte, or plain HTML
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @unnoodle/browser
|
|
26
|
+
# or
|
|
27
|
+
pnpm add @unnoodle/browser
|
|
28
|
+
# or
|
|
29
|
+
yarn add @unnoodle/browser
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { Unnoodle } from "@unnoodle/browser";
|
|
38
|
+
|
|
39
|
+
Unnoodle.init({
|
|
40
|
+
apiKey: process.env.NEXT_PUBLIC_UNNOODLE_API_KEY,
|
|
41
|
+
context: "engineering",
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
That's it. A floating button appears in the bottom-right corner. Click it or press **⌘U** (macOS) / **Ctrl+U** (Windows/Linux) to open the panel.
|
|
46
|
+
|
|
47
|
+
> **Where is my API key?** Log in to your workspace → Settings → API Key.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Full Configuration
|
|
52
|
+
|
|
53
|
+
A starter config file is included in the package as `unnoodle.config.ts`. Copy it into your project root and fill in your values.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { Unnoodle } from "@unnoodle/browser";
|
|
57
|
+
|
|
58
|
+
Unnoodle.init({
|
|
59
|
+
// Required: workspace API key (Settings → API Key in your workspace)
|
|
60
|
+
apiKey: process.env.NEXT_PUBLIC_UNNOODLE_API_KEY,
|
|
61
|
+
|
|
62
|
+
// Required: context slug to load — created automatically if new
|
|
63
|
+
context: "engineering",
|
|
64
|
+
|
|
65
|
+
// Optional: target environment (default: "prod")
|
|
66
|
+
environment: "dev",
|
|
67
|
+
|
|
68
|
+
// Optional: override API base URL (default: https://api.unnoodle.com)
|
|
69
|
+
baseUrl: "http://localhost:8080",
|
|
70
|
+
|
|
71
|
+
// Optional: provide an auth token at request time
|
|
72
|
+
getAuthToken: () => localStorage.getItem("auth_token"),
|
|
73
|
+
|
|
74
|
+
// Optional: theme customization
|
|
75
|
+
theme: {
|
|
76
|
+
primaryColor: "#6366f1",
|
|
77
|
+
borderRadius: "12px",
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// Optional: attach additional metadata to every request
|
|
81
|
+
customMetadata: () => ({
|
|
82
|
+
userId: window.__APP_USER_ID,
|
|
83
|
+
plan: "enterprise",
|
|
84
|
+
}),
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Programmatic Control
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { Unnoodle } from "@unnoodle/browser";
|
|
94
|
+
|
|
95
|
+
// Open the panel manually (e.g. from your own help button)
|
|
96
|
+
Unnoodle.open();
|
|
97
|
+
|
|
98
|
+
// Close the panel
|
|
99
|
+
Unnoodle.close();
|
|
100
|
+
|
|
101
|
+
// Remove the widget entirely (useful in SPAs)
|
|
102
|
+
Unnoodle.destroy();
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The individual named exports (`openUnnoodle`, `closeUnnoodle`, `destroyUnnoodle`) are still available for tree-shaking.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Framework Examples
|
|
110
|
+
|
|
111
|
+
### React / Next.js (App Router)
|
|
112
|
+
|
|
113
|
+
Root layouts in Next.js App Router are **Server Components** and run on the
|
|
114
|
+
server where `document` does not exist. You must call `initUnnoodle()` from a
|
|
115
|
+
separate `"use client"` component rendered inside the layout body.
|
|
116
|
+
|
|
117
|
+
**Step 1 — install the package**
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npm install @unnoodle/browser
|
|
121
|
+
# or
|
|
122
|
+
pnpm add @unnoodle/browser
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Step 2 — create a thin client component** (e.g. `components/UnnoodleInit.tsx`)
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
"use client";
|
|
129
|
+
import { useEffect } from "react";
|
|
130
|
+
import { initUnnoodle } from "@unnoodle/browser";
|
|
131
|
+
|
|
132
|
+
export function UnnoodleInit({
|
|
133
|
+
apiKey,
|
|
134
|
+
context,
|
|
135
|
+
}: {
|
|
136
|
+
apiKey: string;
|
|
137
|
+
context: string;
|
|
138
|
+
}) {
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
initUnnoodle({ apiKey, context });
|
|
141
|
+
}, [apiKey, context]);
|
|
142
|
+
|
|
143
|
+
return null; // renders nothing — side-effect only
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Step 3 — render it inside your layout body**
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
// app/layout.tsx — stays a Server Component, no "use client" needed here
|
|
151
|
+
import { UnnoodleInit } from "../components/UnnoodleInit";
|
|
152
|
+
|
|
153
|
+
export default function RootLayout({ children }) {
|
|
154
|
+
return (
|
|
155
|
+
<html>
|
|
156
|
+
<body>
|
|
157
|
+
<UnnoodleInit
|
|
158
|
+
apiKey={process.env.NEXT_PUBLIC_UNNOODLE_API_KEY!}
|
|
159
|
+
context="engineering"
|
|
160
|
+
/>
|
|
161
|
+
{children}
|
|
162
|
+
</body>
|
|
163
|
+
</html>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
> **Why not `"use client"` on the layout directly?**
|
|
169
|
+
> Next.js App Router layouts that render `<html>` and `<head>` must be Server
|
|
170
|
+
> Components. Adding `"use client"` would cause a build error. The thin wrapper
|
|
171
|
+
> component is the standard pattern recommended by the Next.js team.
|
|
172
|
+
|
|
173
|
+
### React / Next.js (Pages Router)
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
// pages/_app.tsx
|
|
177
|
+
import { useEffect } from "react";
|
|
178
|
+
import { Unnoodle } from "@unnoodle/browser";
|
|
179
|
+
|
|
180
|
+
export default function App({ Component, pageProps }) {
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
Unnoodle.init({
|
|
183
|
+
apiKey: process.env.NEXT_PUBLIC_UNNOODLE_API_KEY,
|
|
184
|
+
context: "engineering",
|
|
185
|
+
});
|
|
186
|
+
}, []);
|
|
187
|
+
|
|
188
|
+
return <Component {...pageProps} />;
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Vue 3
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
// main.ts
|
|
196
|
+
import { createApp } from "vue";
|
|
197
|
+
import App from "./App.vue";
|
|
198
|
+
import { Unnoodle } from "@unnoodle/browser";
|
|
199
|
+
|
|
200
|
+
const app = createApp(App);
|
|
201
|
+
app.mount("#app");
|
|
202
|
+
|
|
203
|
+
Unnoodle.init({
|
|
204
|
+
apiKey: process.env.VITE_UNNOODLE_API_KEY,
|
|
205
|
+
context: "engineering",
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Plain HTML / CDN
|
|
210
|
+
|
|
211
|
+
```html
|
|
212
|
+
<script type="module">
|
|
213
|
+
import { Unnoodle } from "https://cdn.unnoodle.com/browser/latest/index.js";
|
|
214
|
+
Unnoodle.init({
|
|
215
|
+
apiKey: "pk_live_xxxxx",
|
|
216
|
+
context: "engineering",
|
|
217
|
+
});
|
|
218
|
+
</script>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Plugin System (Advanced)
|
|
224
|
+
|
|
225
|
+
The SDK includes an internal plugin interface for advanced use cases. Plugins can intercept the context collection, response rendering, and error handling lifecycle.
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { initUnnoodle, type UnnoodlePlugin } from "@unnoodle/browser";
|
|
229
|
+
|
|
230
|
+
const analyticsPlugin: UnnoodlePlugin = {
|
|
231
|
+
name: "analytics",
|
|
232
|
+
onContextCollected: (ctx) => ({
|
|
233
|
+
...ctx,
|
|
234
|
+
sessionId: getSessionId(),
|
|
235
|
+
}),
|
|
236
|
+
onResponse: (response) => {
|
|
237
|
+
trackEvent("unnoodle_response", { hasActions: !!response.actions?.length });
|
|
238
|
+
},
|
|
239
|
+
onError: (err) => {
|
|
240
|
+
reportError(err);
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
Unnoodle.init({
|
|
245
|
+
apiKey: process.env.NEXT_PUBLIC_UNNOODLE_API_KEY,
|
|
246
|
+
context: "engineering",
|
|
247
|
+
_plugins: [analyticsPlugin],
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
> Note: `_plugins` is prefixed with `_` to indicate it's an advanced internal API. It may be promoted to a stable API in a future major version.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## API Reference
|
|
256
|
+
|
|
257
|
+
### `Unnoodle.init(config: UnnoodleConfig): void`
|
|
258
|
+
|
|
259
|
+
Initializes and mounts the widget. Safe to call in SSR environments — no-ops if `document` is unavailable.
|
|
260
|
+
|
|
261
|
+
Also available as the named export `initUnnoodle()` for tree-shaking.
|
|
262
|
+
|
|
263
|
+
### `openUnnoodle(): void`
|
|
264
|
+
|
|
265
|
+
Programmatically opens the panel.
|
|
266
|
+
|
|
267
|
+
### `closeUnnoodle(): void`
|
|
268
|
+
|
|
269
|
+
Programmatically closes the panel.
|
|
270
|
+
|
|
271
|
+
### `destroyUnnoodle(): void`
|
|
272
|
+
|
|
273
|
+
Tears down the widget, removes DOM elements, and cleans up all event listeners.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## `UnnoodleConfig` Interface
|
|
278
|
+
|
|
279
|
+
| Property | Type | Required | Default | Description |
|
|
280
|
+
| -------------------- | ------------------------------- | -------- | -------------------------- | --------------------------------------------------------------- |
|
|
281
|
+
| `apiKey` | `string` | ✅ | — | Workspace API key (`pk_live_…`). Get it from Settings → API Key |
|
|
282
|
+
| `context` | `string` | ✅ | — | Context slug to load (auto-created if new) |
|
|
283
|
+
| `environment` | `"dev" \| "staging" \| "prod"` | | `"prod"` | Target environment |
|
|
284
|
+
| `baseUrl` | `string` | | `https://api.unnoodle.com` | API base URL override |
|
|
285
|
+
| `getAuthToken` | `() => string \| null` | | — | Returns current user's auth token |
|
|
286
|
+
| `theme.primaryColor` | `string` | | `#6366f1` | Button and panel accent color |
|
|
287
|
+
| `theme.borderRadius` | `string` | | `12px` | Panel corner radius |
|
|
288
|
+
| `customMetadata` | `() => Record<string, unknown>` | | — | Additional context metadata |
|
|
289
|
+
| `_plugins` | `UnnoodlePlugin[]` | | — | Internal plugin hooks |
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Security
|
|
294
|
+
|
|
295
|
+
- Auth tokens are resolved at request time and never stored
|
|
296
|
+
- All API communication uses HTTPS
|
|
297
|
+
- No secrets are bundled in the SDK
|
|
298
|
+
- Shadow DOM prevents CSS bleed-in and bleed-out
|
|
299
|
+
- The SDK fails silently on misconfiguration — it will never crash the host app
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Product Memory — Developer API (V2)
|
|
304
|
+
|
|
305
|
+
The V2 Product Memory sub-system ships as part of the package and is exported for advanced SDK consumers. These modules work in any browser environment and have no React/framework dependency.
|
|
306
|
+
|
|
307
|
+
### `StateWatcher`
|
|
308
|
+
|
|
309
|
+
Attaches a `MutationObserver` to any DOM element and fires a typed `StateDiff` callback when meaningful changes are detected.
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
import { StateWatcher, type StateDiff } from "@unnoodle/browser";
|
|
313
|
+
|
|
314
|
+
const watcher = new StateWatcher();
|
|
315
|
+
const btn = document.getElementById("submit-btn")!;
|
|
316
|
+
|
|
317
|
+
watcher.watch(btn, (diff: StateDiff) => {
|
|
318
|
+
console.log(diff.type); // "text" | "attribute" | "structure"
|
|
319
|
+
console.log(diff.before); // value before the change
|
|
320
|
+
console.log(diff.after); // value after the change
|
|
321
|
+
console.log(diff.attribute); // attribute name (only when type === "attribute")
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Clean up
|
|
325
|
+
watcher.unwatch();
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**Behaviour:**
|
|
329
|
+
|
|
330
|
+
- Changes are debounced for 500 ms — rapid mutations produce a single callback.
|
|
331
|
+
- `style` attribute mutations are filtered out (noise filter).
|
|
332
|
+
- Text changes (`textContent` assignments) are classified as `type: "text"`.
|
|
333
|
+
- Attribute changes (class, disabled, aria-\*) are classified as `type: "attribute"`.
|
|
334
|
+
- Child element additions/removals are classified as `type: "structure"`.
|
|
335
|
+
- `unwatch()` cancels any pending callback and disconnects the observer.
|
|
336
|
+
|
|
337
|
+
### `inferMeaning(diff, el)`
|
|
338
|
+
|
|
339
|
+
Rule-based semantic label inference from a `StateDiff`.
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
import { StateWatcher, inferMeaning } from "@unnoodle/browser";
|
|
343
|
+
|
|
344
|
+
const watcher = new StateWatcher();
|
|
345
|
+
|
|
346
|
+
watcher.watch(document.getElementById("submit-btn")!, (diff) => {
|
|
347
|
+
const meaning = inferMeaning(diff, document.getElementById("submit-btn")!);
|
|
348
|
+
// "Loading state" | "Error state" | "Disabled state" | … | null
|
|
349
|
+
if (meaning) {
|
|
350
|
+
console.log("Detected state:", meaning);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
**Inferred labels:**
|
|
356
|
+
|
|
357
|
+
| Trigger | Label |
|
|
358
|
+
| --------------------------------------------------- | ----------------------------- |
|
|
359
|
+
| Text contains "loading" / "submitting" / "saving" | `"Loading state"` |
|
|
360
|
+
| Text contains "error" / "failed" / "invalid" | `"Error state"` |
|
|
361
|
+
| Text contains "success" / "saved" / "done" | `"Success state"` |
|
|
362
|
+
| Class `"loading"` / `"spinner"` / `"pending"` added | `"Loading state"` |
|
|
363
|
+
| Class `"disabled"` / `"inactive"` added | `"Disabled state"` |
|
|
364
|
+
| Class `"error"` / `"invalid"` / `"danger"` added | `"Error state"` |
|
|
365
|
+
| Class `"active"` / `"selected"` / `"open"` added | `"Active state"` |
|
|
366
|
+
| Class `"collapsed"` / `"hidden"` added | `"Collapsed state"` |
|
|
367
|
+
| `disabled` attribute added | `"Disabled state"` |
|
|
368
|
+
| `aria-expanded` → `"true"` | `"Expanded state"` |
|
|
369
|
+
| `aria-expanded` → `"false"` | `"Collapsed state"` |
|
|
370
|
+
| `aria-busy` → `"true"` | `"Loading state"` |
|
|
371
|
+
| `aria-label` changed | `"Label changed to: <value>"` |
|
|
372
|
+
| Structure change or unknown | `null` |
|
|
373
|
+
|
|
374
|
+
### `LiveRenderer`
|
|
375
|
+
|
|
376
|
+
Highlights a live DOM element with a fixed-position ring (useful for showing which element a stored ProductMemory state belongs to).
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
import { LiveRenderer } from "@unnoodle/browser";
|
|
380
|
+
|
|
381
|
+
const renderer = new LiveRenderer();
|
|
382
|
+
|
|
383
|
+
// Highlight an element
|
|
384
|
+
renderer.show(document.getElementById("submit-btn")!);
|
|
385
|
+
// → scrolls element into view + draws violet highlight ring
|
|
386
|
+
|
|
387
|
+
// Remove highlight
|
|
388
|
+
renderer.hide();
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Calling `hide()` before `show()` is safe. Calling `show()` twice replaces the previous ring.
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Changelog
|
|
396
|
+
|
|
397
|
+
See [CHANGELOG.md](./CHANGELOG.md) for version history.
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## License
|
|
402
|
+
|
|
403
|
+
MIT © [Unnoodle](https://unnoodle.com)
|