@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.
- package/README.md +402 -0
- package/dist/client/AIAssistantProvider.d.ts +21 -0
- package/dist/client/AIAssistantProvider.d.ts.map +1 -0
- package/dist/client/AIAssistantProvider.js +198 -0
- package/dist/client/AIAssistantProvider.js.map +1 -0
- package/dist/client/AIChatBot.d.ts +10 -0
- package/dist/client/AIChatBot.d.ts.map +1 -0
- package/dist/client/AIChatBot.js +139 -0
- package/dist/client/AIChatBot.js.map +1 -0
- package/dist/client/AIFloatingButton.d.ts +7 -0
- package/dist/client/AIFloatingButton.d.ts.map +1 -0
- package/dist/client/AIFloatingButton.js +16 -0
- package/dist/client/AIFloatingButton.js.map +1 -0
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +4 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/styles.d.ts +51 -0
- package/dist/client/styles.d.ts.map +1 -0
- package/dist/client/styles.js +317 -0
- package/dist/client/styles.js.map +1 -0
- package/dist/server/handler.d.ts +38 -0
- package/dist/server/handler.d.ts.map +1 -0
- package/dist/server/handler.js +117 -0
- package/dist/server/handler.js.map +1 -0
- package/dist/server/index.d.ts +6 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +4 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/providers/anthropic.d.ts +9 -0
- package/dist/server/providers/anthropic.d.ts.map +1 -0
- package/dist/server/providers/anthropic.js +40 -0
- package/dist/server/providers/anthropic.js.map +1 -0
- package/dist/server/providers/openai.d.ts +9 -0
- package/dist/server/providers/openai.d.ts.map +1 -0
- package/dist/server/providers/openai.js +46 -0
- package/dist/server/providers/openai.js.map +1 -0
- package/dist/server/providers/types.d.ts +9 -0
- package/dist/server/providers/types.d.ts.map +1 -0
- package/dist/server/providers/types.js +2 -0
- package/dist/server/providers/types.js.map +1 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +45 -0
- package/src/client/AIAssistantProvider.tsx +288 -0
- package/src/client/AIChatBot.tsx +309 -0
- package/src/client/AIFloatingButton.tsx +34 -0
- package/src/client/index.ts +4 -0
- package/src/client/styles.ts +353 -0
- package/src/server/handler.ts +158 -0
- package/src/server/index.ts +5 -0
- package/src/server/providers/anthropic.ts +53 -0
- package/src/server/providers/openai.ts +55 -0
- package/src/server/providers/types.ts +10 -0
- 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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/server/providers/types.ts"],"names":[],"mappings":""}
|
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|