@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/dist/index.mjs ADDED
@@ -0,0 +1,1633 @@
1
+ // src/chatbot/context/chatbot-context.tsx
2
+ import {
3
+ createContext,
4
+ useCallback,
5
+ useContext,
6
+ useMemo,
7
+ useEffect,
8
+ useRef,
9
+ useState
10
+ } from "react";
11
+
12
+ // src/chatbot/lib/defaults.ts
13
+ import {
14
+ Bot,
15
+ ChevronDown,
16
+ Download,
17
+ Ellipsis,
18
+ Maximize2,
19
+ MessageSquare,
20
+ Paperclip,
21
+ SendHorizontal,
22
+ X
23
+ } from "lucide-react";
24
+ var DEFAULT_THEME = {
25
+ primary: "#ffffff",
26
+ primaryForeground: "#111111",
27
+ background: "#0b0b0b",
28
+ surface: "#111111",
29
+ surfaceForeground: "#f5f5f5",
30
+ muted: "#262626",
31
+ mutedForeground: "#a3a3a3",
32
+ border: "#2f2f2f",
33
+ ring: "#737373",
34
+ userBubble: "#ffffff",
35
+ userText: "#111111",
36
+ assistantBubble: "#2a2a2a",
37
+ assistantText: "#f5f5f5",
38
+ radius: "14px",
39
+ fontFamily: "Inter, var(--font-sans), ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif",
40
+ shadowBubble: "0 18px 40px -16px rgba(0, 0, 0, 0.7)",
41
+ shadowPanel: "0 34px 80px -36px rgba(0, 0, 0, 0.85)"
42
+ };
43
+ var DEFAULT_ICONS = {
44
+ launcher: MessageSquare,
45
+ launcherClosed: MessageSquare,
46
+ launcherOpen: ChevronDown,
47
+ close: X,
48
+ send: SendHorizontal,
49
+ attach: Paperclip,
50
+ bot: Bot,
51
+ menu: Ellipsis,
52
+ expand: Maximize2,
53
+ download: Download
54
+ };
55
+ var DEFAULT_AGENT_NAME = "Assistant";
56
+ var DEFAULT_ASSISTANT_NOTE = "Here to help";
57
+
58
+ // src/chatbot/lib/adapter.ts
59
+ function extractFromRecord(record) {
60
+ const keys = ["message", "text", "output", "response", "answer", "content"];
61
+ for (const key of keys) {
62
+ const value = record[key];
63
+ if (typeof value === "string" && value.trim().length > 0) {
64
+ return value;
65
+ }
66
+ }
67
+ return void 0;
68
+ }
69
+ function defaultBuildRequest(input) {
70
+ const { files, metadata, message, headers, sessionId } = input;
71
+ if (files.length > 0) {
72
+ const formData = new FormData();
73
+ formData.set("message", message);
74
+ formData.set("sessionId", sessionId);
75
+ formData.set("metadata", JSON.stringify(metadata));
76
+ files.forEach((file) => {
77
+ formData.append("files[]", file);
78
+ });
79
+ return {
80
+ method: "POST",
81
+ headers,
82
+ body: formData
83
+ };
84
+ }
85
+ return {
86
+ method: "POST",
87
+ headers: {
88
+ "Content-Type": "application/json",
89
+ ...headers
90
+ },
91
+ body: JSON.stringify({
92
+ message,
93
+ sessionId,
94
+ metadata
95
+ })
96
+ };
97
+ }
98
+ function defaultParseJsonResponse(raw) {
99
+ if (typeof raw === "string") {
100
+ return { message: raw };
101
+ }
102
+ if (Array.isArray(raw)) {
103
+ const merged = raw.map((item) => {
104
+ if (typeof item === "string") {
105
+ return item;
106
+ }
107
+ if (item && typeof item === "object") {
108
+ return extractFromRecord(item);
109
+ }
110
+ return void 0;
111
+ }).filter(Boolean).join("\n");
112
+ return { message: merged };
113
+ }
114
+ if (raw && typeof raw === "object") {
115
+ const record = raw;
116
+ if (record.delta && typeof record.delta === "string") {
117
+ return {
118
+ delta: record.delta,
119
+ done: Boolean(record.done)
120
+ };
121
+ }
122
+ const text = extractFromRecord(record);
123
+ if (text) {
124
+ return { message: text, done: true };
125
+ }
126
+ if (record.data && typeof record.data === "object") {
127
+ const nested = extractFromRecord(record.data);
128
+ if (nested) {
129
+ return { message: nested, done: true };
130
+ }
131
+ }
132
+ }
133
+ return { message: "", done: true };
134
+ }
135
+ function defaultParseStreamChunk(chunk) {
136
+ const trimmed = chunk.trim();
137
+ if (!trimmed) {
138
+ return {};
139
+ }
140
+ if (trimmed === "[DONE]") {
141
+ return { done: true };
142
+ }
143
+ try {
144
+ const parsed = JSON.parse(trimmed);
145
+ return defaultParseJsonResponse(parsed);
146
+ } catch {
147
+ return { delta: trimmed };
148
+ }
149
+ }
150
+ function withDefaultAdapter(adapter) {
151
+ return {
152
+ buildRequest: adapter?.buildRequest ?? defaultBuildRequest,
153
+ parseJsonResponse: adapter?.parseJsonResponse ?? defaultParseJsonResponse,
154
+ parseStreamChunk: adapter?.parseStreamChunk ?? defaultParseStreamChunk
155
+ };
156
+ }
157
+
158
+ // src/chatbot/lib/transport.ts
159
+ function looksLikeStream(contentType) {
160
+ if (!contentType) {
161
+ return false;
162
+ }
163
+ const normalized = contentType.toLowerCase();
164
+ return normalized.includes("text/event-stream") || normalized.includes("application/x-ndjson") || normalized.includes("application/stream");
165
+ }
166
+ function parseAndAppend(rawChunk, adapter, onChunk) {
167
+ const parsed = adapter.parseStreamChunk(rawChunk);
168
+ onChunk(rawChunk);
169
+ if (parsed.done && parsed.message) {
170
+ return { text: parsed.message, done: true };
171
+ }
172
+ if (parsed.delta) {
173
+ return { text: parsed.delta, done: Boolean(parsed.done) };
174
+ }
175
+ if (parsed.message) {
176
+ return { text: parsed.message, done: Boolean(parsed.done) };
177
+ }
178
+ return { text: "", done: Boolean(parsed.done) };
179
+ }
180
+ async function consumeEventStream(response, adapter, onDelta, onChunk) {
181
+ const reader = response.body?.getReader();
182
+ if (!reader) {
183
+ return "";
184
+ }
185
+ const decoder = new TextDecoder();
186
+ let buffer = "";
187
+ let fullText = "";
188
+ let done = false;
189
+ while (!done) {
190
+ const result = await reader.read();
191
+ if (result.done) {
192
+ break;
193
+ }
194
+ buffer += decoder.decode(result.value, { stream: true });
195
+ while (buffer.includes("\n\n")) {
196
+ const index = buffer.indexOf("\n\n");
197
+ const block = buffer.slice(0, index);
198
+ buffer = buffer.slice(index + 2);
199
+ const lines = block.split("\n").filter((line) => line.startsWith("data:"));
200
+ for (const line of lines) {
201
+ const data = line.slice(5).trim();
202
+ const parsed = parseAndAppend(data, adapter, onChunk);
203
+ if (parsed.text) {
204
+ fullText += parsed.text;
205
+ onDelta(parsed.text);
206
+ }
207
+ if (parsed.done) {
208
+ done = true;
209
+ break;
210
+ }
211
+ }
212
+ }
213
+ }
214
+ if (buffer.trim()) {
215
+ const fallback = parseAndAppend(buffer.trim(), adapter, onChunk);
216
+ if (fallback.text) {
217
+ fullText += fallback.text;
218
+ onDelta(fallback.text);
219
+ }
220
+ }
221
+ return fullText;
222
+ }
223
+ async function consumeLineStream(response, adapter, onDelta, onChunk) {
224
+ const reader = response.body?.getReader();
225
+ if (!reader) {
226
+ return "";
227
+ }
228
+ const decoder = new TextDecoder();
229
+ let buffer = "";
230
+ let fullText = "";
231
+ let done = false;
232
+ while (!done) {
233
+ const result = await reader.read();
234
+ if (result.done) {
235
+ break;
236
+ }
237
+ buffer += decoder.decode(result.value, { stream: true });
238
+ while (buffer.includes("\n")) {
239
+ const index = buffer.indexOf("\n");
240
+ const line = buffer.slice(0, index).trim();
241
+ buffer = buffer.slice(index + 1);
242
+ if (!line) {
243
+ continue;
244
+ }
245
+ const parsed = parseAndAppend(line, adapter, onChunk);
246
+ if (parsed.text) {
247
+ fullText += parsed.text;
248
+ onDelta(parsed.text);
249
+ }
250
+ if (parsed.done) {
251
+ done = true;
252
+ break;
253
+ }
254
+ }
255
+ }
256
+ if (buffer.trim()) {
257
+ const parsed = parseAndAppend(buffer.trim(), adapter, onChunk);
258
+ if (parsed.text) {
259
+ fullText += parsed.text;
260
+ onDelta(parsed.text);
261
+ }
262
+ }
263
+ return fullText;
264
+ }
265
+ async function parseNonStreaming(response, adapter) {
266
+ const contentType = response.headers.get("content-type") || "";
267
+ if (contentType.includes("application/json")) {
268
+ const payload = await response.json();
269
+ const parsed = adapter.parseJsonResponse(payload);
270
+ return parsed.message ?? parsed.delta ?? "";
271
+ }
272
+ const rawText = await response.text();
273
+ try {
274
+ const parsedPayload = JSON.parse(rawText);
275
+ const parsed = adapter.parseJsonResponse(parsedPayload);
276
+ return parsed.message ?? parsed.delta ?? rawText;
277
+ } catch {
278
+ const parsed = adapter.parseJsonResponse(rawText);
279
+ return parsed.message ?? rawText;
280
+ }
281
+ }
282
+ async function readAssistantResponse({
283
+ response,
284
+ adapter,
285
+ streamingMode,
286
+ onDelta,
287
+ onChunk,
288
+ onRecoverableError
289
+ }) {
290
+ if (!response.ok) {
291
+ const body = await response.text();
292
+ throw new Error(`Webhook request failed (${response.status}): ${body}`);
293
+ }
294
+ const contentType = response.headers.get("content-type");
295
+ const streamEligible = streamingMode !== "off" && Boolean(response.body);
296
+ const shouldStream = streamEligible && (streamingMode === "on" || looksLikeStream(contentType));
297
+ if (!shouldStream) {
298
+ return parseNonStreaming(response, adapter);
299
+ }
300
+ try {
301
+ if ((contentType || "").toLowerCase().includes("text/event-stream")) {
302
+ return await consumeEventStream(response, adapter, onDelta, onChunk);
303
+ }
304
+ return await consumeLineStream(response, adapter, onDelta, onChunk);
305
+ } catch (error) {
306
+ onRecoverableError(error);
307
+ return "";
308
+ }
309
+ }
310
+
311
+ // src/chatbot/lib/id.ts
312
+ function createId(prefix = "cb") {
313
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
314
+ return `${prefix}-${crypto.randomUUID()}`;
315
+ }
316
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
317
+ }
318
+
319
+ // src/chatbot/lib/session.ts
320
+ var SESSION_KEY = "cb:session-id";
321
+ function createSessionId() {
322
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
323
+ return crypto.randomUUID();
324
+ }
325
+ return `cb-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
326
+ }
327
+ function getOrCreateSessionId(storage) {
328
+ if (!storage) {
329
+ return createSessionId();
330
+ }
331
+ const existing = storage.getItem(SESSION_KEY);
332
+ if (existing) {
333
+ return existing;
334
+ }
335
+ const created = createSessionId();
336
+ storage.setItem(SESSION_KEY, created);
337
+ return created;
338
+ }
339
+ function resetSessionId(storage) {
340
+ const next = createSessionId();
341
+ if (storage) {
342
+ storage.setItem(SESSION_KEY, next);
343
+ }
344
+ return next;
345
+ }
346
+ function historyKey(sessionId) {
347
+ return `cb:history:${sessionId}`;
348
+ }
349
+
350
+ // src/chatbot/lib/theme.ts
351
+ var HOST_TOKEN_MAP = {
352
+ primary: ["--color-primary", "--primary"],
353
+ primaryForeground: ["--color-primary-foreground", "--primary-foreground"],
354
+ background: ["--cb-background", "--color-background", "--color-bg", "--background"],
355
+ surface: ["--cb-surface", "--color-surface", "--color-card", "--card"],
356
+ surfaceForeground: ["--cb-surface-foreground", "--color-text", "--color-foreground", "--foreground"],
357
+ muted: ["--color-muted", "--muted"],
358
+ mutedForeground: ["--color-muted-foreground", "--muted-foreground"],
359
+ border: ["--color-border", "--border"],
360
+ ring: ["--color-ring", "--ring"],
361
+ userBubble: ["--cb-user-bubble"],
362
+ userText: ["--cb-user-text"],
363
+ assistantBubble: ["--cb-assistant-bubble", "--color-muted", "--muted"],
364
+ assistantText: ["--cb-assistant-text", "--color-text", "--foreground"],
365
+ radius: ["--radius-md", "--radius", "--rounded"],
366
+ fontFamily: ["--font-sans", "--font-family"],
367
+ shadowBubble: ["--shadow-bubble"],
368
+ shadowPanel: ["--shadow-panel"]
369
+ };
370
+ function readRootVar(name) {
371
+ if (typeof window === "undefined") {
372
+ return void 0;
373
+ }
374
+ const styles = getComputedStyle(document.documentElement);
375
+ const value = styles.getPropertyValue(name).trim();
376
+ return value || void 0;
377
+ }
378
+ function resolveThemeTokens(explicit) {
379
+ const resolved = { ...DEFAULT_THEME };
380
+ Object.keys(DEFAULT_THEME).forEach((key) => {
381
+ if (explicit?.[key]) {
382
+ resolved[key] = explicit[key];
383
+ return;
384
+ }
385
+ const hostCandidates = HOST_TOKEN_MAP[key] ?? [];
386
+ for (const candidate of hostCandidates) {
387
+ const value = readRootVar(candidate);
388
+ if (value) {
389
+ resolved[key] = value;
390
+ break;
391
+ }
392
+ }
393
+ });
394
+ return resolved;
395
+ }
396
+ function toThemeCssVars(tokens) {
397
+ return {
398
+ "--cb-primary": tokens.primary,
399
+ "--cb-primary-foreground": tokens.primaryForeground,
400
+ "--cb-background": tokens.background,
401
+ "--cb-surface": tokens.surface,
402
+ "--cb-surface-foreground": tokens.surfaceForeground,
403
+ "--cb-muted": tokens.muted,
404
+ "--cb-muted-foreground": tokens.mutedForeground,
405
+ "--cb-border": tokens.border,
406
+ "--cb-ring": tokens.ring,
407
+ "--cb-user-bubble": tokens.userBubble,
408
+ "--cb-user-text": tokens.userText,
409
+ "--cb-assistant-bubble": tokens.assistantBubble,
410
+ "--cb-assistant-text": tokens.assistantText,
411
+ "--cb-radius": tokens.radius,
412
+ "--cb-font-family": tokens.fontFamily,
413
+ "--cb-shadow-bubble": tokens.shadowBubble,
414
+ "--cb-shadow-panel": tokens.shadowPanel
415
+ };
416
+ }
417
+
418
+ // src/chatbot/context/chatbot-context.tsx
419
+ import { jsx } from "react/jsx-runtime";
420
+ var ChatbotContext = createContext(void 0);
421
+ var WELCOME_DELAY_MS = 1500;
422
+ var DEFAULT_LOCALE = "en";
423
+ var LOCALE_COPY = {
424
+ en: {
425
+ agentName: DEFAULT_AGENT_NAME,
426
+ assistantNote: DEFAULT_ASSISTANT_NOTE,
427
+ welcomeMessage: "Hi, how can I help you?",
428
+ disclaimer: "AI can make mistakes."
429
+ },
430
+ de: {
431
+ agentName: "Assistent",
432
+ assistantNote: "Wie kann ich helfen?",
433
+ welcomeMessage: "Hallo, wie kann ich Ihnen helfen?",
434
+ disclaimer: "KI kann Fehler machen."
435
+ },
436
+ es: {
437
+ agentName: "Asistente",
438
+ assistantNote: "Como puedo ayudarte?",
439
+ welcomeMessage: "Hola, como puedo ayudarte?",
440
+ disclaimer: "La IA puede cometer errores."
441
+ },
442
+ fr: {
443
+ agentName: "Assistant",
444
+ assistantNote: "Comment puis-je vous aider ?",
445
+ welcomeMessage: "Bonjour, comment puis-je vous aider ?",
446
+ disclaimer: "L'IA peut faire des erreurs."
447
+ },
448
+ it: {
449
+ agentName: "Assistente",
450
+ assistantNote: "Come posso aiutarti?",
451
+ welcomeMessage: "Ciao, come posso aiutarti?",
452
+ disclaimer: "L'IA puo commettere errori."
453
+ },
454
+ nl: {
455
+ agentName: "Assistent",
456
+ assistantNote: "Hoe kan ik je helpen?",
457
+ welcomeMessage: "Hallo, hoe kan ik je helpen?",
458
+ disclaimer: "AI kan fouten maken."
459
+ },
460
+ pt: {
461
+ agentName: "Assistente",
462
+ assistantNote: "Como posso ajudar?",
463
+ welcomeMessage: "Ola, como posso ajudar?",
464
+ disclaimer: "A IA pode cometer erros."
465
+ },
466
+ pl: {
467
+ agentName: "Asystent",
468
+ assistantNote: "Jak moge pomoc?",
469
+ welcomeMessage: "Czesc, jak moge pomoc?",
470
+ disclaimer: "AI moze popelniac bledy."
471
+ },
472
+ tr: {
473
+ agentName: "Asistan",
474
+ assistantNote: "Nasil yardimci olabilirim?",
475
+ welcomeMessage: "Merhaba, nasil yardimci olabilirim?",
476
+ disclaimer: "Yapay zeka hata yapabilir."
477
+ },
478
+ sv: {
479
+ agentName: "Assistent",
480
+ assistantNote: "Hur kan jag hjalpa dig?",
481
+ welcomeMessage: "Hej, hur kan jag hjalpa dig?",
482
+ disclaimer: "AI kan gora misstag."
483
+ },
484
+ da: {
485
+ agentName: "Assistent",
486
+ assistantNote: "Hvordan kan jeg hjaelpe dig?",
487
+ welcomeMessage: "Hej, hvordan kan jeg hjaelpe dig?",
488
+ disclaimer: "AI kan lave fejl."
489
+ },
490
+ no: {
491
+ agentName: "Assistent",
492
+ assistantNote: "Hvordan kan jeg hjelpe deg?",
493
+ welcomeMessage: "Hei, hvordan kan jeg hjelpe deg?",
494
+ disclaimer: "AI kan gj\xF8re feil."
495
+ },
496
+ fi: {
497
+ agentName: "Avustaja",
498
+ assistantNote: "Miten voin auttaa?",
499
+ welcomeMessage: "Hei, miten voin auttaa?",
500
+ disclaimer: "Tekoaly voi tehda virheita."
501
+ }
502
+ };
503
+ function normalizeLocale(input) {
504
+ if (!input) {
505
+ return DEFAULT_LOCALE;
506
+ }
507
+ return input.trim().toLowerCase().split("-")[0] || DEFAULT_LOCALE;
508
+ }
509
+ function getBrowserLocale() {
510
+ if (typeof navigator === "undefined") {
511
+ return DEFAULT_LOCALE;
512
+ }
513
+ return navigator.language || DEFAULT_LOCALE;
514
+ }
515
+ function getLocaleCopy(locale) {
516
+ return LOCALE_COPY[locale] ?? LOCALE_COPY[DEFAULT_LOCALE];
517
+ }
518
+ function readHistory(storage, sessionId) {
519
+ if (!storage) {
520
+ return [];
521
+ }
522
+ const raw = storage.getItem(historyKey(sessionId));
523
+ if (!raw) {
524
+ return [];
525
+ }
526
+ try {
527
+ const parsed = JSON.parse(raw);
528
+ return Array.isArray(parsed) ? parsed : [];
529
+ } catch {
530
+ return [];
531
+ }
532
+ }
533
+ function ChatbotProvider({
534
+ children,
535
+ webhookUrl,
536
+ locale,
537
+ agentName,
538
+ assistantNote,
539
+ enableUploads = false,
540
+ streamingMode = "auto",
541
+ headers,
542
+ theme,
543
+ icons,
544
+ adapter,
545
+ events,
546
+ initialOpen = false,
547
+ position = "bottom-right",
548
+ classNames
549
+ }) {
550
+ const [isHydrated, setIsHydrated] = useState(false);
551
+ const [sessionId, setSessionId] = useState("cb:pending");
552
+ const [messages, setMessages] = useState([]);
553
+ const [isOpen, setIsOpen] = useState(initialOpen);
554
+ const [isAwaiting, setIsAwaiting] = useState(false);
555
+ const [pageConfigs, setPageConfigs] = useState([]);
556
+ const messagesRef = useRef([]);
557
+ useEffect(() => {
558
+ messagesRef.current = messages;
559
+ }, [messages]);
560
+ const activePageConfig = pageConfigs.length > 0 ? pageConfigs[pageConfigs.length - 1].config : void 0;
561
+ const mergedTheme = useMemo(
562
+ () => ({
563
+ ...theme ?? {},
564
+ ...activePageConfig?.theme ?? {}
565
+ }),
566
+ [activePageConfig?.theme, theme]
567
+ );
568
+ const resolvedTheme = useMemo(() => {
569
+ if (!isHydrated) {
570
+ return { ...DEFAULT_THEME, ...mergedTheme ?? {} };
571
+ }
572
+ return resolveThemeTokens(mergedTheme);
573
+ }, [isHydrated, mergedTheme]);
574
+ const themeVars = useMemo(() => toThemeCssVars(resolvedTheme), [resolvedTheme]);
575
+ const mergedIcons = useMemo(() => {
576
+ const merged = { ...DEFAULT_ICONS, ...icons ?? {} };
577
+ merged.launcherClosed = icons?.launcherClosed ?? icons?.launcher ?? DEFAULT_ICONS.launcherClosed;
578
+ merged.launcherOpen = icons?.launcherOpen ?? DEFAULT_ICONS.launcherOpen;
579
+ return merged;
580
+ }, [icons]);
581
+ const resolvedLocale = useMemo(() => normalizeLocale(locale ?? getBrowserLocale()), [locale]);
582
+ const localeCopy = useMemo(() => getLocaleCopy(resolvedLocale), [resolvedLocale]);
583
+ const welcomeAssistantMessage = localeCopy.welcomeMessage;
584
+ const disclaimerText = localeCopy.disclaimer;
585
+ const resolvedAgentName = activePageConfig?.agentName ?? agentName ?? localeCopy.agentName;
586
+ const resolvedAssistantNote = activePageConfig?.assistantNote ?? assistantNote ?? localeCopy.assistantNote;
587
+ const uploadEnabled = activePageConfig?.enableUploads ?? enableUploads;
588
+ const widgetEnabled = activePageConfig?.enabled ?? true;
589
+ useEffect(() => {
590
+ if (typeof window === "undefined") {
591
+ return;
592
+ }
593
+ const storage = window.sessionStorage;
594
+ const nextSessionId = getOrCreateSessionId(storage);
595
+ setSessionId(nextSessionId);
596
+ setMessages(readHistory(storage, nextSessionId));
597
+ setIsHydrated(true);
598
+ }, []);
599
+ useEffect(() => {
600
+ if (!isHydrated || typeof window === "undefined") {
601
+ return;
602
+ }
603
+ const storage = window.sessionStorage;
604
+ storage.setItem(historyKey(sessionId), JSON.stringify(messages));
605
+ }, [isHydrated, messages, sessionId]);
606
+ useEffect(() => {
607
+ if (!isHydrated || !isOpen || !widgetEnabled || typeof window === "undefined") {
608
+ return;
609
+ }
610
+ let timeoutId;
611
+ setMessages((current) => {
612
+ if (current.length > 0) {
613
+ return current;
614
+ }
615
+ const welcomeId = createId("msg");
616
+ timeoutId = window.setTimeout(() => {
617
+ setMessages(
618
+ (next) => next.map(
619
+ (message) => message.id === welcomeId ? {
620
+ ...message,
621
+ content: welcomeAssistantMessage,
622
+ status: "complete"
623
+ } : message
624
+ )
625
+ );
626
+ }, WELCOME_DELAY_MS);
627
+ return [
628
+ ...current,
629
+ {
630
+ id: welcomeId,
631
+ role: "assistant",
632
+ content: "",
633
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
634
+ status: "streaming"
635
+ }
636
+ ];
637
+ });
638
+ return () => {
639
+ if (timeoutId) {
640
+ window.clearTimeout(timeoutId);
641
+ }
642
+ };
643
+ }, [isHydrated, isOpen, welcomeAssistantMessage, widgetEnabled]);
644
+ const open = useCallback(() => {
645
+ setIsOpen(true);
646
+ events?.onOpen?.();
647
+ }, [events]);
648
+ const close = useCallback(() => {
649
+ setIsOpen(false);
650
+ events?.onClose?.();
651
+ }, [events]);
652
+ const toggle = useCallback(() => {
653
+ setIsOpen((current) => {
654
+ const next = !current;
655
+ if (next) {
656
+ events?.onOpen?.();
657
+ } else {
658
+ events?.onClose?.();
659
+ }
660
+ return next;
661
+ });
662
+ }, [events]);
663
+ const clearConversation = useCallback(() => {
664
+ setMessages([]);
665
+ if (typeof window !== "undefined") {
666
+ const storage = window.sessionStorage;
667
+ storage.removeItem(historyKey(sessionId));
668
+ }
669
+ }, [sessionId]);
670
+ const resetSession = useCallback(() => {
671
+ const storage = typeof window !== "undefined" ? window.sessionStorage : void 0;
672
+ const next = resetSessionId(storage);
673
+ setSessionId(next);
674
+ setMessages([]);
675
+ }, []);
676
+ const registerPageConfig = useCallback((id, config) => {
677
+ setPageConfigs((current) => {
678
+ const filtered = current.filter((entry) => entry.id !== id);
679
+ return [...filtered, { id, config }];
680
+ });
681
+ }, []);
682
+ const unregisterPageConfig = useCallback((id) => {
683
+ setPageConfigs((current) => current.filter((entry) => entry.id !== id));
684
+ }, []);
685
+ const sendMessage = useCallback(
686
+ async ({ text, files = [] }) => {
687
+ const trimmed = text.trim();
688
+ if (!trimmed && files.length === 0) {
689
+ return;
690
+ }
691
+ const now = (/* @__PURE__ */ new Date()).toISOString();
692
+ const attachmentDtos = files.map((file) => ({
693
+ id: createId("att"),
694
+ name: file.name,
695
+ size: file.size,
696
+ type: file.type,
697
+ file
698
+ }));
699
+ if (attachmentDtos.length > 0) {
700
+ events?.onAttachmentAdded?.(attachmentDtos);
701
+ }
702
+ const userMessage = {
703
+ id: createId("msg"),
704
+ role: "user",
705
+ content: trimmed,
706
+ createdAt: now,
707
+ status: "complete",
708
+ attachments: attachmentDtos
709
+ };
710
+ const ensureWelcomeMessage = (input) => {
711
+ if (input.some((message) => message.role === "assistant" && message.content === welcomeAssistantMessage)) {
712
+ return input;
713
+ }
714
+ const firstStreamingAssistantIndex = input.findIndex(
715
+ (message) => message.role === "assistant" && message.status === "streaming" && !message.content
716
+ );
717
+ if (firstStreamingAssistantIndex >= 0) {
718
+ return input.map(
719
+ (message, index) => index === firstStreamingAssistantIndex ? {
720
+ ...message,
721
+ content: welcomeAssistantMessage,
722
+ status: "complete"
723
+ } : message
724
+ );
725
+ }
726
+ if (input.some((message) => message.role === "assistant")) {
727
+ return input;
728
+ }
729
+ const welcomeMessage = {
730
+ id: createId("msg"),
731
+ role: "assistant",
732
+ content: welcomeAssistantMessage,
733
+ createdAt: now,
734
+ status: "complete"
735
+ };
736
+ return [...input, welcomeMessage];
737
+ };
738
+ const normalizedHistory = ensureWelcomeMessage(messagesRef.current);
739
+ const requestHistory = [...normalizedHistory, userMessage].map((message) => ({
740
+ id: message.id,
741
+ role: message.role,
742
+ content: message.content,
743
+ createdAt: message.createdAt
744
+ }));
745
+ setMessages((current) => [...ensureWelcomeMessage(current), userMessage]);
746
+ events?.onMessageSent?.(userMessage);
747
+ const assistantId = createId("msg");
748
+ setMessages((current) => [
749
+ ...current,
750
+ {
751
+ id: assistantId,
752
+ role: "assistant",
753
+ content: "",
754
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
755
+ status: "streaming"
756
+ }
757
+ ]);
758
+ setIsAwaiting(true);
759
+ const resolvedAdapter = withDefaultAdapter(adapter);
760
+ try {
761
+ const metadata = {
762
+ sessionId,
763
+ pageUrl: typeof window !== "undefined" ? window.location.href : "",
764
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
765
+ agentName: resolvedAgentName,
766
+ locale: resolvedLocale,
767
+ history: requestHistory
768
+ };
769
+ const requestInit = resolvedAdapter.buildRequest({
770
+ webhookUrl,
771
+ message: trimmed,
772
+ sessionId,
773
+ agentName: resolvedAgentName,
774
+ files: uploadEnabled ? files : [],
775
+ metadata,
776
+ headers
777
+ });
778
+ const url = requestInit.url ?? webhookUrl;
779
+ const response = await fetch(url, requestInit);
780
+ let streamedContent = "";
781
+ const responseText = await readAssistantResponse({
782
+ response,
783
+ adapter: resolvedAdapter,
784
+ streamingMode,
785
+ onDelta: (delta) => {
786
+ streamedContent += delta;
787
+ setMessages(
788
+ (current) => current.map(
789
+ (message) => message.id === assistantId ? {
790
+ ...message,
791
+ content: `${message.content}${delta}`,
792
+ status: "streaming"
793
+ } : message
794
+ )
795
+ );
796
+ },
797
+ onChunk: (chunk) => {
798
+ events?.onChunkReceived?.(chunk);
799
+ },
800
+ onRecoverableError: (error) => {
801
+ events?.onError?.(error);
802
+ }
803
+ });
804
+ setMessages((current) => {
805
+ const next = current.map((message) => {
806
+ if (message.id !== assistantId) {
807
+ return message;
808
+ }
809
+ const hasStreamed = streamedContent.length > 0;
810
+ const content = hasStreamed ? message.content : responseText;
811
+ return {
812
+ ...message,
813
+ content,
814
+ status: "complete"
815
+ };
816
+ });
817
+ const completeMessage = next.find((message) => message.id === assistantId);
818
+ if (completeMessage) {
819
+ events?.onResponseComplete?.(completeMessage);
820
+ }
821
+ return next;
822
+ });
823
+ } catch (error) {
824
+ const typedError = error instanceof Error ? error : new Error(String(error));
825
+ events?.onError?.(typedError);
826
+ setMessages(
827
+ (current) => current.map(
828
+ (message) => message.id === assistantId ? {
829
+ ...message,
830
+ content: message.content || "Sorry, I ran into an issue while generating a response.",
831
+ status: "error"
832
+ } : message
833
+ )
834
+ );
835
+ } finally {
836
+ setIsAwaiting(false);
837
+ }
838
+ },
839
+ [
840
+ adapter,
841
+ events,
842
+ headers,
843
+ resolvedAgentName,
844
+ resolvedLocale,
845
+ sessionId,
846
+ streamingMode,
847
+ uploadEnabled,
848
+ welcomeAssistantMessage,
849
+ webhookUrl
850
+ ]
851
+ );
852
+ const value = useMemo(
853
+ () => ({
854
+ isOpen,
855
+ isAwaiting,
856
+ messages,
857
+ sessionId,
858
+ uploadEnabled,
859
+ agentName: resolvedAgentName,
860
+ assistantNote: resolvedAssistantNote,
861
+ disclaimerText,
862
+ position,
863
+ themeVars,
864
+ icons: mergedIcons,
865
+ classNames: classNames ?? {},
866
+ open,
867
+ close,
868
+ toggle,
869
+ sendMessage,
870
+ clearConversation,
871
+ registerPageConfig,
872
+ unregisterPageConfig,
873
+ widgetEnabled,
874
+ resetSession
875
+ }),
876
+ [
877
+ classNames,
878
+ clearConversation,
879
+ close,
880
+ disclaimerText,
881
+ isAwaiting,
882
+ isOpen,
883
+ mergedIcons,
884
+ messages,
885
+ open,
886
+ position,
887
+ registerPageConfig,
888
+ resolvedAgentName,
889
+ resolvedAssistantNote,
890
+ sendMessage,
891
+ sessionId,
892
+ themeVars,
893
+ toggle,
894
+ unregisterPageConfig,
895
+ uploadEnabled,
896
+ widgetEnabled,
897
+ resetSession
898
+ ]
899
+ );
900
+ return /* @__PURE__ */ jsx(ChatbotContext.Provider, { value, children });
901
+ }
902
+ function useChatbotContext() {
903
+ const context = useContext(ChatbotContext);
904
+ if (!context) {
905
+ throw new Error("useChatbotContext must be used within ChatbotProvider");
906
+ }
907
+ return context;
908
+ }
909
+
910
+ // src/chatbot/components/chatbot-widget.tsx
911
+ import clsx from "clsx";
912
+ import {
913
+ useEffect as useEffect2,
914
+ useMemo as useMemo2,
915
+ useRef as useRef2,
916
+ useState as useState2
917
+ } from "react";
918
+
919
+ // src/chatbot/components/awaiting-dots.tsx
920
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
921
+ function AwaitingDots() {
922
+ return /* @__PURE__ */ jsxs("div", { className: "cb-inline-flex cb-items-center cb-gap-1 cb-px-1", "aria-label": "Assistant is typing", children: [
923
+ /* @__PURE__ */ jsx2("span", { className: "cb-h-1.5 cb-w-1.5 cb-rounded-full cb-bg-[var(--cb-muted-foreground)] cb-opacity-70 cb-animate-cb-dots [animation-delay:-0.2s]" }),
924
+ /* @__PURE__ */ jsx2("span", { className: "cb-h-1.5 cb-w-1.5 cb-rounded-full cb-bg-[var(--cb-muted-foreground)] cb-opacity-70 cb-animate-cb-dots [animation-delay:-0.1s]" }),
925
+ /* @__PURE__ */ jsx2("span", { className: "cb-h-1.5 cb-w-1.5 cb-rounded-full cb-bg-[var(--cb-muted-foreground)] cb-opacity-70 cb-animate-cb-dots" })
926
+ ] });
927
+ }
928
+
929
+ // src/chatbot/components/message-markdown.tsx
930
+ import ReactMarkdown from "react-markdown";
931
+ import rehypeSanitize from "rehype-sanitize";
932
+ import remarkGfm from "remark-gfm";
933
+ import { jsx as jsx3 } from "react/jsx-runtime";
934
+ function MessageMarkdown({ content }) {
935
+ return /* @__PURE__ */ jsx3("div", { className: "cb-markdown cb-message-text cb-text-base cb-leading-6", children: /* @__PURE__ */ jsx3(ReactMarkdown, { remarkPlugins: [remarkGfm], rehypePlugins: [rehypeSanitize], children: content }) });
936
+ }
937
+
938
+ // src/chatbot/components/chatbot-widget.tsx
939
+ import { Fragment, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
940
+ var PANEL_INSET_REM = 1.5;
941
+ var EXPANDED_MAX_WIDTH_PX = 630;
942
+ var MOBILE_BREAKPOINT_PX = 768;
943
+ var ROOT_BOTTOM_OFFSET_REM = 1.5;
944
+ var PANEL_TO_BUBBLE_GAP_REM = 0.75;
945
+ var BUBBLE_SIZE_REM = 3.5;
946
+ var EXPANDED_BOTTOM_OFFSET_REM = ROOT_BOTTOM_OFFSET_REM + PANEL_TO_BUBBLE_GAP_REM + BUBBLE_SIZE_REM;
947
+ var EXPANDED_TOTAL_VERTICAL_INSET_REM = PANEL_INSET_REM + EXPANDED_BOTTOM_OFFSET_REM;
948
+ function formatFileSize(size) {
949
+ if (size < 1024) {
950
+ return `${size} B`;
951
+ }
952
+ if (size < 1024 * 1024) {
953
+ return `${(size / 1024).toFixed(1)} KB`;
954
+ }
955
+ return `${(size / (1024 * 1024)).toFixed(1)} MB`;
956
+ }
957
+ function formatGmtOffset(date) {
958
+ const total = -date.getTimezoneOffset();
959
+ const sign = total >= 0 ? "+" : "-";
960
+ const abs = Math.abs(total);
961
+ const hh = String(Math.floor(abs / 60)).padStart(2, "0");
962
+ const mm = String(abs % 60).padStart(2, "0");
963
+ return `GMT${sign}${hh}${mm}`;
964
+ }
965
+ function readTimeZoneName(date, mode) {
966
+ const parts = new Intl.DateTimeFormat(void 0, { timeZoneName: mode }).formatToParts(date);
967
+ return parts.find((part) => part.type === "timeZoneName")?.value ?? "";
968
+ }
969
+ function formatTranscriptDateTime(date) {
970
+ const datePart = new Intl.DateTimeFormat(void 0, {
971
+ month: "long",
972
+ day: "numeric",
973
+ year: "numeric"
974
+ }).format(date);
975
+ const timePart = new Intl.DateTimeFormat(void 0, {
976
+ hour: "2-digit",
977
+ minute: "2-digit",
978
+ hour12: true
979
+ }).format(date);
980
+ const zoneLong = readTimeZoneName(date, "long");
981
+ const zoneShort = readTimeZoneName(date, "short");
982
+ const gmtOffset = formatGmtOffset(date);
983
+ return `${datePart} at ${timePart} ${zoneLong} time ${zoneShort} (${gmtOffset})`.replace(/\s+/g, " ").trim();
984
+ }
985
+ function formatTranscriptMessageTime(value) {
986
+ const date = new Date(value);
987
+ if (Number.isNaN(date.getTime())) {
988
+ return value;
989
+ }
990
+ return new Intl.DateTimeFormat(void 0, {
991
+ hour: "2-digit",
992
+ minute: "2-digit",
993
+ hour12: true
994
+ }).format(date);
995
+ }
996
+ function toTranscript(messages, agentName) {
997
+ const exportedAt = /* @__PURE__ */ new Date();
998
+ const firstDate = messages.length > 0 ? new Date(messages[0].createdAt) : exportedAt;
999
+ const startedAt = Number.isNaN(firstDate.getTime()) ? exportedAt : firstDate;
1000
+ const lines = [
1001
+ `Conversation with ${agentName}`,
1002
+ `Started on ${formatTranscriptDateTime(startedAt)}`,
1003
+ "",
1004
+ "---",
1005
+ ""
1006
+ ];
1007
+ messages.forEach((message) => {
1008
+ const role = message.role === "user" ? "Visitor" : message.role === "assistant" ? agentName : "System";
1009
+ const content = message.content.trim();
1010
+ if (!content) {
1011
+ return;
1012
+ }
1013
+ lines.push(`${formatTranscriptMessageTime(message.createdAt)} | ${role}: ${content}`);
1014
+ lines.push("");
1015
+ });
1016
+ lines.push("---");
1017
+ lines.push(`Exported from ${agentName} on ${formatTranscriptDateTime(exportedAt)}`);
1018
+ return lines.join("\n");
1019
+ }
1020
+ function toTranscriptFilename(date) {
1021
+ const yyyy = date.getFullYear();
1022
+ const mm = String(date.getMonth() + 1).padStart(2, "0");
1023
+ const dd = String(date.getDate()).padStart(2, "0");
1024
+ const hh = String(date.getHours()).padStart(2, "0");
1025
+ const min = String(date.getMinutes()).padStart(2, "0");
1026
+ const ss = String(date.getSeconds()).padStart(2, "0");
1027
+ return `chat-transcript-${yyyy}${mm}${dd}-${hh}${min}${ss}.txt`;
1028
+ }
1029
+ function ChatbotWidget() {
1030
+ const {
1031
+ isOpen,
1032
+ isAwaiting,
1033
+ messages,
1034
+ uploadEnabled,
1035
+ position,
1036
+ agentName,
1037
+ assistantNote,
1038
+ disclaimerText,
1039
+ themeVars,
1040
+ icons,
1041
+ classNames,
1042
+ close,
1043
+ open,
1044
+ sendMessage,
1045
+ widgetEnabled
1046
+ } = useChatbotContext();
1047
+ const [inputValue, setInputValue] = useState2("");
1048
+ const [queuedFiles, setQueuedFiles] = useState2([]);
1049
+ const [isMenuOpen, setIsMenuOpen] = useState2(false);
1050
+ const [isExpanded, setIsExpanded] = useState2(false);
1051
+ const [isMobileViewport, setIsMobileViewport] = useState2(false);
1052
+ const panelRef = useRef2(null);
1053
+ const messagesScrollRef = useRef2(null);
1054
+ const inputRef = useRef2(null);
1055
+ const fileInputRef = useRef2(null);
1056
+ const menuRef = useRef2(null);
1057
+ const LauncherClosedIcon = icons.launcherClosed;
1058
+ const LauncherOpenIcon = icons.launcherOpen;
1059
+ const BotIcon = icons.bot;
1060
+ const MenuIcon = icons.menu;
1061
+ const CloseIcon = icons.close;
1062
+ const SendIcon = icons.send;
1063
+ const AttachIcon = icons.attach;
1064
+ const ExpandIcon = icons.expand;
1065
+ const DownloadIcon = icons.download;
1066
+ const alignment = position === "bottom-left" ? "cb-left-6" : "cb-right-6";
1067
+ const stackAlignment = position === "bottom-left" ? "cb-items-start" : "cb-items-end";
1068
+ const panelOrigin = position === "bottom-left" ? "cb-origin-bottom-left" : "cb-origin-bottom-right";
1069
+ const panelLayoutMode = isMobileViewport ? "mobile" : isExpanded ? "desktop-expanded" : "desktop-collapsed";
1070
+ const showExpandAction = !isMobileViewport;
1071
+ const showDownloadAction = messages.length > 0;
1072
+ const hasMenuActions = showExpandAction || showDownloadAction;
1073
+ const canSend = useMemo2(() => {
1074
+ return !isAwaiting && (inputValue.trim().length > 0 || queuedFiles.length > 0);
1075
+ }, [inputValue, isAwaiting, queuedFiles.length]);
1076
+ const resizeComposer = () => {
1077
+ const textarea = inputRef.current;
1078
+ if (!textarea) {
1079
+ return;
1080
+ }
1081
+ if (textarea.value.length === 0) {
1082
+ textarea.style.height = "38px";
1083
+ textarea.style.overflowY = "hidden";
1084
+ return;
1085
+ }
1086
+ const maxHeightPx = 168;
1087
+ textarea.style.height = "auto";
1088
+ const nextHeight = Math.min(textarea.scrollHeight, maxHeightPx);
1089
+ textarea.style.height = `${nextHeight}px`;
1090
+ textarea.style.overflowY = textarea.scrollHeight > maxHeightPx ? "auto" : "hidden";
1091
+ };
1092
+ const scrollMessagesToBottom = () => {
1093
+ const container = messagesScrollRef.current;
1094
+ if (!container) {
1095
+ return;
1096
+ }
1097
+ container.scrollTop = container.scrollHeight;
1098
+ };
1099
+ const panelSize = useMemo2(() => {
1100
+ const inset = `${PANEL_INSET_REM}rem`;
1101
+ if (panelLayoutMode === "mobile") {
1102
+ return {
1103
+ className: "cb-fixed cb-max-h-none cb-max-w-none",
1104
+ style: {
1105
+ top: inset,
1106
+ right: inset,
1107
+ bottom: `${EXPANDED_BOTTOM_OFFSET_REM}rem`,
1108
+ left: inset
1109
+ }
1110
+ };
1111
+ }
1112
+ if (panelLayoutMode === "desktop-expanded") {
1113
+ return {
1114
+ className: "cb-relative cb-max-h-none cb-max-w-none",
1115
+ style: {
1116
+ width: `min(${EXPANDED_MAX_WIDTH_PX}px, calc(100vw - ${PANEL_INSET_REM * 2}rem))`,
1117
+ height: `calc(100vh - ${EXPANDED_TOTAL_VERTICAL_INSET_REM}rem)`
1118
+ }
1119
+ };
1120
+ }
1121
+ return {
1122
+ className: "cb-relative cb-h-[min(680px,calc(100vh-7.5rem))] cb-w-[min(92vw,420px)]",
1123
+ style: void 0
1124
+ };
1125
+ }, [panelLayoutMode]);
1126
+ useEffect2(() => {
1127
+ if (typeof window === "undefined") {
1128
+ return;
1129
+ }
1130
+ const mediaQuery = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT_PX}px)`);
1131
+ const syncViewportMode = () => {
1132
+ setIsMobileViewport(mediaQuery.matches);
1133
+ };
1134
+ syncViewportMode();
1135
+ if (typeof mediaQuery.addEventListener === "function") {
1136
+ mediaQuery.addEventListener("change", syncViewportMode);
1137
+ return () => mediaQuery.removeEventListener("change", syncViewportMode);
1138
+ }
1139
+ mediaQuery.addListener(syncViewportMode);
1140
+ return () => mediaQuery.removeListener(syncViewportMode);
1141
+ }, []);
1142
+ useEffect2(() => {
1143
+ if (!isOpen) {
1144
+ setIsMenuOpen(false);
1145
+ return;
1146
+ }
1147
+ inputRef.current?.focus();
1148
+ resizeComposer();
1149
+ }, [isOpen]);
1150
+ useEffect2(() => {
1151
+ resizeComposer();
1152
+ }, [inputValue]);
1153
+ useEffect2(() => {
1154
+ if (!isOpen) {
1155
+ return;
1156
+ }
1157
+ const rafId = window.requestAnimationFrame(() => {
1158
+ scrollMessagesToBottom();
1159
+ });
1160
+ return () => window.cancelAnimationFrame(rafId);
1161
+ }, [isOpen, messages, isAwaiting]);
1162
+ useEffect2(() => {
1163
+ if (!isOpen) {
1164
+ return;
1165
+ }
1166
+ const onPointerDown = (event) => {
1167
+ const target = event.target;
1168
+ if (menuRef.current && !menuRef.current.contains(target)) {
1169
+ setIsMenuOpen(false);
1170
+ }
1171
+ };
1172
+ window.addEventListener("mousedown", onPointerDown);
1173
+ return () => window.removeEventListener("mousedown", onPointerDown);
1174
+ }, [isOpen]);
1175
+ useEffect2(() => {
1176
+ if (!isOpen) {
1177
+ return;
1178
+ }
1179
+ const onKeyDown = (event) => {
1180
+ if (event.key === "Escape") {
1181
+ if (isMenuOpen) {
1182
+ setIsMenuOpen(false);
1183
+ return;
1184
+ }
1185
+ close();
1186
+ }
1187
+ };
1188
+ window.addEventListener("keydown", onKeyDown);
1189
+ return () => window.removeEventListener("keydown", onKeyDown);
1190
+ }, [close, isMenuOpen, isOpen]);
1191
+ useEffect2(() => {
1192
+ setIsMenuOpen(false);
1193
+ }, [isMobileViewport]);
1194
+ useEffect2(() => {
1195
+ if (!hasMenuActions) {
1196
+ setIsMenuOpen(false);
1197
+ }
1198
+ }, [hasMenuActions]);
1199
+ const trapFocus = (event) => {
1200
+ if (event.key !== "Tab") {
1201
+ return;
1202
+ }
1203
+ const root = panelRef.current;
1204
+ if (!root) {
1205
+ return;
1206
+ }
1207
+ const focusables = root.querySelectorAll(
1208
+ "button, [href], textarea, input, select, [tabindex]:not([tabindex='-1'])"
1209
+ );
1210
+ if (focusables.length === 0) {
1211
+ return;
1212
+ }
1213
+ const first = focusables[0];
1214
+ const last = focusables[focusables.length - 1];
1215
+ if (event.shiftKey && document.activeElement === first) {
1216
+ last.focus();
1217
+ event.preventDefault();
1218
+ } else if (!event.shiftKey && document.activeElement === last) {
1219
+ first.focus();
1220
+ event.preventDefault();
1221
+ }
1222
+ };
1223
+ const onSubmit = async (event) => {
1224
+ event.preventDefault();
1225
+ if (!canSend) {
1226
+ return;
1227
+ }
1228
+ const submittedText = inputValue;
1229
+ setInputValue("");
1230
+ scrollMessagesToBottom();
1231
+ await sendMessage({
1232
+ text: submittedText,
1233
+ files: uploadEnabled ? queuedFiles : []
1234
+ });
1235
+ setQueuedFiles([]);
1236
+ if (fileInputRef.current) {
1237
+ fileInputRef.current.value = "";
1238
+ }
1239
+ };
1240
+ const onPickFiles = (event) => {
1241
+ const list = event.target.files;
1242
+ if (!list || !uploadEnabled) {
1243
+ return;
1244
+ }
1245
+ const files = Array.from(list);
1246
+ setQueuedFiles((current) => {
1247
+ const seen = new Set(current.map((file) => `${file.name}:${file.size}:${file.lastModified}`));
1248
+ const next = [...current];
1249
+ files.forEach((file) => {
1250
+ const key = `${file.name}:${file.size}:${file.lastModified}`;
1251
+ if (!seen.has(key)) {
1252
+ seen.add(key);
1253
+ next.push(file);
1254
+ }
1255
+ });
1256
+ return next;
1257
+ });
1258
+ };
1259
+ const createIdFromFile = (file) => `${file.name}:${file.size}:${file.lastModified}`;
1260
+ const removeQueuedFile = (fileId) => {
1261
+ setQueuedFiles((current) => current.filter((file) => createIdFromFile(file) !== fileId));
1262
+ };
1263
+ const downloadTranscript = () => {
1264
+ if (messages.length === 0) {
1265
+ return;
1266
+ }
1267
+ const now = /* @__PURE__ */ new Date();
1268
+ const fileContent = toTranscript(messages, agentName);
1269
+ const blob = new Blob([fileContent], { type: "text/plain;charset=utf-8" });
1270
+ const objectUrl = URL.createObjectURL(blob);
1271
+ const anchor = document.createElement("a");
1272
+ anchor.href = objectUrl;
1273
+ anchor.download = toTranscriptFilename(now);
1274
+ document.body.appendChild(anchor);
1275
+ anchor.click();
1276
+ document.body.removeChild(anchor);
1277
+ URL.revokeObjectURL(objectUrl);
1278
+ setIsMenuOpen(false);
1279
+ };
1280
+ const toggleExpanded = () => {
1281
+ if (isMobileViewport) {
1282
+ return;
1283
+ }
1284
+ setIsExpanded((current) => !current);
1285
+ setIsMenuOpen(false);
1286
+ };
1287
+ if (!widgetEnabled) {
1288
+ return null;
1289
+ }
1290
+ return /* @__PURE__ */ jsx4(
1291
+ "div",
1292
+ {
1293
+ className: clsx(
1294
+ "cb-fixed cb-bottom-6 cb-z-[2147483640] cb-text-[var(--cb-surface-foreground)]",
1295
+ alignment,
1296
+ classNames.root
1297
+ ),
1298
+ style: themeVars,
1299
+ "data-chatbot-root": true,
1300
+ children: /* @__PURE__ */ jsxs2("div", { className: clsx("cb-pointer-events-none cb-relative cb-flex cb-flex-col cb-gap-3", stackAlignment), children: [
1301
+ /* @__PURE__ */ jsxs2(
1302
+ "div",
1303
+ {
1304
+ ref: panelRef,
1305
+ className: clsx(
1306
+ "cb-pointer-events-auto cb-flex cb-flex-col cb-overflow-hidden cb-rounded-[var(--cb-radius)] cb-border cb-border-[var(--cb-border)] cb-bg-[var(--cb-surface)] cb-shadow-[var(--cb-shadow-panel)] cb-transition-[opacity,transform,width,height,top,left,right,bottom] cb-duration-[380ms] cb-ease-[cubic-bezier(0.22,1,0.36,1)]",
1307
+ panelOrigin,
1308
+ panelSize.className,
1309
+ isOpen ? "cb-translate-y-0 cb-scale-100 cb-opacity-100" : "cb-translate-y-3 cb-scale-90 cb-opacity-0 cb-pointer-events-none",
1310
+ classNames.panel
1311
+ ),
1312
+ style: panelSize.style,
1313
+ "aria-hidden": !isOpen,
1314
+ onKeyDown: trapFocus,
1315
+ children: [
1316
+ /* @__PURE__ */ jsxs2(
1317
+ "header",
1318
+ {
1319
+ className: clsx(
1320
+ "cb-flex cb-items-center cb-justify-between cb-gap-2 cb-border-b cb-border-[var(--cb-border)] cb-bg-[var(--cb-surface)] cb-px-4 cb-py-3.5",
1321
+ classNames.header
1322
+ ),
1323
+ style: {
1324
+ borderBottomWidth: "1px",
1325
+ borderBottomStyle: "solid",
1326
+ borderBottomColor: "var(--cb-border)"
1327
+ },
1328
+ children: [
1329
+ /* @__PURE__ */ jsxs2("div", { className: clsx("cb-flex cb-min-w-0 cb-items-center cb-gap-2.5", classNames.headerMeta), children: [
1330
+ /* @__PURE__ */ jsx4("span", { className: "cb-inline-flex cb-h-9 cb-w-9 cb-shrink-0 cb-items-center cb-justify-center cb-self-center cb-text-[var(--cb-surface-foreground)]", children: /* @__PURE__ */ jsx4(BotIcon, { size: 34 }) }),
1331
+ /* @__PURE__ */ jsxs2("div", { className: "cb-min-w-0 cb-space-y-0", children: [
1332
+ /* @__PURE__ */ jsx4("p", { className: "cb-m-0 cb-truncate cb-text-lg cb-font-semibold cb-leading-6 cb-text-[var(--cb-surface-foreground)]", children: agentName }),
1333
+ /* @__PURE__ */ jsx4("p", { className: "cb-m-0 cb-text-base cb-leading-5 cb-text-[var(--cb-muted-foreground)]", children: assistantNote })
1334
+ ] })
1335
+ ] }),
1336
+ /* @__PURE__ */ jsxs2("div", { ref: menuRef, className: "cb-relative cb-flex cb-items-center cb-gap-1", children: [
1337
+ hasMenuActions && /* @__PURE__ */ jsx4(
1338
+ "button",
1339
+ {
1340
+ type: "button",
1341
+ onClick: () => setIsMenuOpen((current) => !current),
1342
+ className: clsx(
1343
+ "cb-flex cb-h-9 cb-w-9 cb-shrink-0 cb-items-center cb-justify-center cb-rounded-lg cb-bg-transparent cb-p-0 cb-text-[var(--cb-muted-foreground)] cb-transition hover:cb-bg-[var(--cb-muted)] hover:cb-text-white focus-visible:cb-bg-[var(--cb-muted)] focus-visible:cb-text-white",
1344
+ isMenuOpen && "cb-bg-[var(--cb-muted)] cb-text-white"
1345
+ ),
1346
+ "aria-label": "More options",
1347
+ "aria-expanded": isMenuOpen,
1348
+ children: /* @__PURE__ */ jsx4(MenuIcon, { size: 18 })
1349
+ }
1350
+ ),
1351
+ isMenuOpen && hasMenuActions && /* @__PURE__ */ jsxs2(
1352
+ "div",
1353
+ {
1354
+ className: clsx(
1355
+ "cb-absolute cb-right-0 cb-top-full cb-z-20 cb-mt-2 cb-flex cb-min-w-[220px] cb-flex-col cb-gap-1 cb-rounded-xl cb-border cb-border-[var(--cb-border)] cb-bg-[var(--cb-surface)] cb-p-1.5 cb-shadow-[var(--cb-shadow-panel)]",
1356
+ classNames.menu
1357
+ ),
1358
+ children: [
1359
+ showExpandAction && /* @__PURE__ */ jsxs2(
1360
+ "button",
1361
+ {
1362
+ type: "button",
1363
+ onClick: toggleExpanded,
1364
+ className: clsx(
1365
+ "cb-flex cb-w-full cb-items-center cb-gap-2 cb-rounded-lg cb-bg-transparent cb-px-3.5 cb-py-2.5 cb-text-left cb-text-sm cb-text-[var(--cb-muted-foreground)] cb-transition hover:cb-bg-[var(--cb-muted)] hover:cb-text-white focus-visible:cb-bg-[var(--cb-muted)] focus-visible:cb-text-white",
1366
+ classNames.menuItem
1367
+ ),
1368
+ children: [
1369
+ /* @__PURE__ */ jsx4(ExpandIcon, { size: 16 }),
1370
+ /* @__PURE__ */ jsx4("span", { children: isExpanded ? "Restore window" : "Expand window" })
1371
+ ]
1372
+ }
1373
+ ),
1374
+ showDownloadAction && /* @__PURE__ */ jsxs2(
1375
+ "button",
1376
+ {
1377
+ type: "button",
1378
+ onClick: downloadTranscript,
1379
+ className: clsx(
1380
+ "cb-flex cb-w-full cb-items-center cb-gap-2 cb-rounded-lg cb-bg-transparent cb-px-3.5 cb-py-2.5 cb-text-left cb-text-sm cb-text-[var(--cb-muted-foreground)] cb-transition hover:cb-bg-[var(--cb-muted)] hover:cb-text-white focus-visible:cb-bg-[var(--cb-muted)] focus-visible:cb-text-white",
1381
+ classNames.menuItem
1382
+ ),
1383
+ children: [
1384
+ /* @__PURE__ */ jsx4(DownloadIcon, { size: 16 }),
1385
+ /* @__PURE__ */ jsx4("span", { children: "Download transcript" })
1386
+ ]
1387
+ }
1388
+ )
1389
+ ]
1390
+ }
1391
+ ),
1392
+ /* @__PURE__ */ jsx4(
1393
+ "button",
1394
+ {
1395
+ type: "button",
1396
+ onClick: close,
1397
+ className: "cb-flex cb-h-9 cb-w-9 cb-shrink-0 cb-items-center cb-justify-center cb-rounded-lg cb-bg-transparent cb-p-0 cb-text-[var(--cb-muted-foreground)] cb-transition hover:cb-bg-[var(--cb-muted)] hover:cb-text-white focus-visible:cb-bg-[var(--cb-muted)] focus-visible:cb-text-white",
1398
+ "aria-label": "Close chatbot",
1399
+ children: /* @__PURE__ */ jsx4(CloseIcon, { size: 18 })
1400
+ }
1401
+ )
1402
+ ] })
1403
+ ]
1404
+ }
1405
+ ),
1406
+ /* @__PURE__ */ jsx4(
1407
+ "section",
1408
+ {
1409
+ ref: messagesScrollRef,
1410
+ className: clsx(
1411
+ "cb-chat-scroll cb-flex-1 cb-overflow-y-auto cb-scroll-smooth cb-bg-[var(--cb-background)] cb-px-4 cb-pt-4 cb-pb-36",
1412
+ classNames.body
1413
+ ),
1414
+ children: /* @__PURE__ */ jsxs2("div", { className: "cb-space-y-3.5", children: [
1415
+ messages.length === 0 && /* @__PURE__ */ jsx4("div", { className: "cb-rounded-xl cb-bg-[var(--cb-muted)] cb-p-3 cb-text-sm cb-text-[var(--cb-muted-foreground)]", children: "Ask anything about this company and the assistant will answer using your RAG knowledge base." }),
1416
+ messages.map((message) => {
1417
+ const isUser = message.role === "user";
1418
+ return /* @__PURE__ */ jsx4(
1419
+ "article",
1420
+ {
1421
+ className: clsx(
1422
+ "cb-flex",
1423
+ isUser ? "cb-justify-end cb-animate-cb-slide-fade-in-right" : "cb-justify-start cb-animate-cb-slide-fade-in-left"
1424
+ ),
1425
+ children: /* @__PURE__ */ jsxs2(
1426
+ "div",
1427
+ {
1428
+ className: clsx(
1429
+ "cb-max-w-[85%] cb-rounded-2xl cb-px-4 cb-py-2",
1430
+ isUser ? "cb-rounded-br-md cb-bg-[var(--cb-user-bubble)] cb-text-[var(--cb-user-text)]" : "cb-rounded-bl-md cb-bg-[var(--cb-assistant-bubble)] cb-text-[var(--cb-assistant-text)]"
1431
+ ),
1432
+ children: [
1433
+ message.role === "assistant" && message.status === "streaming" && !message.content ? /* @__PURE__ */ jsx4(AwaitingDots, {}) : message.role === "assistant" ? /* @__PURE__ */ jsx4(MessageMarkdown, { content: message.content }) : /* @__PURE__ */ jsx4("p", { className: "cb-message-text cb-m-0 cb-whitespace-pre-wrap cb-text-base cb-leading-6", children: message.content }),
1434
+ message.attachments && message.attachments.length > 0 && /* @__PURE__ */ jsx4("div", { className: "cb-mt-2 cb-flex cb-flex-wrap cb-gap-2", children: message.attachments.map((attachment) => /* @__PURE__ */ jsxs2(
1435
+ "span",
1436
+ {
1437
+ className: "cb-inline-flex cb-items-center cb-gap-2 cb-rounded-full cb-bg-black/10 cb-px-2 cb-py-1 cb-text-xs",
1438
+ children: [
1439
+ /* @__PURE__ */ jsx4("span", { children: attachment.name }),
1440
+ /* @__PURE__ */ jsx4("span", { className: "cb-opacity-70", children: formatFileSize(attachment.size) })
1441
+ ]
1442
+ },
1443
+ attachment.id
1444
+ )) })
1445
+ ]
1446
+ }
1447
+ )
1448
+ },
1449
+ message.id
1450
+ );
1451
+ }),
1452
+ /* @__PURE__ */ jsx4("div", {})
1453
+ ] })
1454
+ }
1455
+ ),
1456
+ /* @__PURE__ */ jsxs2(
1457
+ "footer",
1458
+ {
1459
+ className: clsx(
1460
+ "cb-absolute cb-bottom-4 cb-left-4 cb-right-4 cb-z-10 cb-bg-transparent cb-p-0",
1461
+ classNames.footer
1462
+ ),
1463
+ children: [
1464
+ uploadEnabled && queuedFiles.length > 0 && /* @__PURE__ */ jsx4("div", { className: "cb-mb-2 cb-flex cb-flex-wrap cb-gap-2", children: queuedFiles.map((file) => {
1465
+ const fileId = createIdFromFile(file);
1466
+ return /* @__PURE__ */ jsxs2(
1467
+ "button",
1468
+ {
1469
+ type: "button",
1470
+ className: "cb-inline-flex cb-items-center cb-gap-2 cb-rounded-full cb-bg-[var(--cb-muted)] cb-px-3 cb-py-1 cb-text-xs cb-text-[var(--cb-muted-foreground)]",
1471
+ onClick: () => removeQueuedFile(fileId),
1472
+ "aria-label": `Remove ${file.name}`,
1473
+ children: [
1474
+ /* @__PURE__ */ jsx4("span", { className: "cb-max-w-[140px] cb-truncate", children: file.name }),
1475
+ /* @__PURE__ */ jsx4("span", { children: "x" })
1476
+ ]
1477
+ },
1478
+ fileId
1479
+ );
1480
+ }) }),
1481
+ /* @__PURE__ */ jsxs2("form", { onSubmit, children: [
1482
+ /* @__PURE__ */ jsxs2(
1483
+ "div",
1484
+ {
1485
+ className: clsx(
1486
+ "cb-flex cb-items-end cb-gap-2 cb-rounded-2xl cb-bg-[var(--cb-surface)] cb-p-2 cb-shadow-[0_18px_35px_-24px_rgba(0,0,0,0.85)] cb-transition",
1487
+ classNames.composer
1488
+ ),
1489
+ children: [
1490
+ uploadEnabled && /* @__PURE__ */ jsxs2(Fragment, { children: [
1491
+ /* @__PURE__ */ jsx4(
1492
+ "input",
1493
+ {
1494
+ type: "file",
1495
+ multiple: true,
1496
+ ref: fileInputRef,
1497
+ className: "cb-hidden",
1498
+ onChange: onPickFiles,
1499
+ "aria-label": "Add attachments"
1500
+ }
1501
+ ),
1502
+ /* @__PURE__ */ jsx4(
1503
+ "button",
1504
+ {
1505
+ type: "button",
1506
+ className: "cb-flex cb-h-10 cb-w-10 cb-shrink-0 cb-items-center cb-justify-center cb-rounded-lg cb-bg-transparent cb-p-0 cb-text-[var(--cb-muted-foreground)] cb-transition hover:cb-bg-[var(--cb-muted)] hover:cb-text-white focus-visible:cb-bg-[var(--cb-muted)] focus-visible:cb-text-white",
1507
+ onClick: () => fileInputRef.current?.click(),
1508
+ "aria-label": "Attach files",
1509
+ children: /* @__PURE__ */ jsx4(AttachIcon, { size: 18 })
1510
+ }
1511
+ )
1512
+ ] }),
1513
+ /* @__PURE__ */ jsx4("div", { className: "cb-flex-1", children: /* @__PURE__ */ jsx4(
1514
+ "textarea",
1515
+ {
1516
+ ref: inputRef,
1517
+ value: inputValue,
1518
+ onChange: (event) => setInputValue(event.target.value),
1519
+ onKeyDown: (event) => {
1520
+ if (event.key === "Enter" && !event.shiftKey) {
1521
+ event.preventDefault();
1522
+ void onSubmit(event);
1523
+ }
1524
+ },
1525
+ rows: 1,
1526
+ placeholder: "Ask a question...",
1527
+ className: clsx(
1528
+ "cb-composer-input cb-min-h-[38px] cb-w-full cb-resize-none cb-bg-transparent cb-px-2 cb-py-1.5 cb-text-sm cb-leading-6 cb-text-[var(--cb-surface-foreground)] cb-outline-none placeholder:cb-text-[var(--cb-muted-foreground)]",
1529
+ classNames.input
1530
+ )
1531
+ }
1532
+ ) }),
1533
+ /* @__PURE__ */ jsx4(
1534
+ "button",
1535
+ {
1536
+ type: "submit",
1537
+ disabled: !canSend,
1538
+ className: clsx(
1539
+ "cb-flex cb-h-10 cb-w-10 cb-shrink-0 cb-items-center cb-justify-center cb-rounded-full cb-bg-white cb-p-0 cb-text-[#111111] cb-transition cb-duration-200 hover:cb-scale-105 hover:cb-brightness-95 hover:cb-text-[#111111] active:cb-scale-95 focus-visible:cb-text-[#111111] disabled:cb-cursor-not-allowed disabled:cb-bg-white/70 disabled:cb-text-[#111111] disabled:cb-opacity-100",
1540
+ classNames.sendButton
1541
+ ),
1542
+ "aria-label": "Send message",
1543
+ children: /* @__PURE__ */ jsx4(SendIcon, { size: 18 })
1544
+ }
1545
+ )
1546
+ ]
1547
+ }
1548
+ ),
1549
+ /* @__PURE__ */ jsx4("p", { className: "cb-m-0 cb-mt-1 cb-px-1 cb-text-center cb-text-xs cb-leading-4 cb-text-[var(--cb-muted-foreground)]", children: disclaimerText })
1550
+ ] })
1551
+ ]
1552
+ }
1553
+ )
1554
+ ]
1555
+ }
1556
+ ),
1557
+ /* @__PURE__ */ jsx4(
1558
+ "button",
1559
+ {
1560
+ type: "button",
1561
+ onClick: isOpen ? close : open,
1562
+ className: clsx(
1563
+ "cb-pointer-events-auto cb-group cb-flex cb-h-14 cb-w-14 cb-items-center cb-justify-center cb-rounded-full cb-border cb-border-[var(--cb-border)] cb-bg-[#111111] cb-text-[#f5f5f5] cb-shadow-[var(--cb-shadow-bubble)] cb-transition cb-duration-300 hover:cb-scale-110 hover:cb-bg-[#262626] hover:cb-shadow-[0_24px_50px_-18px_rgba(0,0,0,0.9)] active:cb-scale-95",
1564
+ classNames.bubble
1565
+ ),
1566
+ "aria-label": isOpen ? "Close chatbot" : "Open chatbot",
1567
+ children: /* @__PURE__ */ jsxs2("span", { className: "cb-relative cb-flex cb-h-6 cb-w-6 cb-items-center cb-justify-center", children: [
1568
+ /* @__PURE__ */ jsx4(
1569
+ LauncherClosedIcon,
1570
+ {
1571
+ size: 22,
1572
+ className: clsx(
1573
+ "cb-absolute cb-fill-current cb-transition-all cb-duration-300 cb-ease-out",
1574
+ isOpen ? "cb-rotate-90 cb-opacity-0" : "cb-rotate-0 cb-opacity-100"
1575
+ )
1576
+ }
1577
+ ),
1578
+ /* @__PURE__ */ jsx4(
1579
+ LauncherOpenIcon,
1580
+ {
1581
+ size: 22,
1582
+ className: clsx(
1583
+ "cb-absolute cb-transition-all cb-duration-300 cb-ease-out",
1584
+ isOpen ? "cb-rotate-0 cb-opacity-100" : "-cb-rotate-90 cb-opacity-0"
1585
+ )
1586
+ }
1587
+ )
1588
+ ] })
1589
+ }
1590
+ )
1591
+ ] })
1592
+ }
1593
+ );
1594
+ }
1595
+
1596
+ // src/chatbot/components/chatbot-page-config.tsx
1597
+ import { useEffect as useEffect3, useMemo as useMemo3, useRef as useRef3 } from "react";
1598
+ function ChatbotPageConfig(props) {
1599
+ const { registerPageConfig, unregisterPageConfig } = useChatbotContext();
1600
+ const configId = useRef3(createId("pagecfg"));
1601
+ const stableConfig = useMemo3(() => props, [props]);
1602
+ useEffect3(() => {
1603
+ registerPageConfig(configId.current, stableConfig);
1604
+ return () => unregisterPageConfig(configId.current);
1605
+ }, [registerPageConfig, stableConfig, unregisterPageConfig]);
1606
+ return null;
1607
+ }
1608
+
1609
+ // src/chatbot/hooks/use-chatbot.ts
1610
+ function useChatbot() {
1611
+ return useChatbotContext();
1612
+ }
1613
+
1614
+ // src/chatbot/hooks/use-chatbot-session.ts
1615
+ import { useMemo as useMemo4 } from "react";
1616
+ function useChatbotSession() {
1617
+ const { sessionId, resetSession } = useChatbotContext();
1618
+ return useMemo4(
1619
+ () => ({
1620
+ sessionId,
1621
+ resetSession
1622
+ }),
1623
+ [resetSession, sessionId]
1624
+ );
1625
+ }
1626
+ export {
1627
+ ChatbotPageConfig,
1628
+ ChatbotProvider,
1629
+ ChatbotWidget,
1630
+ useChatbot,
1631
+ useChatbotSession
1632
+ };
1633
+ //# sourceMappingURL=index.mjs.map