@nickle/chatbot-react 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 +191 -0
- package/dist/index.d.mts +169 -0
- package/dist/index.d.ts +169 -0
- package/dist/index.js +1651 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1633 -0
- package/dist/index.mjs.map +1 -0
- package/dist/styles.css +1 -0
- package/package.json +58 -0
- package/src/chatbot/components/awaiting-dots.tsx +9 -0
- package/src/chatbot/components/chatbot-page-config.tsx +17 -0
- package/src/chatbot/components/chatbot-widget.tsx +739 -0
- package/src/chatbot/components/message-markdown.tsx +17 -0
- package/src/chatbot/context/chatbot-context.tsx +580 -0
- package/src/chatbot/hooks/use-chatbot-session.ts +14 -0
- package/src/chatbot/hooks/use-chatbot.ts +5 -0
- package/src/chatbot/index.ts +22 -0
- package/src/chatbot/lib/adapter.ts +127 -0
- package/src/chatbot/lib/defaults.ts +48 -0
- package/src/chatbot/lib/id.ts +7 -0
- package/src/chatbot/lib/session.ts +36 -0
- package/src/chatbot/lib/theme.ts +76 -0
- package/src/chatbot/lib/transport.ts +211 -0
- package/src/chatbot/types/index.ts +165 -0
- package/src/index.ts +1 -0
- package/src/styles.css +257 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChatbotAdapter,
|
|
3
|
+
ChatbotAdapterRequestInput,
|
|
4
|
+
NormalizedAssistantChunk,
|
|
5
|
+
} from "../types";
|
|
6
|
+
|
|
7
|
+
function extractFromRecord(record: Record<string, unknown>): string | undefined {
|
|
8
|
+
const keys = ["message", "text", "output", "response", "answer", "content"];
|
|
9
|
+
for (const key of keys) {
|
|
10
|
+
const value = record[key];
|
|
11
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function defaultBuildRequest(input: ChatbotAdapterRequestInput): RequestInit & { url?: string } {
|
|
19
|
+
const { files, metadata, message, headers, sessionId } = input;
|
|
20
|
+
|
|
21
|
+
if (files.length > 0) {
|
|
22
|
+
const formData = new FormData();
|
|
23
|
+
formData.set("message", message);
|
|
24
|
+
formData.set("sessionId", sessionId);
|
|
25
|
+
formData.set("metadata", JSON.stringify(metadata));
|
|
26
|
+
|
|
27
|
+
files.forEach((file) => {
|
|
28
|
+
formData.append("files[]", file);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers,
|
|
34
|
+
body: formData,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: {
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
...headers,
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
message,
|
|
46
|
+
sessionId,
|
|
47
|
+
metadata,
|
|
48
|
+
}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function defaultParseJsonResponse(raw: unknown): NormalizedAssistantChunk {
|
|
53
|
+
if (typeof raw === "string") {
|
|
54
|
+
return { message: raw };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (Array.isArray(raw)) {
|
|
58
|
+
const merged = raw
|
|
59
|
+
.map((item) => {
|
|
60
|
+
if (typeof item === "string") {
|
|
61
|
+
return item;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (item && typeof item === "object") {
|
|
65
|
+
return extractFromRecord(item as Record<string, unknown>);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return undefined;
|
|
69
|
+
})
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.join("\n");
|
|
72
|
+
|
|
73
|
+
return { message: merged };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (raw && typeof raw === "object") {
|
|
77
|
+
const record = raw as Record<string, unknown>;
|
|
78
|
+
|
|
79
|
+
if (record.delta && typeof record.delta === "string") {
|
|
80
|
+
return {
|
|
81
|
+
delta: record.delta,
|
|
82
|
+
done: Boolean(record.done),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const text = extractFromRecord(record);
|
|
87
|
+
if (text) {
|
|
88
|
+
return { message: text, done: true };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (record.data && typeof record.data === "object") {
|
|
92
|
+
const nested = extractFromRecord(record.data as Record<string, unknown>);
|
|
93
|
+
if (nested) {
|
|
94
|
+
return { message: nested, done: true };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { message: "", done: true };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function defaultParseStreamChunk(chunk: string): NormalizedAssistantChunk {
|
|
103
|
+
const trimmed = chunk.trim();
|
|
104
|
+
|
|
105
|
+
if (!trimmed) {
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (trimmed === "[DONE]") {
|
|
110
|
+
return { done: true };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
115
|
+
return defaultParseJsonResponse(parsed);
|
|
116
|
+
} catch {
|
|
117
|
+
return { delta: trimmed };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function withDefaultAdapter(adapter?: ChatbotAdapter): Required<ChatbotAdapter> {
|
|
122
|
+
return {
|
|
123
|
+
buildRequest: adapter?.buildRequest ?? defaultBuildRequest,
|
|
124
|
+
parseJsonResponse: adapter?.parseJsonResponse ?? defaultParseJsonResponse,
|
|
125
|
+
parseStreamChunk: adapter?.parseStreamChunk ?? defaultParseStreamChunk,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Bot,
|
|
3
|
+
ChevronDown,
|
|
4
|
+
Download,
|
|
5
|
+
Ellipsis,
|
|
6
|
+
Maximize2,
|
|
7
|
+
MessageSquare,
|
|
8
|
+
Paperclip,
|
|
9
|
+
SendHorizontal,
|
|
10
|
+
X,
|
|
11
|
+
} from "lucide-react";
|
|
12
|
+
import type { ChatbotIcons, ChatbotThemeTokens } from "../types";
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_THEME: ChatbotThemeTokens = {
|
|
15
|
+
primary: "#ffffff",
|
|
16
|
+
primaryForeground: "#111111",
|
|
17
|
+
background: "#0b0b0b",
|
|
18
|
+
surface: "#111111",
|
|
19
|
+
surfaceForeground: "#f5f5f5",
|
|
20
|
+
muted: "#262626",
|
|
21
|
+
mutedForeground: "#a3a3a3",
|
|
22
|
+
border: "#2f2f2f",
|
|
23
|
+
ring: "#737373",
|
|
24
|
+
userBubble: "#ffffff",
|
|
25
|
+
userText: "#111111",
|
|
26
|
+
assistantBubble: "#2a2a2a",
|
|
27
|
+
assistantText: "#f5f5f5",
|
|
28
|
+
radius: "14px",
|
|
29
|
+
fontFamily: "Inter, var(--font-sans), ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif",
|
|
30
|
+
shadowBubble: "0 18px 40px -16px rgba(0, 0, 0, 0.7)",
|
|
31
|
+
shadowPanel: "0 34px 80px -36px rgba(0, 0, 0, 0.85)",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const DEFAULT_ICONS: ChatbotIcons = {
|
|
35
|
+
launcher: MessageSquare,
|
|
36
|
+
launcherClosed: MessageSquare,
|
|
37
|
+
launcherOpen: ChevronDown,
|
|
38
|
+
close: X,
|
|
39
|
+
send: SendHorizontal,
|
|
40
|
+
attach: Paperclip,
|
|
41
|
+
bot: Bot,
|
|
42
|
+
menu: Ellipsis,
|
|
43
|
+
expand: Maximize2,
|
|
44
|
+
download: Download,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const DEFAULT_AGENT_NAME = "Assistant";
|
|
48
|
+
export const DEFAULT_ASSISTANT_NOTE = "Here to help";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const SESSION_KEY = "cb:session-id";
|
|
2
|
+
|
|
3
|
+
function createSessionId() {
|
|
4
|
+
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
|
5
|
+
return crypto.randomUUID();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return `cb-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getOrCreateSessionId(storage: Storage | undefined): string {
|
|
12
|
+
if (!storage) {
|
|
13
|
+
return createSessionId();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const existing = storage.getItem(SESSION_KEY);
|
|
17
|
+
if (existing) {
|
|
18
|
+
return existing;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const created = createSessionId();
|
|
22
|
+
storage.setItem(SESSION_KEY, created);
|
|
23
|
+
return created;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resetSessionId(storage: Storage | undefined): string {
|
|
27
|
+
const next = createSessionId();
|
|
28
|
+
if (storage) {
|
|
29
|
+
storage.setItem(SESSION_KEY, next);
|
|
30
|
+
}
|
|
31
|
+
return next;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function historyKey(sessionId: string) {
|
|
35
|
+
return `cb:history:${sessionId}`;
|
|
36
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { DEFAULT_THEME } from "./defaults";
|
|
2
|
+
import type { ChatbotThemeTokens } from "../types";
|
|
3
|
+
|
|
4
|
+
const HOST_TOKEN_MAP: Record<keyof ChatbotThemeTokens, string[]> = {
|
|
5
|
+
primary: ["--color-primary", "--primary"],
|
|
6
|
+
primaryForeground: ["--color-primary-foreground", "--primary-foreground"],
|
|
7
|
+
background: ["--cb-background", "--color-background", "--color-bg", "--background"],
|
|
8
|
+
surface: ["--cb-surface", "--color-surface", "--color-card", "--card"],
|
|
9
|
+
surfaceForeground: ["--cb-surface-foreground", "--color-text", "--color-foreground", "--foreground"],
|
|
10
|
+
muted: ["--color-muted", "--muted"],
|
|
11
|
+
mutedForeground: ["--color-muted-foreground", "--muted-foreground"],
|
|
12
|
+
border: ["--color-border", "--border"],
|
|
13
|
+
ring: ["--color-ring", "--ring"],
|
|
14
|
+
userBubble: ["--cb-user-bubble"],
|
|
15
|
+
userText: ["--cb-user-text"],
|
|
16
|
+
assistantBubble: ["--cb-assistant-bubble", "--color-muted", "--muted"],
|
|
17
|
+
assistantText: ["--cb-assistant-text", "--color-text", "--foreground"],
|
|
18
|
+
radius: ["--radius-md", "--radius", "--rounded"],
|
|
19
|
+
fontFamily: ["--font-sans", "--font-family"],
|
|
20
|
+
shadowBubble: ["--shadow-bubble"],
|
|
21
|
+
shadowPanel: ["--shadow-panel"],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function readRootVar(name: string): string | undefined {
|
|
25
|
+
if (typeof window === "undefined") {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const styles = getComputedStyle(document.documentElement);
|
|
30
|
+
const value = styles.getPropertyValue(name).trim();
|
|
31
|
+
return value || undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveThemeTokens(explicit?: Partial<ChatbotThemeTokens>): ChatbotThemeTokens {
|
|
35
|
+
const resolved = { ...DEFAULT_THEME };
|
|
36
|
+
|
|
37
|
+
(Object.keys(DEFAULT_THEME) as Array<keyof ChatbotThemeTokens>).forEach((key) => {
|
|
38
|
+
if (explicit?.[key]) {
|
|
39
|
+
resolved[key] = explicit[key] as string;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const hostCandidates = HOST_TOKEN_MAP[key] ?? [];
|
|
44
|
+
for (const candidate of hostCandidates) {
|
|
45
|
+
const value = readRootVar(candidate);
|
|
46
|
+
if (value) {
|
|
47
|
+
resolved[key] = value;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return resolved;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function toThemeCssVars(tokens: ChatbotThemeTokens): Record<string, string> {
|
|
57
|
+
return {
|
|
58
|
+
"--cb-primary": tokens.primary,
|
|
59
|
+
"--cb-primary-foreground": tokens.primaryForeground,
|
|
60
|
+
"--cb-background": tokens.background,
|
|
61
|
+
"--cb-surface": tokens.surface,
|
|
62
|
+
"--cb-surface-foreground": tokens.surfaceForeground,
|
|
63
|
+
"--cb-muted": tokens.muted,
|
|
64
|
+
"--cb-muted-foreground": tokens.mutedForeground,
|
|
65
|
+
"--cb-border": tokens.border,
|
|
66
|
+
"--cb-ring": tokens.ring,
|
|
67
|
+
"--cb-user-bubble": tokens.userBubble,
|
|
68
|
+
"--cb-user-text": tokens.userText,
|
|
69
|
+
"--cb-assistant-bubble": tokens.assistantBubble,
|
|
70
|
+
"--cb-assistant-text": tokens.assistantText,
|
|
71
|
+
"--cb-radius": tokens.radius,
|
|
72
|
+
"--cb-font-family": tokens.fontFamily,
|
|
73
|
+
"--cb-shadow-bubble": tokens.shadowBubble,
|
|
74
|
+
"--cb-shadow-panel": tokens.shadowPanel,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { ChatbotAdapter } from "../types";
|
|
2
|
+
|
|
3
|
+
interface ResponseInput {
|
|
4
|
+
response: Response;
|
|
5
|
+
adapter: Required<ChatbotAdapter>;
|
|
6
|
+
streamingMode: "auto" | "on" | "off";
|
|
7
|
+
onDelta: (chunk: string) => void;
|
|
8
|
+
onChunk: (chunk: string) => void;
|
|
9
|
+
onRecoverableError: (error: Error) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function looksLikeStream(contentType: string | null) {
|
|
13
|
+
if (!contentType) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const normalized = contentType.toLowerCase();
|
|
18
|
+
return (
|
|
19
|
+
normalized.includes("text/event-stream") ||
|
|
20
|
+
normalized.includes("application/x-ndjson") ||
|
|
21
|
+
normalized.includes("application/stream")
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseAndAppend(
|
|
26
|
+
rawChunk: string,
|
|
27
|
+
adapter: Required<ChatbotAdapter>,
|
|
28
|
+
onChunk: (chunk: string) => void,
|
|
29
|
+
): { text: string; done: boolean } {
|
|
30
|
+
const parsed = adapter.parseStreamChunk(rawChunk);
|
|
31
|
+
onChunk(rawChunk);
|
|
32
|
+
|
|
33
|
+
if (parsed.done && parsed.message) {
|
|
34
|
+
return { text: parsed.message, done: true };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (parsed.delta) {
|
|
38
|
+
return { text: parsed.delta, done: Boolean(parsed.done) };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (parsed.message) {
|
|
42
|
+
return { text: parsed.message, done: Boolean(parsed.done) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { text: "", done: Boolean(parsed.done) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function consumeEventStream(
|
|
49
|
+
response: Response,
|
|
50
|
+
adapter: Required<ChatbotAdapter>,
|
|
51
|
+
onDelta: (chunk: string) => void,
|
|
52
|
+
onChunk: (chunk: string) => void,
|
|
53
|
+
): Promise<string> {
|
|
54
|
+
const reader = response.body?.getReader();
|
|
55
|
+
if (!reader) {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const decoder = new TextDecoder();
|
|
60
|
+
let buffer = "";
|
|
61
|
+
let fullText = "";
|
|
62
|
+
let done = false;
|
|
63
|
+
|
|
64
|
+
while (!done) {
|
|
65
|
+
const result = await reader.read();
|
|
66
|
+
if (result.done) {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
buffer += decoder.decode(result.value, { stream: true });
|
|
71
|
+
|
|
72
|
+
while (buffer.includes("\n\n")) {
|
|
73
|
+
const index = buffer.indexOf("\n\n");
|
|
74
|
+
const block = buffer.slice(0, index);
|
|
75
|
+
buffer = buffer.slice(index + 2);
|
|
76
|
+
|
|
77
|
+
const lines = block.split("\n").filter((line) => line.startsWith("data:"));
|
|
78
|
+
for (const line of lines) {
|
|
79
|
+
const data = line.slice(5).trim();
|
|
80
|
+
const parsed = parseAndAppend(data, adapter, onChunk);
|
|
81
|
+
if (parsed.text) {
|
|
82
|
+
fullText += parsed.text;
|
|
83
|
+
onDelta(parsed.text);
|
|
84
|
+
}
|
|
85
|
+
if (parsed.done) {
|
|
86
|
+
done = true;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (buffer.trim()) {
|
|
94
|
+
const fallback = parseAndAppend(buffer.trim(), adapter, onChunk);
|
|
95
|
+
if (fallback.text) {
|
|
96
|
+
fullText += fallback.text;
|
|
97
|
+
onDelta(fallback.text);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return fullText;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function consumeLineStream(
|
|
105
|
+
response: Response,
|
|
106
|
+
adapter: Required<ChatbotAdapter>,
|
|
107
|
+
onDelta: (chunk: string) => void,
|
|
108
|
+
onChunk: (chunk: string) => void,
|
|
109
|
+
): Promise<string> {
|
|
110
|
+
const reader = response.body?.getReader();
|
|
111
|
+
if (!reader) {
|
|
112
|
+
return "";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const decoder = new TextDecoder();
|
|
116
|
+
let buffer = "";
|
|
117
|
+
let fullText = "";
|
|
118
|
+
let done = false;
|
|
119
|
+
|
|
120
|
+
while (!done) {
|
|
121
|
+
const result = await reader.read();
|
|
122
|
+
if (result.done) {
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
buffer += decoder.decode(result.value, { stream: true });
|
|
127
|
+
|
|
128
|
+
while (buffer.includes("\n")) {
|
|
129
|
+
const index = buffer.indexOf("\n");
|
|
130
|
+
const line = buffer.slice(0, index).trim();
|
|
131
|
+
buffer = buffer.slice(index + 1);
|
|
132
|
+
|
|
133
|
+
if (!line) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const parsed = parseAndAppend(line, adapter, onChunk);
|
|
138
|
+
if (parsed.text) {
|
|
139
|
+
fullText += parsed.text;
|
|
140
|
+
onDelta(parsed.text);
|
|
141
|
+
}
|
|
142
|
+
if (parsed.done) {
|
|
143
|
+
done = true;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (buffer.trim()) {
|
|
150
|
+
const parsed = parseAndAppend(buffer.trim(), adapter, onChunk);
|
|
151
|
+
if (parsed.text) {
|
|
152
|
+
fullText += parsed.text;
|
|
153
|
+
onDelta(parsed.text);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return fullText;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function parseNonStreaming(response: Response, adapter: Required<ChatbotAdapter>) {
|
|
161
|
+
const contentType = response.headers.get("content-type") || "";
|
|
162
|
+
|
|
163
|
+
if (contentType.includes("application/json")) {
|
|
164
|
+
const payload = (await response.json()) as unknown;
|
|
165
|
+
const parsed = adapter.parseJsonResponse(payload);
|
|
166
|
+
return parsed.message ?? parsed.delta ?? "";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const rawText = await response.text();
|
|
170
|
+
try {
|
|
171
|
+
const parsedPayload = JSON.parse(rawText) as unknown;
|
|
172
|
+
const parsed = adapter.parseJsonResponse(parsedPayload);
|
|
173
|
+
return parsed.message ?? parsed.delta ?? rawText;
|
|
174
|
+
} catch {
|
|
175
|
+
const parsed = adapter.parseJsonResponse(rawText);
|
|
176
|
+
return parsed.message ?? rawText;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function readAssistantResponse({
|
|
181
|
+
response,
|
|
182
|
+
adapter,
|
|
183
|
+
streamingMode,
|
|
184
|
+
onDelta,
|
|
185
|
+
onChunk,
|
|
186
|
+
onRecoverableError,
|
|
187
|
+
}: ResponseInput): Promise<string> {
|
|
188
|
+
if (!response.ok) {
|
|
189
|
+
const body = await response.text();
|
|
190
|
+
throw new Error(`Webhook request failed (${response.status}): ${body}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const contentType = response.headers.get("content-type");
|
|
194
|
+
const streamEligible = streamingMode !== "off" && Boolean(response.body);
|
|
195
|
+
const shouldStream = streamEligible && (streamingMode === "on" || looksLikeStream(contentType));
|
|
196
|
+
|
|
197
|
+
if (!shouldStream) {
|
|
198
|
+
return parseNonStreaming(response, adapter);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
if ((contentType || "").toLowerCase().includes("text/event-stream")) {
|
|
203
|
+
return await consumeEventStream(response, adapter, onDelta, onChunk);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return await consumeLineStream(response, adapter, onDelta, onChunk);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
onRecoverableError(error as Error);
|
|
209
|
+
return "";
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export type ChatRole = "user" | "assistant" | "system";
|
|
4
|
+
export type MessageStatus = "pending" | "streaming" | "complete" | "error";
|
|
5
|
+
|
|
6
|
+
export interface ChatAttachment {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
size: number;
|
|
10
|
+
type: string;
|
|
11
|
+
file?: File;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ChatMessage {
|
|
15
|
+
id: string;
|
|
16
|
+
role: ChatRole;
|
|
17
|
+
content: string;
|
|
18
|
+
createdAt: string;
|
|
19
|
+
status?: MessageStatus;
|
|
20
|
+
attachments?: ChatAttachment[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface NormalizedAssistantChunk {
|
|
24
|
+
delta?: string;
|
|
25
|
+
done?: boolean;
|
|
26
|
+
message?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ChatbotAdapterRequestInput {
|
|
30
|
+
webhookUrl: string;
|
|
31
|
+
message: string;
|
|
32
|
+
sessionId: string;
|
|
33
|
+
agentName: string;
|
|
34
|
+
files: File[];
|
|
35
|
+
metadata: Record<string, unknown>;
|
|
36
|
+
headers?: Record<string, string>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ChatbotAdapter {
|
|
40
|
+
buildRequest?: (input: ChatbotAdapterRequestInput) => RequestInit & { url?: string };
|
|
41
|
+
parseJsonResponse?: (raw: unknown) => NormalizedAssistantChunk;
|
|
42
|
+
parseStreamChunk?: (chunk: string) => NormalizedAssistantChunk;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ChatbotThemeTokens {
|
|
46
|
+
primary: string;
|
|
47
|
+
primaryForeground: string;
|
|
48
|
+
background: string;
|
|
49
|
+
surface: string;
|
|
50
|
+
surfaceForeground: string;
|
|
51
|
+
muted: string;
|
|
52
|
+
mutedForeground: string;
|
|
53
|
+
border: string;
|
|
54
|
+
ring: string;
|
|
55
|
+
userBubble: string;
|
|
56
|
+
userText: string;
|
|
57
|
+
assistantBubble: string;
|
|
58
|
+
assistantText: string;
|
|
59
|
+
radius: string;
|
|
60
|
+
fontFamily: string;
|
|
61
|
+
shadowBubble: string;
|
|
62
|
+
shadowPanel: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ChatbotIconProps {
|
|
66
|
+
size?: string | number;
|
|
67
|
+
className?: string;
|
|
68
|
+
strokeWidth?: string | number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ChatbotIcons {
|
|
72
|
+
/**
|
|
73
|
+
* Legacy alias. If provided, it maps to launcherClosed when launcherClosed is not set.
|
|
74
|
+
*/
|
|
75
|
+
launcher?: ComponentType<ChatbotIconProps>;
|
|
76
|
+
launcherClosed: ComponentType<ChatbotIconProps>;
|
|
77
|
+
launcherOpen: ComponentType<ChatbotIconProps>;
|
|
78
|
+
close: ComponentType<ChatbotIconProps>;
|
|
79
|
+
send: ComponentType<ChatbotIconProps>;
|
|
80
|
+
attach: ComponentType<ChatbotIconProps>;
|
|
81
|
+
bot: ComponentType<ChatbotIconProps>;
|
|
82
|
+
menu: ComponentType<ChatbotIconProps>;
|
|
83
|
+
expand: ComponentType<ChatbotIconProps>;
|
|
84
|
+
download: ComponentType<ChatbotIconProps>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ChatbotEventHandlers {
|
|
88
|
+
onOpen: () => void;
|
|
89
|
+
onClose: () => void;
|
|
90
|
+
onMessageSent: (message: ChatMessage) => void;
|
|
91
|
+
onChunkReceived: (chunk: string) => void;
|
|
92
|
+
onResponseComplete: (message: ChatMessage) => void;
|
|
93
|
+
onError: (error: Error) => void;
|
|
94
|
+
onAttachmentAdded: (attachments: ChatAttachment[]) => void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ChatbotClassNames {
|
|
98
|
+
root: string;
|
|
99
|
+
bubble: string;
|
|
100
|
+
panel: string;
|
|
101
|
+
header: string;
|
|
102
|
+
headerMeta: string;
|
|
103
|
+
menu: string;
|
|
104
|
+
menuItem: string;
|
|
105
|
+
body: string;
|
|
106
|
+
footer: string;
|
|
107
|
+
composer: string;
|
|
108
|
+
input: string;
|
|
109
|
+
sendButton: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface PageConfigOverrides {
|
|
113
|
+
enabled?: boolean;
|
|
114
|
+
agentName?: string;
|
|
115
|
+
assistantNote?: string;
|
|
116
|
+
enableUploads?: boolean;
|
|
117
|
+
theme?: Partial<ChatbotThemeTokens>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface ChatbotProviderProps {
|
|
121
|
+
webhookUrl: string;
|
|
122
|
+
children: ReactNode;
|
|
123
|
+
locale?: string;
|
|
124
|
+
agentName?: string;
|
|
125
|
+
assistantNote?: string;
|
|
126
|
+
enableUploads?: boolean;
|
|
127
|
+
streamingMode?: "auto" | "on" | "off";
|
|
128
|
+
headers?: Record<string, string>;
|
|
129
|
+
theme?: Partial<ChatbotThemeTokens>;
|
|
130
|
+
icons?: Partial<ChatbotIcons>;
|
|
131
|
+
adapter?: ChatbotAdapter;
|
|
132
|
+
events?: Partial<ChatbotEventHandlers>;
|
|
133
|
+
initialOpen?: boolean;
|
|
134
|
+
position?: "bottom-right" | "bottom-left";
|
|
135
|
+
classNames?: Partial<ChatbotClassNames>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface SendMessageInput {
|
|
139
|
+
text: string;
|
|
140
|
+
files?: File[];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface ChatbotContextValue {
|
|
144
|
+
isOpen: boolean;
|
|
145
|
+
isAwaiting: boolean;
|
|
146
|
+
messages: ChatMessage[];
|
|
147
|
+
sessionId: string;
|
|
148
|
+
uploadEnabled: boolean;
|
|
149
|
+
agentName: string;
|
|
150
|
+
assistantNote: string;
|
|
151
|
+
disclaimerText: string;
|
|
152
|
+
position: "bottom-right" | "bottom-left";
|
|
153
|
+
themeVars: Record<string, string>;
|
|
154
|
+
icons: ChatbotIcons;
|
|
155
|
+
classNames: Partial<ChatbotClassNames>;
|
|
156
|
+
open: () => void;
|
|
157
|
+
close: () => void;
|
|
158
|
+
toggle: () => void;
|
|
159
|
+
sendMessage: (input: SendMessageInput) => Promise<void>;
|
|
160
|
+
clearConversation: () => void;
|
|
161
|
+
registerPageConfig: (id: string, config: PageConfigOverrides) => void;
|
|
162
|
+
unregisterPageConfig: (id: string) => void;
|
|
163
|
+
widgetEnabled: boolean;
|
|
164
|
+
resetSession: () => void;
|
|
165
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./chatbot";
|