@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
@@ -0,0 +1,117 @@
1
+ import { AnthropicProvider } from "./providers/anthropic";
2
+ import { OpenAIProvider } from "./providers/openai";
3
+ function getHeader(req, name) {
4
+ if (typeof req.headers === "object" && req.headers !== null) {
5
+ if ("get" in req.headers && typeof req.headers.get === "function") {
6
+ return req.headers.get(name);
7
+ }
8
+ const headers = req.headers;
9
+ const value = headers[name] ?? headers[name.toLowerCase()];
10
+ if (Array.isArray(value))
11
+ return value[0] ?? null;
12
+ return value ?? null;
13
+ }
14
+ return null;
15
+ }
16
+ function sendJson(res, statusCode, data) {
17
+ if (typeof res.status === "function" && typeof res.json === "function") {
18
+ const chained = res.status(statusCode);
19
+ if (chained && typeof chained.json === "function") {
20
+ chained.json(data);
21
+ return;
22
+ }
23
+ }
24
+ if (typeof res.writeHead === "function" && typeof res.end === "function") {
25
+ res.writeHead(statusCode, { "Content-Type": "application/json" });
26
+ res.end(JSON.stringify(data));
27
+ return;
28
+ }
29
+ if (typeof res.end === "function") {
30
+ if (res.statusCode !== undefined)
31
+ res.statusCode = statusCode;
32
+ res.end(JSON.stringify(data));
33
+ }
34
+ }
35
+ async function parseBody(req) {
36
+ if (req.body !== undefined && req.body !== null) {
37
+ return req.body;
38
+ }
39
+ if (typeof req.on === "function") {
40
+ const onFn = req.on.bind(req);
41
+ return new Promise((resolve, reject) => {
42
+ const chunks = [];
43
+ onFn("data", (chunk) => chunks.push(String(chunk)));
44
+ onFn("end", () => {
45
+ try {
46
+ resolve(JSON.parse(chunks.join("")));
47
+ }
48
+ catch {
49
+ reject(new Error("Invalid JSON body"));
50
+ }
51
+ });
52
+ onFn("error", reject);
53
+ });
54
+ }
55
+ throw new Error("Unable to parse request body");
56
+ }
57
+ function createProvider(config) {
58
+ switch (config.provider) {
59
+ case "anthropic":
60
+ return new AnthropicProvider({ apiKey: config.apiKey, model: config.model });
61
+ case "openai":
62
+ return new OpenAIProvider({ apiKey: config.apiKey, model: config.model });
63
+ default:
64
+ throw new Error(`Unknown provider: ${config.provider}`);
65
+ }
66
+ }
67
+ export function createAssistant(config) {
68
+ const provider = createProvider(config);
69
+ async function chat(req) {
70
+ if (!req.messages || !Array.isArray(req.messages) || req.messages.length === 0) {
71
+ throw new Error("messages array is required and must not be empty");
72
+ }
73
+ const reply = await provider.chat(req.messages, config.systemPrompt);
74
+ return { reply };
75
+ }
76
+ function handler(opts) {
77
+ return async (req, res) => {
78
+ try {
79
+ if (opts?.apiKey) {
80
+ const authHeader = getHeader(req, "Authorization") || getHeader(req, "authorization");
81
+ const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
82
+ if (token !== opts.apiKey) {
83
+ sendJson(res, 401, { error: "Unauthorized" });
84
+ return;
85
+ }
86
+ }
87
+ let body;
88
+ try {
89
+ body = await parseBody(req);
90
+ }
91
+ catch {
92
+ sendJson(res, 400, { error: "Invalid request body" });
93
+ return;
94
+ }
95
+ const { messages } = body;
96
+ if (!messages || !Array.isArray(messages) || messages.length === 0) {
97
+ sendJson(res, 400, { error: "messages array is required and must not be empty" });
98
+ return;
99
+ }
100
+ try {
101
+ const result = await chat({ messages });
102
+ sendJson(res, 200, result);
103
+ }
104
+ catch (err) {
105
+ const message = err instanceof Error ? err.message : "LLM request failed";
106
+ sendJson(res, 502, { error: message });
107
+ }
108
+ }
109
+ catch (err) {
110
+ const message = err instanceof Error ? err.message : "Internal server error";
111
+ sendJson(res, 500, { error: message });
112
+ }
113
+ };
114
+ }
115
+ return { chat, handler };
116
+ }
117
+ //# sourceMappingURL=handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.js","sourceRoot":"","sources":["../../src/server/handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export { createAssistant } from "./handler";
2
+ export type { AssistantConfig } from "./handler";
3
+ export { AnthropicProvider } from "./providers/anthropic";
4
+ export { OpenAIProvider } from "./providers/openai";
5
+ export type { LLMProvider, ProviderConfig } from "./providers/types";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5C,YAAY,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { createAssistant } from "./handler";
2
+ export { AnthropicProvider } from "./providers/anthropic";
3
+ export { OpenAIProvider } from "./providers/openai";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { Message } from "../../types";
2
+ import type { LLMProvider, ProviderConfig } from "./types";
3
+ export declare class AnthropicProvider implements LLMProvider {
4
+ private apiKey;
5
+ private model;
6
+ constructor(config: ProviderConfig);
7
+ chat(messages: Message[], systemPrompt?: string): Promise<string>;
8
+ }
9
+ //# sourceMappingURL=anthropic.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anthropic.d.ts","sourceRoot":"","sources":["../../../src/server/providers/anthropic.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE3D,qBAAa,iBAAkB,YAAW,WAAW;IACnD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAS;gBAEV,MAAM,EAAE,cAAc;IAK5B,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAwCxE"}
@@ -0,0 +1,40 @@
1
+ export class AnthropicProvider {
2
+ constructor(config) {
3
+ this.apiKey = config.apiKey;
4
+ this.model = config.model || "claude-sonnet-4-20250514";
5
+ }
6
+ async chat(messages, systemPrompt) {
7
+ const anthropicMessages = messages.map((m) => ({
8
+ role: m.role === "context" || m.role === "system" ? "user" : m.role,
9
+ content: m.role === "context" ? `[Context]: ${m.content}` : m.content,
10
+ }));
11
+ const body = {
12
+ model: this.model,
13
+ max_tokens: 4096,
14
+ messages: anthropicMessages,
15
+ };
16
+ if (systemPrompt) {
17
+ body.system = systemPrompt;
18
+ }
19
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
20
+ method: "POST",
21
+ headers: {
22
+ "Content-Type": "application/json",
23
+ "x-api-key": this.apiKey,
24
+ "anthropic-version": "2023-06-01",
25
+ },
26
+ body: JSON.stringify(body),
27
+ });
28
+ if (!response.ok) {
29
+ const text = await response.text();
30
+ throw new Error(`Anthropic API error ${response.status}: ${text}`);
31
+ }
32
+ const data = await response.json();
33
+ const textBlock = data.content?.find((block) => block.type === "text");
34
+ if (!textBlock) {
35
+ throw new Error("No text content in Anthropic response");
36
+ }
37
+ return textBlock.text;
38
+ }
39
+ }
40
+ //# sourceMappingURL=anthropic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anthropic.js","sourceRoot":"","sources":["../../../src/server/providers/anthropic.ts"],"names":[],"mappings":"AAGA,MAAM,OAAO,iBAAiB;IAI5B,YAAY,MAAsB;QAChC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,0BAA0B,CAAC;IAC1D,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,QAAmB,EAAE,YAAqB;QACnD,MAAM,iBAAiB,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC7C,IAAI,EAAE,CAAC,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAE,MAAgB,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI;YAC9E,OAAO,EAAE,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO;SACtE,CAAC,CAAC,CAAC;QAEJ,MAAM,IAAI,GAA4B;YACpC,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,UAAU,EAAE,IAAI;YAChB,QAAQ,EAAE,iBAAiB;SAC5B,CAAC;QAEF,IAAI,YAAY,EAAE,CAAC;YACjB,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC;QAC7B,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,uCAAuC,EAAE;YACpE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,WAAW,EAAE,IAAI,CAAC,MAAM;gBACxB,mBAAmB,EAAE,YAAY;aAClC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;SAC3B,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;YACnC,MAAM,IAAI,KAAK,CAAC,uBAAuB,QAAQ,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC,CAAC;QACrE,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,EAAE,IAAI,CAClC,CAAC,KAAuB,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,CACnD,CAAC;QACF,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;QAC3D,CAAC;QACD,OAAO,SAAS,CAAC,IAAI,CAAC;IACxB,CAAC;CACF"}
@@ -0,0 +1,9 @@
1
+ import type { Message } from "../../types";
2
+ import type { LLMProvider, ProviderConfig } from "./types";
3
+ export declare class OpenAIProvider implements LLMProvider {
4
+ private apiKey;
5
+ private model;
6
+ constructor(config: ProviderConfig);
7
+ chat(messages: Message[], systemPrompt?: string): Promise<string>;
8
+ }
9
+ //# sourceMappingURL=openai.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai.d.ts","sourceRoot":"","sources":["../../../src/server/providers/openai.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE3D,qBAAa,cAAe,YAAW,WAAW;IAChD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAS;gBAEV,MAAM,EAAE,cAAc;IAK5B,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CA0CxE"}
@@ -0,0 +1,46 @@
1
+ export class OpenAIProvider {
2
+ constructor(config) {
3
+ this.apiKey = config.apiKey;
4
+ this.model = config.model || "gpt-4o";
5
+ }
6
+ async chat(messages, systemPrompt) {
7
+ const openaiMessages = [];
8
+ if (systemPrompt) {
9
+ openaiMessages.push({ role: "system", content: systemPrompt });
10
+ }
11
+ for (const m of messages) {
12
+ if (m.role === "context") {
13
+ openaiMessages.push({ role: "user", content: `[Context]: ${m.content}` });
14
+ }
15
+ else if (m.role === "system") {
16
+ openaiMessages.push({ role: "user", content: m.content });
17
+ }
18
+ else {
19
+ openaiMessages.push({ role: m.role, content: m.content });
20
+ }
21
+ }
22
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
23
+ method: "POST",
24
+ headers: {
25
+ "Content-Type": "application/json",
26
+ Authorization: `Bearer ${this.apiKey}`,
27
+ },
28
+ body: JSON.stringify({
29
+ model: this.model,
30
+ messages: openaiMessages,
31
+ max_tokens: 4096,
32
+ }),
33
+ });
34
+ if (!response.ok) {
35
+ const text = await response.text();
36
+ throw new Error(`OpenAI API error ${response.status}: ${text}`);
37
+ }
38
+ const data = await response.json();
39
+ const choice = data.choices?.[0];
40
+ if (!choice?.message?.content) {
41
+ throw new Error("No content in OpenAI response");
42
+ }
43
+ return choice.message.content;
44
+ }
45
+ }
46
+ //# sourceMappingURL=openai.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai.js","sourceRoot":"","sources":["../../../src/server/providers/openai.ts"],"names":[],"mappings":"AAGA,MAAM,OAAO,cAAc;IAIzB,YAAY,MAAsB;QAChC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,QAAQ,CAAC;IACxC,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,QAAmB,EAAE,YAAqB;QACnD,MAAM,cAAc,GAA6C,EAAE,CAAC;QAEpE,IAAI,YAAY,EAAE,CAAC;YACjB,cAAc,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,CAAC;QACjE,CAAC;QAED,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;YACzB,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;gBACzB,cAAc,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;YAC5E,CAAC;iBAAM,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAC/B,cAAc,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;YAC5D,CAAC;iBAAM,CAAC;gBACN,cAAc,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;YAC5D,CAAC;QACH,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,4CAA4C,EAAE;YACzE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,aAAa,EAAE,UAAU,IAAI,CAAC,MAAM,EAAE;aACvC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,QAAQ,EAAE,cAAc;gBACxB,UAAU,EAAE,IAAI;aACjB,CAAC;SACH,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;YACnC,MAAM,IAAI,KAAK,CAAC,oBAAoB,QAAQ,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC,CAAC;QAClE,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;QACjC,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;QACnD,CAAC;QACD,OAAO,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;IAChC,CAAC;CACF"}
@@ -0,0 +1,9 @@
1
+ import type { Message } from "../../types";
2
+ export interface LLMProvider {
3
+ chat(messages: Message[], systemPrompt?: string): Promise<string>;
4
+ }
5
+ export type ProviderConfig = {
6
+ apiKey: string;
7
+ model?: string;
8
+ };
9
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/server/providers/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACnE;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/server/providers/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,11 @@
1
+ export type Message = {
2
+ id?: string;
3
+ role: "user" | "assistant" | "context" | "system";
4
+ content: string;
5
+ };
6
+ export type ExplainContext = {
7
+ modalName?: string;
8
+ pageName?: string;
9
+ docsUrls?: string[];
10
+ };
11
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,OAAO,GAAG;IACpB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;IAClD,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@netbirdio/explain",
3
+ "version": "0.1.0",
4
+ "description": "Full-stack AI assistant library with React frontend components and Node.js backend handler",
5
+ "license": "BSD-3-Clause",
6
+ "main": "./dist/client/index.js",
7
+ "types": "./dist/client/index.d.ts",
8
+ "exports": {
9
+ "./client": {
10
+ "types": "./dist/client/index.d.ts",
11
+ "default": "./dist/client/index.js"
12
+ },
13
+ "./server": {
14
+ "types": "./dist/server/index.d.ts",
15
+ "default": "./dist/server/index.js"
16
+ }
17
+ },
18
+ "typesVersions": {
19
+ "*": {
20
+ "client": ["./dist/client/index.d.ts"],
21
+ "server": ["./dist/server/index.d.ts"]
22
+ }
23
+ },
24
+ "files": ["dist", "src"],
25
+ "scripts": {
26
+ "build": "tsc",
27
+ "typecheck": "tsc --noEmit"
28
+ },
29
+ "peerDependencies": {
30
+ "react": ">=18.0.0",
31
+ "react-dom": ">=18.0.0"
32
+ },
33
+ "dependencies": {
34
+ "lucide-react": ">=0.300.0"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/netbirdio/explain.git"
39
+ },
40
+ "devDependencies": {
41
+ "@types/react": "^18.0.0",
42
+ "@types/react-dom": "^18.0.0",
43
+ "typescript": "^5.0.0"
44
+ }
45
+ }
@@ -0,0 +1,288 @@
1
+ "use client";
2
+
3
+ import React, {
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useState,
9
+ } from "react";
10
+ import type { ExplainContext } from "../types";
11
+ import AIChatBot from "./AIChatBot";
12
+ import AIFloatingButton from "./AIFloatingButton";
13
+ import * as S from "./styles";
14
+
15
+ type AIAssistantContextType = {
16
+ openChat: (selectedText?: string) => void;
17
+ closeChat: () => void;
18
+ isChatOpen: boolean;
19
+ explainMode: boolean;
20
+ enterExplainMode: () => void;
21
+ exitExplainMode: () => void;
22
+ setExplainContext: (ctx: ExplainContext) => void;
23
+ clearExplainContext: () => void;
24
+ };
25
+
26
+ const AIAssistantContext = createContext<AIAssistantContextType>({
27
+ openChat: () => {},
28
+ closeChat: () => {},
29
+ isChatOpen: false,
30
+ explainMode: false,
31
+ enterExplainMode: () => {},
32
+ exitExplainMode: () => {},
33
+ setExplainContext: () => {},
34
+ clearExplainContext: () => {},
35
+ });
36
+
37
+ export const useAIAssistant = () => useContext(AIAssistantContext);
38
+
39
+ type AIAssistantProviderProps = {
40
+ endpoint: string;
41
+ apiKey?: string;
42
+ children: React.ReactNode;
43
+ };
44
+
45
+ /**
46
+ * Find the closest ancestor (or self) with a data-nb-explain attribute.
47
+ * Returns null if nothing is explainable.
48
+ */
49
+ function findExplainable(el: HTMLElement): HTMLElement | null {
50
+ return el.closest("[data-nb-explain]") as HTMLElement | null;
51
+ }
52
+
53
+ /**
54
+ * Extract a short label from an explainable element by looking for
55
+ * a label, heading, or first bit of text content.
56
+ */
57
+ function extractLabel(el: HTMLElement): string {
58
+ const label = el.querySelector("label") as HTMLElement | null;
59
+ if (label?.innerText?.trim()) return label.innerText.trim();
60
+
61
+ const heading = el.querySelector("h1, h2, h3, h4") as HTMLElement | null;
62
+ if (heading?.innerText?.trim()) return heading.innerText.trim();
63
+
64
+ const text = el.innerText?.trim();
65
+ if (text && text.length <= 80) return text;
66
+ if (text) return text.slice(0, 80) + "...";
67
+
68
+ return "this element";
69
+ }
70
+
71
+ /**
72
+ * Look for data-nb-explain-docs on the element or its ancestors.
73
+ * Returns an array of documentation URLs, or an empty array.
74
+ */
75
+ function findExplainDocs(el: HTMLElement): string[] {
76
+ const withDocs = el.closest("[data-nb-explain-docs]") as HTMLElement | null;
77
+ if (!withDocs) return [];
78
+
79
+ const raw = withDocs.getAttribute("data-nb-explain-docs") || "";
80
+ try {
81
+ const parsed = JSON.parse(raw);
82
+ if (Array.isArray(parsed)) return parsed;
83
+ } catch {
84
+ // If not valid JSON, treat as a single URL
85
+ if (raw.trim()) return [raw.trim()];
86
+ }
87
+ return [];
88
+ }
89
+
90
+ function buildQuery(
91
+ label: string,
92
+ ctx: ExplainContext | null,
93
+ elementDocs: string[],
94
+ ): string {
95
+ let userMessage = `Explain "${label}"`;
96
+ if (ctx) {
97
+ if (ctx.modalName) userMessage += ` on ${ctx.modalName} modal`;
98
+ if (ctx.pageName) userMessage += ` in ${ctx.pageName}`;
99
+ }
100
+
101
+ // Merge docs from context and element attribute
102
+ const allDocs = [
103
+ ...(ctx?.docsUrls || []),
104
+ ...elementDocs,
105
+ ];
106
+ // Deduplicate
107
+ const uniqueDocs = [...new Set(allDocs)];
108
+
109
+ const parts = [userMessage];
110
+ if (uniqueDocs.length > 0) {
111
+ parts.push(`Docs: ${uniqueDocs.join(", ")}`);
112
+ }
113
+ return parts.join("\n");
114
+ }
115
+
116
+ export default function AIAssistantProvider({
117
+ endpoint,
118
+ apiKey,
119
+ children,
120
+ }: AIAssistantProviderProps) {
121
+ const [isChatOpen, setIsChatOpen] = useState(false);
122
+ const [initialQuery, setInitialQuery] = useState("");
123
+ const [explainMode, setExplainMode] = useState(false);
124
+ const [hoveredEl, setHoveredEl] = useState<HTMLElement | null>(null);
125
+ const [explainCtx, setExplainCtx] = useState<ExplainContext | null>(null);
126
+
127
+ const openChat = useCallback((selectedText?: string) => {
128
+ setInitialQuery(selectedText || "");
129
+ setIsChatOpen(true);
130
+ setExplainMode(false);
131
+ setHoveredEl(null);
132
+ }, []);
133
+
134
+ const closeChat = useCallback(() => {
135
+ setIsChatOpen(false);
136
+ setInitialQuery("");
137
+ }, []);
138
+
139
+ const enterExplainMode = useCallback(() => {
140
+ setExplainMode(true);
141
+ }, []);
142
+
143
+ const exitExplainMode = useCallback(() => {
144
+ setExplainMode(false);
145
+ setHoveredEl(null);
146
+ }, []);
147
+
148
+ const setExplainContext = useCallback((ctx: ExplainContext) => {
149
+ setExplainCtx(ctx);
150
+ }, []);
151
+
152
+ const clearExplainContext = useCallback(() => {
153
+ setExplainCtx(null);
154
+ }, []);
155
+
156
+ // Explain mode: highlight explainable elements on hover, open chat on click
157
+ useEffect(() => {
158
+ if (!explainMode) return;
159
+
160
+ const handleMouseOver = (e: MouseEvent) => {
161
+ const target = e.target as HTMLElement;
162
+ if (
163
+ target.closest("[data-nb-explain-ignore]") ||
164
+ target.closest("[data-nb-explain-banner]")
165
+ )
166
+ return;
167
+ const explainable = findExplainable(target);
168
+ setHoveredEl(explainable);
169
+ };
170
+
171
+ const handleMouseOut = () => {
172
+ setHoveredEl(null);
173
+ };
174
+
175
+ const handleClick = (e: MouseEvent) => {
176
+ const target = e.target as HTMLElement;
177
+ if (
178
+ target.closest("[data-nb-explain-ignore]") ||
179
+ target.closest("[data-nb-explain-banner]")
180
+ )
181
+ return;
182
+
183
+ e.preventDefault();
184
+ e.stopPropagation();
185
+
186
+ const explainable = findExplainable(target);
187
+ if (!explainable) return;
188
+
189
+ const attrValue = explainable.getAttribute("data-nb-explain") || "";
190
+ const label =
191
+ attrValue && attrValue !== "true"
192
+ ? attrValue
193
+ : extractLabel(explainable);
194
+
195
+ const elementDocs = findExplainDocs(explainable);
196
+ const query = buildQuery(label, explainCtx, elementDocs);
197
+ openChat(query);
198
+ };
199
+
200
+ const handleKeyDown = (e: KeyboardEvent) => {
201
+ if (e.key === "Escape") {
202
+ exitExplainMode();
203
+ }
204
+ };
205
+
206
+ document.addEventListener("mouseover", handleMouseOver, true);
207
+ document.addEventListener("mouseout", handleMouseOut, true);
208
+ document.addEventListener("click", handleClick, true);
209
+ document.addEventListener("keydown", handleKeyDown);
210
+
211
+ return () => {
212
+ document.removeEventListener("mouseover", handleMouseOver, true);
213
+ document.removeEventListener("mouseout", handleMouseOut, true);
214
+ document.removeEventListener("click", handleClick, true);
215
+ document.removeEventListener("keydown", handleKeyDown);
216
+ };
217
+ }, [explainMode, explainCtx, openChat, exitExplainMode]);
218
+
219
+ // Apply/remove highlight on hovered explainable element via CSS class
220
+ useEffect(() => {
221
+ if (!hoveredEl) return;
222
+ hoveredEl.setAttribute("data-nb-explain-highlight", "");
223
+ return () => {
224
+ hoveredEl.removeAttribute("data-nb-explain-highlight");
225
+ };
226
+ }, [hoveredEl]);
227
+
228
+ // Force-remove all highlights when leaving explain mode
229
+ useEffect(() => {
230
+ if (!explainMode) {
231
+ document.querySelectorAll("[data-nb-explain-highlight]").forEach((el) => {
232
+ el.removeAttribute("data-nb-explain-highlight");
233
+ });
234
+ }
235
+ }, [explainMode]);
236
+
237
+ return (
238
+ <AIAssistantContext.Provider
239
+ value={{
240
+ openChat,
241
+ closeChat,
242
+ isChatOpen,
243
+ explainMode,
244
+ enterExplainMode,
245
+ exitExplainMode,
246
+ setExplainContext,
247
+ clearExplainContext,
248
+ }}
249
+ >
250
+ {/* Inject CSS custom properties, animations, and highlight styles */}
251
+ <style>{S.CSS_VARS + S.ANIMATIONS + S.HIGHLIGHT_STYLES}</style>
252
+
253
+ {children}
254
+
255
+ {/* Explain mode banner */}
256
+ {explainMode && (
257
+ <div data-nb-explain-banner style={S.banner}>
258
+ <span>Click on a highlighted element to explain it</span>
259
+ <button
260
+ onClick={() => exitExplainMode()}
261
+ style={S.bannerCancel}
262
+ >
263
+ Cancel
264
+ </button>
265
+ </div>
266
+ )}
267
+
268
+ <AIFloatingButton
269
+ isOpen={isChatOpen}
270
+ onClick={() => {
271
+ if (isChatOpen) {
272
+ closeChat();
273
+ } else {
274
+ openChat();
275
+ }
276
+ }}
277
+ />
278
+
279
+ <AIChatBot
280
+ open={isChatOpen}
281
+ onClose={closeChat}
282
+ initialQuery={initialQuery}
283
+ endpoint={endpoint}
284
+ apiKey={apiKey}
285
+ />
286
+ </AIAssistantContext.Provider>
287
+ );
288
+ }