@netbirdio/explain 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.
Files changed (57) hide show
  1. package/README.md +402 -0
  2. package/dist/client/AIAssistantProvider.d.ts +21 -0
  3. package/dist/client/AIAssistantProvider.d.ts.map +1 -0
  4. package/dist/client/AIAssistantProvider.js +198 -0
  5. package/dist/client/AIAssistantProvider.js.map +1 -0
  6. package/dist/client/AIChatBot.d.ts +10 -0
  7. package/dist/client/AIChatBot.d.ts.map +1 -0
  8. package/dist/client/AIChatBot.js +139 -0
  9. package/dist/client/AIChatBot.js.map +1 -0
  10. package/dist/client/AIFloatingButton.d.ts +7 -0
  11. package/dist/client/AIFloatingButton.d.ts.map +1 -0
  12. package/dist/client/AIFloatingButton.js +16 -0
  13. package/dist/client/AIFloatingButton.js.map +1 -0
  14. package/dist/client/index.d.ts +5 -0
  15. package/dist/client/index.d.ts.map +1 -0
  16. package/dist/client/index.js +4 -0
  17. package/dist/client/index.js.map +1 -0
  18. package/dist/client/styles.d.ts +51 -0
  19. package/dist/client/styles.d.ts.map +1 -0
  20. package/dist/client/styles.js +317 -0
  21. package/dist/client/styles.js.map +1 -0
  22. package/dist/server/handler.d.ts +38 -0
  23. package/dist/server/handler.d.ts.map +1 -0
  24. package/dist/server/handler.js +117 -0
  25. package/dist/server/handler.js.map +1 -0
  26. package/dist/server/index.d.ts +6 -0
  27. package/dist/server/index.d.ts.map +1 -0
  28. package/dist/server/index.js +4 -0
  29. package/dist/server/index.js.map +1 -0
  30. package/dist/server/providers/anthropic.d.ts +9 -0
  31. package/dist/server/providers/anthropic.d.ts.map +1 -0
  32. package/dist/server/providers/anthropic.js +40 -0
  33. package/dist/server/providers/anthropic.js.map +1 -0
  34. package/dist/server/providers/openai.d.ts +9 -0
  35. package/dist/server/providers/openai.d.ts.map +1 -0
  36. package/dist/server/providers/openai.js +46 -0
  37. package/dist/server/providers/openai.js.map +1 -0
  38. package/dist/server/providers/types.d.ts +9 -0
  39. package/dist/server/providers/types.d.ts.map +1 -0
  40. package/dist/server/providers/types.js +2 -0
  41. package/dist/server/providers/types.js.map +1 -0
  42. package/dist/types.d.ts +11 -0
  43. package/dist/types.d.ts.map +1 -0
  44. package/dist/types.js +2 -0
  45. package/dist/types.js.map +1 -0
  46. package/package.json +45 -0
  47. package/src/client/AIAssistantProvider.tsx +288 -0
  48. package/src/client/AIChatBot.tsx +309 -0
  49. package/src/client/AIFloatingButton.tsx +34 -0
  50. package/src/client/index.ts +4 -0
  51. package/src/client/styles.ts +353 -0
  52. package/src/server/handler.ts +158 -0
  53. package/src/server/index.ts +5 -0
  54. package/src/server/providers/anthropic.ts +53 -0
  55. package/src/server/providers/openai.ts +55 -0
  56. package/src/server/providers/types.ts +10 -0
  57. package/src/types.ts +11 -0
package/README.md ADDED
@@ -0,0 +1,402 @@
1
+ # netbird-explain
2
+
3
+ AI-powered "Explain" assistant for React apps. Users click on UI elements and get contextual AI explanations via a chat panel.
4
+
5
+ The package has two entry points:
6
+
7
+ - `netbird-explain/client` — React components (provider, chat panel, floating button)
8
+ - `netbird-explain/server` — Node.js handler that proxies requests to Anthropic or OpenAI
9
+
10
+ No CSS framework required — the package is fully self-contained with inline styles and CSS custom properties.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install netbird-explain
16
+ ```
17
+
18
+ Peer dependencies: `react >=18`, `react-dom >=18`.
19
+
20
+ For local development as a workspace package, add it to your `package.json`:
21
+
22
+ ```json
23
+ {
24
+ "dependencies": {
25
+ "netbird-explain": "file:./packages/netbird-explain"
26
+ }
27
+ }
28
+ ```
29
+
30
+ If you're using Next.js, add to `next.config.js`:
31
+
32
+ ```js
33
+ module.exports = {
34
+ transpilePackages: ["netbird-explain"],
35
+ };
36
+ ```
37
+
38
+ ---
39
+
40
+ ## Client
41
+
42
+ ### Setup
43
+
44
+ Wrap your app with `AIAssistantProvider`:
45
+
46
+ ```tsx
47
+ import { AIAssistantProvider } from "netbird-explain/client";
48
+
49
+ export default function App({ children }) {
50
+ return (
51
+ <AIAssistantProvider
52
+ endpoint="http://localhost:3080/api/ai/chat"
53
+ apiKey="your-api-key"
54
+ >
55
+ {children}
56
+ </AIAssistantProvider>
57
+ );
58
+ }
59
+ ```
60
+
61
+ This renders the floating action button and chat panel automatically. No CSS imports are needed — the provider injects all required styles via CSS custom properties.
62
+
63
+ ### Props
64
+
65
+ | Prop | Type | Required | Description |
66
+ | ---------- | ----------- | -------- | ----------------------------------- |
67
+ | `endpoint` | `string` | Yes | URL of the AI chat API |
68
+ | `apiKey` | `string` | No | Bearer token sent with each request |
69
+ | `children` | `ReactNode` | Yes | Your application |
70
+
71
+ ### Marking elements as explainable
72
+
73
+ Add the `data-nb-explain` attribute to any element you want users to be able to click on in explain mode:
74
+
75
+ ```tsx
76
+ <div data-nb-explain>
77
+ <Label>Name</Label>
78
+ <Input value={name} onChange={setName} />
79
+ </div>
80
+ ```
81
+
82
+ When clicked, the library extracts a label from the element (first `<label>`, heading, or text content) and sends it as the query.
83
+
84
+ You can also pass a custom label directly:
85
+
86
+ ```tsx
87
+ <div data-nb-explain="Database connection string">...</div>
88
+ ```
89
+
90
+ ### Documentation URLs
91
+
92
+ Attach docs to an element or its parent so the AI can reference them:
93
+
94
+ ```tsx
95
+ <div data-nb-explain-docs='["https://docs.example.com/resources"]'>
96
+ <div data-nb-explain>...</div>
97
+ </div>
98
+ ```
99
+
100
+ ### Excluding elements
101
+
102
+ Use `data-nb-explain-ignore` to prevent an element from being selectable in explain mode:
103
+
104
+ ```tsx
105
+ <button data-nb-explain-ignore onClick={enterExplainMode}>
106
+ Explain
107
+ </button>
108
+ ```
109
+
110
+ ### Setting page/modal context
111
+
112
+ Use the `useAIAssistant` hook to provide context that gets included in every query:
113
+
114
+ ```tsx
115
+ import { useAIAssistant } from "netbird-explain/client";
116
+
117
+ function MyModal() {
118
+ const { setExplainContext, clearExplainContext } = useAIAssistant();
119
+
120
+ useEffect(() => {
121
+ setExplainContext({
122
+ modalName: "Add Resource",
123
+ pageName: "Networks",
124
+ docsUrls: ["https://docs.example.com/networks"],
125
+ });
126
+ return () => clearExplainContext();
127
+ }, []);
128
+
129
+ return <div data-nb-explain>...</div>;
130
+ }
131
+ ```
132
+
133
+ This produces queries like: `Explain "Name" on Add Resource modal in Networks`.
134
+
135
+ ### Hook API
136
+
137
+ `useAIAssistant()` returns:
138
+
139
+ | Method / Property | Description |
140
+ | ------------------------ | ------------------------------------------ |
141
+ | `openChat(query?)` | Open the chat panel, optionally with a query |
142
+ | `closeChat()` | Close the chat panel |
143
+ | `isChatOpen` | Whether the chat panel is open |
144
+ | `explainMode` | Whether explain mode is active |
145
+ | `enterExplainMode()` | Activate explain mode (click-to-explain) |
146
+ | `exitExplainMode()` | Deactivate explain mode |
147
+ | `setExplainContext(ctx)` | Set page/modal context for queries |
148
+ | `clearExplainContext()` | Clear the context |
149
+
150
+ ### Theming
151
+
152
+ The package ships with a dark theme out of the box. All visual properties are controlled via CSS custom properties (`--nb-explain-*`) injected into `:root` by the provider. Override any of them in your own CSS to match your app's look and feel.
153
+
154
+ #### How it works
155
+
156
+ 1. `AIAssistantProvider` injects a `<style>` tag with default values for all `--nb-explain-*` variables.
157
+ 2. Components use inline styles that reference these variables (e.g., `background: var(--nb-explain-bg)`).
158
+ 3. Your CSS can override any variable — later declarations on `:root` or more specific selectors win.
159
+
160
+ #### Overriding in CSS
161
+
162
+ Add a stylesheet or `<style>` block **after** the provider mounts (or use higher specificity):
163
+
164
+ ```css
165
+ :root {
166
+ /* Change to a light theme */
167
+ --nb-explain-bg: #ffffff;
168
+ --nb-explain-bg-subtle: rgba(0, 0, 0, 0.04);
169
+ --nb-explain-bg-hover: rgba(0, 0, 0, 0.06);
170
+ --nb-explain-border: rgba(0, 0, 0, 0.12);
171
+ --nb-explain-text: #1a1a1a;
172
+ --nb-explain-text-muted: #6b7280;
173
+ --nb-explain-text-dim: #9ca3af;
174
+ --nb-explain-accent: #2563eb;
175
+ --nb-explain-accent-hover: #3b82f6;
176
+ --nb-explain-user-bg: #2563eb;
177
+ --nb-explain-user-text: #ffffff;
178
+ }
179
+ ```
180
+
181
+ #### Full variable reference
182
+
183
+ | Variable | Default | Description |
184
+ | --------------------- | -------------------------------- | ---------------------------------- |
185
+ | `--nb-explain-bg` | `#0a0a0f` | Chat panel background |
186
+ | `--nb-explain-bg-subtle` | `rgba(255,255,255,0.06)` | Input field & assistant message bg |
187
+ | `--nb-explain-bg-hover` | `rgba(255,255,255,0.08)` | Hover state background |
188
+ | `--nb-explain-border` | `rgba(255,255,255,0.1)` | Border color |
189
+ | `--nb-explain-text` | `#f0f0f5` | Primary text color |
190
+ | `--nb-explain-text-muted` | `#9ca3af` | Secondary text color |
191
+ | `--nb-explain-text-dim` | `#6b7280` | Placeholder / tertiary text |
192
+ | `--nb-explain-accent` | `#eab308` | Accent color (buttons, icons) |
193
+ | `--nb-explain-accent-hover` | `#facc15` | Accent hover state |
194
+ | `--nb-explain-accent-glow` | `rgba(234,179,8,0.15)` | Accent glow (avatar backgrounds) |
195
+ | `--nb-explain-user-bg` | `#4f46e5` | User message bubble background |
196
+ | `--nb-explain-user-text` | `#ffffff` | User message text color |
197
+ | `--nb-explain-user-glow` | `rgba(79,70,229,0.25)` | User avatar glow |
198
+ | `--nb-explain-radius` | `12px` | Panel border radius |
199
+ | `--nb-explain-radius-sm` | `8px` | Message bubble border radius |
200
+ | `--nb-explain-radius-xs` | `6px` | Button border radius |
201
+ | `--nb-explain-font` | system font stack | Font family for all components |
202
+ | `--nb-explain-shadow` | large drop shadow | Chat panel box shadow |
203
+ | `--nb-explain-banner-bg` | `rgba(234,179,8,0.92)` | Explain mode banner background |
204
+ | `--nb-explain-banner-text` | `#000000` | Explain mode banner text |
205
+ | `--nb-explain-error-text` | `#f87171` | Error message text |
206
+
207
+ ### Data attributes
208
+
209
+ | Attribute | Description |
210
+ | -------------------- | ------------------------------------------------------------------ |
211
+ | `data-nb-explain` | Marks element as explainable. Value can be a custom label or boolean. |
212
+ | `data-nb-explain-docs` | JSON array of documentation URLs for context. |
213
+ | `data-nb-explain-ignore` | Element is non-interactive during explain mode. |
214
+
215
+ ---
216
+
217
+ ## Server
218
+
219
+ The server module provides a framework-agnostic handler that proxies chat requests to Anthropic or OpenAI.
220
+
221
+ ### With Express
222
+
223
+ ```ts
224
+ import express from "express";
225
+ import { createAssistant } from "netbird-explain/server";
226
+
227
+ const assistant = createAssistant({
228
+ provider: "anthropic",
229
+ apiKey: process.env.ANTHROPIC_API_KEY!,
230
+ model: "claude-sonnet-4-20250514",
231
+ systemPrompt: "You are a helpful assistant for MyApp.",
232
+ });
233
+
234
+ const app = express();
235
+ app.use(express.json());
236
+
237
+ app.post("/api/ai/chat", assistant.handler({ apiKey: "your-api-key" }));
238
+
239
+ app.listen(3080);
240
+ ```
241
+
242
+ ### With plain Node.js HTTP
243
+
244
+ ```ts
245
+ import http from "http";
246
+ import { createAssistant } from "netbird-explain/server";
247
+
248
+ const assistant = createAssistant({
249
+ provider: "openai",
250
+ apiKey: process.env.OPENAI_API_KEY!,
251
+ model: "gpt-4o",
252
+ });
253
+
254
+ const handle = assistant.handler({ apiKey: "your-api-key" });
255
+
256
+ http.createServer(handle).listen(3080);
257
+ ```
258
+
259
+ ### Programmatic usage (no HTTP)
260
+
261
+ ```ts
262
+ const assistant = createAssistant({
263
+ provider: "anthropic",
264
+ apiKey: process.env.ANTHROPIC_API_KEY!,
265
+ });
266
+
267
+ const { reply } = await assistant.chat({
268
+ messages: [{ role: "user", content: "What is a network resource?" }],
269
+ });
270
+ ```
271
+
272
+ ### `createAssistant(config)`
273
+
274
+ | Option | Type | Required | Default |
275
+ | -------------- | ----------------------------- | -------- | ------------------------ |
276
+ | `provider` | `"anthropic"` \| `"openai"` | Yes | — |
277
+ | `apiKey` | `string` | Yes | — |
278
+ | `model` | `string` | No | Provider default |
279
+ | `systemPrompt` | `string` | No | Generic assistant prompt |
280
+
281
+ ### `assistant.handler(opts?)`
282
+
283
+ Returns a `(req, res) => Promise<void>` handler compatible with Express, plain `http`, and similar frameworks.
284
+
285
+ | Option | Type | Description |
286
+ | -------- | -------- | -------------------------------------------------------------- |
287
+ | `apiKey` | `string` | If set, requires `Authorization: Bearer <key>` on requests |
288
+
289
+ ### API contract
290
+
291
+ **Request** `POST /api/ai/chat`
292
+
293
+ ```json
294
+ {
295
+ "messages": [
296
+ { "role": "context", "content": "Docs: https://..." },
297
+ { "role": "user", "content": "Explain network routes" }
298
+ ]
299
+ }
300
+ ```
301
+
302
+ **Response**
303
+
304
+ ```json
305
+ {
306
+ "reply": "Network routes allow you to..."
307
+ }
308
+ ```
309
+
310
+ **Error codes:** `400` (bad request), `401` (unauthorized), `502` (LLM error).
311
+
312
+ ---
313
+
314
+ ## Standalone dev server
315
+
316
+ The `server/` directory in the dashboard repo contains a ready-to-run Express server for local development.
317
+
318
+ ```bash
319
+ cd server
320
+ cp .env .env.local # edit with your LLM API key
321
+ npm install
322
+ node index.js
323
+ ```
324
+
325
+ Environment variables:
326
+
327
+ | Variable | Default | Description |
328
+ | ------------------- | -------------------------- | ---------------------- |
329
+ | `PORT` | `3080` | Server port |
330
+ | `API_KEY` | `nb-ai-dev-key-change-me` | Bearer token for auth |
331
+ | `LLM_PROVIDER` | `anthropic` | `anthropic` or `openai` |
332
+ | `ANTHROPIC_API_KEY` | — | Anthropic API key |
333
+ | `ANTHROPIC_MODEL` | `claude-sonnet-4-20250514` | Model ID |
334
+ | `OPENAI_API_KEY` | — | OpenAI API key |
335
+ | `OPENAI_MODEL` | `gpt-4o` | Model ID |
336
+ | `SYSTEM_PROMPT` | Generic NetBird prompt | System prompt for the LLM |
337
+
338
+ ---
339
+
340
+ ## Full integration example
341
+
342
+ ```tsx
343
+ // layout.tsx — wrap app with provider
344
+ import { AIAssistantProvider } from "netbird-explain/client";
345
+
346
+ export default function Layout({ children }) {
347
+ return (
348
+ <AIAssistantProvider
349
+ endpoint={process.env.NEXT_PUBLIC_AI_SERVER_URL || "http://localhost:3080/api/ai/chat"}
350
+ apiKey={process.env.NEXT_PUBLIC_AI_API_KEY || "nb-ai-dev-key-change-me"}
351
+ >
352
+ {children}
353
+ </AIAssistantProvider>
354
+ );
355
+ }
356
+ ```
357
+
358
+ ```tsx
359
+ // MyModal.tsx — add explain support to a modal
360
+ import { useAIAssistant } from "netbird-explain/client";
361
+ import { Sparkles } from "lucide-react";
362
+
363
+ function MyModal() {
364
+ const { setExplainContext, clearExplainContext, explainMode, enterExplainMode, exitExplainMode } =
365
+ useAIAssistant();
366
+
367
+ useEffect(() => {
368
+ setExplainContext({
369
+ modalName: "Add Resource",
370
+ pageName: "Networks",
371
+ docsUrls: ["https://docs.netbird.io/manage/networks"],
372
+ });
373
+ return () => clearExplainContext();
374
+ }, []);
375
+
376
+ return (
377
+ <div data-nb-explain>
378
+ <button
379
+ data-nb-explain-ignore
380
+ onClick={() => (explainMode ? exitExplainMode() : enterExplainMode())}
381
+ >
382
+ <Sparkles size={13} />
383
+ {explainMode ? "Click an element..." : "Explain"}
384
+ </button>
385
+
386
+ <div data-nb-explain>
387
+ <label>Name</label>
388
+ <input placeholder="e.g., Postgres Database" />
389
+ </div>
390
+
391
+ <div data-nb-explain>
392
+ <label>Address</label>
393
+ <input placeholder="e.g., 10.0.0.1" />
394
+ </div>
395
+ </div>
396
+ );
397
+ }
398
+ ```
399
+
400
+ ## License
401
+
402
+ BSD-3-Clause
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+ import type { ExplainContext } from "../types";
3
+ type AIAssistantContextType = {
4
+ openChat: (selectedText?: string) => void;
5
+ closeChat: () => void;
6
+ isChatOpen: boolean;
7
+ explainMode: boolean;
8
+ enterExplainMode: () => void;
9
+ exitExplainMode: () => void;
10
+ setExplainContext: (ctx: ExplainContext) => void;
11
+ clearExplainContext: () => void;
12
+ };
13
+ export declare const useAIAssistant: () => AIAssistantContextType;
14
+ type AIAssistantProviderProps = {
15
+ endpoint: string;
16
+ apiKey?: string;
17
+ children: React.ReactNode;
18
+ };
19
+ export default function AIAssistantProvider({ endpoint, apiKey, children, }: AIAssistantProviderProps): import("react/jsx-runtime").JSX.Element;
20
+ export {};
21
+ //# sourceMappingURL=AIAssistantProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AIAssistantProvider.d.ts","sourceRoot":"","sources":["../../src/client/AIAssistantProvider.tsx"],"names":[],"mappings":"AAEA,OAAO,KAMN,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAK/C,KAAK,sBAAsB,GAAG;IAC5B,QAAQ,EAAE,CAAC,YAAY,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;IACrB,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B,eAAe,EAAE,MAAM,IAAI,CAAC;IAC5B,iBAAiB,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,IAAI,CAAC;IACjD,mBAAmB,EAAE,MAAM,IAAI,CAAC;CACjC,CAAC;AAaF,eAAO,MAAM,cAAc,8BAAuC,CAAC;AAEnE,KAAK,wBAAwB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,CAAC;AAyEF,MAAM,CAAC,OAAO,UAAU,mBAAmB,CAAC,EAC1C,QAAQ,EACR,MAAM,EACN,QAAQ,GACT,EAAE,wBAAwB,2CAwK1B"}
@@ -0,0 +1,198 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { createContext, useCallback, useContext, useEffect, useState, } from "react";
4
+ import AIChatBot from "./AIChatBot";
5
+ import AIFloatingButton from "./AIFloatingButton";
6
+ import * as S from "./styles";
7
+ const AIAssistantContext = createContext({
8
+ openChat: () => { },
9
+ closeChat: () => { },
10
+ isChatOpen: false,
11
+ explainMode: false,
12
+ enterExplainMode: () => { },
13
+ exitExplainMode: () => { },
14
+ setExplainContext: () => { },
15
+ clearExplainContext: () => { },
16
+ });
17
+ export const useAIAssistant = () => useContext(AIAssistantContext);
18
+ /**
19
+ * Find the closest ancestor (or self) with a data-nb-explain attribute.
20
+ * Returns null if nothing is explainable.
21
+ */
22
+ function findExplainable(el) {
23
+ return el.closest("[data-nb-explain]");
24
+ }
25
+ /**
26
+ * Extract a short label from an explainable element by looking for
27
+ * a label, heading, or first bit of text content.
28
+ */
29
+ function extractLabel(el) {
30
+ const label = el.querySelector("label");
31
+ if (label?.innerText?.trim())
32
+ return label.innerText.trim();
33
+ const heading = el.querySelector("h1, h2, h3, h4");
34
+ if (heading?.innerText?.trim())
35
+ return heading.innerText.trim();
36
+ const text = el.innerText?.trim();
37
+ if (text && text.length <= 80)
38
+ return text;
39
+ if (text)
40
+ return text.slice(0, 80) + "...";
41
+ return "this element";
42
+ }
43
+ /**
44
+ * Look for data-nb-explain-docs on the element or its ancestors.
45
+ * Returns an array of documentation URLs, or an empty array.
46
+ */
47
+ function findExplainDocs(el) {
48
+ const withDocs = el.closest("[data-nb-explain-docs]");
49
+ if (!withDocs)
50
+ return [];
51
+ const raw = withDocs.getAttribute("data-nb-explain-docs") || "";
52
+ try {
53
+ const parsed = JSON.parse(raw);
54
+ if (Array.isArray(parsed))
55
+ return parsed;
56
+ }
57
+ catch {
58
+ // If not valid JSON, treat as a single URL
59
+ if (raw.trim())
60
+ return [raw.trim()];
61
+ }
62
+ return [];
63
+ }
64
+ function buildQuery(label, ctx, elementDocs) {
65
+ let userMessage = `Explain "${label}"`;
66
+ if (ctx) {
67
+ if (ctx.modalName)
68
+ userMessage += ` on ${ctx.modalName} modal`;
69
+ if (ctx.pageName)
70
+ userMessage += ` in ${ctx.pageName}`;
71
+ }
72
+ // Merge docs from context and element attribute
73
+ const allDocs = [
74
+ ...(ctx?.docsUrls || []),
75
+ ...elementDocs,
76
+ ];
77
+ // Deduplicate
78
+ const uniqueDocs = [...new Set(allDocs)];
79
+ const parts = [userMessage];
80
+ if (uniqueDocs.length > 0) {
81
+ parts.push(`Docs: ${uniqueDocs.join(", ")}`);
82
+ }
83
+ return parts.join("\n");
84
+ }
85
+ export default function AIAssistantProvider({ endpoint, apiKey, children, }) {
86
+ const [isChatOpen, setIsChatOpen] = useState(false);
87
+ const [initialQuery, setInitialQuery] = useState("");
88
+ const [explainMode, setExplainMode] = useState(false);
89
+ const [hoveredEl, setHoveredEl] = useState(null);
90
+ const [explainCtx, setExplainCtx] = useState(null);
91
+ const openChat = useCallback((selectedText) => {
92
+ setInitialQuery(selectedText || "");
93
+ setIsChatOpen(true);
94
+ setExplainMode(false);
95
+ setHoveredEl(null);
96
+ }, []);
97
+ const closeChat = useCallback(() => {
98
+ setIsChatOpen(false);
99
+ setInitialQuery("");
100
+ }, []);
101
+ const enterExplainMode = useCallback(() => {
102
+ setExplainMode(true);
103
+ }, []);
104
+ const exitExplainMode = useCallback(() => {
105
+ setExplainMode(false);
106
+ setHoveredEl(null);
107
+ }, []);
108
+ const setExplainContext = useCallback((ctx) => {
109
+ setExplainCtx(ctx);
110
+ }, []);
111
+ const clearExplainContext = useCallback(() => {
112
+ setExplainCtx(null);
113
+ }, []);
114
+ // Explain mode: highlight explainable elements on hover, open chat on click
115
+ useEffect(() => {
116
+ if (!explainMode)
117
+ return;
118
+ const handleMouseOver = (e) => {
119
+ const target = e.target;
120
+ if (target.closest("[data-nb-explain-ignore]") ||
121
+ target.closest("[data-nb-explain-banner]"))
122
+ return;
123
+ const explainable = findExplainable(target);
124
+ setHoveredEl(explainable);
125
+ };
126
+ const handleMouseOut = () => {
127
+ setHoveredEl(null);
128
+ };
129
+ const handleClick = (e) => {
130
+ const target = e.target;
131
+ if (target.closest("[data-nb-explain-ignore]") ||
132
+ target.closest("[data-nb-explain-banner]"))
133
+ return;
134
+ e.preventDefault();
135
+ e.stopPropagation();
136
+ const explainable = findExplainable(target);
137
+ if (!explainable)
138
+ return;
139
+ const attrValue = explainable.getAttribute("data-nb-explain") || "";
140
+ const label = attrValue && attrValue !== "true"
141
+ ? attrValue
142
+ : extractLabel(explainable);
143
+ const elementDocs = findExplainDocs(explainable);
144
+ const query = buildQuery(label, explainCtx, elementDocs);
145
+ openChat(query);
146
+ };
147
+ const handleKeyDown = (e) => {
148
+ if (e.key === "Escape") {
149
+ exitExplainMode();
150
+ }
151
+ };
152
+ document.addEventListener("mouseover", handleMouseOver, true);
153
+ document.addEventListener("mouseout", handleMouseOut, true);
154
+ document.addEventListener("click", handleClick, true);
155
+ document.addEventListener("keydown", handleKeyDown);
156
+ return () => {
157
+ document.removeEventListener("mouseover", handleMouseOver, true);
158
+ document.removeEventListener("mouseout", handleMouseOut, true);
159
+ document.removeEventListener("click", handleClick, true);
160
+ document.removeEventListener("keydown", handleKeyDown);
161
+ };
162
+ }, [explainMode, explainCtx, openChat, exitExplainMode]);
163
+ // Apply/remove highlight on hovered explainable element via CSS class
164
+ useEffect(() => {
165
+ if (!hoveredEl)
166
+ return;
167
+ hoveredEl.setAttribute("data-nb-explain-highlight", "");
168
+ return () => {
169
+ hoveredEl.removeAttribute("data-nb-explain-highlight");
170
+ };
171
+ }, [hoveredEl]);
172
+ // Force-remove all highlights when leaving explain mode
173
+ useEffect(() => {
174
+ if (!explainMode) {
175
+ document.querySelectorAll("[data-nb-explain-highlight]").forEach((el) => {
176
+ el.removeAttribute("data-nb-explain-highlight");
177
+ });
178
+ }
179
+ }, [explainMode]);
180
+ return (_jsxs(AIAssistantContext.Provider, { value: {
181
+ openChat,
182
+ closeChat,
183
+ isChatOpen,
184
+ explainMode,
185
+ enterExplainMode,
186
+ exitExplainMode,
187
+ setExplainContext,
188
+ clearExplainContext,
189
+ }, children: [_jsx("style", { children: S.CSS_VARS + S.ANIMATIONS + S.HIGHLIGHT_STYLES }), children, explainMode && (_jsxs("div", { "data-nb-explain-banner": true, style: S.banner, children: [_jsx("span", { children: "Click on a highlighted element to explain it" }), _jsx("button", { onClick: () => exitExplainMode(), style: S.bannerCancel, children: "Cancel" })] })), _jsx(AIFloatingButton, { isOpen: isChatOpen, onClick: () => {
190
+ if (isChatOpen) {
191
+ closeChat();
192
+ }
193
+ else {
194
+ openChat();
195
+ }
196
+ } }), _jsx(AIChatBot, { open: isChatOpen, onClose: closeChat, initialQuery: initialQuery, endpoint: endpoint, apiKey: apiKey })] }));
197
+ }
198
+ //# sourceMappingURL=AIAssistantProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AIAssistantProvider.js","sourceRoot":"","sources":["../../src/client/AIAssistantProvider.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ type Props = {
2
+ open: boolean;
3
+ onClose: () => void;
4
+ initialQuery: string;
5
+ endpoint: string;
6
+ apiKey?: string;
7
+ };
8
+ export default function AIChatBot({ open, onClose, initialQuery, endpoint, apiKey, }: Props): import("react/jsx-runtime").JSX.Element | null;
9
+ export {};
10
+ //# sourceMappingURL=AIChatBot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AIChatBot.d.ts","sourceRoot":"","sources":["../../src/client/AIChatBot.tsx"],"names":[],"mappings":"AAcA,KAAK,KAAK,GAAG;IACX,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAkCF,MAAM,CAAC,OAAO,UAAU,SAAS,CAAC,EAChC,IAAI,EACJ,OAAO,EACP,YAAY,EACZ,QAAQ,EACR,MAAM,GACP,EAAE,KAAK,kDAwPP"}