@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,158 @@
1
+ import type { Message } from "../types";
2
+ import { AnthropicProvider } from "./providers/anthropic";
3
+ import { OpenAIProvider } from "./providers/openai";
4
+ import type { LLMProvider } from "./providers/types";
5
+
6
+ export type AssistantConfig = {
7
+ provider: "anthropic" | "openai";
8
+ apiKey: string;
9
+ model?: string;
10
+ systemPrompt?: string;
11
+ };
12
+
13
+ type HandlerOptions = {
14
+ apiKey?: string;
15
+ };
16
+
17
+ type ChatRequest = {
18
+ messages: Message[];
19
+ };
20
+
21
+ type ChatResponse = {
22
+ reply: string;
23
+ };
24
+
25
+ interface IncomingRequest {
26
+ headers: Record<string, string | string[] | undefined> | { get(name: string): string | null };
27
+ body?: unknown;
28
+ method?: string;
29
+ on?(event: string, callback: (...args: unknown[]) => void): unknown;
30
+ }
31
+
32
+ interface OutgoingResponse {
33
+ status?(code: number): Pick<OutgoingResponse, "json" | "end">;
34
+ statusCode?: number;
35
+ json?(data: unknown): void;
36
+ end?(data?: string): void;
37
+ setHeader?(name: string, value: string): void;
38
+ writeHead?(statusCode: number, headers?: Record<string, string>): void;
39
+ }
40
+
41
+ function getHeader(req: IncomingRequest, name: string): string | null {
42
+ if (typeof req.headers === "object" && req.headers !== null) {
43
+ if ("get" in req.headers && typeof req.headers.get === "function") {
44
+ return req.headers.get(name);
45
+ }
46
+ const headers = req.headers as Record<string, string | string[] | undefined>;
47
+ const value = headers[name] ?? headers[name.toLowerCase()];
48
+ if (Array.isArray(value)) return value[0] ?? null;
49
+ return value ?? null;
50
+ }
51
+ return null;
52
+ }
53
+
54
+ function sendJson(res: OutgoingResponse, statusCode: number, data: unknown): void {
55
+ if (typeof res.status === "function" && typeof res.json === "function") {
56
+ const chained = res.status(statusCode);
57
+ if (chained && typeof chained.json === "function") {
58
+ chained.json(data);
59
+ return;
60
+ }
61
+ }
62
+ if (typeof res.writeHead === "function" && typeof res.end === "function") {
63
+ res.writeHead(statusCode, { "Content-Type": "application/json" });
64
+ res.end(JSON.stringify(data));
65
+ return;
66
+ }
67
+ if (typeof res.end === "function") {
68
+ if (res.statusCode !== undefined) res.statusCode = statusCode;
69
+ res.end(JSON.stringify(data));
70
+ }
71
+ }
72
+
73
+ async function parseBody(req: IncomingRequest): Promise<unknown> {
74
+ if (req.body !== undefined && req.body !== null) {
75
+ return req.body;
76
+ }
77
+ if (typeof req.on === "function") {
78
+ const onFn = req.on.bind(req);
79
+ return new Promise((resolve, reject) => {
80
+ const chunks: string[] = [];
81
+ onFn("data", (chunk: unknown) => chunks.push(String(chunk)));
82
+ onFn("end", () => {
83
+ try {
84
+ resolve(JSON.parse(chunks.join("")));
85
+ } catch {
86
+ reject(new Error("Invalid JSON body"));
87
+ }
88
+ });
89
+ onFn("error", reject);
90
+ });
91
+ }
92
+ throw new Error("Unable to parse request body");
93
+ }
94
+
95
+ function createProvider(config: AssistantConfig): LLMProvider {
96
+ switch (config.provider) {
97
+ case "anthropic":
98
+ return new AnthropicProvider({ apiKey: config.apiKey, model: config.model });
99
+ case "openai":
100
+ return new OpenAIProvider({ apiKey: config.apiKey, model: config.model });
101
+ default:
102
+ throw new Error(`Unknown provider: ${config.provider}`);
103
+ }
104
+ }
105
+
106
+ export function createAssistant(config: AssistantConfig) {
107
+ const provider = createProvider(config);
108
+
109
+ async function chat(req: ChatRequest): Promise<ChatResponse> {
110
+ if (!req.messages || !Array.isArray(req.messages) || req.messages.length === 0) {
111
+ throw new Error("messages array is required and must not be empty");
112
+ }
113
+ const reply = await provider.chat(req.messages, config.systemPrompt);
114
+ return { reply };
115
+ }
116
+
117
+ function handler(opts?: HandlerOptions) {
118
+ return async (req: IncomingRequest, res: OutgoingResponse): Promise<void> => {
119
+ try {
120
+ if (opts?.apiKey) {
121
+ const authHeader = getHeader(req, "Authorization") || getHeader(req, "authorization");
122
+ const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
123
+ if (token !== opts.apiKey) {
124
+ sendJson(res, 401, { error: "Unauthorized" });
125
+ return;
126
+ }
127
+ }
128
+
129
+ let body: unknown;
130
+ try {
131
+ body = await parseBody(req);
132
+ } catch {
133
+ sendJson(res, 400, { error: "Invalid request body" });
134
+ return;
135
+ }
136
+
137
+ const { messages } = body as { messages?: Message[] };
138
+ if (!messages || !Array.isArray(messages) || messages.length === 0) {
139
+ sendJson(res, 400, { error: "messages array is required and must not be empty" });
140
+ return;
141
+ }
142
+
143
+ try {
144
+ const result = await chat({ messages });
145
+ sendJson(res, 200, result);
146
+ } catch (err) {
147
+ const message = err instanceof Error ? err.message : "LLM request failed";
148
+ sendJson(res, 502, { error: message });
149
+ }
150
+ } catch (err) {
151
+ const message = err instanceof Error ? err.message : "Internal server error";
152
+ sendJson(res, 500, { error: message });
153
+ }
154
+ };
155
+ }
156
+
157
+ return { chat, handler };
158
+ }
@@ -0,0 +1,5 @@
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";
@@ -0,0 +1,53 @@
1
+ import type { Message } from "../../types";
2
+ import type { LLMProvider, ProviderConfig } from "./types";
3
+
4
+ export class AnthropicProvider implements LLMProvider {
5
+ private apiKey: string;
6
+ private model: string;
7
+
8
+ constructor(config: ProviderConfig) {
9
+ this.apiKey = config.apiKey;
10
+ this.model = config.model || "claude-sonnet-4-20250514";
11
+ }
12
+
13
+ async chat(messages: Message[], systemPrompt?: string): Promise<string> {
14
+ const anthropicMessages = messages.map((m) => ({
15
+ role: m.role === "context" || m.role === "system" ? ("user" as const) : m.role,
16
+ content: m.role === "context" ? `[Context]: ${m.content}` : m.content,
17
+ }));
18
+
19
+ const body: Record<string, unknown> = {
20
+ model: this.model,
21
+ max_tokens: 4096,
22
+ messages: anthropicMessages,
23
+ };
24
+
25
+ if (systemPrompt) {
26
+ body.system = systemPrompt;
27
+ }
28
+
29
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
30
+ method: "POST",
31
+ headers: {
32
+ "Content-Type": "application/json",
33
+ "x-api-key": this.apiKey,
34
+ "anthropic-version": "2023-06-01",
35
+ },
36
+ body: JSON.stringify(body),
37
+ });
38
+
39
+ if (!response.ok) {
40
+ const text = await response.text();
41
+ throw new Error(`Anthropic API error ${response.status}: ${text}`);
42
+ }
43
+
44
+ const data = await response.json();
45
+ const textBlock = data.content?.find(
46
+ (block: { type: string }) => block.type === "text",
47
+ );
48
+ if (!textBlock) {
49
+ throw new Error("No text content in Anthropic response");
50
+ }
51
+ return textBlock.text;
52
+ }
53
+ }
@@ -0,0 +1,55 @@
1
+ import type { Message } from "../../types";
2
+ import type { LLMProvider, ProviderConfig } from "./types";
3
+
4
+ export class OpenAIProvider implements LLMProvider {
5
+ private apiKey: string;
6
+ private model: string;
7
+
8
+ constructor(config: ProviderConfig) {
9
+ this.apiKey = config.apiKey;
10
+ this.model = config.model || "gpt-4o";
11
+ }
12
+
13
+ async chat(messages: Message[], systemPrompt?: string): Promise<string> {
14
+ const openaiMessages: Array<{ role: string; content: string }> = [];
15
+
16
+ if (systemPrompt) {
17
+ openaiMessages.push({ role: "system", content: systemPrompt });
18
+ }
19
+
20
+ for (const m of messages) {
21
+ if (m.role === "context") {
22
+ openaiMessages.push({ role: "user", content: `[Context]: ${m.content}` });
23
+ } else if (m.role === "system") {
24
+ openaiMessages.push({ role: "user", content: m.content });
25
+ } else {
26
+ openaiMessages.push({ role: m.role, content: m.content });
27
+ }
28
+ }
29
+
30
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
31
+ method: "POST",
32
+ headers: {
33
+ "Content-Type": "application/json",
34
+ Authorization: `Bearer ${this.apiKey}`,
35
+ },
36
+ body: JSON.stringify({
37
+ model: this.model,
38
+ messages: openaiMessages,
39
+ max_tokens: 4096,
40
+ }),
41
+ });
42
+
43
+ if (!response.ok) {
44
+ const text = await response.text();
45
+ throw new Error(`OpenAI API error ${response.status}: ${text}`);
46
+ }
47
+
48
+ const data = await response.json();
49
+ const choice = data.choices?.[0];
50
+ if (!choice?.message?.content) {
51
+ throw new Error("No content in OpenAI response");
52
+ }
53
+ return choice.message.content;
54
+ }
55
+ }
@@ -0,0 +1,10 @@
1
+ import type { Message } from "../../types";
2
+
3
+ export interface LLMProvider {
4
+ chat(messages: Message[], systemPrompt?: string): Promise<string>;
5
+ }
6
+
7
+ export type ProviderConfig = {
8
+ apiKey: string;
9
+ model?: string;
10
+ };
package/src/types.ts ADDED
@@ -0,0 +1,11 @@
1
+ export type Message = {
2
+ id?: string;
3
+ role: "user" | "assistant" | "context" | "system";
4
+ content: string;
5
+ };
6
+
7
+ export type ExplainContext = {
8
+ modalName?: string;
9
+ pageName?: string;
10
+ docsUrls?: string[];
11
+ };