@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,17 @@
|
|
|
1
|
+
import ReactMarkdown from "react-markdown";
|
|
2
|
+
import rehypeSanitize from "rehype-sanitize";
|
|
3
|
+
import remarkGfm from "remark-gfm";
|
|
4
|
+
|
|
5
|
+
interface MessageMarkdownProps {
|
|
6
|
+
content: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function MessageMarkdown({ content }: MessageMarkdownProps) {
|
|
10
|
+
return (
|
|
11
|
+
<div className="cb-markdown cb-message-text cb-text-base cb-leading-6">
|
|
12
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeSanitize]}>
|
|
13
|
+
{content}
|
|
14
|
+
</ReactMarkdown>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useMemo,
|
|
6
|
+
useEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from "react";
|
|
10
|
+
import type {
|
|
11
|
+
ChatMessage,
|
|
12
|
+
ChatbotContextValue,
|
|
13
|
+
ChatbotProviderProps,
|
|
14
|
+
PageConfigOverrides,
|
|
15
|
+
} from "../types";
|
|
16
|
+
import { DEFAULT_AGENT_NAME, DEFAULT_ASSISTANT_NOTE, DEFAULT_ICONS, DEFAULT_THEME } from "../lib/defaults";
|
|
17
|
+
import { withDefaultAdapter } from "../lib/adapter";
|
|
18
|
+
import { readAssistantResponse } from "../lib/transport";
|
|
19
|
+
import { createId } from "../lib/id";
|
|
20
|
+
import { getOrCreateSessionId, historyKey, resetSessionId } from "../lib/session";
|
|
21
|
+
import { resolveThemeTokens, toThemeCssVars } from "../lib/theme";
|
|
22
|
+
|
|
23
|
+
const ChatbotContext = createContext<ChatbotContextValue | undefined>(undefined);
|
|
24
|
+
const WELCOME_DELAY_MS = 1500;
|
|
25
|
+
const DEFAULT_LOCALE = "en";
|
|
26
|
+
const LOCALE_COPY: Record<
|
|
27
|
+
string,
|
|
28
|
+
{
|
|
29
|
+
agentName: string;
|
|
30
|
+
assistantNote: string;
|
|
31
|
+
welcomeMessage: string;
|
|
32
|
+
disclaimer: string;
|
|
33
|
+
}
|
|
34
|
+
> = {
|
|
35
|
+
en: {
|
|
36
|
+
agentName: DEFAULT_AGENT_NAME,
|
|
37
|
+
assistantNote: DEFAULT_ASSISTANT_NOTE,
|
|
38
|
+
welcomeMessage: "Hi, how can I help you?",
|
|
39
|
+
disclaimer: "AI can make mistakes.",
|
|
40
|
+
},
|
|
41
|
+
de: {
|
|
42
|
+
agentName: "Assistent",
|
|
43
|
+
assistantNote: "Wie kann ich helfen?",
|
|
44
|
+
welcomeMessage: "Hallo, wie kann ich Ihnen helfen?",
|
|
45
|
+
disclaimer: "KI kann Fehler machen.",
|
|
46
|
+
},
|
|
47
|
+
es: {
|
|
48
|
+
agentName: "Asistente",
|
|
49
|
+
assistantNote: "Como puedo ayudarte?",
|
|
50
|
+
welcomeMessage: "Hola, como puedo ayudarte?",
|
|
51
|
+
disclaimer: "La IA puede cometer errores.",
|
|
52
|
+
},
|
|
53
|
+
fr: {
|
|
54
|
+
agentName: "Assistant",
|
|
55
|
+
assistantNote: "Comment puis-je vous aider ?",
|
|
56
|
+
welcomeMessage: "Bonjour, comment puis-je vous aider ?",
|
|
57
|
+
disclaimer: "L'IA peut faire des erreurs.",
|
|
58
|
+
},
|
|
59
|
+
it: {
|
|
60
|
+
agentName: "Assistente",
|
|
61
|
+
assistantNote: "Come posso aiutarti?",
|
|
62
|
+
welcomeMessage: "Ciao, come posso aiutarti?",
|
|
63
|
+
disclaimer: "L'IA puo commettere errori.",
|
|
64
|
+
},
|
|
65
|
+
nl: {
|
|
66
|
+
agentName: "Assistent",
|
|
67
|
+
assistantNote: "Hoe kan ik je helpen?",
|
|
68
|
+
welcomeMessage: "Hallo, hoe kan ik je helpen?",
|
|
69
|
+
disclaimer: "AI kan fouten maken.",
|
|
70
|
+
},
|
|
71
|
+
pt: {
|
|
72
|
+
agentName: "Assistente",
|
|
73
|
+
assistantNote: "Como posso ajudar?",
|
|
74
|
+
welcomeMessage: "Ola, como posso ajudar?",
|
|
75
|
+
disclaimer: "A IA pode cometer erros.",
|
|
76
|
+
},
|
|
77
|
+
pl: {
|
|
78
|
+
agentName: "Asystent",
|
|
79
|
+
assistantNote: "Jak moge pomoc?",
|
|
80
|
+
welcomeMessage: "Czesc, jak moge pomoc?",
|
|
81
|
+
disclaimer: "AI moze popelniac bledy.",
|
|
82
|
+
},
|
|
83
|
+
tr: {
|
|
84
|
+
agentName: "Asistan",
|
|
85
|
+
assistantNote: "Nasil yardimci olabilirim?",
|
|
86
|
+
welcomeMessage: "Merhaba, nasil yardimci olabilirim?",
|
|
87
|
+
disclaimer: "Yapay zeka hata yapabilir.",
|
|
88
|
+
},
|
|
89
|
+
sv: {
|
|
90
|
+
agentName: "Assistent",
|
|
91
|
+
assistantNote: "Hur kan jag hjalpa dig?",
|
|
92
|
+
welcomeMessage: "Hej, hur kan jag hjalpa dig?",
|
|
93
|
+
disclaimer: "AI kan gora misstag.",
|
|
94
|
+
},
|
|
95
|
+
da: {
|
|
96
|
+
agentName: "Assistent",
|
|
97
|
+
assistantNote: "Hvordan kan jeg hjaelpe dig?",
|
|
98
|
+
welcomeMessage: "Hej, hvordan kan jeg hjaelpe dig?",
|
|
99
|
+
disclaimer: "AI kan lave fejl.",
|
|
100
|
+
},
|
|
101
|
+
no: {
|
|
102
|
+
agentName: "Assistent",
|
|
103
|
+
assistantNote: "Hvordan kan jeg hjelpe deg?",
|
|
104
|
+
welcomeMessage: "Hei, hvordan kan jeg hjelpe deg?",
|
|
105
|
+
disclaimer: "AI kan gjøre feil.",
|
|
106
|
+
},
|
|
107
|
+
fi: {
|
|
108
|
+
agentName: "Avustaja",
|
|
109
|
+
assistantNote: "Miten voin auttaa?",
|
|
110
|
+
welcomeMessage: "Hei, miten voin auttaa?",
|
|
111
|
+
disclaimer: "Tekoaly voi tehda virheita.",
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
function normalizeLocale(input: string | undefined): string {
|
|
116
|
+
if (!input) {
|
|
117
|
+
return DEFAULT_LOCALE;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return input.trim().toLowerCase().split("-")[0] || DEFAULT_LOCALE;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getBrowserLocale() {
|
|
124
|
+
if (typeof navigator === "undefined") {
|
|
125
|
+
return DEFAULT_LOCALE;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return navigator.language || DEFAULT_LOCALE;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getLocaleCopy(locale: string) {
|
|
132
|
+
return LOCALE_COPY[locale] ?? LOCALE_COPY[DEFAULT_LOCALE];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function readHistory(storage: Storage | undefined, sessionId: string): ChatMessage[] {
|
|
136
|
+
if (!storage) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const raw = storage.getItem(historyKey(sessionId));
|
|
141
|
+
if (!raw) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const parsed = JSON.parse(raw) as ChatMessage[];
|
|
147
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
148
|
+
} catch {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function ChatbotProvider({
|
|
154
|
+
children,
|
|
155
|
+
webhookUrl,
|
|
156
|
+
locale,
|
|
157
|
+
agentName,
|
|
158
|
+
assistantNote,
|
|
159
|
+
enableUploads = false,
|
|
160
|
+
streamingMode = "auto",
|
|
161
|
+
headers,
|
|
162
|
+
theme,
|
|
163
|
+
icons,
|
|
164
|
+
adapter,
|
|
165
|
+
events,
|
|
166
|
+
initialOpen = false,
|
|
167
|
+
position = "bottom-right",
|
|
168
|
+
classNames,
|
|
169
|
+
}: ChatbotProviderProps) {
|
|
170
|
+
const [isHydrated, setIsHydrated] = useState(false);
|
|
171
|
+
const [sessionId, setSessionId] = useState("cb:pending");
|
|
172
|
+
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
173
|
+
const [isOpen, setIsOpen] = useState(initialOpen);
|
|
174
|
+
const [isAwaiting, setIsAwaiting] = useState(false);
|
|
175
|
+
const [pageConfigs, setPageConfigs] = useState<Array<{ id: string; config: PageConfigOverrides }>>([]);
|
|
176
|
+
const messagesRef = useRef<ChatMessage[]>([]);
|
|
177
|
+
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
messagesRef.current = messages;
|
|
180
|
+
}, [messages]);
|
|
181
|
+
|
|
182
|
+
const activePageConfig = pageConfigs.length > 0 ? pageConfigs[pageConfigs.length - 1].config : undefined;
|
|
183
|
+
const mergedTheme = useMemo(
|
|
184
|
+
() => ({
|
|
185
|
+
...(theme ?? {}),
|
|
186
|
+
...(activePageConfig?.theme ?? {}),
|
|
187
|
+
}),
|
|
188
|
+
[activePageConfig?.theme, theme],
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const resolvedTheme = useMemo(() => {
|
|
192
|
+
if (!isHydrated) {
|
|
193
|
+
return { ...DEFAULT_THEME, ...(mergedTheme ?? {}) };
|
|
194
|
+
}
|
|
195
|
+
return resolveThemeTokens(mergedTheme);
|
|
196
|
+
}, [isHydrated, mergedTheme]);
|
|
197
|
+
const themeVars = useMemo(() => toThemeCssVars(resolvedTheme), [resolvedTheme]);
|
|
198
|
+
const mergedIcons = useMemo(() => {
|
|
199
|
+
const merged = { ...DEFAULT_ICONS, ...(icons ?? {}) };
|
|
200
|
+
merged.launcherClosed = icons?.launcherClosed ?? icons?.launcher ?? DEFAULT_ICONS.launcherClosed;
|
|
201
|
+
merged.launcherOpen = icons?.launcherOpen ?? DEFAULT_ICONS.launcherOpen;
|
|
202
|
+
return merged;
|
|
203
|
+
}, [icons]);
|
|
204
|
+
|
|
205
|
+
const resolvedLocale = useMemo(() => normalizeLocale(locale ?? getBrowserLocale()), [locale]);
|
|
206
|
+
const localeCopy = useMemo(() => getLocaleCopy(resolvedLocale), [resolvedLocale]);
|
|
207
|
+
const welcomeAssistantMessage = localeCopy.welcomeMessage;
|
|
208
|
+
const disclaimerText = localeCopy.disclaimer;
|
|
209
|
+
const resolvedAgentName = activePageConfig?.agentName ?? agentName ?? localeCopy.agentName;
|
|
210
|
+
const resolvedAssistantNote = activePageConfig?.assistantNote ?? assistantNote ?? localeCopy.assistantNote;
|
|
211
|
+
const uploadEnabled = activePageConfig?.enableUploads ?? enableUploads;
|
|
212
|
+
const widgetEnabled = activePageConfig?.enabled ?? true;
|
|
213
|
+
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
if (typeof window === "undefined") {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const storage = window.sessionStorage;
|
|
220
|
+
const nextSessionId = getOrCreateSessionId(storage);
|
|
221
|
+
setSessionId(nextSessionId);
|
|
222
|
+
setMessages(readHistory(storage, nextSessionId));
|
|
223
|
+
setIsHydrated(true);
|
|
224
|
+
}, []);
|
|
225
|
+
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
if (!isHydrated || typeof window === "undefined") {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const storage = window.sessionStorage;
|
|
232
|
+
storage.setItem(historyKey(sessionId), JSON.stringify(messages));
|
|
233
|
+
}, [isHydrated, messages, sessionId]);
|
|
234
|
+
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
if (!isHydrated || !isOpen || !widgetEnabled || typeof window === "undefined") {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let timeoutId: number | undefined;
|
|
241
|
+
|
|
242
|
+
setMessages((current) => {
|
|
243
|
+
if (current.length > 0) {
|
|
244
|
+
return current;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const welcomeId = createId("msg");
|
|
248
|
+
timeoutId = window.setTimeout(() => {
|
|
249
|
+
setMessages((next) =>
|
|
250
|
+
next.map((message): ChatMessage =>
|
|
251
|
+
message.id === welcomeId
|
|
252
|
+
? {
|
|
253
|
+
...message,
|
|
254
|
+
content: welcomeAssistantMessage,
|
|
255
|
+
status: "complete",
|
|
256
|
+
}
|
|
257
|
+
: message,
|
|
258
|
+
),
|
|
259
|
+
);
|
|
260
|
+
}, WELCOME_DELAY_MS);
|
|
261
|
+
|
|
262
|
+
return [
|
|
263
|
+
...current,
|
|
264
|
+
{
|
|
265
|
+
id: welcomeId,
|
|
266
|
+
role: "assistant",
|
|
267
|
+
content: "",
|
|
268
|
+
createdAt: new Date().toISOString(),
|
|
269
|
+
status: "streaming",
|
|
270
|
+
},
|
|
271
|
+
];
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
return () => {
|
|
275
|
+
if (timeoutId) {
|
|
276
|
+
window.clearTimeout(timeoutId);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}, [isHydrated, isOpen, welcomeAssistantMessage, widgetEnabled]);
|
|
280
|
+
|
|
281
|
+
const open = useCallback(() => {
|
|
282
|
+
setIsOpen(true);
|
|
283
|
+
events?.onOpen?.();
|
|
284
|
+
}, [events]);
|
|
285
|
+
|
|
286
|
+
const close = useCallback(() => {
|
|
287
|
+
setIsOpen(false);
|
|
288
|
+
events?.onClose?.();
|
|
289
|
+
}, [events]);
|
|
290
|
+
|
|
291
|
+
const toggle = useCallback(() => {
|
|
292
|
+
setIsOpen((current) => {
|
|
293
|
+
const next = !current;
|
|
294
|
+
if (next) {
|
|
295
|
+
events?.onOpen?.();
|
|
296
|
+
} else {
|
|
297
|
+
events?.onClose?.();
|
|
298
|
+
}
|
|
299
|
+
return next;
|
|
300
|
+
});
|
|
301
|
+
}, [events]);
|
|
302
|
+
|
|
303
|
+
const clearConversation = useCallback(() => {
|
|
304
|
+
setMessages([]);
|
|
305
|
+
if (typeof window !== "undefined") {
|
|
306
|
+
const storage = window.sessionStorage;
|
|
307
|
+
storage.removeItem(historyKey(sessionId));
|
|
308
|
+
}
|
|
309
|
+
}, [sessionId]);
|
|
310
|
+
|
|
311
|
+
const resetSession = useCallback(() => {
|
|
312
|
+
const storage = typeof window !== "undefined" ? window.sessionStorage : undefined;
|
|
313
|
+
const next = resetSessionId(storage);
|
|
314
|
+
setSessionId(next);
|
|
315
|
+
setMessages([]);
|
|
316
|
+
}, []);
|
|
317
|
+
|
|
318
|
+
const registerPageConfig = useCallback((id: string, config: PageConfigOverrides) => {
|
|
319
|
+
setPageConfigs((current) => {
|
|
320
|
+
const filtered = current.filter((entry) => entry.id !== id);
|
|
321
|
+
return [...filtered, { id, config }];
|
|
322
|
+
});
|
|
323
|
+
}, []);
|
|
324
|
+
|
|
325
|
+
const unregisterPageConfig = useCallback((id: string) => {
|
|
326
|
+
setPageConfigs((current) => current.filter((entry) => entry.id !== id));
|
|
327
|
+
}, []);
|
|
328
|
+
|
|
329
|
+
const sendMessage = useCallback<ChatbotContextValue["sendMessage"]>(
|
|
330
|
+
async ({ text, files = [] }) => {
|
|
331
|
+
const trimmed = text.trim();
|
|
332
|
+
if (!trimmed && files.length === 0) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const now = new Date().toISOString();
|
|
337
|
+
const attachmentDtos = files.map((file) => ({
|
|
338
|
+
id: createId("att"),
|
|
339
|
+
name: file.name,
|
|
340
|
+
size: file.size,
|
|
341
|
+
type: file.type,
|
|
342
|
+
file,
|
|
343
|
+
}));
|
|
344
|
+
if (attachmentDtos.length > 0) {
|
|
345
|
+
events?.onAttachmentAdded?.(attachmentDtos);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const userMessage: ChatMessage = {
|
|
349
|
+
id: createId("msg"),
|
|
350
|
+
role: "user",
|
|
351
|
+
content: trimmed,
|
|
352
|
+
createdAt: now,
|
|
353
|
+
status: "complete",
|
|
354
|
+
attachments: attachmentDtos,
|
|
355
|
+
};
|
|
356
|
+
const ensureWelcomeMessage = (input: ChatMessage[]): ChatMessage[] => {
|
|
357
|
+
if (input.some((message) => message.role === "assistant" && message.content === welcomeAssistantMessage)) {
|
|
358
|
+
return input;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const firstStreamingAssistantIndex = input.findIndex(
|
|
362
|
+
(message) => message.role === "assistant" && message.status === "streaming" && !message.content,
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
if (firstStreamingAssistantIndex >= 0) {
|
|
366
|
+
return input.map((message, index): ChatMessage =>
|
|
367
|
+
index === firstStreamingAssistantIndex
|
|
368
|
+
? {
|
|
369
|
+
...message,
|
|
370
|
+
content: welcomeAssistantMessage,
|
|
371
|
+
status: "complete",
|
|
372
|
+
}
|
|
373
|
+
: message,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (input.some((message) => message.role === "assistant")) {
|
|
378
|
+
return input;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const welcomeMessage: ChatMessage = {
|
|
382
|
+
id: createId("msg"),
|
|
383
|
+
role: "assistant",
|
|
384
|
+
content: welcomeAssistantMessage,
|
|
385
|
+
createdAt: now,
|
|
386
|
+
status: "complete",
|
|
387
|
+
};
|
|
388
|
+
return [...input, welcomeMessage];
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const normalizedHistory = ensureWelcomeMessage(messagesRef.current);
|
|
392
|
+
const requestHistory = [...normalizedHistory, userMessage].map((message) => ({
|
|
393
|
+
id: message.id,
|
|
394
|
+
role: message.role,
|
|
395
|
+
content: message.content,
|
|
396
|
+
createdAt: message.createdAt,
|
|
397
|
+
}));
|
|
398
|
+
|
|
399
|
+
setMessages((current) => [...ensureWelcomeMessage(current), userMessage]);
|
|
400
|
+
events?.onMessageSent?.(userMessage);
|
|
401
|
+
|
|
402
|
+
const assistantId = createId("msg");
|
|
403
|
+
setMessages((current) => [
|
|
404
|
+
...current,
|
|
405
|
+
{
|
|
406
|
+
id: assistantId,
|
|
407
|
+
role: "assistant",
|
|
408
|
+
content: "",
|
|
409
|
+
createdAt: new Date().toISOString(),
|
|
410
|
+
status: "streaming",
|
|
411
|
+
},
|
|
412
|
+
]);
|
|
413
|
+
|
|
414
|
+
setIsAwaiting(true);
|
|
415
|
+
const resolvedAdapter = withDefaultAdapter(adapter);
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
const metadata = {
|
|
419
|
+
sessionId,
|
|
420
|
+
pageUrl: typeof window !== "undefined" ? window.location.href : "",
|
|
421
|
+
timestamp: new Date().toISOString(),
|
|
422
|
+
agentName: resolvedAgentName,
|
|
423
|
+
locale: resolvedLocale,
|
|
424
|
+
history: requestHistory,
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const requestInit = resolvedAdapter.buildRequest({
|
|
428
|
+
webhookUrl,
|
|
429
|
+
message: trimmed,
|
|
430
|
+
sessionId,
|
|
431
|
+
agentName: resolvedAgentName,
|
|
432
|
+
files: uploadEnabled ? files : [],
|
|
433
|
+
metadata,
|
|
434
|
+
headers,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const url = requestInit.url ?? webhookUrl;
|
|
438
|
+
const response = await fetch(url, requestInit);
|
|
439
|
+
|
|
440
|
+
let streamedContent = "";
|
|
441
|
+
const responseText = await readAssistantResponse({
|
|
442
|
+
response,
|
|
443
|
+
adapter: resolvedAdapter,
|
|
444
|
+
streamingMode,
|
|
445
|
+
onDelta: (delta) => {
|
|
446
|
+
streamedContent += delta;
|
|
447
|
+
setMessages((current) =>
|
|
448
|
+
current.map((message): ChatMessage =>
|
|
449
|
+
message.id === assistantId
|
|
450
|
+
? {
|
|
451
|
+
...message,
|
|
452
|
+
content: `${message.content}${delta}`,
|
|
453
|
+
status: "streaming",
|
|
454
|
+
}
|
|
455
|
+
: message,
|
|
456
|
+
),
|
|
457
|
+
);
|
|
458
|
+
},
|
|
459
|
+
onChunk: (chunk) => {
|
|
460
|
+
events?.onChunkReceived?.(chunk);
|
|
461
|
+
},
|
|
462
|
+
onRecoverableError: (error) => {
|
|
463
|
+
events?.onError?.(error);
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
setMessages((current) => {
|
|
468
|
+
const next: ChatMessage[] = current.map((message): ChatMessage => {
|
|
469
|
+
if (message.id !== assistantId) {
|
|
470
|
+
return message;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const hasStreamed = streamedContent.length > 0;
|
|
474
|
+
const content = hasStreamed ? message.content : responseText;
|
|
475
|
+
return {
|
|
476
|
+
...message,
|
|
477
|
+
content,
|
|
478
|
+
status: "complete",
|
|
479
|
+
};
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const completeMessage = next.find((message) => message.id === assistantId);
|
|
483
|
+
if (completeMessage) {
|
|
484
|
+
events?.onResponseComplete?.(completeMessage);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return next;
|
|
488
|
+
});
|
|
489
|
+
} catch (error) {
|
|
490
|
+
const typedError = error instanceof Error ? error : new Error(String(error));
|
|
491
|
+
events?.onError?.(typedError);
|
|
492
|
+
|
|
493
|
+
setMessages((current) =>
|
|
494
|
+
current.map((message): ChatMessage =>
|
|
495
|
+
message.id === assistantId
|
|
496
|
+
? {
|
|
497
|
+
...message,
|
|
498
|
+
content: message.content || "Sorry, I ran into an issue while generating a response.",
|
|
499
|
+
status: "error",
|
|
500
|
+
}
|
|
501
|
+
: message,
|
|
502
|
+
),
|
|
503
|
+
);
|
|
504
|
+
} finally {
|
|
505
|
+
setIsAwaiting(false);
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
[
|
|
509
|
+
adapter,
|
|
510
|
+
events,
|
|
511
|
+
headers,
|
|
512
|
+
resolvedAgentName,
|
|
513
|
+
resolvedLocale,
|
|
514
|
+
sessionId,
|
|
515
|
+
streamingMode,
|
|
516
|
+
uploadEnabled,
|
|
517
|
+
welcomeAssistantMessage,
|
|
518
|
+
webhookUrl,
|
|
519
|
+
],
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
const value = useMemo<ChatbotContextValue>(
|
|
523
|
+
() => ({
|
|
524
|
+
isOpen,
|
|
525
|
+
isAwaiting,
|
|
526
|
+
messages,
|
|
527
|
+
sessionId,
|
|
528
|
+
uploadEnabled,
|
|
529
|
+
agentName: resolvedAgentName,
|
|
530
|
+
assistantNote: resolvedAssistantNote,
|
|
531
|
+
disclaimerText,
|
|
532
|
+
position,
|
|
533
|
+
themeVars,
|
|
534
|
+
icons: mergedIcons,
|
|
535
|
+
classNames: classNames ?? {},
|
|
536
|
+
open,
|
|
537
|
+
close,
|
|
538
|
+
toggle,
|
|
539
|
+
sendMessage,
|
|
540
|
+
clearConversation,
|
|
541
|
+
registerPageConfig,
|
|
542
|
+
unregisterPageConfig,
|
|
543
|
+
widgetEnabled,
|
|
544
|
+
resetSession,
|
|
545
|
+
}),
|
|
546
|
+
[
|
|
547
|
+
classNames,
|
|
548
|
+
clearConversation,
|
|
549
|
+
close,
|
|
550
|
+
disclaimerText,
|
|
551
|
+
isAwaiting,
|
|
552
|
+
isOpen,
|
|
553
|
+
mergedIcons,
|
|
554
|
+
messages,
|
|
555
|
+
open,
|
|
556
|
+
position,
|
|
557
|
+
registerPageConfig,
|
|
558
|
+
resolvedAgentName,
|
|
559
|
+
resolvedAssistantNote,
|
|
560
|
+
sendMessage,
|
|
561
|
+
sessionId,
|
|
562
|
+
themeVars,
|
|
563
|
+
toggle,
|
|
564
|
+
unregisterPageConfig,
|
|
565
|
+
uploadEnabled,
|
|
566
|
+
widgetEnabled,
|
|
567
|
+
resetSession,
|
|
568
|
+
],
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
return <ChatbotContext.Provider value={value}>{children}</ChatbotContext.Provider>;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export function useChatbotContext() {
|
|
575
|
+
const context = useContext(ChatbotContext);
|
|
576
|
+
if (!context) {
|
|
577
|
+
throw new Error("useChatbotContext must be used within ChatbotProvider");
|
|
578
|
+
}
|
|
579
|
+
return context;
|
|
580
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useChatbotContext } from "../context/chatbot-context";
|
|
3
|
+
|
|
4
|
+
export function useChatbotSession() {
|
|
5
|
+
const { sessionId, resetSession } = useChatbotContext();
|
|
6
|
+
|
|
7
|
+
return useMemo(
|
|
8
|
+
() => ({
|
|
9
|
+
sessionId,
|
|
10
|
+
resetSession,
|
|
11
|
+
}),
|
|
12
|
+
[resetSession, sessionId],
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { ChatbotProvider } from "./context/chatbot-context";
|
|
2
|
+
export { ChatbotWidget } from "./components/chatbot-widget";
|
|
3
|
+
export { ChatbotPageConfig } from "./components/chatbot-page-config";
|
|
4
|
+
|
|
5
|
+
export { useChatbot } from "./hooks/use-chatbot";
|
|
6
|
+
export { useChatbotSession } from "./hooks/use-chatbot-session";
|
|
7
|
+
|
|
8
|
+
export type {
|
|
9
|
+
ChatMessage,
|
|
10
|
+
ChatAttachment,
|
|
11
|
+
ChatbotAdapter,
|
|
12
|
+
ChatbotAdapterRequestInput,
|
|
13
|
+
ChatbotClassNames,
|
|
14
|
+
ChatbotContextValue,
|
|
15
|
+
ChatbotEventHandlers,
|
|
16
|
+
ChatbotIcons,
|
|
17
|
+
ChatbotProviderProps,
|
|
18
|
+
ChatbotThemeTokens,
|
|
19
|
+
NormalizedAssistantChunk,
|
|
20
|
+
PageConfigOverrides,
|
|
21
|
+
SendMessageInput,
|
|
22
|
+
} from "./types";
|