@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 ADDED
@@ -0,0 +1,403 @@
1
+ # @unnoodle/browser
2
+
3
+ > Embed AI context assistance into any web application.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@unnoodle/browser)](https://www.npmjs.com/package/@unnoodle/browser)
6
+ [![Bundle Size](https://img.shields.io/bundlephobia/minzip/@unnoodle/browser)](https://bundlephobia.com/package/@unnoodle/browser)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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)