@iota-uz/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +201 -0
  2. package/README.MD +164 -0
  3. package/assets/fonts/Actay/Actay-Regular.otf +0 -0
  4. package/assets/fonts/Actay/Actay-RegularItalic.otf +0 -0
  5. package/assets/fonts/Actay/ActayCondensed-Thin.otf +0 -0
  6. package/assets/fonts/Actay/ActayCondensed-ThinItalic.otf +0 -0
  7. package/assets/fonts/Actay/ActayWide-Bold.otf +0 -0
  8. package/assets/fonts/Actay/ActayWide-BoldItalic.otf +0 -0
  9. package/assets/fonts/Gilroy/Gilroy-Black.woff2 +0 -0
  10. package/assets/fonts/Gilroy/Gilroy-BlackItalic.woff2 +0 -0
  11. package/assets/fonts/Gilroy/Gilroy-Bold.woff2 +0 -0
  12. package/assets/fonts/Gilroy/Gilroy-BoldItalic.woff2 +0 -0
  13. package/assets/fonts/Gilroy/Gilroy-Extrabold.woff2 +0 -0
  14. package/assets/fonts/Gilroy/Gilroy-ExtraboldItalic.woff2 +0 -0
  15. package/assets/fonts/Gilroy/Gilroy-Heavy.woff2 +0 -0
  16. package/assets/fonts/Gilroy/Gilroy-HeavyItalic.woff2 +0 -0
  17. package/assets/fonts/Gilroy/Gilroy-Light.woff2 +0 -0
  18. package/assets/fonts/Gilroy/Gilroy-LightItalic.woff2 +0 -0
  19. package/assets/fonts/Gilroy/Gilroy-Medium.woff2 +0 -0
  20. package/assets/fonts/Gilroy/Gilroy-MediumItalic.woff2 +0 -0
  21. package/assets/fonts/Gilroy/Gilroy-Regular.woff2 +0 -0
  22. package/assets/fonts/Gilroy/Gilroy-RegularItalic.woff2 +0 -0
  23. package/assets/fonts/Gilroy/Gilroy-Semibold.woff2 +0 -0
  24. package/assets/fonts/Gilroy/Gilroy-SemiboldItalic.woff2 +0 -0
  25. package/assets/fonts/Gilroy/Gilroy-Thin.woff2 +0 -0
  26. package/assets/fonts/Gilroy/Gilroy-ThinItalic.woff2 +0 -0
  27. package/assets/fonts/Gilroy/Gilroy-UltraLight.woff2 +0 -0
  28. package/assets/fonts/Gilroy/Gilroy-UltraLightItalic.woff2 +0 -0
  29. package/assets/fonts/Inter.var.woff2 +0 -0
  30. package/dist/bichat/index.cjs +3033 -0
  31. package/dist/bichat/index.cjs.map +1 -0
  32. package/dist/bichat/index.css +139 -0
  33. package/dist/bichat/index.css.map +1 -0
  34. package/dist/bichat/index.d.cts +1081 -0
  35. package/dist/bichat/index.d.ts +1081 -0
  36. package/dist/bichat/index.mjs +2959 -0
  37. package/dist/bichat/index.mjs.map +1 -0
  38. package/dist/bichat/styles.css +160 -0
  39. package/dist/index.cjs +191 -0
  40. package/dist/index.cjs.map +1 -0
  41. package/dist/index.d.cts +237 -0
  42. package/dist/index.d.ts +237 -0
  43. package/dist/index.mjs +178 -0
  44. package/dist/index.mjs.map +1 -0
  45. package/package.json +70 -0
  46. package/tailwind/create-config.cjs +142 -0
  47. package/tailwind/iota.css +1106 -0
  48. package/tailwind/main.css +2 -0
@@ -0,0 +1,3033 @@
1
+ 'use strict';
2
+
3
+ var ReactMarkdown = require('react-markdown');
4
+ var remarkGfm = require('remark-gfm');
5
+ var rehypeSanitize = require('rehype-sanitize');
6
+ var reactSyntaxHighlighter = require('react-syntax-highlighter');
7
+ var prism = require('react-syntax-highlighter/dist/esm/styles/prism');
8
+ var jsxRuntime = require('react/jsx-runtime');
9
+ var react = require('react');
10
+ var react$1 = require('@phosphor-icons/react');
11
+ var dateFns = require('date-fns');
12
+ var recharts = require('recharts');
13
+ var framerMotion = require('framer-motion');
14
+
15
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
16
+
17
+ var ReactMarkdown__default = /*#__PURE__*/_interopDefault(ReactMarkdown);
18
+ var remarkGfm__default = /*#__PURE__*/_interopDefault(remarkGfm);
19
+ var rehypeSanitize__default = /*#__PURE__*/_interopDefault(rehypeSanitize);
20
+
21
+ var __defProp = Object.defineProperty;
22
+ var __getOwnPropNames = Object.getOwnPropertyNames;
23
+ var __esm = (fn, res) => function __init() {
24
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
25
+ };
26
+ var __export = (target, all) => {
27
+ for (var name in all)
28
+ __defProp(target, name, { get: all[name], enumerable: true });
29
+ };
30
+
31
+ // ui/src/bichat/components/MarkdownRenderer.tsx
32
+ var MarkdownRenderer_exports = {};
33
+ __export(MarkdownRenderer_exports, {
34
+ MarkdownRenderer: () => MarkdownRenderer
35
+ });
36
+ function MarkdownRenderer({ content }) {
37
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "prose prose-sm max-w-none", children: /* @__PURE__ */ jsxRuntime.jsx(
38
+ ReactMarkdown__default.default,
39
+ {
40
+ remarkPlugins: [remarkGfm__default.default],
41
+ rehypePlugins: [rehypeSanitize__default.default],
42
+ components: {
43
+ code({ className, children, ...props }) {
44
+ const match = /language-(\w+)/.exec(className || "");
45
+ const isInline = !match;
46
+ return !isInline && match ? /* @__PURE__ */ jsxRuntime.jsx(
47
+ reactSyntaxHighlighter.Prism,
48
+ {
49
+ style: prism.tomorrow,
50
+ language: match[1],
51
+ PreTag: "div",
52
+ children: String(children).replace(/\n$/, "")
53
+ }
54
+ ) : /* @__PURE__ */ jsxRuntime.jsx("code", { className, ...props, children });
55
+ }
56
+ },
57
+ children: content
58
+ }
59
+ ) });
60
+ }
61
+ var init_MarkdownRenderer = __esm({
62
+ "ui/src/bichat/components/MarkdownRenderer.tsx"() {
63
+ }
64
+ });
65
+
66
+ // ui/src/bichat/types/index.ts
67
+ var MessageRole = /* @__PURE__ */ ((MessageRole2) => {
68
+ MessageRole2["User"] = "user";
69
+ MessageRole2["Assistant"] = "assistant";
70
+ MessageRole2["System"] = "system";
71
+ MessageRole2["Tool"] = "tool";
72
+ return MessageRole2;
73
+ })(MessageRole || {});
74
+
75
+ // ui/src/bichat/utils/RateLimiter.ts
76
+ var RateLimiter = class {
77
+ constructor(config) {
78
+ this.timestamps = [];
79
+ this.maxRequests = config.maxRequests;
80
+ this.windowMs = config.windowMs;
81
+ }
82
+ /**
83
+ * Check if a request can be made
84
+ * Updates internal state if request is allowed
85
+ */
86
+ canMakeRequest() {
87
+ const now = Date.now();
88
+ this.timestamps = this.timestamps.filter((t) => now - t < this.windowMs);
89
+ if (this.timestamps.length >= this.maxRequests) {
90
+ return false;
91
+ }
92
+ this.timestamps.push(now);
93
+ return true;
94
+ }
95
+ /**
96
+ * Get milliseconds until next request is allowed
97
+ * Returns 0 if request can be made immediately
98
+ */
99
+ getTimeUntilNextRequest() {
100
+ if (this.timestamps.length < this.maxRequests) {
101
+ return 0;
102
+ }
103
+ const now = Date.now();
104
+ const oldestTimestamp = this.timestamps[0];
105
+ const timeElapsed = now - oldestTimestamp;
106
+ const timeRemaining = this.windowMs - timeElapsed;
107
+ return Math.max(0, timeRemaining);
108
+ }
109
+ /**
110
+ * Reset the rate limiter state
111
+ */
112
+ reset() {
113
+ this.timestamps = [];
114
+ }
115
+ };
116
+ var ChatSessionContext = react.createContext(null);
117
+ var DEFAULT_RATE_LIMIT_CONFIG = {
118
+ maxRequests: 20,
119
+ windowMs: 6e4
120
+ // 1 minute
121
+ };
122
+ function ChatSessionProvider({
123
+ dataSource,
124
+ sessionId,
125
+ rateLimiter: externalRateLimiter,
126
+ children
127
+ }) {
128
+ const [message, setMessage] = react.useState("");
129
+ const [messages, setMessages] = react.useState([]);
130
+ const [loading, setLoading] = react.useState(false);
131
+ const [error, setError] = react.useState(null);
132
+ const [currentSessionId, setCurrentSessionId] = react.useState(sessionId);
133
+ const [session, setSession] = react.useState(null);
134
+ const [fetching, setFetching] = react.useState(false);
135
+ const [pendingQuestion, setPendingQuestion] = react.useState(null);
136
+ const [streamingContent, setStreamingContent] = react.useState("");
137
+ const [isStreaming, setIsStreaming] = react.useState(false);
138
+ const abortControllerRef = react.useRef(null);
139
+ const [messageQueue, setMessageQueue] = react.useState([]);
140
+ const [codeOutputs, setCodeOutputs] = react.useState([]);
141
+ const rateLimiterRef = react.useRef(
142
+ externalRateLimiter || new RateLimiter(DEFAULT_RATE_LIMIT_CONFIG)
143
+ );
144
+ react.useEffect(() => {
145
+ setCurrentSessionId(sessionId);
146
+ }, [sessionId]);
147
+ react.useEffect(() => {
148
+ if (!currentSessionId || currentSessionId === "new") {
149
+ setSession(null);
150
+ setMessages([]);
151
+ setPendingQuestion(null);
152
+ setFetching(false);
153
+ return;
154
+ }
155
+ let cancelled = false;
156
+ setFetching(true);
157
+ setError(null);
158
+ dataSource.fetchSession(currentSessionId).then((state) => {
159
+ if (cancelled) return;
160
+ if (state) {
161
+ setSession(state.session);
162
+ setMessages(state.messages);
163
+ setPendingQuestion(state.pendingQuestion || null);
164
+ } else {
165
+ setError("Session not found");
166
+ }
167
+ setFetching(false);
168
+ }).catch((err) => {
169
+ if (cancelled) return;
170
+ setError(err.message || "Failed to load session");
171
+ setFetching(false);
172
+ });
173
+ return () => {
174
+ cancelled = true;
175
+ };
176
+ }, [dataSource, currentSessionId]);
177
+ const handleCopy = react.useCallback(async (text) => {
178
+ await navigator.clipboard.writeText(text);
179
+ }, []);
180
+ const sendMessageDirect = react.useCallback(
181
+ async (content, attachments = []) => {
182
+ if (!content.trim() || loading) return;
183
+ if (!rateLimiterRef.current.canMakeRequest()) {
184
+ const timeUntilNext = rateLimiterRef.current.getTimeUntilNextRequest();
185
+ const seconds = Math.ceil(timeUntilNext / 1e3);
186
+ setError(`Rate limit exceeded. Please wait ${seconds} seconds before sending another message.`);
187
+ setTimeout(() => setError(null), 5e3);
188
+ return;
189
+ }
190
+ setMessage("");
191
+ setLoading(true);
192
+ setError(null);
193
+ setStreamingContent("");
194
+ abortControllerRef.current = new AbortController();
195
+ const tempUserMessage = {
196
+ id: `temp-user-${Date.now()}`,
197
+ sessionId: currentSessionId || "new",
198
+ role: "user" /* User */,
199
+ content,
200
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
201
+ };
202
+ setMessages((prev) => [...prev, tempUserMessage]);
203
+ try {
204
+ let activeSessionId = currentSessionId;
205
+ let shouldNavigateAfter = false;
206
+ if (!activeSessionId || activeSessionId === "new") {
207
+ const result = await dataSource.createSession();
208
+ if (result) {
209
+ activeSessionId = result.id;
210
+ setCurrentSessionId(activeSessionId);
211
+ shouldNavigateAfter = true;
212
+ }
213
+ }
214
+ let accumulatedContent = "";
215
+ let createdSessionId;
216
+ setIsStreaming(true);
217
+ for await (const chunk of dataSource.sendMessage(
218
+ activeSessionId || "new",
219
+ content,
220
+ attachments,
221
+ abortControllerRef.current?.signal
222
+ )) {
223
+ if (abortControllerRef.current?.signal.aborted) {
224
+ break;
225
+ }
226
+ if (chunk.type === "chunk" && chunk.content) {
227
+ accumulatedContent += chunk.content;
228
+ setStreamingContent(accumulatedContent);
229
+ } else if (chunk.type === "error") {
230
+ throw new Error(chunk.error || "Stream error");
231
+ } else if (chunk.type === "done") {
232
+ if (chunk.sessionId) {
233
+ createdSessionId = chunk.sessionId;
234
+ }
235
+ const finalSessionId = createdSessionId || activeSessionId;
236
+ if (finalSessionId && finalSessionId !== "new") {
237
+ const state = await dataSource.fetchSession(finalSessionId);
238
+ if (state) {
239
+ setSession(state.session);
240
+ setMessages(state.messages);
241
+ setPendingQuestion(state.pendingQuestion || null);
242
+ }
243
+ }
244
+ } else if (chunk.type === "user_message" && chunk.sessionId) {
245
+ createdSessionId = chunk.sessionId;
246
+ }
247
+ }
248
+ const targetSessionId = createdSessionId || activeSessionId;
249
+ if (shouldNavigateAfter && targetSessionId && targetSessionId !== "new") {
250
+ dataSource.navigateToSession?.(targetSessionId);
251
+ }
252
+ } catch (err) {
253
+ if (err instanceof Error && err.name === "AbortError") {
254
+ setMessage(content);
255
+ return;
256
+ }
257
+ setMessages((prev) => prev.filter((m) => m.id !== tempUserMessage.id));
258
+ const errorMessage = err instanceof Error ? err.message : "Failed to send message";
259
+ setError(errorMessage);
260
+ console.error("Send message error:", err);
261
+ } finally {
262
+ setLoading(false);
263
+ setStreamingContent("");
264
+ setIsStreaming(false);
265
+ abortControllerRef.current = null;
266
+ }
267
+ },
268
+ [currentSessionId, loading, dataSource]
269
+ );
270
+ const cancelStream = react.useCallback(() => {
271
+ if (abortControllerRef.current) {
272
+ abortControllerRef.current.abort();
273
+ abortControllerRef.current = null;
274
+ setIsStreaming(false);
275
+ setLoading(false);
276
+ }
277
+ }, []);
278
+ const handleSubmit = react.useCallback(
279
+ (e, attachments = []) => {
280
+ e.preventDefault();
281
+ if (!message.trim() && attachments.length === 0) return;
282
+ const convertedAttachments = attachments.map((att) => ({
283
+ id: "",
284
+ // Will be assigned by backend
285
+ filename: att.filename,
286
+ mimeType: att.mimeType,
287
+ sizeBytes: att.sizeBytes,
288
+ base64Data: att.base64Data
289
+ }));
290
+ sendMessageDirect(message, convertedAttachments);
291
+ },
292
+ [message, sendMessageDirect]
293
+ );
294
+ const handleUnqueue = react.useCallback(() => {
295
+ if (messageQueue.length === 0) {
296
+ return null;
297
+ }
298
+ const lastQueued = messageQueue[messageQueue.length - 1];
299
+ setMessageQueue((prev) => prev.slice(0, -1));
300
+ return {
301
+ content: lastQueued.content,
302
+ attachments: lastQueued.attachments
303
+ };
304
+ }, [messageQueue]);
305
+ const handleRegenerate = react.useCallback(
306
+ async (messageId) => {
307
+ if (!currentSessionId || currentSessionId === "new") return;
308
+ const messageIndex = messages.findIndex((m) => m.id === messageId);
309
+ if (messageIndex <= 0) return;
310
+ setLoading(true);
311
+ setError(null);
312
+ try {
313
+ const userMessage = messages[messageIndex - 1];
314
+ if (userMessage && userMessage.role === "user" /* User */) {
315
+ await sendMessageDirect(userMessage.content, []);
316
+ }
317
+ } catch (err) {
318
+ const errorMessage = err instanceof Error ? err.message : "Failed to regenerate message";
319
+ setError(errorMessage);
320
+ console.error("Regenerate error:", err);
321
+ } finally {
322
+ setLoading(false);
323
+ }
324
+ },
325
+ [messages, currentSessionId, sendMessageDirect]
326
+ );
327
+ const handleEdit = react.useCallback(
328
+ async (messageId, newContent) => {
329
+ if (!currentSessionId || currentSessionId === "new") {
330
+ setMessage(newContent);
331
+ setMessages((prev) => prev.filter((m) => m.id !== messageId));
332
+ return;
333
+ }
334
+ setLoading(true);
335
+ setError(null);
336
+ try {
337
+ await sendMessageDirect(newContent, []);
338
+ } catch (err) {
339
+ const errorMessage = err instanceof Error ? err.message : "Failed to edit message";
340
+ setError(errorMessage);
341
+ console.error("Edit error:", err);
342
+ } finally {
343
+ setLoading(false);
344
+ }
345
+ },
346
+ [currentSessionId, sendMessageDirect]
347
+ );
348
+ const handleSubmitQuestionAnswers = react.useCallback(
349
+ (answers) => {
350
+ if (!currentSessionId || !pendingQuestion) return;
351
+ setLoading(true);
352
+ setError(null);
353
+ const previousPendingQuestion = pendingQuestion;
354
+ setPendingQuestion(null);
355
+ (async () => {
356
+ try {
357
+ const result = await dataSource.submitQuestionAnswers(
358
+ currentSessionId,
359
+ previousPendingQuestion.id,
360
+ answers
361
+ );
362
+ if (result.success) {
363
+ if (currentSessionId !== "new") {
364
+ try {
365
+ const state = await dataSource.fetchSession(currentSessionId);
366
+ if (state) {
367
+ setMessages(state.messages);
368
+ setPendingQuestion(state.pendingQuestion || null);
369
+ } else {
370
+ setPendingQuestion(previousPendingQuestion);
371
+ setError("Failed to load updated session");
372
+ }
373
+ } catch (fetchErr) {
374
+ setPendingQuestion(previousPendingQuestion);
375
+ const errorMessage = fetchErr instanceof Error ? fetchErr.message : "Failed to load updated session";
376
+ setError(errorMessage);
377
+ }
378
+ }
379
+ } else {
380
+ setPendingQuestion(previousPendingQuestion);
381
+ setError(result.error || "Failed to submit answers");
382
+ }
383
+ } catch (err) {
384
+ setPendingQuestion(previousPendingQuestion);
385
+ const errorMessage = err instanceof Error ? err.message : "Failed to submit answers";
386
+ setError(errorMessage);
387
+ } finally {
388
+ setLoading(false);
389
+ }
390
+ })();
391
+ },
392
+ [currentSessionId, pendingQuestion, dataSource]
393
+ );
394
+ const handleCancelPendingQuestion = react.useCallback(async () => {
395
+ if (!currentSessionId || !pendingQuestion) return;
396
+ try {
397
+ const result = await dataSource.cancelPendingQuestion(pendingQuestion.id);
398
+ if (result.success) {
399
+ setPendingQuestion(null);
400
+ } else {
401
+ setError(result.error || "Failed to cancel question");
402
+ }
403
+ } catch (err) {
404
+ const errorMessage = err instanceof Error ? err.message : "Failed to cancel question";
405
+ setError(errorMessage);
406
+ }
407
+ }, [currentSessionId, pendingQuestion, dataSource]);
408
+ const value = {
409
+ // State
410
+ message,
411
+ messages,
412
+ loading,
413
+ error,
414
+ currentSessionId,
415
+ pendingQuestion,
416
+ session,
417
+ fetching,
418
+ streamingContent,
419
+ isStreaming,
420
+ messageQueue,
421
+ codeOutputs,
422
+ // Setters
423
+ setMessage,
424
+ setError,
425
+ setCodeOutputs,
426
+ // Handlers
427
+ handleCopy,
428
+ handleRegenerate,
429
+ handleEdit,
430
+ handleSubmit,
431
+ handleSubmitQuestionAnswers,
432
+ handleCancelPendingQuestion,
433
+ handleUnqueue,
434
+ sendMessage: sendMessageDirect,
435
+ cancel: cancelStream
436
+ };
437
+ return /* @__PURE__ */ jsxRuntime.jsx(ChatSessionContext.Provider, { value, children });
438
+ }
439
+ function useChat() {
440
+ const context = react.useContext(ChatSessionContext);
441
+ if (!context) {
442
+ throw new Error("useChat must be used within ChatSessionProvider");
443
+ }
444
+ return context;
445
+ }
446
+ var IotaContext = react.createContext(null);
447
+ function IotaContextProvider({ children }) {
448
+ const initialContext = window.__BICHAT_CONTEXT__;
449
+ if (!initialContext) {
450
+ throw new Error("BICHAT_CONTEXT not found. Ensure server injected context into window object.");
451
+ }
452
+ return /* @__PURE__ */ jsxRuntime.jsx(IotaContext.Provider, { value: initialContext, children });
453
+ }
454
+ function useIotaContext() {
455
+ const context = react.useContext(IotaContext);
456
+ if (!context) {
457
+ throw new Error("useIotaContext must be used within IotaContextProvider");
458
+ }
459
+ return context;
460
+ }
461
+ function hasPermission(permission) {
462
+ const context = window.__BICHAT_CONTEXT__;
463
+ if (!context) {
464
+ return false;
465
+ }
466
+ return context.user.permissions.includes(permission);
467
+ }
468
+
469
+ // ui/src/bichat/locales/defaults.ts
470
+ var defaultTranslations = {
471
+ // Welcome screen
472
+ "welcome.title": "Welcome to BiChat",
473
+ "welcome.description": "Your intelligent business analytics assistant. Ask questions about your data, generate reports, or explore insights.",
474
+ "welcome.tryAsking": "Try asking",
475
+ // Chat header
476
+ "chat.newChat": "New Chat",
477
+ "chat.archived": "Archived",
478
+ "chat.pinned": "Pinned",
479
+ "chat.goBack": "Go back",
480
+ // Message input
481
+ "input.placeholder": "Type a message...",
482
+ "input.attachFiles": "Attach files",
483
+ "input.attachImages": "Attach images",
484
+ "input.dropImages": "Drop images here",
485
+ "input.sendMessage": "Send message",
486
+ "input.aiThinking": "AI is thinking...",
487
+ "input.processing": "Processing...",
488
+ "input.messagesQueued": "{count} message(s) queued",
489
+ "input.dismissError": "Dismiss error",
490
+ // Message actions
491
+ "message.copy": "Copy",
492
+ "message.copied": "Copied!",
493
+ "message.regenerate": "Regenerate",
494
+ "message.edit": "Edit",
495
+ "message.save": "Save",
496
+ "message.cancel": "Cancel",
497
+ // Assistant turn
498
+ "assistant.thinking": "Thinking...",
499
+ "assistant.toolCall": "Using tool: {name}",
500
+ "assistant.generating": "Generating response...",
501
+ // Question form
502
+ "question.submit": "Submit",
503
+ "question.selectOne": "Select one option",
504
+ "question.selectMulti": "Select one or more options",
505
+ "question.required": "This field is required",
506
+ "question.other": "Other",
507
+ "question.specifyOther": "Please specify",
508
+ // Errors
509
+ "error.generic": "Something went wrong",
510
+ "error.networkError": "Network error. Please try again.",
511
+ "error.sessionExpired": "Session expired. Please refresh.",
512
+ "error.fileTooLarge": "File is too large",
513
+ "error.invalidFile": "Invalid file type",
514
+ "error.maxFiles": "Maximum {max} files allowed",
515
+ // Empty states
516
+ "empty.noMessages": "No messages yet",
517
+ "empty.noSessions": "No chat sessions",
518
+ "empty.startChat": "Start a new chat to begin",
519
+ // Sources panel
520
+ "sources.title": "Sources",
521
+ "sources.viewMore": "View more",
522
+ "sources.citations": "{count} citation(s)",
523
+ // Code outputs
524
+ "codeOutputs.title": "Code Outputs",
525
+ "codeOutputs.download": "Download",
526
+ "codeOutputs.expand": "Expand",
527
+ "codeOutputs.collapse": "Collapse",
528
+ // Charts
529
+ "chart.download": "Download chart",
530
+ "chart.fullscreen": "View fullscreen",
531
+ "chart.noData": "No data available",
532
+ // Example prompt categories
533
+ "category.analysis": "Data Analysis",
534
+ "category.reports": "Reports",
535
+ "category.insights": "Insights"
536
+ };
537
+
538
+ // ui/src/bichat/hooks/useTranslation.ts
539
+ function useTranslation() {
540
+ const { locale } = useIotaContext();
541
+ const t = (key, params) => {
542
+ let text = locale.translations[key] || defaultTranslations[key] || key;
543
+ if (params) {
544
+ Object.keys(params).forEach((paramKey) => {
545
+ const value = params[paramKey];
546
+ text = text.replace(new RegExp(`{{${paramKey}}}`, "g"), String(value));
547
+ });
548
+ }
549
+ return text;
550
+ };
551
+ return {
552
+ t,
553
+ locale: locale.language
554
+ };
555
+ }
556
+ var defaultExamplePrompts = [
557
+ {
558
+ category: "Data Analysis",
559
+ text: "Show me sales trends for the last quarter",
560
+ icon: "chart-bar"
561
+ },
562
+ {
563
+ category: "Reports",
564
+ text: "Generate a summary of recent activity",
565
+ icon: "file-text"
566
+ },
567
+ {
568
+ category: "Insights",
569
+ text: "What are the top performing items?",
570
+ icon: "lightbulb"
571
+ }
572
+ ];
573
+ function useBranding() {
574
+ const context = useIotaContext();
575
+ const { t } = useTranslation();
576
+ const branding = react.useMemo(() => {
577
+ const customBranding = context.extensions?.branding || {};
578
+ let examplePrompts = customBranding.welcome?.examplePrompts;
579
+ if (!examplePrompts || examplePrompts.length === 0) {
580
+ examplePrompts = defaultExamplePrompts.map((p) => ({
581
+ ...p,
582
+ category: t(`category.${p.category.toLowerCase().replace(/\s+/g, "")}`) || p.category
583
+ }));
584
+ }
585
+ return {
586
+ appName: customBranding.appName || "BiChat",
587
+ logoUrl: customBranding.logoUrl,
588
+ welcome: {
589
+ title: customBranding.welcome?.title || t("welcome.title"),
590
+ description: customBranding.welcome?.description || t("welcome.description"),
591
+ examplePrompts
592
+ },
593
+ theme: customBranding.theme
594
+ };
595
+ }, [context.extensions?.branding, t]);
596
+ return branding;
597
+ }
598
+ function ChatHeader({ session, onBack, logoSlot, actionsSlot }) {
599
+ const { t } = useTranslation();
600
+ const branding = useBranding();
601
+ const BackButton = onBack ? /* @__PURE__ */ jsxRuntime.jsx(
602
+ "button",
603
+ {
604
+ onClick: onBack,
605
+ className: "p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors",
606
+ "aria-label": t("chat.goBack"),
607
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 19l-7-7 7-7" }) })
608
+ }
609
+ ) : null;
610
+ const Logo = logoSlot || (branding.logoUrl ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: branding.logoUrl, alt: branding.appName, className: "h-6 w-auto" }) : null);
611
+ if (!session) {
612
+ return /* @__PURE__ */ jsxRuntime.jsx("header", { className: "bichat-header border-b border-[var(--bichat-border)] px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
613
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
614
+ BackButton,
615
+ Logo,
616
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-lg font-semibold text-[var(--bichat-text)]", children: t("chat.newChat") })
617
+ ] }),
618
+ actionsSlot && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2", children: actionsSlot })
619
+ ] }) });
620
+ }
621
+ return /* @__PURE__ */ jsxRuntime.jsx("header", { className: "bichat-header border-b border-[var(--bichat-border)] px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
622
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
623
+ BackButton,
624
+ Logo,
625
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-lg font-semibold text-[var(--bichat-text)]", children: session.title }),
626
+ session.pinned && /* @__PURE__ */ jsxRuntime.jsx(
627
+ "svg",
628
+ {
629
+ className: "w-4 h-4 text-[var(--bichat-primary)]",
630
+ fill: "currentColor",
631
+ viewBox: "0 0 20 20",
632
+ "aria-label": t("chat.pinned"),
633
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M10 2a1 1 0 011 1v1.323l3.954 1.582 1.599-.8a1 1 0 01.894 1.79l-1.233.616 1.738 5.42a1 1 0 01-.285 1.05A3.989 3.989 0 0115 15a3.989 3.989 0 01-2.667-1.019 1 1 0 01-.285-1.05l1.715-5.349L11 6.477V16h2a1 1 0 110 2H7a1 1 0 110-2h2V6.477L6.237 7.582l1.715 5.349a1 1 0 01-.285 1.05A3.989 3.989 0 015 15a3.989 3.989 0 01-2.667-1.019 1 1 0 01-.285-1.05l1.738-5.42-1.233-.617a1 1 0 01.894-1.788l1.599.799L9 4.323V3a1 1 0 011-1z" })
634
+ }
635
+ )
636
+ ] }),
637
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
638
+ session.status === "archived" && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-2 py-1 text-xs bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded", children: t("chat.archived") }),
639
+ actionsSlot
640
+ ] })
641
+ ] }) });
642
+ }
643
+
644
+ // ui/src/bichat/utils/fileUtils.ts
645
+ var MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024;
646
+ var ALLOWED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
647
+ function validateImageFile(file, maxSizeBytes = MAX_FILE_SIZE_BYTES) {
648
+ if (!ALLOWED_IMAGE_TYPES.includes(file.type)) {
649
+ throw new Error(`Invalid file type: ${file.type}. Only PNG, JPEG, WebP, and GIF are allowed.`);
650
+ }
651
+ if (file.size > maxSizeBytes) {
652
+ const sizeMB = (file.size / 1024 / 1024).toFixed(1);
653
+ const maxSizeMB = (maxSizeBytes / 1024 / 1024).toFixed(0);
654
+ throw new Error(`File too large: ${sizeMB}MB exceeds ${maxSizeMB}MB limit`);
655
+ }
656
+ }
657
+ async function convertToBase64(file) {
658
+ return new Promise((resolve, reject) => {
659
+ const reader = new FileReader();
660
+ reader.onload = () => {
661
+ const result = reader.result;
662
+ const base64 = result.split(",")[1];
663
+ resolve(base64);
664
+ };
665
+ reader.onerror = () => reject(new Error("Failed to read file"));
666
+ reader.readAsDataURL(file);
667
+ });
668
+ }
669
+ function createDataUrl(base64, mimeType) {
670
+ return `data:${mimeType};base64,${base64}`;
671
+ }
672
+ function formatFileSize(bytes) {
673
+ if (bytes < 1024) return `${bytes} B`;
674
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
675
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
676
+ }
677
+ function validateFileCount(currentCount, newCount, maxCount = 10) {
678
+ const total = currentCount + newCount;
679
+ if (total > maxCount) {
680
+ throw new Error(`Cannot attach more than ${maxCount} files (attempting to add ${total})`);
681
+ }
682
+ }
683
+ function AttachmentGrid({
684
+ attachments,
685
+ onRemove,
686
+ onView,
687
+ className = ""
688
+ }) {
689
+ if (attachments.length === 0) return null;
690
+ const isEditable = !!onRemove;
691
+ const isViewable = !!onView;
692
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2 ${className}`, children: attachments.map((attachment, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative group", children: [
693
+ /* @__PURE__ */ jsxRuntime.jsx(
694
+ "img",
695
+ {
696
+ src: attachment.preview,
697
+ alt: attachment.filename,
698
+ className: `w-full h-24 object-cover rounded-lg border border-gray-200 dark:border-gray-700 ${isViewable ? "cursor-pointer hover:opacity-80 transition-opacity" : ""}`,
699
+ onClick: () => isViewable && onView(index),
700
+ role: isViewable ? "button" : void 0,
701
+ tabIndex: isViewable ? 0 : void 0,
702
+ onKeyDown: (e) => {
703
+ if (isViewable && (e.key === "Enter" || e.key === " ")) {
704
+ e.preventDefault();
705
+ onView(index);
706
+ }
707
+ }
708
+ }
709
+ ),
710
+ isEditable && /* @__PURE__ */ jsxRuntime.jsx(
711
+ "button",
712
+ {
713
+ type: "button",
714
+ onClick: (e) => {
715
+ e.stopPropagation();
716
+ onRemove(index);
717
+ },
718
+ className: "absolute top-1 right-1 p-1 bg-red-500 hover:bg-red-600 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity shadow-md",
719
+ "aria-label": `Remove ${attachment.filename}`,
720
+ children: /* @__PURE__ */ jsxRuntime.jsx(react$1.X, { size: 16, weight: "bold" })
721
+ }
722
+ ),
723
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-1 px-1", children: [
724
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-600 dark:text-gray-400 truncate", title: attachment.filename, children: attachment.filename.length > 20 ? `${attachment.filename.substring(0, 20)}...` : attachment.filename }),
725
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-500 dark:text-gray-500", children: formatFileSize(attachment.sizeBytes) })
726
+ ] })
727
+ ] }, index)) });
728
+ }
729
+ function ImageModal({ images, initialIndex, onClose }) {
730
+ const [currentIndex, setCurrentIndex] = react.useState(initialIndex);
731
+ const handlePrevious = () => {
732
+ setCurrentIndex((prev) => prev > 0 ? prev - 1 : images.length - 1);
733
+ };
734
+ const handleNext = () => {
735
+ setCurrentIndex((prev) => prev < images.length - 1 ? prev + 1 : 0);
736
+ };
737
+ react.useEffect(() => {
738
+ const handleKeyDown = (e) => {
739
+ if (e.key === "Escape") {
740
+ onClose();
741
+ } else if (e.key === "ArrowLeft") {
742
+ handlePrevious();
743
+ } else if (e.key === "ArrowRight") {
744
+ handleNext();
745
+ }
746
+ };
747
+ window.addEventListener("keydown", handleKeyDown);
748
+ return () => window.removeEventListener("keydown", handleKeyDown);
749
+ }, [onClose]);
750
+ react.useEffect(() => {
751
+ document.body.style.overflow = "hidden";
752
+ return () => {
753
+ document.body.style.overflow = "";
754
+ };
755
+ }, []);
756
+ return /* @__PURE__ */ jsxRuntime.jsxs(
757
+ "div",
758
+ {
759
+ className: "fixed inset-0 z-50 bg-black/90 flex items-center justify-center",
760
+ onClick: onClose,
761
+ role: "dialog",
762
+ "aria-modal": "true",
763
+ "aria-label": "Image viewer",
764
+ children: [
765
+ /* @__PURE__ */ jsxRuntime.jsx(
766
+ "button",
767
+ {
768
+ onClick: onClose,
769
+ className: "absolute top-4 right-4 p-2 text-white hover:bg-white/10 rounded-lg transition-colors",
770
+ "aria-label": "Close image viewer",
771
+ children: /* @__PURE__ */ jsxRuntime.jsx(react$1.X, { size: 24, weight: "bold" })
772
+ }
773
+ ),
774
+ images.length > 1 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
775
+ /* @__PURE__ */ jsxRuntime.jsx(
776
+ "button",
777
+ {
778
+ onClick: (e) => {
779
+ e.stopPropagation();
780
+ handlePrevious();
781
+ },
782
+ className: "absolute left-4 p-2 text-white hover:bg-white/10 rounded-lg transition-colors",
783
+ "aria-label": "Previous image",
784
+ children: /* @__PURE__ */ jsxRuntime.jsx(react$1.CaretLeft, { size: 32, weight: "bold" })
785
+ }
786
+ ),
787
+ /* @__PURE__ */ jsxRuntime.jsx(
788
+ "button",
789
+ {
790
+ onClick: (e) => {
791
+ e.stopPropagation();
792
+ handleNext();
793
+ },
794
+ className: "absolute right-4 p-2 text-white hover:bg-white/10 rounded-lg transition-colors",
795
+ "aria-label": "Next image",
796
+ children: /* @__PURE__ */ jsxRuntime.jsx(react$1.CaretRight, { size: 32, weight: "bold" })
797
+ }
798
+ )
799
+ ] }),
800
+ /* @__PURE__ */ jsxRuntime.jsx(
801
+ "img",
802
+ {
803
+ src: images[currentIndex].preview,
804
+ alt: images[currentIndex].filename,
805
+ className: "max-w-[90vw] max-h-[90vh] object-contain",
806
+ onClick: (e) => e.stopPropagation()
807
+ }
808
+ ),
809
+ /* @__PURE__ */ jsxRuntime.jsxs(
810
+ "div",
811
+ {
812
+ className: "absolute bottom-4 left-1/2 transform -translate-x-1/2 text-white text-sm bg-black/50 px-4 py-2 rounded-lg backdrop-blur",
813
+ onClick: (e) => e.stopPropagation(),
814
+ children: [
815
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center font-medium mb-1", children: images[currentIndex].filename }),
816
+ images.length > 1 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center text-xs opacity-80", children: [
817
+ currentIndex + 1,
818
+ " / ",
819
+ images.length
820
+ ] })
821
+ ]
822
+ }
823
+ )
824
+ ]
825
+ }
826
+ );
827
+ }
828
+ function UserTurnView({ message }) {
829
+ const { handleEdit, handleCopy } = useChat();
830
+ const [selectedImageIndex, setSelectedImageIndex] = react.useState(null);
831
+ const handleCopyClick = async () => {
832
+ if (handleCopy) {
833
+ await handleCopy(message.content);
834
+ } else {
835
+ try {
836
+ await navigator.clipboard.writeText(message.content);
837
+ } catch (err) {
838
+ console.error("Failed to copy:", err);
839
+ }
840
+ }
841
+ };
842
+ const handleEditClick = () => {
843
+ if (handleEdit) {
844
+ const newContent = prompt("Edit message:", message.content);
845
+ if (newContent && newContent !== message.content) {
846
+ handleEdit(message.id, newContent);
847
+ }
848
+ }
849
+ };
850
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-3 justify-end group", children: [
851
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex flex-col items-end max-w-[75%]", children: [
852
+ message.attachments && message.attachments.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-2 w-full", children: /* @__PURE__ */ jsxRuntime.jsx(
853
+ AttachmentGrid,
854
+ {
855
+ attachments: message.attachments,
856
+ onView: (index) => setSelectedImageIndex(index)
857
+ }
858
+ ) }),
859
+ message.content && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-primary-600 text-white rounded-2xl rounded-br-sm px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-[15px] whitespace-pre-wrap break-words leading-relaxed", children: message.content }) }),
860
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 mt-2 opacity-0 group-hover:opacity-100 transition-opacity duration-150", children: [
861
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400 dark:text-gray-500 mr-1", children: dateFns.formatDistanceToNow(new Date(message.createdAt), { addSuffix: true }) }),
862
+ /* @__PURE__ */ jsxRuntime.jsx(
863
+ "button",
864
+ {
865
+ onClick: handleCopyClick,
866
+ className: "p-1.5 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors duration-150",
867
+ "aria-label": "Copy message",
868
+ title: "Copy",
869
+ children: /* @__PURE__ */ jsxRuntime.jsx(react$1.Copy, { size: 14, weight: "regular" })
870
+ }
871
+ ),
872
+ handleEdit && /* @__PURE__ */ jsxRuntime.jsx(
873
+ "button",
874
+ {
875
+ onClick: handleEditClick,
876
+ className: "p-1.5 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors duration-150",
877
+ "aria-label": "Edit message",
878
+ title: "Edit",
879
+ children: /* @__PURE__ */ jsxRuntime.jsx(react$1.PencilSimple, { size: 14, weight: "regular" })
880
+ }
881
+ )
882
+ ] })
883
+ ] }),
884
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-shrink-0 w-8 h-8 rounded-full bg-primary-600 flex items-center justify-center text-white font-medium text-sm", children: "U" }),
885
+ selectedImageIndex !== null && message.attachments && /* @__PURE__ */ jsxRuntime.jsx(
886
+ ImageModal,
887
+ {
888
+ images: message.attachments,
889
+ initialIndex: selectedImageIndex,
890
+ onClose: () => setSelectedImageIndex(null)
891
+ }
892
+ )
893
+ ] });
894
+ }
895
+ function CodeOutputsPanel({ outputs }) {
896
+ if (!outputs || outputs.length === 0) return null;
897
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-2 p-3 bg-gray-50 dark:bg-gray-900/50 rounded-lg border border-gray-200 dark:border-gray-700", children: [
898
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs font-semibold text-gray-600 dark:text-gray-400 mb-2", children: "Code Output" }),
899
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", children: outputs.map((output, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
900
+ output.type === "image" && /* @__PURE__ */ jsxRuntime.jsx(
901
+ "img",
902
+ {
903
+ src: output.content.preview,
904
+ alt: "Code output",
905
+ className: "max-w-full rounded border border-gray-300 dark:border-gray-600"
906
+ }
907
+ ),
908
+ output.type === "text" && /* @__PURE__ */ jsxRuntime.jsx("pre", { className: "text-xs bg-white dark:bg-gray-800 p-2 rounded overflow-x-auto border border-gray-200 dark:border-gray-700", children: /* @__PURE__ */ jsxRuntime.jsx("code", { className: "text-gray-900 dark:text-gray-100", children: output.content }) }),
909
+ output.type === "error" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20 p-2 rounded border border-red-200 dark:border-red-800", children: output.content })
910
+ ] }, index)) })
911
+ ] });
912
+ }
913
+ function StreamingCursor() {
914
+ return /* @__PURE__ */ jsxRuntime.jsx(
915
+ "span",
916
+ {
917
+ className: "inline-block w-1.5 h-4 ml-0.5 bg-primary-600 dark:bg-primary-500 animate-pulse",
918
+ "aria-label": "AI is typing"
919
+ }
920
+ );
921
+ }
922
+ var COLORS = ["#3b82f6", "#10b981", "#f59e0b", "#ef4444", "#8b5cf6", "#ec4899"];
923
+ function ChartCard({ chartData }) {
924
+ const { type, title, data, xAxisKey = "name", yAxisKey = "value" } = chartData;
925
+ const renderChart = () => {
926
+ switch (type) {
927
+ case "bar":
928
+ return /* @__PURE__ */ jsxRuntime.jsxs(recharts.BarChart, { data, children: [
929
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.CartesianGrid, { strokeDasharray: "3 3" }),
930
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.XAxis, { dataKey: xAxisKey }),
931
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.YAxis, {}),
932
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.Tooltip, {}),
933
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.Legend, {}),
934
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.Bar, { dataKey: yAxisKey, fill: "#3b82f6" })
935
+ ] });
936
+ case "line":
937
+ return /* @__PURE__ */ jsxRuntime.jsxs(recharts.LineChart, { data, children: [
938
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.CartesianGrid, { strokeDasharray: "3 3" }),
939
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.XAxis, { dataKey: xAxisKey }),
940
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.YAxis, {}),
941
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.Tooltip, {}),
942
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.Legend, {}),
943
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.Line, { type: "monotone", dataKey: yAxisKey, stroke: "#3b82f6" })
944
+ ] });
945
+ case "pie":
946
+ return /* @__PURE__ */ jsxRuntime.jsxs(recharts.PieChart, { children: [
947
+ /* @__PURE__ */ jsxRuntime.jsx(
948
+ recharts.Pie,
949
+ {
950
+ data,
951
+ dataKey: yAxisKey,
952
+ nameKey: xAxisKey,
953
+ cx: "50%",
954
+ cy: "50%",
955
+ outerRadius: 80,
956
+ label: true,
957
+ children: data.map((_entry, index) => /* @__PURE__ */ jsxRuntime.jsx(recharts.Cell, { fill: COLORS[index % COLORS.length] }, `cell-${index}`))
958
+ }
959
+ ),
960
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.Tooltip, {}),
961
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.Legend, {})
962
+ ] });
963
+ case "area":
964
+ return /* @__PURE__ */ jsxRuntime.jsxs(recharts.AreaChart, { data, children: [
965
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.CartesianGrid, { strokeDasharray: "3 3" }),
966
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.XAxis, { dataKey: xAxisKey }),
967
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.YAxis, {}),
968
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.Tooltip, {}),
969
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.Legend, {}),
970
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.Area, { type: "monotone", dataKey: yAxisKey, stroke: "#3b82f6", fill: "#3b82f6" })
971
+ ] });
972
+ default:
973
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { children: "Unsupported chart type" });
974
+ }
975
+ };
976
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border border-[var(--bichat-border)] rounded-lg p-4 bg-white", children: [
977
+ title && /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold mb-4", children: title }),
978
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.ResponsiveContainer, { width: "100%", height: 300, children: renderChart() })
979
+ ] });
980
+ }
981
+ function SourcesPanel({ citations }) {
982
+ const [expanded, setExpanded] = react.useState(false);
983
+ if (!citations || citations.length === 0) {
984
+ return null;
985
+ }
986
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-4 border-t border-[var(--bichat-border)] pt-3", children: [
987
+ /* @__PURE__ */ jsxRuntime.jsxs(
988
+ "button",
989
+ {
990
+ type: "button",
991
+ onClick: () => setExpanded(!expanded),
992
+ className: "flex items-center gap-2 text-sm text-gray-600 hover:text-gray-800 transition-colors",
993
+ "aria-expanded": expanded,
994
+ children: [
995
+ /* @__PURE__ */ jsxRuntime.jsx(
996
+ "svg",
997
+ {
998
+ className: `w-4 h-4 transition-transform ${expanded ? "rotate-90" : ""}`,
999
+ fill: "none",
1000
+ stroke: "currentColor",
1001
+ viewBox: "0 0 24 24",
1002
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1003
+ "path",
1004
+ {
1005
+ strokeLinecap: "round",
1006
+ strokeLinejoin: "round",
1007
+ strokeWidth: 2,
1008
+ d: "M9 5l7 7-7 7"
1009
+ }
1010
+ )
1011
+ }
1012
+ ),
1013
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
1014
+ citations.length,
1015
+ " ",
1016
+ citations.length === 1 ? "source" : "sources"
1017
+ ] })
1018
+ ]
1019
+ }
1020
+ ),
1021
+ expanded && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2 space-y-2", children: citations.map((citation, index) => /* @__PURE__ */ jsxRuntime.jsx(
1022
+ "div",
1023
+ {
1024
+ className: "p-3 bg-gray-50 rounded-lg text-sm",
1025
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2", children: [
1026
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-shrink-0 w-5 h-5 bg-[var(--bichat-primary)] text-white rounded-full flex items-center justify-center text-xs", children: index + 1 }),
1027
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
1028
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-medium text-gray-900", children: citation.source }),
1029
+ citation.url && /* @__PURE__ */ jsxRuntime.jsx(
1030
+ "a",
1031
+ {
1032
+ href: citation.url,
1033
+ target: "_blank",
1034
+ rel: "noopener noreferrer",
1035
+ className: "text-[var(--bichat-primary)] hover:underline",
1036
+ children: citation.url
1037
+ }
1038
+ ),
1039
+ citation.excerpt && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-1 text-gray-600 italic", children: [
1040
+ '"',
1041
+ citation.excerpt,
1042
+ '"'
1043
+ ] })
1044
+ ] })
1045
+ ] })
1046
+ },
1047
+ citation.id
1048
+ )) })
1049
+ ] });
1050
+ }
1051
+ function DownloadCard({ artifact }) {
1052
+ const { type, filename, url, sizeReadable, rowCount, description } = artifact;
1053
+ const icon = type === "excel" ? /* @__PURE__ */ jsxRuntime.jsxs("svg", { className: "w-8 h-8 text-green-600", fill: "currentColor", viewBox: "0 0 20 20", children: [
1054
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 2a2 2 0 00-2 2v8a2 2 0 002 2h6a2 2 0 002-2V6.414A2 2 0 0016.414 5L14 2.586A2 2 0 0012.586 2H9z" }),
1055
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 8a2 2 0 012-2v10h8a2 2 0 01-2 2H5a2 2 0 01-2-2V8z" })
1056
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-8 h-8 text-red-600", fill: "currentColor", viewBox: "0 0 20 20", children: /* @__PURE__ */ jsxRuntime.jsx(
1057
+ "path",
1058
+ {
1059
+ fillRule: "evenodd",
1060
+ d: "M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z",
1061
+ clipRule: "evenodd"
1062
+ }
1063
+ ) });
1064
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1065
+ "a",
1066
+ {
1067
+ href: url,
1068
+ download: filename,
1069
+ className: "flex items-center gap-3 p-4 border border-[var(--bichat-border)] rounded-lg hover:bg-gray-50 transition-colors",
1070
+ children: [
1071
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: icon }),
1072
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
1073
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-medium text-gray-900 truncate", children: filename }),
1074
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm text-gray-600", children: [
1075
+ sizeReadable && /* @__PURE__ */ jsxRuntime.jsx("span", { children: sizeReadable }),
1076
+ rowCount !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1077
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "\u2022" }),
1078
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
1079
+ rowCount,
1080
+ " rows"
1081
+ ] })
1082
+ ] })
1083
+ ] }),
1084
+ description && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm text-gray-600 mt-1", children: description })
1085
+ ] }),
1086
+ /* @__PURE__ */ jsxRuntime.jsx(
1087
+ "svg",
1088
+ {
1089
+ className: "w-5 h-5 text-gray-400",
1090
+ fill: "none",
1091
+ stroke: "currentColor",
1092
+ viewBox: "0 0 24 24",
1093
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1094
+ "path",
1095
+ {
1096
+ strokeLinecap: "round",
1097
+ strokeLinejoin: "round",
1098
+ strokeWidth: 2,
1099
+ d: "M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
1100
+ }
1101
+ )
1102
+ }
1103
+ )
1104
+ ]
1105
+ }
1106
+ );
1107
+ }
1108
+ function InlineQuestionForm({ pendingQuestion }) {
1109
+ const { handleSubmitQuestionAnswers, handleCancelPendingQuestion } = useChat();
1110
+ const [answers, setAnswers] = react.useState({});
1111
+ const [textInput, setTextInput] = react.useState("");
1112
+ const handleSubmit = (e) => {
1113
+ e.preventDefault();
1114
+ if (pendingQuestion.type === "MULTIPLE_CHOICE" && !answers.choice) {
1115
+ return;
1116
+ }
1117
+ if (pendingQuestion.type === "FREE_TEXT" && !textInput.trim()) {
1118
+ return;
1119
+ }
1120
+ const finalAnswers = pendingQuestion.type === "MULTIPLE_CHOICE" ? answers : { answer: textInput };
1121
+ handleSubmitQuestionAnswers(finalAnswers);
1122
+ };
1123
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border border-[var(--bichat-border)] rounded-lg p-4 bg-yellow-50", children: /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleSubmit, children: [
1124
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2 mb-4", children: [
1125
+ /* @__PURE__ */ jsxRuntime.jsx(
1126
+ "svg",
1127
+ {
1128
+ className: "w-5 h-5 text-yellow-600 flex-shrink-0 mt-0.5",
1129
+ fill: "currentColor",
1130
+ viewBox: "0 0 20 20",
1131
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1132
+ "path",
1133
+ {
1134
+ fillRule: "evenodd",
1135
+ d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z",
1136
+ clipRule: "evenodd"
1137
+ }
1138
+ )
1139
+ }
1140
+ ),
1141
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
1142
+ /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "font-medium text-gray-900 mb-2", children: "Question from AI" }),
1143
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-700", children: pendingQuestion.question })
1144
+ ] })
1145
+ ] }),
1146
+ pendingQuestion.type === "MULTIPLE_CHOICE" && pendingQuestion.options && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2 mb-4", children: pendingQuestion.options.map((option, index) => /* @__PURE__ */ jsxRuntime.jsxs(
1147
+ "label",
1148
+ {
1149
+ className: "flex items-center gap-2 p-2 hover:bg-yellow-100 rounded cursor-pointer",
1150
+ children: [
1151
+ /* @__PURE__ */ jsxRuntime.jsx(
1152
+ "input",
1153
+ {
1154
+ type: "radio",
1155
+ name: "choice",
1156
+ value: option,
1157
+ checked: answers.choice === option,
1158
+ onChange: (e) => setAnswers({ ...answers, choice: e.target.value }),
1159
+ className: "w-4 h-4 text-[var(--bichat-primary)]"
1160
+ }
1161
+ ),
1162
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-900", children: option })
1163
+ ]
1164
+ },
1165
+ index
1166
+ )) }),
1167
+ pendingQuestion.type === "FREE_TEXT" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-4", children: /* @__PURE__ */ jsxRuntime.jsx(
1168
+ "textarea",
1169
+ {
1170
+ value: textInput,
1171
+ onChange: (e) => setTextInput(e.target.value),
1172
+ placeholder: "Type your answer...",
1173
+ className: "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[var(--bichat-primary)] focus:border-transparent resize-none",
1174
+ rows: 3
1175
+ }
1176
+ ) }),
1177
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
1178
+ /* @__PURE__ */ jsxRuntime.jsx(
1179
+ "button",
1180
+ {
1181
+ type: "submit",
1182
+ className: "px-4 py-2 bg-[var(--bichat-primary)] text-white rounded-lg hover:opacity-90 transition-opacity",
1183
+ children: "Submit Answer"
1184
+ }
1185
+ ),
1186
+ /* @__PURE__ */ jsxRuntime.jsx(
1187
+ "button",
1188
+ {
1189
+ type: "button",
1190
+ onClick: handleCancelPendingQuestion,
1191
+ className: "px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors",
1192
+ children: "Cancel"
1193
+ }
1194
+ )
1195
+ ] })
1196
+ ] }) });
1197
+ }
1198
+ var MarkdownRenderer2 = react.lazy(
1199
+ () => Promise.resolve().then(() => (init_MarkdownRenderer(), MarkdownRenderer_exports)).then((module) => ({ default: module.MarkdownRenderer }))
1200
+ );
1201
+ function AssistantTurnView({ message }) {
1202
+ const { handleCopy, handleRegenerate, pendingQuestion } = useChat();
1203
+ const [explanationExpanded, setExplanationExpanded] = react.useState(false);
1204
+ const hasContent = message.content?.trim().length > 0;
1205
+ const hasExplanation = !!message.explanation?.trim();
1206
+ const hasPendingQuestion = !!pendingQuestion && pendingQuestion.status === "PENDING" && pendingQuestion.turnId === message.id;
1207
+ const handleCopyClick = async () => {
1208
+ if (handleCopy) {
1209
+ await handleCopy(message.content);
1210
+ } else {
1211
+ try {
1212
+ await navigator.clipboard.writeText(message.content);
1213
+ } catch (err) {
1214
+ console.error("Failed to copy:", err);
1215
+ }
1216
+ }
1217
+ };
1218
+ const handleRegenerateClick = async () => {
1219
+ if (handleRegenerate) {
1220
+ await handleRegenerate(message.id);
1221
+ }
1222
+ };
1223
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-3 group", children: [
1224
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-shrink-0 w-8 h-8 rounded-full bg-primary-600 flex items-center justify-center text-white font-medium text-xs", children: "AI" }),
1225
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex flex-col gap-3 max-w-[85%]", children: [
1226
+ message.codeOutputs && message.codeOutputs.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(CodeOutputsPanel, { outputs: message.codeOutputs }),
1227
+ message.chartData && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-1 w-full", children: /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { chartData: message.chartData }) }),
1228
+ message.artifacts && message.artifacts.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-1 flex flex-wrap gap-2", children: message.artifacts.map((artifact, index) => /* @__PURE__ */ jsxRuntime.jsx(DownloadCard, { artifact }, `${artifact.filename}-${index}`)) }),
1229
+ hasContent && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-2xl rounded-bl-sm px-4 py-3", children: [
1230
+ /* @__PURE__ */ jsxRuntime.jsx(
1231
+ react.Suspense,
1232
+ {
1233
+ fallback: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm text-gray-400 dark:text-gray-500", children: [
1234
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-4 h-4 border-2 border-gray-300 dark:border-gray-600 border-t-transparent rounded-full animate-spin" }),
1235
+ "Loading..."
1236
+ ] }),
1237
+ children: /* @__PURE__ */ jsxRuntime.jsx(MarkdownRenderer2, { content: message.content, citations: message.citations })
1238
+ }
1239
+ ),
1240
+ message.isStreaming && /* @__PURE__ */ jsxRuntime.jsx(StreamingCursor, {}),
1241
+ message.citations && message.citations.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(SourcesPanel, { citations: message.citations }),
1242
+ hasExplanation && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-4 border-t border-gray-100 dark:border-gray-700 pt-4", children: [
1243
+ /* @__PURE__ */ jsxRuntime.jsxs(
1244
+ "button",
1245
+ {
1246
+ type: "button",
1247
+ onClick: () => setExplanationExpanded(!explanationExpanded),
1248
+ className: "flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors",
1249
+ "aria-expanded": explanationExpanded,
1250
+ children: [
1251
+ /* @__PURE__ */ jsxRuntime.jsx(
1252
+ "svg",
1253
+ {
1254
+ className: `w-4 h-4 transition-transform duration-150 ${explanationExpanded ? "rotate-90" : ""}`,
1255
+ fill: "none",
1256
+ stroke: "currentColor",
1257
+ viewBox: "0 0 24 24",
1258
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1259
+ "path",
1260
+ {
1261
+ strokeLinecap: "round",
1262
+ strokeLinejoin: "round",
1263
+ strokeWidth: 2,
1264
+ d: "M9 5l7 7-7 7"
1265
+ }
1266
+ )
1267
+ }
1268
+ ),
1269
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: "How I arrived at this" })
1270
+ ]
1271
+ }
1272
+ ),
1273
+ explanationExpanded && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pt-3 text-sm text-gray-600 dark:text-gray-400", children: /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: /* @__PURE__ */ jsxRuntime.jsx("div", { children: "Loading..." }), children: /* @__PURE__ */ jsxRuntime.jsx(MarkdownRenderer2, { content: message.explanation }) }) })
1274
+ ] })
1275
+ ] }),
1276
+ hasPendingQuestion && /* @__PURE__ */ jsxRuntime.jsx(InlineQuestionForm, { pendingQuestion }),
1277
+ hasContent && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity duration-150", children: [
1278
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400 dark:text-gray-500 mr-1", children: dateFns.formatDistanceToNow(new Date(message.createdAt), { addSuffix: true }) }),
1279
+ /* @__PURE__ */ jsxRuntime.jsx(
1280
+ "button",
1281
+ {
1282
+ onClick: handleCopyClick,
1283
+ className: "p-1.5 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors duration-150",
1284
+ "aria-label": "Copy message",
1285
+ title: "Copy",
1286
+ children: /* @__PURE__ */ jsxRuntime.jsx(react$1.Copy, { size: 14, weight: "regular" })
1287
+ }
1288
+ ),
1289
+ handleRegenerate && /* @__PURE__ */ jsxRuntime.jsx(
1290
+ "button",
1291
+ {
1292
+ onClick: handleRegenerateClick,
1293
+ className: "p-1.5 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors duration-150",
1294
+ "aria-label": "Regenerate message",
1295
+ title: "Regenerate",
1296
+ children: /* @__PURE__ */ jsxRuntime.jsx(react$1.ArrowsClockwise, { size: 14, weight: "regular" })
1297
+ }
1298
+ )
1299
+ ] })
1300
+ ] })
1301
+ ] });
1302
+ }
1303
+ function TurnBubble({
1304
+ message,
1305
+ renderUserMessage,
1306
+ renderAssistantMessage
1307
+ }) {
1308
+ if (message.role === "user" /* User */) {
1309
+ if (renderUserMessage) {
1310
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderUserMessage(message) });
1311
+ }
1312
+ return /* @__PURE__ */ jsxRuntime.jsx(UserTurnView, { message });
1313
+ }
1314
+ if (message.role === "assistant" /* Assistant */) {
1315
+ if (renderAssistantMessage) {
1316
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderAssistantMessage(message) });
1317
+ }
1318
+ return /* @__PURE__ */ jsxRuntime.jsx(AssistantTurnView, { message });
1319
+ }
1320
+ return null;
1321
+ }
1322
+ function ScrollToBottomButton({
1323
+ show,
1324
+ onClick,
1325
+ unreadCount = 0
1326
+ }) {
1327
+ return /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: show && /* @__PURE__ */ jsxRuntime.jsx(
1328
+ framerMotion.motion.button,
1329
+ {
1330
+ initial: { opacity: 0, y: 10 },
1331
+ animate: { opacity: 1, y: 0 },
1332
+ exit: { opacity: 0, y: 10 },
1333
+ transition: { duration: 0.2 },
1334
+ onClick,
1335
+ className: "absolute bottom-24 right-8 p-3 bg-white dark:bg-gray-800 rounded-full shadow-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors z-10",
1336
+ "aria-label": "Scroll to bottom",
1337
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
1338
+ /* @__PURE__ */ jsxRuntime.jsx(react$1.ArrowDown, { size: 20, weight: "bold", className: "text-gray-700 dark:text-gray-300" }),
1339
+ unreadCount > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute -top-2 -right-2 min-w-[18px] h-[18px] bg-primary-600 dark:bg-primary-500 text-white text-xs font-semibold rounded-full flex items-center justify-center px-1", children: unreadCount > 99 ? "99+" : unreadCount })
1340
+ ] })
1341
+ }
1342
+ ) });
1343
+ }
1344
+ function MessageList({ renderUserMessage, renderAssistantMessage }) {
1345
+ const { messages, streamingContent, isStreaming } = useChat();
1346
+ const messagesEndRef = react.useRef(null);
1347
+ const containerRef = react.useRef(null);
1348
+ const [showScrollButton, setShowScrollButton] = react.useState(false);
1349
+ react.useEffect(() => {
1350
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
1351
+ }, [messages.length, streamingContent]);
1352
+ react.useEffect(() => {
1353
+ const container = containerRef.current;
1354
+ if (!container) return;
1355
+ const handleScroll = () => {
1356
+ const { scrollTop, scrollHeight, clientHeight } = container;
1357
+ const isNearBottom = scrollHeight - scrollTop - clientHeight < 100;
1358
+ setShowScrollButton(!isNearBottom);
1359
+ };
1360
+ container.addEventListener("scroll", handleScroll);
1361
+ return () => container.removeEventListener("scroll", handleScroll);
1362
+ }, []);
1363
+ const scrollToBottom = () => {
1364
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
1365
+ };
1366
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex-1 min-h-0", children: [
1367
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className: "h-full overflow-y-auto px-4 py-6", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-4xl mx-auto space-y-6", children: [
1368
+ messages.map((message) => /* @__PURE__ */ jsxRuntime.jsx(
1369
+ TurnBubble,
1370
+ {
1371
+ message,
1372
+ renderUserMessage,
1373
+ renderAssistantMessage
1374
+ },
1375
+ message.id
1376
+ )),
1377
+ isStreaming && streamingContent && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-3", children: [
1378
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-shrink-0 w-8 h-8 rounded-full bg-primary-600 flex items-center justify-center text-white", children: "AI" }),
1379
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 max-w-[80%] rounded-2xl px-4 py-3 bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "prose prose-sm max-w-none dark:prose-invert", children: [
1380
+ streamingContent,
1381
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-block w-2 h-4 ml-1 bg-primary-600 animate-pulse" })
1382
+ ] }) })
1383
+ ] }),
1384
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: messagesEndRef })
1385
+ ] }) }),
1386
+ /* @__PURE__ */ jsxRuntime.jsx(ScrollToBottomButton, { show: showScrollButton, onClick: scrollToBottom })
1387
+ ] });
1388
+ }
1389
+ var MAX_FILES_DEFAULT = 10;
1390
+ var MAX_FILE_SIZE_DEFAULT = 20 * 1024 * 1024;
1391
+ var MAX_HEIGHT = 192;
1392
+ var MessageInput = react.forwardRef(
1393
+ ({
1394
+ message,
1395
+ loading,
1396
+ fetching = false,
1397
+ disabled = false,
1398
+ messageQueue = [],
1399
+ onMessageChange,
1400
+ onSubmit,
1401
+ onUnqueue,
1402
+ placeholder: placeholderOverride,
1403
+ maxFiles = MAX_FILES_DEFAULT,
1404
+ maxFileSize = MAX_FILE_SIZE_DEFAULT,
1405
+ containerClassName
1406
+ }, ref) => {
1407
+ const { t } = useTranslation();
1408
+ const [attachments, setAttachments] = react.useState([]);
1409
+ const [isDragging, setIsDragging] = react.useState(false);
1410
+ const [error, setError] = react.useState(null);
1411
+ const [isFocused, setIsFocused] = react.useState(false);
1412
+ const placeholder = placeholderOverride || t("input.placeholder");
1413
+ const textareaRef = react.useRef(null);
1414
+ const fileInputRef = react.useRef(null);
1415
+ const containerRef = react.useRef(null);
1416
+ react.useImperativeHandle(ref, () => ({
1417
+ focus: () => textareaRef.current?.focus(),
1418
+ clear: () => {
1419
+ onMessageChange("");
1420
+ setAttachments([]);
1421
+ setError(null);
1422
+ }
1423
+ }));
1424
+ react.useEffect(() => {
1425
+ const textarea = textareaRef.current;
1426
+ if (!textarea) return;
1427
+ textarea.style.height = "auto";
1428
+ const newHeight = Math.min(textarea.scrollHeight, MAX_HEIGHT);
1429
+ textarea.style.height = `${newHeight}px`;
1430
+ }, [message]);
1431
+ react.useEffect(() => {
1432
+ if (!error) return;
1433
+ const timer = setTimeout(() => setError(null), 5e3);
1434
+ return () => clearTimeout(timer);
1435
+ }, [error]);
1436
+ const handleFileSelect = async (files) => {
1437
+ if (!files || files.length === 0) return;
1438
+ try {
1439
+ validateFileCount(attachments.length, files.length, maxFiles);
1440
+ const newAttachments = [];
1441
+ for (let i = 0; i < files.length; i++) {
1442
+ const file = files[i];
1443
+ validateImageFile(file, maxFileSize);
1444
+ const base64Data = await convertToBase64(file);
1445
+ const preview = createDataUrl(base64Data, file.type);
1446
+ newAttachments.push({
1447
+ filename: file.name,
1448
+ mimeType: file.type,
1449
+ sizeBytes: file.size,
1450
+ base64Data,
1451
+ preview
1452
+ });
1453
+ }
1454
+ setAttachments((prev) => [...prev, ...newAttachments]);
1455
+ setError(null);
1456
+ } catch (err) {
1457
+ setError(err instanceof Error ? err.message : "Failed to process files");
1458
+ }
1459
+ };
1460
+ const handleFileInputChange = (e) => {
1461
+ handleFileSelect(e.target.files);
1462
+ e.target.value = "";
1463
+ };
1464
+ const handleRemoveAttachment = (index) => {
1465
+ setAttachments((prev) => prev.filter((_, i) => i !== index));
1466
+ setError(null);
1467
+ };
1468
+ const handleDragOver = (e) => {
1469
+ e.preventDefault();
1470
+ e.stopPropagation();
1471
+ setIsDragging(true);
1472
+ };
1473
+ const handleDragLeave = (e) => {
1474
+ e.preventDefault();
1475
+ e.stopPropagation();
1476
+ setIsDragging(false);
1477
+ };
1478
+ const handleDrop = async (e) => {
1479
+ e.preventDefault();
1480
+ e.stopPropagation();
1481
+ setIsDragging(false);
1482
+ await handleFileSelect(e.dataTransfer.files);
1483
+ };
1484
+ const handleKeyDown = (e) => {
1485
+ if (e.key === "Enter" && !e.shiftKey) {
1486
+ e.preventDefault();
1487
+ if (!loading && (message.trim() || attachments.length > 0)) {
1488
+ handleFormSubmit(e);
1489
+ }
1490
+ }
1491
+ if (e.key === "Escape") {
1492
+ onMessageChange("");
1493
+ setAttachments([]);
1494
+ setError(null);
1495
+ }
1496
+ if (e.key === "ArrowUp" && !message.trim() && onUnqueue) {
1497
+ const unqueued = onUnqueue();
1498
+ if (unqueued) {
1499
+ onMessageChange(unqueued.content);
1500
+ setAttachments(unqueued.attachments);
1501
+ }
1502
+ }
1503
+ };
1504
+ const handleFormSubmit = (e) => {
1505
+ e.preventDefault();
1506
+ if (loading || disabled || !message.trim() && attachments.length === 0) {
1507
+ return;
1508
+ }
1509
+ onSubmit(e, attachments);
1510
+ setAttachments([]);
1511
+ setError(null);
1512
+ };
1513
+ const canSubmit = !loading && !disabled && (message.trim() || attachments.length > 0);
1514
+ const defaultContainerClassName = "shrink-0 p-4 pb-6";
1515
+ return /* @__PURE__ */ jsxRuntime.jsx(
1516
+ "div",
1517
+ {
1518
+ ref: containerRef,
1519
+ className: containerClassName ?? defaultContainerClassName,
1520
+ children: /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleFormSubmit, className: "max-w-4xl mx-auto", children: [
1521
+ error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-3 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg text-sm text-red-600 dark:text-red-400 flex items-center justify-between", children: [
1522
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: error }),
1523
+ /* @__PURE__ */ jsxRuntime.jsx(
1524
+ "button",
1525
+ {
1526
+ type: "button",
1527
+ onClick: () => setError(null),
1528
+ className: "ml-2 p-1 hover:bg-red-100 dark:hover:bg-red-800 rounded transition-colors",
1529
+ "aria-label": t("input.dismissError"),
1530
+ children: /* @__PURE__ */ jsxRuntime.jsx(react$1.X, { size: 14 })
1531
+ }
1532
+ )
1533
+ ] }),
1534
+ messageQueue.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-3 text-xs text-gray-500 dark:text-gray-400", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-2.5 py-1 bg-primary-50 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 rounded font-medium", children: t("input.messagesQueued", { count: messageQueue.length }) }) }),
1535
+ attachments.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-3", children: /* @__PURE__ */ jsxRuntime.jsx(AttachmentGrid, { attachments, onRemove: handleRemoveAttachment }) }),
1536
+ /* @__PURE__ */ jsxRuntime.jsxs(
1537
+ "div",
1538
+ {
1539
+ className: "relative",
1540
+ onDragOver: handleDragOver,
1541
+ onDragLeave: handleDragLeave,
1542
+ onDrop: handleDrop,
1543
+ children: [
1544
+ isDragging && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 z-10 bg-primary-50/95 dark:bg-primary-900/90 border-2 border-dashed border-primary-400 rounded-2xl flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-2", children: [
1545
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-800 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(react$1.Paperclip, { size: 20, className: "text-primary-600 dark:text-primary-400" }) }),
1546
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-primary-700 dark:text-primary-300 font-medium", children: t("input.dropImages") })
1547
+ ] }) }),
1548
+ /* @__PURE__ */ jsxRuntime.jsxs(
1549
+ "div",
1550
+ {
1551
+ className: `flex items-center gap-2 rounded-2xl p-2.5 bg-white dark:bg-gray-800 border shadow-sm transition-all duration-150 ${isFocused ? "border-primary-400 dark:border-primary-500 ring-3 ring-primary-500/10 dark:ring-primary-500/15" : "border-gray-200 dark:border-gray-700"}`,
1552
+ children: [
1553
+ /* @__PURE__ */ jsxRuntime.jsx(
1554
+ "button",
1555
+ {
1556
+ type: "button",
1557
+ onClick: () => fileInputRef.current?.click(),
1558
+ disabled: loading || disabled || attachments.length >= maxFiles,
1559
+ className: "flex-shrink-0 self-center p-2 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors disabled:opacity-40 disabled:cursor-not-allowed",
1560
+ "aria-label": t("input.attachFiles"),
1561
+ title: t("input.attachImages"),
1562
+ children: /* @__PURE__ */ jsxRuntime.jsx(react$1.Paperclip, { size: 18 })
1563
+ }
1564
+ ),
1565
+ /* @__PURE__ */ jsxRuntime.jsx(
1566
+ "input",
1567
+ {
1568
+ ref: fileInputRef,
1569
+ type: "file",
1570
+ accept: "image/png,image/jpeg,image/webp,image/gif",
1571
+ multiple: true,
1572
+ onChange: handleFileInputChange,
1573
+ className: "hidden",
1574
+ "aria-label": "File input"
1575
+ }
1576
+ ),
1577
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 self-stretch flex items-center", children: /* @__PURE__ */ jsxRuntime.jsx(
1578
+ "textarea",
1579
+ {
1580
+ ref: textareaRef,
1581
+ value: message,
1582
+ onChange: (e) => onMessageChange(e.target.value),
1583
+ onKeyDown: handleKeyDown,
1584
+ onFocus: () => setIsFocused(true),
1585
+ onBlur: () => setIsFocused(false),
1586
+ placeholder,
1587
+ className: "resize-none bg-transparent border-none outline-none px-1 py-2 w-full text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 text-[15px] leading-relaxed",
1588
+ style: { maxHeight: `${MAX_HEIGHT}px` },
1589
+ rows: 1,
1590
+ disabled: loading || disabled,
1591
+ "aria-label": "Message input"
1592
+ }
1593
+ ) }),
1594
+ /* @__PURE__ */ jsxRuntime.jsx(
1595
+ "button",
1596
+ {
1597
+ type: "submit",
1598
+ disabled: !canSubmit,
1599
+ className: "flex-shrink-0 self-center p-2 rounded-lg bg-primary-600 hover:bg-primary-700 active:bg-primary-800 text-white shadow-sm transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-primary-600",
1600
+ "aria-label": t("input.sendMessage"),
1601
+ children: loading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-[18px] h-[18px] border-2 border-white/60 border-t-transparent rounded-full animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(react$1.PaperPlaneRight, { size: 18, weight: "fill" })
1602
+ }
1603
+ )
1604
+ ]
1605
+ }
1606
+ )
1607
+ ]
1608
+ }
1609
+ ),
1610
+ (loading || fetching) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-3 flex items-center justify-center gap-2", children: [
1611
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
1612
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 bg-gray-400 dark:bg-gray-500 rounded-full animate-pulse" }),
1613
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 bg-gray-400 dark:bg-gray-500 rounded-full animate-pulse", style: { animationDelay: "0.15s" } }),
1614
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 bg-gray-400 dark:bg-gray-500 rounded-full animate-pulse", style: { animationDelay: "0.3s" } })
1615
+ ] }),
1616
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: loading ? t("input.aiThinking") : t("input.processing") })
1617
+ ] })
1618
+ ] })
1619
+ }
1620
+ );
1621
+ }
1622
+ );
1623
+ MessageInput.displayName = "MessageInput";
1624
+ var EXAMPLE_PROMPTS = [
1625
+ {
1626
+ category: "Data Analysis",
1627
+ icon: react$1.ChartBar,
1628
+ text: "Show me sales trends for the last quarter"
1629
+ },
1630
+ {
1631
+ category: "Reports",
1632
+ icon: react$1.FileText,
1633
+ text: "Generate a summary of customer feedback"
1634
+ },
1635
+ {
1636
+ category: "Insights",
1637
+ icon: react$1.Lightbulb,
1638
+ text: "What are the top performing products?"
1639
+ }
1640
+ ];
1641
+ var containerVariants = {
1642
+ hidden: { opacity: 0 },
1643
+ visible: {
1644
+ opacity: 1,
1645
+ transition: {
1646
+ staggerChildren: 0.08,
1647
+ delayChildren: 0.05
1648
+ }
1649
+ }
1650
+ };
1651
+ var itemVariants = {
1652
+ hidden: { opacity: 0, y: 12 },
1653
+ visible: {
1654
+ opacity: 1,
1655
+ y: 0,
1656
+ transition: {
1657
+ duration: 0.3,
1658
+ ease: [0.4, 0, 0.2, 1]
1659
+ }
1660
+ }
1661
+ };
1662
+ function WelcomeContent({
1663
+ onPromptSelect,
1664
+ title = "Welcome to BiChat",
1665
+ description = "Your intelligent business analytics assistant. Ask questions about your data, generate reports, or explore insights.",
1666
+ disabled = false
1667
+ }) {
1668
+ const handlePromptClick = (prompt2) => {
1669
+ if (onPromptSelect && !disabled) {
1670
+ onPromptSelect(prompt2);
1671
+ }
1672
+ };
1673
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1674
+ framerMotion.motion.div,
1675
+ {
1676
+ className: "w-full max-w-3xl mx-auto px-6 py-12 text-center",
1677
+ variants: containerVariants,
1678
+ initial: "hidden",
1679
+ animate: "visible",
1680
+ children: [
1681
+ /* @__PURE__ */ jsxRuntime.jsx(
1682
+ framerMotion.motion.h1,
1683
+ {
1684
+ className: "text-3xl sm:text-4xl font-semibold text-gray-900 dark:text-white mb-4",
1685
+ variants: itemVariants,
1686
+ children: title
1687
+ }
1688
+ ),
1689
+ /* @__PURE__ */ jsxRuntime.jsx(
1690
+ framerMotion.motion.p,
1691
+ {
1692
+ className: "text-base text-gray-500 dark:text-gray-400 mb-12 max-w-xl mx-auto leading-relaxed",
1693
+ variants: itemVariants,
1694
+ children: description
1695
+ }
1696
+ ),
1697
+ /* @__PURE__ */ jsxRuntime.jsxs(framerMotion.motion.div, { variants: itemVariants, children: [
1698
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs font-medium text-gray-400 dark:text-gray-500 uppercase tracking-wide mb-4", children: "Try asking" }),
1699
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-3", children: EXAMPLE_PROMPTS.map((prompt2, index) => /* @__PURE__ */ jsxRuntime.jsxs(
1700
+ framerMotion.motion.button,
1701
+ {
1702
+ onClick: () => handlePromptClick(prompt2.text),
1703
+ disabled,
1704
+ className: "group flex flex-col items-start text-left p-5 rounded-xl bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow-sm hover:shadow-md hover:border-gray-300 dark:hover:border-gray-600 transition-all duration-150 focus:outline-none focus:ring-2 focus:ring-primary-500/50 focus:ring-offset-2 dark:focus:ring-offset-gray-900 disabled:opacity-50 disabled:cursor-not-allowed",
1705
+ variants: itemVariants,
1706
+ whileHover: disabled ? {} : { y: -2 },
1707
+ whileTap: disabled ? {} : { scale: 0.98 },
1708
+ children: [
1709
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-9 h-9 rounded-lg bg-gray-100 dark:bg-gray-700 flex items-center justify-center mb-3", children: /* @__PURE__ */ jsxRuntime.jsx(
1710
+ prompt2.icon,
1711
+ {
1712
+ size: 18,
1713
+ weight: "regular",
1714
+ className: "text-gray-600 dark:text-gray-300"
1715
+ }
1716
+ ) }),
1717
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-gray-400 dark:text-gray-500 mb-1.5", children: prompt2.category }),
1718
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-700 dark:text-gray-200 leading-relaxed", children: prompt2.text })
1719
+ ]
1720
+ },
1721
+ index
1722
+ )) })
1723
+ ] })
1724
+ ]
1725
+ }
1726
+ );
1727
+ }
1728
+ function ChatSessionCore({
1729
+ isReadOnly,
1730
+ renderUserMessage,
1731
+ renderAssistantMessage,
1732
+ className = "",
1733
+ headerSlot,
1734
+ welcomeSlot,
1735
+ logoSlot,
1736
+ actionsSlot,
1737
+ onBack
1738
+ }) {
1739
+ const { t } = useTranslation();
1740
+ const {
1741
+ session,
1742
+ messages,
1743
+ fetching,
1744
+ error,
1745
+ message,
1746
+ setMessage,
1747
+ loading,
1748
+ handleSubmit,
1749
+ messageQueue,
1750
+ handleUnqueue
1751
+ } = useChat();
1752
+ if (fetching) {
1753
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-gray-500 dark:text-gray-400", children: t("input.processing") }) });
1754
+ }
1755
+ if (error) {
1756
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-red-500 dark:text-red-400", children: [
1757
+ t("error.generic"),
1758
+ ": ",
1759
+ error
1760
+ ] }) });
1761
+ }
1762
+ const showWelcome = !session && messages.length === 0;
1763
+ const handlePromptSelect = (prompt2) => {
1764
+ setMessage(prompt2);
1765
+ };
1766
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1767
+ "main",
1768
+ {
1769
+ className: `flex-1 flex flex-col overflow-hidden min-h-0 bg-gray-50 dark:bg-gray-900 ${className}`,
1770
+ children: [
1771
+ headerSlot || /* @__PURE__ */ jsxRuntime.jsx(ChatHeader, { session, onBack, logoSlot, actionsSlot }),
1772
+ showWelcome ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 flex items-center justify-center overflow-auto", children: welcomeSlot || /* @__PURE__ */ jsxRuntime.jsx(WelcomeContent, { onPromptSelect: handlePromptSelect, disabled: loading }) }) : /* @__PURE__ */ jsxRuntime.jsx(
1773
+ MessageList,
1774
+ {
1775
+ renderUserMessage,
1776
+ renderAssistantMessage
1777
+ }
1778
+ ),
1779
+ !isReadOnly && /* @__PURE__ */ jsxRuntime.jsx(
1780
+ MessageInput,
1781
+ {
1782
+ message,
1783
+ loading,
1784
+ fetching,
1785
+ onMessageChange: setMessage,
1786
+ onSubmit: handleSubmit,
1787
+ messageQueue,
1788
+ onUnqueue: handleUnqueue
1789
+ }
1790
+ )
1791
+ ]
1792
+ }
1793
+ );
1794
+ }
1795
+ function ChatSession(props) {
1796
+ const { dataSource, sessionId, ...coreProps } = props;
1797
+ return /* @__PURE__ */ jsxRuntime.jsx(ChatSessionProvider, { dataSource, sessionId, children: /* @__PURE__ */ jsxRuntime.jsx(ChatSessionCore, { ...coreProps }) });
1798
+ }
1799
+
1800
+ // ui/src/bichat/index.ts
1801
+ init_MarkdownRenderer();
1802
+
1803
+ // ui/src/bichat/animations/variants.ts
1804
+ var prefersReducedMotion = () => {
1805
+ if (typeof window === "undefined") return false;
1806
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1807
+ };
1808
+ var fadeInVariants = {
1809
+ initial: { opacity: 0 },
1810
+ animate: {
1811
+ opacity: 1,
1812
+ transition: {
1813
+ duration: prefersReducedMotion() ? 0 : 0.2
1814
+ }
1815
+ },
1816
+ exit: {
1817
+ opacity: 0,
1818
+ transition: {
1819
+ duration: prefersReducedMotion() ? 0 : 0.15
1820
+ }
1821
+ }
1822
+ };
1823
+ var fadeInUpVariants = {
1824
+ initial: { opacity: 0, y: 8 },
1825
+ animate: {
1826
+ opacity: 1,
1827
+ y: 0,
1828
+ transition: {
1829
+ duration: prefersReducedMotion() ? 0 : 0.2,
1830
+ ease: [0.4, 0, 0.2, 1]
1831
+ }
1832
+ },
1833
+ exit: {
1834
+ opacity: 0,
1835
+ y: 8,
1836
+ transition: {
1837
+ duration: prefersReducedMotion() ? 0 : 0.15
1838
+ }
1839
+ }
1840
+ };
1841
+ var scaleFadeVariants = {
1842
+ initial: { opacity: 0, scale: 0.98 },
1843
+ animate: {
1844
+ opacity: 1,
1845
+ scale: 1,
1846
+ transition: {
1847
+ duration: prefersReducedMotion() ? 0 : 0.15
1848
+ }
1849
+ },
1850
+ exit: {
1851
+ opacity: 0,
1852
+ scale: 0.98,
1853
+ transition: {
1854
+ duration: prefersReducedMotion() ? 0 : 0.1
1855
+ }
1856
+ }
1857
+ };
1858
+ var backdropVariants = {
1859
+ initial: { opacity: 0 },
1860
+ animate: {
1861
+ opacity: 1,
1862
+ transition: {
1863
+ duration: prefersReducedMotion() ? 0 : 0.15
1864
+ }
1865
+ },
1866
+ exit: {
1867
+ opacity: 0,
1868
+ transition: {
1869
+ duration: 0.15
1870
+ }
1871
+ }
1872
+ };
1873
+ var buttonVariants = {
1874
+ tap: {
1875
+ scale: 0.98
1876
+ }
1877
+ };
1878
+ var staggerContainerVariants = {
1879
+ hidden: { opacity: 0 },
1880
+ visible: {
1881
+ opacity: 1,
1882
+ transition: {
1883
+ staggerChildren: 0.03,
1884
+ delayChildren: 0.05
1885
+ }
1886
+ }
1887
+ };
1888
+ var listItemVariants = {
1889
+ initial: { opacity: 0, x: -8 },
1890
+ animate: {
1891
+ opacity: 1,
1892
+ x: 0,
1893
+ transition: { duration: 0.2 }
1894
+ },
1895
+ exit: {
1896
+ opacity: 0,
1897
+ x: -8,
1898
+ transition: { duration: 0.15 }
1899
+ }
1900
+ };
1901
+ var messageVariants = {
1902
+ initial: {
1903
+ opacity: 0,
1904
+ y: 8
1905
+ },
1906
+ animate: {
1907
+ opacity: 1,
1908
+ y: 0,
1909
+ transition: {
1910
+ duration: prefersReducedMotion() ? 0 : 0.2,
1911
+ ease: [0.4, 0, 0.2, 1]
1912
+ }
1913
+ },
1914
+ exit: {
1915
+ opacity: 0,
1916
+ transition: {
1917
+ duration: prefersReducedMotion() ? 0 : 0.15
1918
+ }
1919
+ }
1920
+ };
1921
+ var messageContainerVariants = {
1922
+ initial: { opacity: 0 },
1923
+ animate: {
1924
+ opacity: 1,
1925
+ transition: {
1926
+ staggerChildren: 0.05,
1927
+ delayChildren: 0.05
1928
+ }
1929
+ }
1930
+ };
1931
+ var typingDotVariants = {
1932
+ initial: { opacity: 0.4 },
1933
+ animate: {
1934
+ opacity: [0.4, 1, 0.4],
1935
+ transition: {
1936
+ duration: prefersReducedMotion() ? 0 : 1,
1937
+ repeat: Infinity,
1938
+ ease: "easeInOut"
1939
+ }
1940
+ }
1941
+ };
1942
+ var floatingButtonVariants = {
1943
+ initial: {
1944
+ opacity: 0,
1945
+ scale: 0.9
1946
+ },
1947
+ animate: {
1948
+ opacity: 1,
1949
+ scale: 1,
1950
+ transition: {
1951
+ duration: prefersReducedMotion() ? 0 : 0.2
1952
+ }
1953
+ },
1954
+ exit: {
1955
+ opacity: 0,
1956
+ scale: 0.9,
1957
+ transition: {
1958
+ duration: prefersReducedMotion() ? 0 : 0.15
1959
+ }
1960
+ }
1961
+ };
1962
+ var dropdownVariants = {
1963
+ initial: { opacity: 0, y: -4 },
1964
+ animate: {
1965
+ opacity: 1,
1966
+ y: 0,
1967
+ transition: { duration: prefersReducedMotion() ? 0 : 0.15 }
1968
+ },
1969
+ exit: {
1970
+ opacity: 0,
1971
+ y: -4,
1972
+ transition: { duration: 0.1 }
1973
+ }
1974
+ };
1975
+ var toastVariants = {
1976
+ initial: { opacity: 0, y: -8 },
1977
+ animate: {
1978
+ opacity: 1,
1979
+ y: 0,
1980
+ transition: {
1981
+ duration: prefersReducedMotion() ? 0 : 0.2
1982
+ }
1983
+ },
1984
+ exit: {
1985
+ opacity: 0,
1986
+ y: -8,
1987
+ transition: {
1988
+ duration: prefersReducedMotion() ? 0 : 0.15
1989
+ }
1990
+ }
1991
+ };
1992
+ var sizeClasses = {
1993
+ sm: {
1994
+ container: "py-6 px-3",
1995
+ title: "text-sm",
1996
+ description: "text-xs"
1997
+ },
1998
+ md: {
1999
+ container: "py-8 px-4",
2000
+ title: "text-base",
2001
+ description: "text-sm"
2002
+ },
2003
+ lg: {
2004
+ container: "py-12 px-6",
2005
+ title: "text-lg",
2006
+ description: "text-base"
2007
+ }
2008
+ };
2009
+ function EmptyState({
2010
+ icon,
2011
+ title,
2012
+ description,
2013
+ action,
2014
+ className = "",
2015
+ size = "md"
2016
+ }) {
2017
+ const sizes = sizeClasses[size];
2018
+ return /* @__PURE__ */ jsxRuntime.jsx(
2019
+ framerMotion.motion.div,
2020
+ {
2021
+ className: `flex items-center justify-center ${sizes.container} ${className}`,
2022
+ variants: fadeInVariants,
2023
+ initial: "initial",
2024
+ animate: "animate",
2025
+ exit: "exit",
2026
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center max-w-md", children: [
2027
+ icon && /* @__PURE__ */ jsxRuntime.jsx(
2028
+ framerMotion.motion.div,
2029
+ {
2030
+ className: "mb-4 flex justify-center",
2031
+ initial: { opacity: 0, scale: 0.8 },
2032
+ animate: { opacity: 1, scale: 1 },
2033
+ transition: { duration: 0.4, delay: 0.1 },
2034
+ children: icon
2035
+ }
2036
+ ),
2037
+ /* @__PURE__ */ jsxRuntime.jsx(
2038
+ framerMotion.motion.h3,
2039
+ {
2040
+ className: `${sizes.title} font-medium text-gray-900 dark:text-white mb-2`,
2041
+ initial: { opacity: 0, y: 10 },
2042
+ animate: { opacity: 1, y: 0 },
2043
+ transition: { duration: 0.4, delay: 0.2 },
2044
+ children: title
2045
+ }
2046
+ ),
2047
+ description && /* @__PURE__ */ jsxRuntime.jsx(
2048
+ framerMotion.motion.p,
2049
+ {
2050
+ className: `${sizes.description} text-gray-500 dark:text-gray-400 mb-4`,
2051
+ initial: { opacity: 0, y: 10 },
2052
+ animate: { opacity: 1, y: 0 },
2053
+ transition: { duration: 0.4, delay: 0.3 },
2054
+ children: description
2055
+ }
2056
+ ),
2057
+ action && /* @__PURE__ */ jsxRuntime.jsx(
2058
+ framerMotion.motion.div,
2059
+ {
2060
+ initial: { opacity: 0, y: 10 },
2061
+ animate: { opacity: 1, y: 0 },
2062
+ transition: { duration: 0.4, delay: 0.4 },
2063
+ children: action
2064
+ }
2065
+ )
2066
+ ] })
2067
+ }
2068
+ );
2069
+ }
2070
+ var EmptyState_default = react.memo(EmptyState);
2071
+ var sizeClasses2 = {
2072
+ sm: "text-sm",
2073
+ md: "text-base",
2074
+ lg: "text-lg"
2075
+ };
2076
+ var EditableText = react.forwardRef(
2077
+ ({
2078
+ value,
2079
+ onSave,
2080
+ maxLength = 100,
2081
+ isLoading = false,
2082
+ placeholder = "Untitled",
2083
+ className = "",
2084
+ inputClassName = "",
2085
+ size = "sm"
2086
+ }, ref) => {
2087
+ const [isEditing, setIsEditing] = react.useState(false);
2088
+ const [editValue, setEditValue] = react.useState(value);
2089
+ const inputRef = react.useRef(null);
2090
+ react.useImperativeHandle(ref, () => ({
2091
+ startEditing: () => {
2092
+ if (!isLoading) {
2093
+ setIsEditing(true);
2094
+ }
2095
+ },
2096
+ cancelEditing: () => {
2097
+ setEditValue(value);
2098
+ setIsEditing(false);
2099
+ }
2100
+ }));
2101
+ react.useEffect(() => {
2102
+ setEditValue(value);
2103
+ }, [value]);
2104
+ react.useEffect(() => {
2105
+ if (isEditing && inputRef.current) {
2106
+ inputRef.current.focus();
2107
+ inputRef.current.select();
2108
+ }
2109
+ }, [isEditing]);
2110
+ const handleSave = () => {
2111
+ const trimmed = editValue.trim();
2112
+ if (!trimmed) {
2113
+ setEditValue(value);
2114
+ setIsEditing(false);
2115
+ return;
2116
+ }
2117
+ if (trimmed !== value) {
2118
+ onSave(trimmed);
2119
+ }
2120
+ setIsEditing(false);
2121
+ };
2122
+ const handleCancel = () => {
2123
+ setEditValue(value);
2124
+ setIsEditing(false);
2125
+ };
2126
+ const handleKeyDown = (e) => {
2127
+ if (e.key === "Enter") {
2128
+ e.preventDefault();
2129
+ handleSave();
2130
+ } else if (e.key === "Escape") {
2131
+ e.preventDefault();
2132
+ handleCancel();
2133
+ }
2134
+ };
2135
+ const handleDoubleClick = () => {
2136
+ if (!isLoading) {
2137
+ setIsEditing(true);
2138
+ }
2139
+ };
2140
+ const handleBlur = () => {
2141
+ handleSave();
2142
+ };
2143
+ const sizeClass = sizeClasses2[size];
2144
+ if (isEditing) {
2145
+ return /* @__PURE__ */ jsxRuntime.jsx(
2146
+ "div",
2147
+ {
2148
+ className: "flex items-center gap-2 flex-1",
2149
+ onClick: (e) => e.preventDefault(),
2150
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2151
+ "input",
2152
+ {
2153
+ ref: inputRef,
2154
+ type: "text",
2155
+ value: editValue,
2156
+ onChange: (e) => setEditValue(e.target.value),
2157
+ onKeyDown: handleKeyDown,
2158
+ onBlur: handleBlur,
2159
+ maxLength,
2160
+ placeholder,
2161
+ className: `flex-1 px-2 py-1 ${sizeClass} bg-white dark:bg-gray-700 border border-primary-500 dark:border-primary-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500/30 dark:focus:ring-primary-600/30 text-gray-900 dark:text-white ${inputClassName}`,
2162
+ "aria-label": "Edit text. Press Enter to save, Escape to cancel"
2163
+ }
2164
+ )
2165
+ }
2166
+ );
2167
+ }
2168
+ const displayValue = value || placeholder;
2169
+ return /* @__PURE__ */ jsxRuntime.jsx(
2170
+ "span",
2171
+ {
2172
+ onDoubleClick: handleDoubleClick,
2173
+ className: `${sizeClass} font-medium truncate flex-1 cursor-pointer select-none hover:text-primary-600 dark:hover:text-primary-400 transition-colors ${className}`,
2174
+ title: "Double-click to edit",
2175
+ role: "button",
2176
+ tabIndex: 0,
2177
+ onKeyDown: (e) => {
2178
+ if (e.key === "Enter" || e.key === " ") {
2179
+ e.preventDefault();
2180
+ handleDoubleClick();
2181
+ }
2182
+ },
2183
+ children: isLoading ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-2 text-gray-400 dark:text-gray-500", children: [
2184
+ /* @__PURE__ */ jsxRuntime.jsx(react$1.CircleNotch, { size: 12, className: "animate-spin" }),
2185
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "italic", children: displayValue })
2186
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: !value ? "text-gray-400 dark:text-gray-500 italic" : "", children: displayValue })
2187
+ }
2188
+ );
2189
+ }
2190
+ );
2191
+ EditableText.displayName = "EditableText";
2192
+ var EditableText_default = react.memo(EditableText);
2193
+ var sizeClasses3 = {
2194
+ sm: {
2195
+ container: "py-1.5 pl-8 pr-8 text-xs",
2196
+ icon: 14,
2197
+ clearBtn: "p-1"
2198
+ },
2199
+ md: {
2200
+ container: "py-2.5 pl-10 pr-10 text-sm",
2201
+ icon: 16,
2202
+ clearBtn: "p-1.5"
2203
+ },
2204
+ lg: {
2205
+ container: "py-3 pl-12 pr-12 text-base",
2206
+ icon: 18,
2207
+ clearBtn: "p-2"
2208
+ }
2209
+ };
2210
+ function SearchInput({
2211
+ value,
2212
+ onChange,
2213
+ placeholder = "Search...",
2214
+ autoFocus = false,
2215
+ onSubmit,
2216
+ onEscape,
2217
+ className = "",
2218
+ size = "md",
2219
+ disabled = false,
2220
+ ariaLabel = "Search"
2221
+ }) {
2222
+ const inputRef = react.useRef(null);
2223
+ const sizes = sizeClasses3[size];
2224
+ react.useEffect(() => {
2225
+ if (autoFocus && inputRef.current) {
2226
+ inputRef.current.focus();
2227
+ }
2228
+ }, [autoFocus]);
2229
+ const handleClear = () => {
2230
+ onChange("");
2231
+ inputRef.current?.focus();
2232
+ };
2233
+ const handleKeyDown = (e) => {
2234
+ if (e.key === "Enter" && onSubmit) {
2235
+ e.preventDefault();
2236
+ onSubmit(value);
2237
+ } else if (e.key === "Escape") {
2238
+ e.preventDefault();
2239
+ if (value && !onEscape) {
2240
+ handleClear();
2241
+ } else if (onEscape) {
2242
+ onEscape();
2243
+ }
2244
+ }
2245
+ };
2246
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `relative w-full ${className}`, role: "search", children: [
2247
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute inset-y-0 left-3 flex items-center pointer-events-none", children: /* @__PURE__ */ jsxRuntime.jsx(
2248
+ react$1.MagnifyingGlass,
2249
+ {
2250
+ size: sizes.icon,
2251
+ weight: "bold",
2252
+ className: "text-gray-400 dark:text-gray-500",
2253
+ "aria-hidden": "true"
2254
+ }
2255
+ ) }),
2256
+ /* @__PURE__ */ jsxRuntime.jsx(
2257
+ "input",
2258
+ {
2259
+ ref: inputRef,
2260
+ type: "search",
2261
+ value,
2262
+ onChange: (e) => onChange(e.target.value),
2263
+ onKeyDown: handleKeyDown,
2264
+ placeholder,
2265
+ disabled,
2266
+ className: `w-full ${sizes.container} bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-gray-700/50 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary-500/30 dark:focus:ring-primary-500/20 focus:border-primary-400 dark:focus:border-primary-600 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed`,
2267
+ "aria-label": ariaLabel
2268
+ }
2269
+ ),
2270
+ value && !disabled && /* @__PURE__ */ jsxRuntime.jsx(
2271
+ "button",
2272
+ {
2273
+ type: "button",
2274
+ onClick: handleClear,
2275
+ className: `absolute inset-y-0 right-2 flex items-center ${sizes.clearBtn} rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 transition-all duration-200 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300`,
2276
+ "aria-label": "Clear search",
2277
+ title: "Clear search",
2278
+ children: /* @__PURE__ */ jsxRuntime.jsx(react$1.X, { size: sizes.icon - 2, weight: "bold" })
2279
+ }
2280
+ )
2281
+ ] });
2282
+ }
2283
+ var SearchInput_default = react.memo(SearchInput);
2284
+ var variantClasses = {
2285
+ text: "rounded",
2286
+ circular: "rounded-full",
2287
+ rectangular: "rounded-none",
2288
+ rounded: "rounded-lg"
2289
+ };
2290
+ var gapClasses = {
2291
+ sm: "space-y-1",
2292
+ md: "space-y-2",
2293
+ lg: "space-y-3"
2294
+ };
2295
+ function Skeleton({
2296
+ variant = "text",
2297
+ width,
2298
+ height,
2299
+ className = "",
2300
+ animate = true
2301
+ }) {
2302
+ const variantClass = variantClasses[variant];
2303
+ const style = {
2304
+ width: typeof width === "number" ? `${width}px` : width,
2305
+ height: typeof height === "number" ? `${height}px` : height
2306
+ };
2307
+ return /* @__PURE__ */ jsxRuntime.jsx(
2308
+ "div",
2309
+ {
2310
+ className: `bg-gray-200 dark:bg-gray-700 ${variantClass} ${animate ? "animate-pulse" : ""} ${className}`,
2311
+ style,
2312
+ "aria-hidden": "true"
2313
+ }
2314
+ );
2315
+ }
2316
+ function SkeletonGroup({
2317
+ count = 3,
2318
+ gap = "md",
2319
+ className = "",
2320
+ children
2321
+ }) {
2322
+ const gapClass = gapClasses[gap];
2323
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `${gapClass} ${className}`, "aria-hidden": "true", children: Array.from({ length: count }).map(
2324
+ (_, index) => children ? /* @__PURE__ */ jsxRuntime.jsx("div", { children: children(index) }, index) : /* @__PURE__ */ jsxRuntime.jsx(Skeleton, { variant: "text", height: 16 }, index)
2325
+ ) });
2326
+ }
2327
+ function SkeletonText({
2328
+ lines = 1,
2329
+ className = ""
2330
+ }) {
2331
+ const widths = ["100%", "90%", "80%", "95%", "85%"];
2332
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `space-y-2 ${className}`, "aria-hidden": "true", children: Array.from({ length: lines }).map((_, index) => /* @__PURE__ */ jsxRuntime.jsx(
2333
+ Skeleton,
2334
+ {
2335
+ variant: "text",
2336
+ width: widths[index % widths.length],
2337
+ height: 14
2338
+ },
2339
+ index
2340
+ )) });
2341
+ }
2342
+ function SkeletonAvatar({
2343
+ size = 40,
2344
+ className = ""
2345
+ }) {
2346
+ return /* @__PURE__ */ jsxRuntime.jsx(
2347
+ Skeleton,
2348
+ {
2349
+ variant: "circular",
2350
+ width: size,
2351
+ height: size,
2352
+ className
2353
+ }
2354
+ );
2355
+ }
2356
+ function SkeletonCard({
2357
+ width,
2358
+ height = 120,
2359
+ className = ""
2360
+ }) {
2361
+ return /* @__PURE__ */ jsxRuntime.jsx(
2362
+ Skeleton,
2363
+ {
2364
+ variant: "rounded",
2365
+ width,
2366
+ height,
2367
+ className
2368
+ }
2369
+ );
2370
+ }
2371
+ function ListItemSkeleton({ className = "" }) {
2372
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex items-center gap-3 px-3 py-2 ${className}`, children: [
2373
+ /* @__PURE__ */ jsxRuntime.jsx(Skeleton, { variant: "rounded", width: 20, height: 20 }),
2374
+ /* @__PURE__ */ jsxRuntime.jsx(Skeleton, { variant: "text", height: 16, className: "flex-1" })
2375
+ ] });
2376
+ }
2377
+ var Skeleton_default = react.memo(Skeleton);
2378
+ var ConfigContext = react.createContext(null);
2379
+ function ConfigProvider({ config, useGlobalConfig = false, children }) {
2380
+ let resolvedConfig = null;
2381
+ if (config) {
2382
+ resolvedConfig = config;
2383
+ } else if (useGlobalConfig && typeof window !== "undefined") {
2384
+ const globalContext = window.__BICHAT_CONTEXT__;
2385
+ const globalCSRF = window.__CSRF_TOKEN__;
2386
+ if (globalContext) {
2387
+ resolvedConfig = {
2388
+ user: {
2389
+ id: String(globalContext.user?.id || ""),
2390
+ email: globalContext.user?.email || "",
2391
+ firstName: globalContext.user?.firstName || "",
2392
+ lastName: globalContext.user?.lastName || "",
2393
+ permissions: globalContext.user?.permissions || []
2394
+ },
2395
+ tenant: {
2396
+ id: globalContext.tenant?.id || "",
2397
+ name: globalContext.tenant?.name || ""
2398
+ },
2399
+ locale: {
2400
+ language: globalContext.locale?.language || "en",
2401
+ translations: globalContext.locale?.translations || {}
2402
+ },
2403
+ endpoints: {
2404
+ graphQL: globalContext.config?.graphQLEndpoint || "/graphql",
2405
+ stream: globalContext.config?.streamEndpoint || "/stream"
2406
+ },
2407
+ csrfToken: globalCSRF
2408
+ };
2409
+ }
2410
+ }
2411
+ return /* @__PURE__ */ jsxRuntime.jsx(ConfigContext.Provider, { value: resolvedConfig, children });
2412
+ }
2413
+ function useConfig() {
2414
+ return react.useContext(ConfigContext);
2415
+ }
2416
+ function useRequiredConfig() {
2417
+ const config = react.useContext(ConfigContext);
2418
+ if (!config) {
2419
+ throw new Error(
2420
+ "BiChat configuration not found. Wrap your app with <ConfigProvider config={...}> or use useGlobalConfig={true}."
2421
+ );
2422
+ }
2423
+ return config;
2424
+ }
2425
+ function hasPermission2(config, permission) {
2426
+ if (!config) {
2427
+ return false;
2428
+ }
2429
+ return config.user.permissions.includes(permission);
2430
+ }
2431
+ function useStreaming(options = {}) {
2432
+ const [content, setContent] = react.useState("");
2433
+ const [isStreaming, setIsStreaming] = react.useState(false);
2434
+ const [error, setError] = react.useState(null);
2435
+ const abortControllerRef = react.useRef(null);
2436
+ const processStream = react.useCallback(
2437
+ async (stream, signal) => {
2438
+ setIsStreaming(true);
2439
+ setError(null);
2440
+ setContent("");
2441
+ abortControllerRef.current = new AbortController();
2442
+ if (signal) {
2443
+ signal.addEventListener("abort", () => {
2444
+ abortControllerRef.current?.abort();
2445
+ });
2446
+ }
2447
+ try {
2448
+ for await (const chunk of stream) {
2449
+ if (abortControllerRef.current?.signal.aborted) {
2450
+ break;
2451
+ }
2452
+ if (chunk.type === "chunk" && chunk.content) {
2453
+ setContent((prev) => {
2454
+ const newContent = prev + chunk.content;
2455
+ options.onChunk?.(newContent);
2456
+ return newContent;
2457
+ });
2458
+ } else if (chunk.type === "error") {
2459
+ const errorMsg = chunk.error || "Stream error";
2460
+ const err = new Error(errorMsg);
2461
+ setError(err);
2462
+ options.onError?.(errorMsg);
2463
+ break;
2464
+ } else if (chunk.type === "done") {
2465
+ options.onDone?.();
2466
+ break;
2467
+ }
2468
+ }
2469
+ } catch (err) {
2470
+ if (err instanceof Error && err.name === "AbortError") {
2471
+ return;
2472
+ }
2473
+ const errorObj = err instanceof Error ? err : new Error("Unknown error");
2474
+ setError(errorObj);
2475
+ options.onError?.(errorObj.message);
2476
+ } finally {
2477
+ setIsStreaming(false);
2478
+ abortControllerRef.current = null;
2479
+ }
2480
+ },
2481
+ [options]
2482
+ );
2483
+ const cancel = react.useCallback(() => {
2484
+ if (abortControllerRef.current) {
2485
+ abortControllerRef.current.abort();
2486
+ abortControllerRef.current = null;
2487
+ setIsStreaming(false);
2488
+ }
2489
+ }, []);
2490
+ const reset = react.useCallback(() => {
2491
+ setContent("");
2492
+ setError(null);
2493
+ setIsStreaming(false);
2494
+ }, []);
2495
+ return {
2496
+ content,
2497
+ isStreaming,
2498
+ error,
2499
+ processStream,
2500
+ cancel,
2501
+ reset
2502
+ };
2503
+ }
2504
+
2505
+ // ui/src/bichat/theme/themes.ts
2506
+ var lightTheme = {
2507
+ name: "light",
2508
+ colors: {
2509
+ background: "#ffffff",
2510
+ surface: "#f9fafb",
2511
+ primary: "#3b82f6",
2512
+ secondary: "#6b7280",
2513
+ text: "#111827",
2514
+ textMuted: "#6b7280",
2515
+ border: "#e5e7eb",
2516
+ error: "#ef4444",
2517
+ success: "#10b981",
2518
+ warning: "#f59e0b",
2519
+ userBubble: "#3b82f6",
2520
+ assistantBubble: "#f3f4f6",
2521
+ userText: "#ffffff",
2522
+ assistantText: "#111827"
2523
+ },
2524
+ spacing: {
2525
+ xs: "0.25rem",
2526
+ sm: "0.5rem",
2527
+ md: "1rem",
2528
+ lg: "1.5rem",
2529
+ xl: "2rem"
2530
+ },
2531
+ borderRadius: {
2532
+ sm: "0.25rem",
2533
+ md: "0.5rem",
2534
+ lg: "0.75rem",
2535
+ full: "9999px"
2536
+ }
2537
+ };
2538
+ var darkTheme = {
2539
+ name: "dark",
2540
+ colors: {
2541
+ background: "#111827",
2542
+ surface: "#1f2937",
2543
+ primary: "#60a5fa",
2544
+ secondary: "#9ca3af",
2545
+ text: "#f9fafb",
2546
+ textMuted: "#9ca3af",
2547
+ border: "#374151",
2548
+ error: "#f87171",
2549
+ success: "#34d399",
2550
+ warning: "#fbbf24",
2551
+ userBubble: "#2563eb",
2552
+ assistantBubble: "#1f2937",
2553
+ userText: "#f9fafb",
2554
+ assistantText: "#f9fafb"
2555
+ },
2556
+ spacing: {
2557
+ xs: "0.25rem",
2558
+ sm: "0.5rem",
2559
+ md: "1rem",
2560
+ lg: "1.5rem",
2561
+ xl: "2rem"
2562
+ },
2563
+ borderRadius: {
2564
+ sm: "0.25rem",
2565
+ md: "0.5rem",
2566
+ lg: "0.75rem",
2567
+ full: "9999px"
2568
+ }
2569
+ };
2570
+ var ThemeContext = react.createContext(null);
2571
+ function getSystemTheme() {
2572
+ if (typeof window === "undefined") {
2573
+ return lightTheme;
2574
+ }
2575
+ const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
2576
+ return prefersDark ? darkTheme : lightTheme;
2577
+ }
2578
+ function resolveTheme(themeProp) {
2579
+ if (typeof themeProp === "object") {
2580
+ return themeProp;
2581
+ }
2582
+ switch (themeProp) {
2583
+ case "light":
2584
+ return lightTheme;
2585
+ case "dark":
2586
+ return darkTheme;
2587
+ case "system":
2588
+ return getSystemTheme();
2589
+ default:
2590
+ return lightTheme;
2591
+ }
2592
+ }
2593
+ function applyThemeVariables(theme) {
2594
+ if (typeof document === "undefined") {
2595
+ return;
2596
+ }
2597
+ const root = document.documentElement;
2598
+ Object.entries(theme.colors).forEach(([key, value]) => {
2599
+ root.style.setProperty(`--bichat-${key}`, value);
2600
+ });
2601
+ Object.entries(theme.spacing).forEach(([key, value]) => {
2602
+ root.style.setProperty(`--bichat-spacing-${key}`, value);
2603
+ });
2604
+ Object.entries(theme.borderRadius).forEach(([key, value]) => {
2605
+ root.style.setProperty(`--bichat-radius-${key}`, value);
2606
+ });
2607
+ }
2608
+ function ThemeProvider({ theme = "system", children }) {
2609
+ const resolvedTheme = react.useMemo(() => resolveTheme(theme), [theme]);
2610
+ react.useEffect(() => {
2611
+ applyThemeVariables(resolvedTheme);
2612
+ }, [resolvedTheme]);
2613
+ react.useEffect(() => {
2614
+ if (theme !== "system") {
2615
+ return;
2616
+ }
2617
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
2618
+ const handleChange = () => {
2619
+ const newTheme = getSystemTheme();
2620
+ applyThemeVariables(newTheme);
2621
+ };
2622
+ mediaQuery.addEventListener("change", handleChange);
2623
+ return () => {
2624
+ mediaQuery.removeEventListener("change", handleChange);
2625
+ };
2626
+ }, [theme]);
2627
+ const value = {
2628
+ theme: resolvedTheme
2629
+ };
2630
+ return /* @__PURE__ */ jsxRuntime.jsx(ThemeContext.Provider, { value, children });
2631
+ }
2632
+ function useTheme() {
2633
+ const context = react.useContext(ThemeContext);
2634
+ if (!context) {
2635
+ throw new Error("useTheme must be used within ThemeProvider");
2636
+ }
2637
+ return context.theme;
2638
+ }
2639
+
2640
+ // ui/src/bichat/api/csrf.ts
2641
+ function getCSRFToken() {
2642
+ const token = window.__CSRF_TOKEN__;
2643
+ if (!token) {
2644
+ console.warn("CSRF token not found in window object");
2645
+ return "";
2646
+ }
2647
+ return token;
2648
+ }
2649
+ function addCSRFHeader(headers) {
2650
+ const token = getCSRFToken();
2651
+ if (token) {
2652
+ headers.set("X-CSRF-Token", token);
2653
+ }
2654
+ return headers;
2655
+ }
2656
+ function createHeadersWithCSRF(init) {
2657
+ const headers = new Headers(init);
2658
+ return addCSRFHeader(headers);
2659
+ }
2660
+
2661
+ // ui/src/bichat/data/HttpDataSource.ts
2662
+ var HttpDataSource = class {
2663
+ constructor(config) {
2664
+ this.abortController = null;
2665
+ this.config = {
2666
+ graphQLEndpoint: "/graphql",
2667
+ streamEndpoint: "/stream",
2668
+ timeout: 3e4,
2669
+ ...config
2670
+ };
2671
+ }
2672
+ /**
2673
+ * Get CSRF token from config
2674
+ */
2675
+ getCSRFToken() {
2676
+ if (!this.config.csrfToken) {
2677
+ return "";
2678
+ }
2679
+ return typeof this.config.csrfToken === "function" ? this.config.csrfToken() : this.config.csrfToken;
2680
+ }
2681
+ /**
2682
+ * Create headers for HTTP requests
2683
+ */
2684
+ createHeaders(additionalHeaders) {
2685
+ const headers = new Headers({
2686
+ "Content-Type": "application/json",
2687
+ ...this.config.headers,
2688
+ ...additionalHeaders
2689
+ });
2690
+ const csrfToken = this.getCSRFToken();
2691
+ if (csrfToken) {
2692
+ headers.set("X-CSRF-Token", csrfToken);
2693
+ }
2694
+ return headers;
2695
+ }
2696
+ /**
2697
+ * Execute GraphQL query
2698
+ */
2699
+ async graphql(query, variables) {
2700
+ const url = `${this.config.baseUrl}${this.config.graphQLEndpoint}`;
2701
+ const response = await fetch(url, {
2702
+ method: "POST",
2703
+ headers: this.createHeaders(),
2704
+ body: JSON.stringify({ query, variables }),
2705
+ signal: this.abortController?.signal
2706
+ });
2707
+ if (!response.ok) {
2708
+ throw new Error(`GraphQL request failed: ${response.statusText}`);
2709
+ }
2710
+ const result = await response.json();
2711
+ if (result.errors && result.errors.length > 0) {
2712
+ throw new Error(result.errors[0].message || "GraphQL error");
2713
+ }
2714
+ return result.data;
2715
+ }
2716
+ /**
2717
+ * Create a new chat session
2718
+ */
2719
+ async createSession() {
2720
+ const query = `
2721
+ mutation CreateChatSession {
2722
+ createChatSession {
2723
+ id
2724
+ title
2725
+ status
2726
+ pinned
2727
+ createdAt
2728
+ updatedAt
2729
+ }
2730
+ }
2731
+ `;
2732
+ const data = await this.graphql(query);
2733
+ return data.createChatSession;
2734
+ }
2735
+ /**
2736
+ * Fetch an existing session with messages
2737
+ */
2738
+ async fetchSession(id) {
2739
+ const query = `
2740
+ query GetChatSession($id: ID!) {
2741
+ chatSession(id: $id) {
2742
+ session {
2743
+ id
2744
+ title
2745
+ status
2746
+ pinned
2747
+ createdAt
2748
+ updatedAt
2749
+ }
2750
+ messages {
2751
+ id
2752
+ sessionId
2753
+ role
2754
+ content
2755
+ createdAt
2756
+ toolCalls {
2757
+ id
2758
+ name
2759
+ arguments
2760
+ }
2761
+ citations {
2762
+ id
2763
+ source
2764
+ url
2765
+ excerpt
2766
+ }
2767
+ chartData {
2768
+ type
2769
+ title
2770
+ data
2771
+ xAxisKey
2772
+ yAxisKey
2773
+ }
2774
+ artifacts {
2775
+ type
2776
+ filename
2777
+ url
2778
+ sizeReadable
2779
+ rowCount
2780
+ description
2781
+ }
2782
+ explanation
2783
+ }
2784
+ pendingQuestion {
2785
+ id
2786
+ turnId
2787
+ question
2788
+ type
2789
+ options
2790
+ status
2791
+ }
2792
+ }
2793
+ }
2794
+ `;
2795
+ try {
2796
+ const data = await this.graphql(query, { id });
2797
+ return data.chatSession;
2798
+ } catch (err) {
2799
+ console.error("Failed to fetch session:", err);
2800
+ return null;
2801
+ }
2802
+ }
2803
+ /**
2804
+ * Send a message and stream the response using SSE
2805
+ */
2806
+ async *sendMessage(sessionId, content, attachments = [], signal) {
2807
+ this.abortController = new AbortController();
2808
+ if (signal) {
2809
+ signal.addEventListener("abort", () => {
2810
+ this.abortController?.abort();
2811
+ });
2812
+ }
2813
+ const url = `${this.config.baseUrl}${this.config.streamEndpoint}`;
2814
+ const payload = {
2815
+ sessionId,
2816
+ content,
2817
+ attachments: attachments.map((a) => ({
2818
+ id: a.id,
2819
+ filename: a.filename,
2820
+ mimeType: a.mimeType,
2821
+ sizeBytes: a.sizeBytes,
2822
+ base64Data: a.base64Data
2823
+ }))
2824
+ };
2825
+ try {
2826
+ const response = await fetch(url, {
2827
+ method: "POST",
2828
+ headers: this.createHeaders(),
2829
+ body: JSON.stringify(payload),
2830
+ signal: this.abortController.signal
2831
+ });
2832
+ if (!response.ok) {
2833
+ throw new Error(`Stream request failed: ${response.statusText}`);
2834
+ }
2835
+ if (!response.body) {
2836
+ throw new Error("Response body is null");
2837
+ }
2838
+ const reader = response.body.getReader();
2839
+ const decoder = new TextDecoder();
2840
+ let buffer = "";
2841
+ try {
2842
+ while (true) {
2843
+ const { done, value } = await reader.read();
2844
+ if (done) {
2845
+ break;
2846
+ }
2847
+ buffer += decoder.decode(value, { stream: true });
2848
+ const lines = buffer.split("\n");
2849
+ buffer = lines.pop() || "";
2850
+ for (const line of lines) {
2851
+ if (!line.trim() || line.startsWith(":")) {
2852
+ continue;
2853
+ }
2854
+ if (line.startsWith("data: ")) {
2855
+ const data = line.slice(6);
2856
+ try {
2857
+ const chunk = JSON.parse(data);
2858
+ yield chunk;
2859
+ if (chunk.type === "done" || chunk.type === "error") {
2860
+ return;
2861
+ }
2862
+ } catch (parseErr) {
2863
+ console.error("Failed to parse SSE data:", parseErr);
2864
+ yield {
2865
+ type: "error",
2866
+ error: "Failed to parse stream data"
2867
+ };
2868
+ return;
2869
+ }
2870
+ }
2871
+ }
2872
+ }
2873
+ } finally {
2874
+ reader.releaseLock();
2875
+ }
2876
+ } catch (err) {
2877
+ if (err instanceof Error) {
2878
+ if (err.name === "AbortError") {
2879
+ yield {
2880
+ type: "error",
2881
+ error: "Stream cancelled"
2882
+ };
2883
+ } else {
2884
+ yield {
2885
+ type: "error",
2886
+ error: err.message
2887
+ };
2888
+ }
2889
+ } else {
2890
+ yield {
2891
+ type: "error",
2892
+ error: "Unknown error"
2893
+ };
2894
+ }
2895
+ } finally {
2896
+ this.abortController = null;
2897
+ }
2898
+ }
2899
+ /**
2900
+ * Cancel ongoing stream
2901
+ */
2902
+ cancelStream() {
2903
+ if (this.abortController) {
2904
+ this.abortController.abort();
2905
+ this.abortController = null;
2906
+ }
2907
+ }
2908
+ /**
2909
+ * Submit answers to a pending question
2910
+ */
2911
+ async submitQuestionAnswers(sessionId, questionId, answers) {
2912
+ const query = `
2913
+ mutation SubmitQuestionAnswers($sessionId: ID!, $questionId: ID!, $answers: JSON!) {
2914
+ submitQuestionAnswers(sessionId: $sessionId, questionId: $questionId, answers: $answers) {
2915
+ success
2916
+ error
2917
+ }
2918
+ }
2919
+ `;
2920
+ try {
2921
+ const data = await this.graphql(query, { sessionId, questionId, answers });
2922
+ return data.submitQuestionAnswers;
2923
+ } catch (err) {
2924
+ return {
2925
+ success: false,
2926
+ error: err instanceof Error ? err.message : "Failed to submit answers"
2927
+ };
2928
+ }
2929
+ }
2930
+ /**
2931
+ * Cancel a pending question
2932
+ */
2933
+ async cancelPendingQuestion(questionId) {
2934
+ const query = `
2935
+ mutation CancelPendingQuestion($questionId: ID!) {
2936
+ cancelPendingQuestion(questionId: $questionId) {
2937
+ success
2938
+ error
2939
+ }
2940
+ }
2941
+ `;
2942
+ try {
2943
+ const data = await this.graphql(query, { questionId });
2944
+ return data.cancelPendingQuestion;
2945
+ } catch (err) {
2946
+ return {
2947
+ success: false,
2948
+ error: err instanceof Error ? err.message : "Failed to cancel question"
2949
+ };
2950
+ }
2951
+ }
2952
+ /**
2953
+ * Navigate to a session (optional, for SPA routing)
2954
+ */
2955
+ navigateToSession(sessionId) {
2956
+ if (typeof window !== "undefined") {
2957
+ window.location.href = `/chat/${sessionId}`;
2958
+ }
2959
+ }
2960
+ };
2961
+ function createHttpDataSource(config) {
2962
+ return new HttpDataSource(config);
2963
+ }
2964
+
2965
+ exports.AssistantTurnView = AssistantTurnView;
2966
+ exports.AttachmentGrid = AttachmentGrid;
2967
+ exports.ChartCard = ChartCard;
2968
+ exports.ChatHeader = ChatHeader;
2969
+ exports.ChatSession = ChatSession;
2970
+ exports.ChatSessionProvider = ChatSessionProvider;
2971
+ exports.CodeOutputsPanel = CodeOutputsPanel;
2972
+ exports.ConfigProvider = ConfigProvider;
2973
+ exports.DownloadCard = DownloadCard;
2974
+ exports.EditableText = EditableText_default;
2975
+ exports.EmptyState = EmptyState_default;
2976
+ exports.HttpDataSource = HttpDataSource;
2977
+ exports.ImageModal = ImageModal;
2978
+ exports.InlineQuestionForm = InlineQuestionForm;
2979
+ exports.IotaContextProvider = IotaContextProvider;
2980
+ exports.ListItemSkeleton = ListItemSkeleton;
2981
+ exports.MarkdownRenderer = MarkdownRenderer;
2982
+ exports.MessageInput = MessageInput;
2983
+ exports.MessageList = MessageList;
2984
+ exports.MessageRole = MessageRole;
2985
+ exports.RateLimiter = RateLimiter;
2986
+ exports.ScrollToBottomButton = ScrollToBottomButton;
2987
+ exports.SearchInput = SearchInput_default;
2988
+ exports.Skeleton = Skeleton_default;
2989
+ exports.SkeletonAvatar = SkeletonAvatar;
2990
+ exports.SkeletonCard = SkeletonCard;
2991
+ exports.SkeletonGroup = SkeletonGroup;
2992
+ exports.SkeletonText = SkeletonText;
2993
+ exports.SourcesPanel = SourcesPanel;
2994
+ exports.StreamingCursor = StreamingCursor;
2995
+ exports.ThemeProvider = ThemeProvider;
2996
+ exports.TurnBubble = TurnBubble;
2997
+ exports.UserTurnView = UserTurnView;
2998
+ exports.WelcomeContent = WelcomeContent;
2999
+ exports.addCSRFHeader = addCSRFHeader;
3000
+ exports.backdropVariants = backdropVariants;
3001
+ exports.buttonVariants = buttonVariants;
3002
+ exports.convertToBase64 = convertToBase64;
3003
+ exports.createDataUrl = createDataUrl;
3004
+ exports.createHeadersWithCSRF = createHeadersWithCSRF;
3005
+ exports.createHttpDataSource = createHttpDataSource;
3006
+ exports.darkTheme = darkTheme;
3007
+ exports.dropdownVariants = dropdownVariants;
3008
+ exports.fadeInUpVariants = fadeInUpVariants;
3009
+ exports.fadeInVariants = fadeInVariants;
3010
+ exports.floatingButtonVariants = floatingButtonVariants;
3011
+ exports.formatFileSize = formatFileSize;
3012
+ exports.getCSRFToken = getCSRFToken;
3013
+ exports.hasConfigPermission = hasPermission2;
3014
+ exports.hasPermission = hasPermission;
3015
+ exports.lightTheme = lightTheme;
3016
+ exports.listItemVariants = listItemVariants;
3017
+ exports.messageContainerVariants = messageContainerVariants;
3018
+ exports.messageVariants = messageVariants;
3019
+ exports.scaleFadeVariants = scaleFadeVariants;
3020
+ exports.staggerContainerVariants = staggerContainerVariants;
3021
+ exports.toastVariants = toastVariants;
3022
+ exports.typingDotVariants = typingDotVariants;
3023
+ exports.useChat = useChat;
3024
+ exports.useConfig = useConfig;
3025
+ exports.useIotaContext = useIotaContext;
3026
+ exports.useRequiredConfig = useRequiredConfig;
3027
+ exports.useStreaming = useStreaming;
3028
+ exports.useTheme = useTheme;
3029
+ exports.useTranslation = useTranslation;
3030
+ exports.validateFileCount = validateFileCount;
3031
+ exports.validateImageFile = validateImageFile;
3032
+ //# sourceMappingURL=index.cjs.map
3033
+ //# sourceMappingURL=index.cjs.map