@iota-uz/sdk 0.4.17 → 0.4.20

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.
@@ -1,16 +1,16 @@
1
- import React, { createContext, lazy, memo, forwardRef, useState, useRef, useMemo, useEffect, useImperativeHandle, useCallback, isValidElement, cloneElement, useContext, useId, useSyncExternalStore, Suspense, Component, Children } from 'react';
2
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
1
+ import React, { createContext, memo, useState, useEffect, useMemo, useCallback, lazy, forwardRef, useRef, useImperativeHandle, isValidElement, cloneElement, useContext, useId, useSyncExternalStore, Suspense, Component, Children } from 'react';
2
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
3
3
  import ReactApexChart from 'react-apexcharts';
4
4
  import ApexCharts from 'apexcharts';
5
- import { X, Bug, ArrowUp, ArrowDown, Stack, Paperclip, PaperPlaneRight, CircleNotch, ArrowUUpLeft, PencilSimple, Check, Bookmark, ArrowsClockwise, Archive, Trash, DotsThree, Warning, ArrowClockwise, Image, ArrowCounterClockwise, ImageBroken, CaretLeft, CaretRight, Info, CheckCircle, XCircle, Spinner, MagnifyingGlass, WarningCircle, CaretDown, Copy, FilePdf, FileXls, FileCsv, FileDoc, FileCode, FileText, File, ChartBar, DownloadSimple, Download, ChatCircleDots, PencilSimpleLine, ArrowLeft, PaperPlaneTilt, ArrowRight, Timer, Lightning, Database, Wrench, ClockCounterClockwise, Lightbulb, Package, Plus, ArrowsCounterClockwise, Gear, Users, List, CaretLineLeft, CaretLineRight, Code, ArrowSquareOut, SpinnerGap, FloppyDisk, Sidebar } from '@phosphor-icons/react';
5
+ import { Warning, ArrowClockwise, X, Bug, ArrowUp, ArrowDown, Stack, Paperclip, PaperPlaneRight, CircleNotch, ArrowUUpLeft, PencilSimple, Check, Bookmark, ArrowsClockwise, Archive, Trash, DotsThree, Image, ArrowCounterClockwise, ImageBroken, CaretLeft, CaretRight, MagnifyingGlassMinus, MagnifyingGlassPlus, ArrowsIn, Info, CheckCircle, XCircle, Spinner, MagnifyingGlass, WarningCircle, CaretDown, Copy, FilePdf, FileXls, FileCsv, FileDoc, FileCode, FileText, File as File$1, ChartBar, DownloadSimple, Download, ChatCircleDots, PencilSimpleLine, ArrowLeft, PaperPlaneTilt, ArrowRight, Timer, Lightning, Database, Wrench, ClockCounterClockwise, Lightbulb, Package, Plus, ArrowsCounterClockwise, Gear, Users, List, CaretLineLeft, CaretLineRight, Code, ArrowSquareOut, SpinnerGap, FloppyDisk, Sidebar } from '@phosphor-icons/react';
6
6
  import { Prism } from 'react-syntax-highlighter';
7
7
  import { vscDarkPlus, vs } from 'react-syntax-highlighter/dist/esm/styles/prism';
8
8
  import ReactMarkdown from 'react-markdown';
9
9
  import remarkGfm from 'remark-gfm';
10
- import { useMotionValue, useTransform, motion, AnimatePresence, useReducedMotion } from 'framer-motion';
10
+ import { motion, useMotionValue, useTransform, AnimatePresence, useReducedMotion } from 'framer-motion';
11
11
  import 'react-dom/client';
12
12
  import { startOfDay, differenceInDays, differenceInMinutes, differenceInHours, format } from 'date-fns';
13
- import { Menu, MenuButton, MenuItems, MenuItem, Dialog, DialogBackdrop, DialogPanel, DialogTitle, Description, Disclosure, DisclosureButton, DisclosurePanel, Transition } from '@headlessui/react';
13
+ import { Menu, MenuButton, MenuItems, MenuItem, Dialog, DialogBackdrop, DialogPanel, DialogTitle, Description, Transition } from '@headlessui/react';
14
14
  import { createPortal } from 'react-dom';
15
15
 
16
16
  var __defProp = Object.defineProperty;
@@ -180,6 +180,37 @@ var init_ChartCard = __esm({
180
180
  DEFAULT_COLORS = ["#6366f1", "#06b6d4", "#f59e0b", "#ef4444", "#8b5cf6"];
181
181
  }
182
182
  });
183
+ var TableExportButton;
184
+ var init_TableExportButton = __esm({
185
+ "ui/src/bichat/components/TableExportButton.tsx"() {
186
+ init_useTranslation();
187
+ TableExportButton = memo(function TableExportButton2({
188
+ onClick,
189
+ disabled = false,
190
+ label,
191
+ disabledTooltip
192
+ }) {
193
+ const { t } = useTranslation();
194
+ const resolvedLabel = label ?? t("BiChat.Export");
195
+ const resolvedDisabledTooltip = disabledTooltip ?? t("BiChat.Common.PleaseWait");
196
+ return /* @__PURE__ */ jsxs(
197
+ "button",
198
+ {
199
+ type: "button",
200
+ onClick,
201
+ disabled,
202
+ className: "cursor-pointer inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-green-700 dark:text-green-400 hover:text-green-800 dark:hover:text-green-300 disabled:text-gray-400 disabled:cursor-not-allowed transition-colors rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50",
203
+ "aria-label": resolvedLabel,
204
+ title: disabled ? resolvedDisabledTooltip : resolvedLabel,
205
+ children: [
206
+ /* @__PURE__ */ jsx(FileXls, { size: 16, weight: "fill" }),
207
+ /* @__PURE__ */ jsx("span", { children: resolvedLabel })
208
+ ]
209
+ }
210
+ );
211
+ });
212
+ }
213
+ });
183
214
 
184
215
  // ui/src/bichat/utils/citationProcessor.ts
185
216
  function processCitations(content, citations) {
@@ -292,37 +323,6 @@ var init_chartSpec = __esm({
292
323
  ]);
293
324
  }
294
325
  });
295
- var TableExportButton;
296
- var init_TableExportButton = __esm({
297
- "ui/src/bichat/components/TableExportButton.tsx"() {
298
- init_useTranslation();
299
- TableExportButton = memo(function TableExportButton2({
300
- onClick,
301
- disabled = false,
302
- label,
303
- disabledTooltip
304
- }) {
305
- const { t } = useTranslation();
306
- const resolvedLabel = label ?? t("BiChat.Export");
307
- const resolvedDisabledTooltip = disabledTooltip ?? t("BiChat.Common.PleaseWait");
308
- return /* @__PURE__ */ jsxs(
309
- "button",
310
- {
311
- type: "button",
312
- onClick,
313
- disabled,
314
- className: "cursor-pointer inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-green-700 dark:text-green-400 hover:text-green-800 dark:hover:text-green-300 disabled:text-gray-400 disabled:cursor-not-allowed transition-colors rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50",
315
- "aria-label": resolvedLabel,
316
- title: disabled ? resolvedDisabledTooltip : resolvedLabel,
317
- children: [
318
- /* @__PURE__ */ jsx(FileXls, { size: 16, weight: "fill" }),
319
- /* @__PURE__ */ jsx("span", { children: resolvedLabel })
320
- ]
321
- }
322
- );
323
- });
324
- }
325
- });
326
326
  var TableWithExport;
327
327
  var init_TableWithExport = __esm({
328
328
  "ui/src/bichat/components/TableWithExport.tsx"() {
@@ -1508,6 +1508,10 @@ var ChatMachine = class {
1508
1508
  }
1509
1509
  /** Sets turns from fetch, preserving pending user-only turns if server hasn't caught up. */
1510
1510
  _setTurnsFromFetch(fetchedTurns) {
1511
+ if (!Array.isArray(fetchedTurns)) {
1512
+ console.warn("[ChatMachine] Ignoring malformed turns payload from fetchSession");
1513
+ return;
1514
+ }
1511
1515
  const prev = this.state.messaging.turns;
1512
1516
  const hasPendingUserOnly = prev.length > 0 && !prev[prev.length - 1].assistantTurn;
1513
1517
  if (hasPendingUserOnly && (!fetchedTurns || fetchedTurns.length === 0)) {
@@ -1533,6 +1537,12 @@ var ChatMachine = class {
1533
1537
  streamErrorRetryable: false
1534
1538
  });
1535
1539
  }
1540
+ _notifySessionsUpdated(reason, sessionId) {
1541
+ if (typeof window === "undefined") return;
1542
+ window.dispatchEvent(new CustomEvent("bichat:sessions-updated", {
1543
+ detail: { reason, sessionId }
1544
+ }));
1545
+ }
1536
1546
  _cancel() {
1537
1547
  if (this.abortController) {
1538
1548
  this.abortController.abort();
@@ -1800,7 +1810,11 @@ var ChatMachine = class {
1800
1810
  }
1801
1811
  }
1802
1812
  const targetSessionId = createdSessionId || activeSessionId;
1813
+ if (targetSessionId && targetSessionId !== "new") {
1814
+ this._notifySessionsUpdated("message_sent", targetSessionId);
1815
+ }
1803
1816
  if (shouldNavigateAfter && targetSessionId && targetSessionId !== "new") {
1817
+ this._notifySessionsUpdated("session_created", targetSessionId);
1804
1818
  if (this.onSessionCreated) {
1805
1819
  this.onSessionCreated(targetSessionId);
1806
1820
  } else {
@@ -1897,6 +1911,7 @@ var ChatMachine = class {
1897
1911
  const curSessionId = this.state.session.currentSessionId;
1898
1912
  const curPendingQuestion = this.state.messaging.pendingQuestion;
1899
1913
  if (!curSessionId || !curPendingQuestion) return;
1914
+ const previousTurns = this.state.messaging.turns;
1900
1915
  this._updateMessaging({ loading: true });
1901
1916
  this._updateSession({ error: null, errorRetryable: false });
1902
1917
  const previousPendingQuestion = curPendingQuestion;
@@ -1914,19 +1929,28 @@ var ChatMachine = class {
1914
1929
  const fetchResult = await this.dataSource.fetchSession(curSessionId);
1915
1930
  if (this.disposed) return;
1916
1931
  if (fetchResult) {
1917
- this._updateMessaging({
1918
- turns: fetchResult.turns,
1919
- pendingQuestion: fetchResult.pendingQuestion || null
1920
- });
1932
+ this._updateSession({ session: fetchResult.session });
1933
+ this._updateMessaging({ pendingQuestion: fetchResult.pendingQuestion || null });
1934
+ const hasMalformedRefresh = previousTurns.length > 0 && Array.isArray(fetchResult.turns) && fetchResult.turns.length === 0;
1935
+ if (hasMalformedRefresh) {
1936
+ console.warn("[ChatMachine] Preserving previous turns due to empty post-HITL refetch payload", {
1937
+ sessionId: curSessionId,
1938
+ previousTurnCount: previousTurns.length
1939
+ });
1940
+ this._updateSession({
1941
+ error: "Failed to fully refresh session. Showing last known messages.",
1942
+ errorRetryable: true
1943
+ });
1944
+ } else {
1945
+ this._setTurnsFromFetch(fetchResult.turns);
1946
+ }
1921
1947
  } else {
1922
- this._updateMessaging({ pendingQuestion: previousPendingQuestion });
1923
- this._updateSession({ error: "Failed to load updated session", errorRetryable: false });
1948
+ this._updateSession({ error: "Failed to load updated session", errorRetryable: true });
1924
1949
  }
1925
1950
  } catch (fetchErr) {
1926
1951
  if (this.disposed) return;
1927
- this._updateMessaging({ pendingQuestion: previousPendingQuestion });
1928
1952
  const normalized = normalizeRPCError(fetchErr, "Failed to load updated session");
1929
- this._updateSession({ error: normalized.userMessage, errorRetryable: normalized.retryable });
1953
+ this._updateSession({ error: normalized.userMessage, errorRetryable: true });
1930
1954
  }
1931
1955
  }
1932
1956
  } else {
@@ -1980,6 +2004,8 @@ var ChatMachine = class {
1980
2004
  this._clearStreamError();
1981
2005
  const convertedAttachments = attachments.map((att) => ({
1982
2006
  clientKey: att.clientKey || crypto.randomUUID(),
2007
+ id: att.id,
2008
+ uploadId: att.uploadId,
1983
2009
  filename: att.filename,
1984
2010
  mimeType: att.mimeType,
1985
2011
  sizeBytes: att.sizeBytes,
@@ -2448,7 +2474,7 @@ function getFileVisual(mimeType, filename) {
2448
2474
  };
2449
2475
  }
2450
2476
  return {
2451
- icon: File,
2477
+ icon: File$1,
2452
2478
  iconColor: "text-gray-400 dark:text-gray-500",
2453
2479
  bgColor: "bg-gray-100 dark:bg-gray-800",
2454
2480
  label: (mime.split("/")[1] || "FILE").toUpperCase().slice(0, 4)
@@ -2642,6 +2668,9 @@ var MemoizedAttachmentGrid = React.memo(AttachmentGrid);
2642
2668
  MemoizedAttachmentGrid.displayName = "AttachmentGrid";
2643
2669
  var AttachmentGrid_default = MemoizedAttachmentGrid;
2644
2670
  init_useTranslation();
2671
+ var MIN_SCALE = 0.25;
2672
+ var MAX_SCALE = 5;
2673
+ var ZOOM_STEP = 0.25;
2645
2674
  function ImageModal({
2646
2675
  isOpen,
2647
2676
  onClose,
@@ -2654,9 +2683,23 @@ function ImageModal({
2654
2683
  const [isImageLoaded, setIsImageLoaded] = useState(false);
2655
2684
  const [imageError, setImageError] = useState(false);
2656
2685
  const [retryKey, setRetryKey] = useState(0);
2686
+ const [scale, setScale] = useState(1);
2687
+ const [position, setPosition] = useState({ x: 0, y: 0 });
2688
+ const [isDragging, setIsDragging] = useState(false);
2689
+ const dragStartRef = useRef({ x: 0, y: 0 });
2690
+ const positionRef = useRef({ x: 0, y: 0 });
2691
+ const scaleRef = useRef(1);
2692
+ const imageAreaRef = useRef(null);
2657
2693
  const hasMultipleImages = allAttachments && allAttachments.length > 1;
2658
2694
  const canNavigatePrev = hasMultipleImages && currentIndex > 0;
2659
2695
  const canNavigateNext = hasMultipleImages && currentIndex < (allAttachments?.length || 1) - 1;
2696
+ const isZoomed = scale > 1;
2697
+ useEffect(() => {
2698
+ scaleRef.current = scale;
2699
+ }, [scale]);
2700
+ useEffect(() => {
2701
+ positionRef.current = position;
2702
+ }, [position]);
2660
2703
  useEffect(() => {
2661
2704
  if (!isOpen) return;
2662
2705
  const handleKeyDown = (e) => {
@@ -2664,6 +2707,14 @@ function ImageModal({
2664
2707
  onNavigate("prev");
2665
2708
  } else if (e.key === "ArrowRight" && onNavigate && canNavigateNext) {
2666
2709
  onNavigate("next");
2710
+ } else if (e.key === "+" || e.key === "=") {
2711
+ setScale((s) => Math.min(s + ZOOM_STEP, MAX_SCALE));
2712
+ } else if (e.key === "-") {
2713
+ setScale((s) => Math.max(s - ZOOM_STEP, MIN_SCALE));
2714
+ if (scaleRef.current - ZOOM_STEP <= 1) setPosition({ x: 0, y: 0 });
2715
+ } else if (e.key === "0") {
2716
+ setScale(1);
2717
+ setPosition({ x: 0, y: 0 });
2667
2718
  }
2668
2719
  };
2669
2720
  document.addEventListener("keydown", handleKeyDown);
@@ -2672,128 +2723,246 @@ function ImageModal({
2672
2723
  useEffect(() => {
2673
2724
  setIsImageLoaded(false);
2674
2725
  setImageError(false);
2726
+ setScale(1);
2727
+ setPosition({ x: 0, y: 0 });
2675
2728
  }, [attachment]);
2729
+ useEffect(() => {
2730
+ const el = imageAreaRef.current;
2731
+ if (!el || !isOpen) return;
2732
+ const handler = (e) => {
2733
+ const delta = e.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
2734
+ const current = scaleRef.current;
2735
+ const newScale = Math.min(Math.max(current + delta, MIN_SCALE), MAX_SCALE);
2736
+ if (newScale === current) return;
2737
+ e.preventDefault();
2738
+ setScale(newScale);
2739
+ if (newScale <= 1) setPosition({ x: 0, y: 0 });
2740
+ };
2741
+ el.addEventListener("wheel", handler, { passive: false });
2742
+ return () => el.removeEventListener("wheel", handler);
2743
+ }, [isOpen]);
2676
2744
  const handleRetry = useCallback(() => {
2677
2745
  setImageError(false);
2678
2746
  setIsImageLoaded(false);
2679
2747
  setRetryKey((k) => k + 1);
2680
2748
  }, []);
2749
+ const zoomIn = useCallback(() => {
2750
+ setScale((s) => Math.min(s + ZOOM_STEP, MAX_SCALE));
2751
+ }, []);
2752
+ const zoomOut = useCallback(() => {
2753
+ setScale((s) => Math.max(s - ZOOM_STEP, MIN_SCALE));
2754
+ if (scaleRef.current - ZOOM_STEP <= 1) setPosition({ x: 0, y: 0 });
2755
+ }, []);
2756
+ const resetZoom = useCallback(() => {
2757
+ setScale(1);
2758
+ setPosition({ x: 0, y: 0 });
2759
+ }, []);
2760
+ const handleDoubleClick = useCallback(() => {
2761
+ const current = scaleRef.current;
2762
+ if (current !== 1) {
2763
+ setScale(1);
2764
+ setPosition({ x: 0, y: 0 });
2765
+ } else {
2766
+ setScale(2);
2767
+ }
2768
+ }, []);
2769
+ const handleMouseDown = useCallback((e) => {
2770
+ if (scaleRef.current <= 1) return;
2771
+ e.preventDefault();
2772
+ setIsDragging(true);
2773
+ dragStartRef.current = {
2774
+ x: e.clientX - positionRef.current.x,
2775
+ y: e.clientY - positionRef.current.y
2776
+ };
2777
+ }, []);
2778
+ const handleMouseMove = useCallback((e) => {
2779
+ if (!isDragging) return;
2780
+ setPosition({
2781
+ x: e.clientX - dragStartRef.current.x,
2782
+ y: e.clientY - dragStartRef.current.y
2783
+ });
2784
+ }, [isDragging]);
2785
+ const handleMouseUp = useCallback(() => {
2786
+ setIsDragging(false);
2787
+ }, []);
2788
+ const handleBackdropClick = useCallback((e) => {
2789
+ if (e.target === e.currentTarget && !isZoomed) {
2790
+ onClose();
2791
+ }
2792
+ }, [isZoomed, onClose]);
2681
2793
  const previewUrl = attachment.preview || createDataUrl(attachment.base64Data, attachment.mimeType);
2794
+ const zoomPercent = Math.round(scale * 100);
2682
2795
  return /* @__PURE__ */ jsxs(Dialog, { open: isOpen, onClose, className: "relative", style: { zIndex: 99999 }, children: [
2683
2796
  /* @__PURE__ */ jsx(
2684
2797
  DialogBackdrop,
2685
2798
  {
2686
- className: "fixed inset-0",
2687
- style: { zIndex: 99999, backgroundColor: "rgba(0, 0, 0, 0.85)" }
2799
+ className: "fixed inset-0 bg-black/90 backdrop-blur-sm",
2800
+ style: { zIndex: 99999 }
2688
2801
  }
2689
2802
  ),
2690
- /* @__PURE__ */ jsxs(DialogPanel, { className: "fixed inset-0 flex flex-col", style: { zIndex: 1e5 }, children: [
2691
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-3 shrink-0 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800", children: [
2692
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [
2693
- hasMultipleImages && /* @__PURE__ */ jsxs("span", { className: "text-xs text-gray-500 dark:text-gray-400 tabular-nums whitespace-nowrap", children: [
2694
- currentIndex + 1,
2695
- " / ",
2696
- allAttachments?.length
2697
- ] }),
2698
- /* @__PURE__ */ jsx("span", { className: "text-sm text-gray-900 dark:text-gray-200 truncate", children: attachment.filename }),
2699
- /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap", children: formatFileSize(attachment.sizeBytes) })
2700
- ] }),
2701
- /* @__PURE__ */ jsx(
2702
- "button",
2703
- {
2704
- onClick: onClose,
2705
- className: "cursor-pointer flex items-center justify-center w-8 h-8 rounded-md bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400",
2706
- "aria-label": t("BiChat.Image.Close"),
2707
- type: "button",
2708
- children: /* @__PURE__ */ jsx(X, { size: 18, weight: "bold" })
2709
- }
2710
- )
2711
- ] }),
2712
- /* @__PURE__ */ jsxs(
2713
- "div",
2714
- {
2715
- className: "relative flex-1 flex items-center justify-center min-h-0",
2716
- onClick: (e) => {
2717
- if (e.target === e.currentTarget) onClose();
2718
- },
2719
- children: [
2720
- !isImageLoaded && !imageError && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center pointer-events-none", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-3", children: [
2721
- /* @__PURE__ */ jsx("div", { className: "w-8 h-8 border-2 border-gray-300 dark:border-gray-700 border-t-gray-500 dark:border-t-gray-400 rounded-full animate-spin" }),
2722
- /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400 dark:text-gray-500", children: t("BiChat.Loading") })
2723
- ] }) }),
2724
- imageError && /* @__PURE__ */ jsxs("div", { role: "alert", className: "flex flex-col items-center justify-center text-center max-w-xs", children: [
2725
- /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center w-16 h-16 rounded-2xl bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 mb-5", children: /* @__PURE__ */ jsx(ImageBroken, { size: 28, className: "text-gray-400 dark:text-gray-500", weight: "duotone" }) }),
2726
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-gray-700 dark:text-gray-300 mb-1", children: t("BiChat.Image.FailedToLoad") }),
2727
- /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400 dark:text-gray-500 mb-5 truncate max-w-full", children: attachment.filename }),
2728
- /* @__PURE__ */ jsxs(
2729
- "button",
2730
- {
2731
- type: "button",
2732
- onClick: handleRetry,
2733
- className: "cursor-pointer inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 border border-gray-200 dark:border-gray-700 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400",
2734
- "aria-label": t("BiChat.Image.Retry"),
2735
- children: [
2736
- /* @__PURE__ */ jsx(ArrowClockwise, { size: 16, weight: "bold" }),
2737
- t("BiChat.Retry.Label")
2738
- ]
2739
- }
2740
- )
2803
+ /* @__PURE__ */ jsxs(
2804
+ DialogPanel,
2805
+ {
2806
+ className: "fixed inset-0 flex flex-col",
2807
+ style: { zIndex: 1e5 },
2808
+ onMouseMove: handleMouseMove,
2809
+ onMouseUp: handleMouseUp,
2810
+ onMouseLeave: handleMouseUp,
2811
+ children: [
2812
+ /* @__PURE__ */ jsx("div", { className: "flex items-center px-5 py-3 shrink-0", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [
2813
+ hasMultipleImages && /* @__PURE__ */ jsxs("span", { className: "text-xs text-white/50 tabular-nums whitespace-nowrap font-medium", children: [
2814
+ currentIndex + 1,
2815
+ " / ",
2816
+ allAttachments?.length
2741
2817
  ] }),
2742
- /* @__PURE__ */ jsx(
2743
- "img",
2744
- {
2745
- src: previewUrl,
2746
- alt: attachment.filename,
2747
- className: [
2748
- "max-w-[90vw] max-h-[calc(100vh-120px)] object-contain select-none",
2749
- "transition-opacity duration-300 ease-out",
2750
- isImageLoaded ? "opacity-100" : "opacity-0"
2751
- ].join(" "),
2752
- onLoad: () => setIsImageLoaded(true),
2753
- onError: () => setImageError(true),
2754
- loading: "lazy",
2755
- draggable: false
2756
- },
2757
- retryKey
2758
- ),
2759
- hasMultipleImages && /* @__PURE__ */ jsxs(Fragment, { children: [
2760
- /* @__PURE__ */ jsx(
2761
- "button",
2762
- {
2763
- onClick: () => onNavigate?.("prev"),
2764
- disabled: !canNavigatePrev || !isImageLoaded || imageError,
2765
- className: [
2766
- "absolute left-3 top-1/2 -translate-y-1/2",
2767
- "flex items-center justify-center w-10 h-10 rounded-md",
2768
- "transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400",
2769
- canNavigatePrev && isImageLoaded && !imageError ? "cursor-pointer bg-white/90 hover:bg-white dark:bg-gray-800/90 dark:hover:bg-gray-700 text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white shadow-sm" : "bg-white/40 dark:bg-gray-800/40 text-gray-300 dark:text-gray-700 cursor-not-allowed"
2770
- ].join(" "),
2771
- "aria-label": t("BiChat.Image.Previous"),
2772
- type: "button",
2773
- children: /* @__PURE__ */ jsx(CaretLeft, { size: 20, weight: "bold" })
2774
- }
2775
- ),
2776
- /* @__PURE__ */ jsx(
2777
- "button",
2778
- {
2779
- onClick: () => onNavigate?.("next"),
2780
- disabled: !canNavigateNext || !isImageLoaded || imageError,
2781
- className: [
2782
- "absolute right-3 top-1/2 -translate-y-1/2",
2783
- "flex items-center justify-center w-10 h-10 rounded-md",
2784
- "transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400",
2785
- canNavigateNext && isImageLoaded && !imageError ? "cursor-pointer bg-white/90 hover:bg-white dark:bg-gray-800/90 dark:hover:bg-gray-700 text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white shadow-sm" : "bg-white/40 dark:bg-gray-800/40 text-gray-300 dark:text-gray-700 cursor-not-allowed"
2786
- ].join(" "),
2787
- "aria-label": t("BiChat.Image.Next"),
2788
- type: "button",
2789
- children: /* @__PURE__ */ jsx(CaretRight, { size: 20, weight: "bold" })
2790
- }
2791
- )
2792
- ] })
2793
- ]
2794
- }
2795
- )
2796
- ] })
2818
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-white/90 truncate font-medium", children: attachment.filename }),
2819
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-white/40 whitespace-nowrap", children: formatFileSize(attachment.sizeBytes) })
2820
+ ] }) }),
2821
+ /* @__PURE__ */ jsxs(
2822
+ "div",
2823
+ {
2824
+ ref: imageAreaRef,
2825
+ className: "relative flex-1 flex items-center justify-center min-h-0 px-4 pb-4",
2826
+ onClick: handleBackdropClick,
2827
+ style: { cursor: isZoomed ? isDragging ? "grabbing" : "grab" : "default" },
2828
+ children: [
2829
+ /* @__PURE__ */ jsx(
2830
+ "button",
2831
+ {
2832
+ onClick: onClose,
2833
+ className: "absolute top-3 right-5 z-30 cursor-pointer flex items-center justify-center w-10 h-10 rounded-full bg-black/50 hover:bg-black/70 backdrop-blur-md text-white/80 hover:text-white border border-white/10 transition-all duration-200 shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30",
2834
+ "aria-label": t("BiChat.Image.Close"),
2835
+ type: "button",
2836
+ children: /* @__PURE__ */ jsx(X, { size: 20, weight: "bold" })
2837
+ }
2838
+ ),
2839
+ !isImageLoaded && !imageError && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center pointer-events-none", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-3", children: [
2840
+ /* @__PURE__ */ jsx("div", { className: "w-8 h-8 border-2 border-white/20 border-t-white/60 rounded-full animate-spin" }),
2841
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-white/40", children: t("BiChat.Loading") })
2842
+ ] }) }),
2843
+ imageError && /* @__PURE__ */ jsxs("div", { role: "alert", className: "flex flex-col items-center justify-center text-center max-w-xs", children: [
2844
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center w-16 h-16 rounded-2xl bg-white/5 border border-white/10 mb-5", children: /* @__PURE__ */ jsx(ImageBroken, { size: 28, className: "text-white/30", weight: "duotone" }) }),
2845
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-white/70 mb-1", children: t("BiChat.Image.FailedToLoad") }),
2846
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-white/30 mb-5 truncate max-w-full", children: attachment.filename }),
2847
+ /* @__PURE__ */ jsxs(
2848
+ "button",
2849
+ {
2850
+ type: "button",
2851
+ onClick: handleRetry,
2852
+ className: "cursor-pointer inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white/80 bg-white/10 hover:bg-white/15 border border-white/10 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30",
2853
+ "aria-label": t("BiChat.Image.Retry"),
2854
+ children: [
2855
+ /* @__PURE__ */ jsx(ArrowClockwise, { size: 16, weight: "bold" }),
2856
+ t("BiChat.Retry.Label")
2857
+ ]
2858
+ }
2859
+ )
2860
+ ] }),
2861
+ /* @__PURE__ */ jsx(
2862
+ "img",
2863
+ {
2864
+ src: previewUrl,
2865
+ alt: attachment.filename,
2866
+ className: [
2867
+ "relative z-0 max-w-[85vw] max-h-[calc(100vh-160px)] object-contain select-none rounded-lg",
2868
+ "transition-opacity duration-300 ease-out",
2869
+ isImageLoaded ? "opacity-100" : "opacity-0"
2870
+ ].join(" "),
2871
+ style: {
2872
+ transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`,
2873
+ transformOrigin: "center center",
2874
+ transition: isDragging ? "opacity 0.3s ease-out" : "transform 0.2s ease-out, opacity 0.3s ease-out"
2875
+ },
2876
+ onLoad: () => setIsImageLoaded(true),
2877
+ onError: () => setImageError(true),
2878
+ onMouseDown: handleMouseDown,
2879
+ onDoubleClick: handleDoubleClick,
2880
+ loading: "lazy",
2881
+ draggable: false
2882
+ },
2883
+ retryKey
2884
+ ),
2885
+ hasMultipleImages && /* @__PURE__ */ jsxs(Fragment, { children: [
2886
+ /* @__PURE__ */ jsx(
2887
+ "button",
2888
+ {
2889
+ onClick: () => onNavigate?.("prev"),
2890
+ disabled: !canNavigatePrev || !isImageLoaded || imageError,
2891
+ className: [
2892
+ "absolute left-4 top-1/2 -translate-y-1/2 z-20",
2893
+ "flex items-center justify-center w-11 h-11 rounded-full",
2894
+ "transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30",
2895
+ canNavigatePrev && isImageLoaded && !imageError ? "cursor-pointer bg-black/40 hover:bg-black/60 backdrop-blur-md text-white/80 hover:text-white shadow-lg border border-white/10" : "bg-black/20 text-white/20 cursor-not-allowed"
2896
+ ].join(" "),
2897
+ "aria-label": t("BiChat.Image.Previous"),
2898
+ type: "button",
2899
+ children: /* @__PURE__ */ jsx(CaretLeft, { size: 20, weight: "bold" })
2900
+ }
2901
+ ),
2902
+ /* @__PURE__ */ jsx(
2903
+ "button",
2904
+ {
2905
+ onClick: () => onNavigate?.("next"),
2906
+ disabled: !canNavigateNext || !isImageLoaded || imageError,
2907
+ className: [
2908
+ "absolute right-4 top-1/2 -translate-y-1/2 z-20",
2909
+ "flex items-center justify-center w-11 h-11 rounded-full",
2910
+ "transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30",
2911
+ canNavigateNext && isImageLoaded && !imageError ? "cursor-pointer bg-black/40 hover:bg-black/60 backdrop-blur-md text-white/80 hover:text-white shadow-lg border border-white/10" : "bg-black/20 text-white/20 cursor-not-allowed"
2912
+ ].join(" "),
2913
+ "aria-label": t("BiChat.Image.Next"),
2914
+ type: "button",
2915
+ children: /* @__PURE__ */ jsx(CaretRight, { size: 20, weight: "bold" })
2916
+ }
2917
+ )
2918
+ ] }),
2919
+ isImageLoaded && !imageError && /* @__PURE__ */ jsxs("div", { className: "absolute bottom-6 left-1/2 -translate-x-1/2 z-20 flex items-center gap-0.5 bg-black/50 backdrop-blur-xl rounded-full px-1.5 py-1.5 border border-white/10 shadow-2xl", children: [
2920
+ /* @__PURE__ */ jsx(
2921
+ "button",
2922
+ {
2923
+ type: "button",
2924
+ onClick: zoomOut,
2925
+ disabled: scale <= MIN_SCALE,
2926
+ className: "cursor-pointer flex items-center justify-center w-8 h-8 rounded-full text-white/70 hover:text-white hover:bg-white/10 transition-colors disabled:text-white/20 disabled:cursor-not-allowed disabled:hover:bg-transparent",
2927
+ "aria-label": t("BiChat.Image.ZoomOut"),
2928
+ children: /* @__PURE__ */ jsx(MagnifyingGlassMinus, { size: 16, weight: "bold" })
2929
+ }
2930
+ ),
2931
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-white/60 tabular-nums font-medium min-w-[3.5rem] text-center select-none", children: [
2932
+ zoomPercent,
2933
+ "%"
2934
+ ] }),
2935
+ /* @__PURE__ */ jsx(
2936
+ "button",
2937
+ {
2938
+ type: "button",
2939
+ onClick: zoomIn,
2940
+ disabled: scale >= MAX_SCALE,
2941
+ className: "cursor-pointer flex items-center justify-center w-8 h-8 rounded-full text-white/70 hover:text-white hover:bg-white/10 transition-colors disabled:text-white/20 disabled:cursor-not-allowed disabled:hover:bg-transparent",
2942
+ "aria-label": t("BiChat.Image.ZoomIn"),
2943
+ children: /* @__PURE__ */ jsx(MagnifyingGlassPlus, { size: 16, weight: "bold" })
2944
+ }
2945
+ ),
2946
+ isZoomed && /* @__PURE__ */ jsxs(Fragment, { children: [
2947
+ /* @__PURE__ */ jsx("div", { className: "w-px h-4 bg-white/15 mx-1" }),
2948
+ /* @__PURE__ */ jsx(
2949
+ "button",
2950
+ {
2951
+ type: "button",
2952
+ onClick: resetZoom,
2953
+ className: "cursor-pointer flex items-center justify-center w-8 h-8 rounded-full text-white/70 hover:text-white hover:bg-white/10 transition-colors",
2954
+ "aria-label": t("BiChat.Image.ResetZoom"),
2955
+ children: /* @__PURE__ */ jsx(ArrowsIn, { size: 16, weight: "bold" })
2956
+ }
2957
+ )
2958
+ ] })
2959
+ ] })
2960
+ ]
2961
+ }
2962
+ )
2963
+ ]
2964
+ }
2965
+ )
2797
2966
  ] });
2798
2967
  }
2799
2968
  var ImageModal_default = ImageModal;
@@ -2845,6 +3014,7 @@ function UserMessage({
2845
3014
  const [draftContent, setDraftContent] = useState("");
2846
3015
  const [isCopied, setIsCopied] = useState(false);
2847
3016
  const copyFeedbackTimeoutRef = useRef(null);
3017
+ const editTextareaRef = useRef(null);
2848
3018
  const classes = mergeClassNames(defaultClassNames, classNameOverrides);
2849
3019
  useEffect(() => {
2850
3020
  return () => {
@@ -2854,6 +3024,16 @@ function UserMessage({
2854
3024
  }
2855
3025
  };
2856
3026
  }, []);
3027
+ useEffect(() => {
3028
+ if (isEditing && editTextareaRef.current) {
3029
+ const textarea = editTextareaRef.current;
3030
+ textarea.focus();
3031
+ textarea.selectionStart = textarea.value.length;
3032
+ textarea.selectionEnd = textarea.value.length;
3033
+ textarea.style.height = "auto";
3034
+ textarea.style.height = `${Math.min(textarea.scrollHeight, 300)}px`;
3035
+ }
3036
+ }, [isEditing]);
2857
3037
  const normalizedAttachments = turn.attachments.map((attachment) => {
2858
3038
  if (!attachment.mimeType.startsWith("image/")) {
2859
3039
  return attachment;
@@ -2938,6 +3118,21 @@ function UserMessage({
2938
3118
  onEdit(turnId, newContent);
2939
3119
  setIsEditing(false);
2940
3120
  }, [onEdit, turnId, draftContent, turn.content]);
3121
+ const handleEditKeyDown = useCallback((e) => {
3122
+ if (e.key === "Escape") {
3123
+ e.preventDefault();
3124
+ handleEditCancel();
3125
+ } else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
3126
+ e.preventDefault();
3127
+ handleEditSave();
3128
+ }
3129
+ }, [handleEditCancel, handleEditSave]);
3130
+ const handleDraftChange = useCallback((e) => {
3131
+ setDraftContent(e.target.value);
3132
+ const el = e.target;
3133
+ el.style.height = "auto";
3134
+ el.style.height = `${Math.min(el.scrollHeight, 300)}px`;
3135
+ }, []);
2941
3136
  const handleNavigate = useCallback(
2942
3137
  (direction) => {
2943
3138
  if (selectedImageIndex === null) return;
@@ -2988,36 +3183,48 @@ function UserMessage({
2988
3183
  }
2989
3184
  )
2990
3185
  ) }),
2991
- turn.content && /* @__PURE__ */ jsx("div", { className: classes.bubble, children: /* @__PURE__ */ jsx("div", { className: classes.content, children: isEditing ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
3186
+ turn.content && /* @__PURE__ */ jsx("div", { className: classes.bubble, children: /* @__PURE__ */ jsx("div", { className: classes.content, children: isEditing ? /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
2992
3187
  /* @__PURE__ */ jsx(
2993
3188
  "textarea",
2994
3189
  {
3190
+ ref: editTextareaRef,
2995
3191
  value: draftContent,
2996
- onChange: (e) => setDraftContent(e.target.value),
2997
- className: "w-full min-h-[80px] resize-y rounded-lg px-3 py-2 bg-white/10 text-white placeholder-white/70 outline-none focus:ring-2 focus:ring-white/30",
2998
- "aria-label": t("BiChat.Message.EditMessage")
3192
+ onChange: handleDraftChange,
3193
+ onKeyDown: handleEditKeyDown,
3194
+ className: "w-full min-h-[60px] max-h-[300px] resize-none rounded-xl border border-white/20 bg-white/[0.08] px-3.5 py-2.5 text-sm text-white leading-relaxed outline-none focus:bg-white/[0.12] focus:border-white/30 focus:ring-1 focus:ring-white/20 transition-all duration-200",
3195
+ "aria-label": t("BiChat.Message.EditMessage"),
3196
+ rows: 1
2999
3197
  }
3000
3198
  ),
3001
- /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
3002
- /* @__PURE__ */ jsx(
3003
- "button",
3004
- {
3005
- type: "button",
3006
- onClick: handleEditCancel,
3007
- className: "cursor-pointer px-3 py-1.5 rounded-lg bg-white/10 hover:bg-white/15 transition-colors text-sm font-medium",
3008
- children: t("BiChat.Message.Cancel")
3009
- }
3010
- ),
3011
- /* @__PURE__ */ jsx(
3012
- "button",
3013
- {
3014
- type: "button",
3015
- onClick: handleEditSave,
3016
- className: "cursor-pointer px-3 py-1.5 rounded-lg bg-white/20 hover:bg-white/25 transition-colors text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed",
3017
- disabled: !draftContent.trim() || draftContent === turn.content,
3018
- children: t("BiChat.Message.Save")
3019
- }
3020
- )
3199
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
3200
+ /* @__PURE__ */ jsxs("span", { className: "text-[11px] text-white/30 select-none hidden sm:inline", children: [
3201
+ "Esc \xB7 ",
3202
+ typeof navigator !== "undefined" && /mac|iphone|ipad/i.test(
3203
+ navigator.userAgentData?.platform ?? navigator?.platform ?? ""
3204
+ ) ? "\u2318" : "Ctrl",
3205
+ "+Enter"
3206
+ ] }),
3207
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 ml-auto", children: [
3208
+ /* @__PURE__ */ jsx(
3209
+ "button",
3210
+ {
3211
+ type: "button",
3212
+ onClick: handleEditCancel,
3213
+ className: "cursor-pointer px-3 py-1.5 rounded-lg text-white/60 hover:text-white hover:bg-white/10 transition-colors text-sm",
3214
+ children: t("BiChat.Message.Cancel")
3215
+ }
3216
+ ),
3217
+ /* @__PURE__ */ jsx(
3218
+ "button",
3219
+ {
3220
+ type: "button",
3221
+ onClick: handleEditSave,
3222
+ className: "cursor-pointer px-4 py-1.5 rounded-lg bg-white text-primary-700 font-medium text-sm hover:bg-white/90 transition-all shadow-sm disabled:opacity-40 disabled:cursor-not-allowed disabled:shadow-none",
3223
+ disabled: !draftContent.trim() || draftContent === turn.content,
3224
+ children: t("BiChat.Message.Save")
3225
+ }
3226
+ )
3227
+ ] })
3021
3228
  ] })
3022
3229
  ] }) : renderSlot(slots?.content, contentSlotProps, turn.content) }) }),
3023
3230
  !hideActions && /* @__PURE__ */ jsx("div", { className: `${classes.actions} ${isCopied ? "opacity-100" : ""}`, children: renderSlot(
@@ -3167,65 +3374,293 @@ var StreamingCursor_default = StreamingCursor;
3167
3374
 
3168
3375
  // ui/src/bichat/components/AssistantMessage.tsx
3169
3376
  init_ChartCard();
3170
- function SourcesPanel({ citations }) {
3171
- if (!citations || citations.length === 0) {
3172
- return null;
3377
+
3378
+ // ui/src/bichat/components/InteractiveTableCard.tsx
3379
+ init_useTranslation();
3380
+ init_TableExportButton();
3381
+ var PAGE_SIZE_OPTIONS = [10, 25, 50, 100, 200];
3382
+ function formatCell(value) {
3383
+ if (value === null || value === void 0) return "NULL";
3384
+ if (typeof value === "object") {
3385
+ try {
3386
+ return JSON.stringify(value);
3387
+ } catch {
3388
+ return String(value);
3389
+ }
3173
3390
  }
3174
- return /* @__PURE__ */ jsx("div", { className: "mt-4 border-t border-[var(--bichat-border)] pt-3", children: /* @__PURE__ */ jsx(Disclosure, { children: ({ open }) => /* @__PURE__ */ jsxs(Fragment, { children: [
3175
- /* @__PURE__ */ jsxs(DisclosureButton, { className: "cursor-pointer flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50 rounded-md p-1 -m-1", children: [
3391
+ return String(value);
3392
+ }
3393
+ var InteractiveTableCard = memo(function InteractiveTableCard2({
3394
+ table,
3395
+ onSendMessage,
3396
+ sendDisabled = false
3397
+ }) {
3398
+ const { t } = useTranslation();
3399
+ const defaultPageSize = Math.min(Math.max(table.pageSize || 25, 1), 200);
3400
+ const [page, setPage] = useState(1);
3401
+ const [pageSize, setPageSize] = useState(defaultPageSize);
3402
+ useEffect(() => {
3403
+ const nextPageSize = Math.min(Math.max(table.pageSize || 25, 1), 200);
3404
+ setPage(1);
3405
+ setPageSize(nextPageSize);
3406
+ }, [table.id, table.pageSize]);
3407
+ const totalRows = table.rows.length;
3408
+ const totalPages = Math.max(1, Math.ceil(totalRows / pageSize));
3409
+ useEffect(() => {
3410
+ if (page > totalPages) {
3411
+ setPage(totalPages);
3412
+ }
3413
+ }, [page, totalPages]);
3414
+ const pageRows = useMemo(() => {
3415
+ const start = (page - 1) * pageSize;
3416
+ return table.rows.slice(start, start + pageSize);
3417
+ }, [page, pageSize, table.rows]);
3418
+ const pageSizeOptions = useMemo(() => {
3419
+ const set = /* @__PURE__ */ new Set([...PAGE_SIZE_OPTIONS, defaultPageSize]);
3420
+ return [...set].sort((a, b) => a - b);
3421
+ }, [defaultPageSize]);
3422
+ const canExportViaPrompt = !!onSendMessage && !!table.exportPrompt;
3423
+ const exportDisabled = sendDisabled || !table.export?.url && !canExportViaPrompt;
3424
+ const handleExport = useCallback(() => {
3425
+ if (table.export?.url) {
3426
+ try {
3427
+ const parsed = new URL(table.export.url, window.location.origin);
3428
+ if (!["http:", "https:", "blob:"].includes(parsed.protocol)) {
3429
+ console.warn("[InteractiveTableCard] Blocked export URL with unsafe protocol:", parsed.protocol);
3430
+ return;
3431
+ }
3432
+ } catch {
3433
+ console.warn("[InteractiveTableCard] Blocked malformed export URL");
3434
+ return;
3435
+ }
3436
+ const link = document.createElement("a");
3437
+ link.href = table.export.url;
3438
+ link.download = table.export.filename || "table_export.xlsx";
3439
+ link.rel = "noopener noreferrer";
3440
+ document.body.appendChild(link);
3441
+ link.click();
3442
+ document.body.removeChild(link);
3443
+ return;
3444
+ }
3445
+ if (canExportViaPrompt && table.exportPrompt) {
3446
+ onSendMessage?.(table.exportPrompt);
3447
+ }
3448
+ }, [canExportViaPrompt, onSendMessage, table.export, table.exportPrompt]);
3449
+ const from = totalRows === 0 ? 0 : (page - 1) * pageSize + 1;
3450
+ const to = Math.min(page * pageSize, totalRows);
3451
+ return /* @__PURE__ */ jsxs("section", { className: "w-full rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900/40", children: [
3452
+ /* @__PURE__ */ jsxs("header", { className: "flex flex-wrap items-center justify-between gap-2 border-b border-gray-200 dark:border-gray-700 px-3 py-2", children: [
3453
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
3454
+ /* @__PURE__ */ jsx("h4", { className: "truncate text-sm font-semibold text-gray-900 dark:text-gray-100", children: table.title || t("BiChat.Table.QueryResults") }),
3455
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: [
3456
+ totalRows === 1 ? t("BiChat.Table.OneRowLoaded") : t("BiChat.Table.RowsLoaded", { count: String(totalRows) }),
3457
+ table.truncated ? ` ${t("BiChat.Table.TruncatedSuffix")}` : ""
3458
+ ] })
3459
+ ] }),
3176
3460
  /* @__PURE__ */ jsx(
3177
- "svg",
3461
+ TableExportButton,
3178
3462
  {
3179
- className: `w-4 h-4 transition-transform duration-150 ${open ? "rotate-90" : ""}`,
3180
- fill: "none",
3181
- stroke: "currentColor",
3182
- viewBox: "0 0 24 24",
3183
- children: /* @__PURE__ */ jsx(
3184
- "path",
3185
- {
3186
- strokeLinecap: "round",
3187
- strokeLinejoin: "round",
3188
- strokeWidth: 2,
3189
- d: "M9 5l7 7-7 7"
3190
- }
3191
- )
3463
+ onClick: handleExport,
3464
+ disabled: exportDisabled,
3465
+ label: t("BiChat.Table.ExportToExcel"),
3466
+ disabledTooltip: sendDisabled ? t("BiChat.Table.PleaseWait") : t("BiChat.Table.ExportUnavailable")
3192
3467
  }
3193
- ),
3194
- /* @__PURE__ */ jsxs("span", { children: [
3195
- citations.length,
3196
- " ",
3197
- citations.length === 1 ? "source" : "sources"
3468
+ )
3469
+ ] }),
3470
+ /* @__PURE__ */ jsx("div", { className: "max-h-[420px] overflow-auto", children: /* @__PURE__ */ jsxs("table", { className: "w-full border-collapse text-sm", children: [
3471
+ /* @__PURE__ */ jsx("thead", { className: "sticky top-0 bg-gray-100 dark:bg-gray-800 z-10", children: /* @__PURE__ */ jsx("tr", { className: "border-b border-gray-200 dark:border-gray-700", children: table.headers.map((header, index) => /* @__PURE__ */ jsx(
3472
+ "th",
3473
+ {
3474
+ className: "px-3 py-2 text-left font-semibold text-gray-700 dark:text-gray-200 whitespace-nowrap",
3475
+ children: header
3476
+ },
3477
+ `${table.id}-header-${index}`
3478
+ )) }) }),
3479
+ /* @__PURE__ */ jsxs("tbody", { children: [
3480
+ pageRows.map((row, rowIndex) => /* @__PURE__ */ jsx(
3481
+ "tr",
3482
+ {
3483
+ className: "border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-800/40",
3484
+ children: table.columns.map((_, columnIndex) => /* @__PURE__ */ jsx(
3485
+ "td",
3486
+ {
3487
+ className: "px-3 py-2 text-gray-700 dark:text-gray-300 align-top",
3488
+ children: /* @__PURE__ */ jsx("span", { className: "block max-w-[420px] truncate", title: formatCell(row[columnIndex]), children: formatCell(row[columnIndex]) })
3489
+ },
3490
+ `${table.id}-cell-${rowIndex}-${columnIndex}`
3491
+ ))
3492
+ },
3493
+ `${table.id}-row-${rowIndex}`
3494
+ )),
3495
+ pageRows.length === 0 && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
3496
+ "td",
3497
+ {
3498
+ colSpan: table.columns.length,
3499
+ className: "px-3 py-6 text-center text-sm text-gray-500 dark:text-gray-400",
3500
+ children: t("BiChat.Table.NoRows")
3501
+ }
3502
+ ) })
3503
+ ] })
3504
+ ] }) }),
3505
+ /* @__PURE__ */ jsxs("footer", { className: "flex flex-wrap items-center justify-between gap-2 border-t border-gray-200 dark:border-gray-700 px-3 py-2", children: [
3506
+ /* @__PURE__ */ jsx("div", { className: "text-xs text-gray-500 dark:text-gray-400", children: t("BiChat.Table.Showing", {
3507
+ from: String(from),
3508
+ to: String(to),
3509
+ total: String(totalRows)
3510
+ }) }),
3511
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
3512
+ /* @__PURE__ */ jsx("label", { className: "text-xs text-gray-500 dark:text-gray-400", htmlFor: `${table.id}-page-size`, children: t("BiChat.Table.RowsLabel") }),
3513
+ /* @__PURE__ */ jsx(
3514
+ "select",
3515
+ {
3516
+ id: `${table.id}-page-size`,
3517
+ value: pageSize,
3518
+ onChange: (event) => {
3519
+ setPageSize(Number(event.target.value));
3520
+ setPage(1);
3521
+ },
3522
+ className: "rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-2 py-1 text-xs text-gray-700 dark:text-gray-200",
3523
+ children: pageSizeOptions.map((option) => /* @__PURE__ */ jsx("option", { value: option, children: option }, `${table.id}-size-${option}`))
3524
+ }
3525
+ ),
3526
+ /* @__PURE__ */ jsx(
3527
+ "button",
3528
+ {
3529
+ type: "button",
3530
+ onClick: () => setPage((current) => Math.max(1, current - 1)),
3531
+ disabled: page <= 1,
3532
+ className: "cursor-pointer rounded border border-gray-300 dark:border-gray-600 px-2 py-1 text-xs text-gray-700 dark:text-gray-200 disabled:cursor-not-allowed disabled:opacity-50",
3533
+ children: t("BiChat.Table.Prev")
3534
+ }
3535
+ ),
3536
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-500 dark:text-gray-400", children: t("BiChat.Table.PageOf", { page: String(page), total: String(totalPages) }) }),
3537
+ /* @__PURE__ */ jsx(
3538
+ "button",
3539
+ {
3540
+ type: "button",
3541
+ onClick: () => setPage((current) => Math.min(totalPages, current + 1)),
3542
+ disabled: page >= totalPages,
3543
+ className: "cursor-pointer rounded border border-gray-300 dark:border-gray-600 px-2 py-1 text-xs text-gray-700 dark:text-gray-200 disabled:cursor-not-allowed disabled:opacity-50",
3544
+ children: t("BiChat.Table.Next")
3545
+ }
3546
+ )
3198
3547
  ] })
3199
3548
  ] }),
3200
- /* @__PURE__ */ jsx(DisclosurePanel, { className: "mt-2 space-y-2", children: citations.map((citation, index) => /* @__PURE__ */ jsx(
3201
- "div",
3549
+ table.truncated && /* @__PURE__ */ jsx("p", { className: "border-t border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-700 dark:border-amber-700/60 dark:bg-amber-900/20 dark:text-amber-300", children: t("BiChat.Table.TruncatedNotice") })
3550
+ ] });
3551
+ });
3552
+
3553
+ // ui/src/bichat/components/SourcesPanel.tsx
3554
+ init_useTranslation();
3555
+ function extractDomain(url) {
3556
+ try {
3557
+ return new URL(url).hostname.replace(/^www\./, "");
3558
+ } catch {
3559
+ return "";
3560
+ }
3561
+ }
3562
+ var PALETTE = [
3563
+ "#c0392b",
3564
+ "#d35400",
3565
+ "#f39c12",
3566
+ "#27ae60",
3567
+ "#16a085",
3568
+ "#2980b9",
3569
+ "#8e44ad",
3570
+ "#d63384"
3571
+ ];
3572
+ function domainColor(domain) {
3573
+ let h = 0;
3574
+ for (let i = 0; i < domain.length; i++) h = domain.charCodeAt(i) + ((h << 5) - h);
3575
+ return PALETTE[Math.abs(h) % PALETTE.length];
3576
+ }
3577
+ function SourcesPanel({ citations }) {
3578
+ const { t } = useTranslation();
3579
+ const [isOpen, setIsOpen] = useState(false);
3580
+ const open = useCallback(() => setIsOpen(true), []);
3581
+ const close = useCallback(() => setIsOpen(false), []);
3582
+ if (!citations?.length) return null;
3583
+ const domains = [...new Set(
3584
+ citations.filter((c) => c.url).map((c) => extractDomain(c.url)).filter(Boolean)
3585
+ )];
3586
+ const previewDomains = domains.slice(0, 5);
3587
+ if (!isOpen) {
3588
+ return /* @__PURE__ */ jsx("div", { className: "mt-3", children: /* @__PURE__ */ jsxs(
3589
+ "button",
3202
3590
  {
3203
- className: "p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg text-sm",
3204
- children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
3205
- /* @__PURE__ */ 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 }),
3206
- /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
3207
- /* @__PURE__ */ jsx("div", { className: "font-medium text-gray-900 dark:text-gray-100", children: citation.title }),
3208
- citation.url && /* @__PURE__ */ jsx(
3209
- "a",
3210
- {
3211
- href: citation.url,
3212
- target: "_blank",
3213
- rel: "noopener noreferrer",
3214
- className: "text-[var(--bichat-primary)] hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50 rounded",
3215
- children: citation.url
3216
- }
3217
- ),
3218
- citation.excerpt && /* @__PURE__ */ jsxs("div", { className: "mt-1 text-gray-600 dark:text-gray-400 italic", children: [
3219
- '"',
3220
- citation.excerpt,
3221
- '"'
3222
- ] })
3591
+ type: "button",
3592
+ onClick: open,
3593
+ className: "cursor-pointer inline-flex items-center gap-2 rounded-full px-3 py-1.5\n bg-gray-50 hover:bg-gray-100 dark:bg-gray-700/50 dark:hover:bg-gray-600/60\n border border-gray-200/70 dark:border-gray-600/40\n transition-colors duration-150\n focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--bichat-primary,theme(colors.blue.500))]/40",
3594
+ children: [
3595
+ previewDomains.length > 0 && /* @__PURE__ */ jsx("span", { className: "flex -space-x-1.5", children: previewDomains.map((domain, i) => /* @__PURE__ */ jsx(
3596
+ "span",
3597
+ {
3598
+ className: "relative w-5 h-5 rounded-full flex items-center justify-center text-[8px] font-bold text-white\n ring-2 ring-white dark:ring-gray-800 select-none",
3599
+ style: { backgroundColor: domainColor(domain), zIndex: previewDomains.length - i },
3600
+ "aria-hidden": "true",
3601
+ children: domain[0]?.toUpperCase()
3602
+ },
3603
+ domain
3604
+ )) }),
3605
+ /* @__PURE__ */ jsxs("span", { className: "text-xs font-medium text-gray-600 dark:text-gray-300 tabular-nums", children: [
3606
+ citations.length,
3607
+ " ",
3608
+ t(citations.length === 1 ? "BiChat.Sources.Source" : "BiChat.Sources.Sources")
3223
3609
  ] })
3610
+ ]
3611
+ }
3612
+ ) });
3613
+ }
3614
+ return /* @__PURE__ */ jsxs("div", { className: "mt-3 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800/90 shadow-sm overflow-hidden", children: [
3615
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-3", children: [
3616
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: t("BiChat.Sources.Title") }),
3617
+ /* @__PURE__ */ jsx(
3618
+ "button",
3619
+ {
3620
+ type: "button",
3621
+ onClick: close,
3622
+ className: "cursor-pointer flex items-center justify-center w-7 h-7 rounded-full\n text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300\n hover:bg-gray-100 dark:hover:bg-gray-700\n transition-colors duration-150\n focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--bichat-primary)]/40",
3623
+ "aria-label": t("BiChat.Sources.Close"),
3624
+ children: /* @__PURE__ */ jsx(X, { size: 14, weight: "bold" })
3625
+ }
3626
+ )
3627
+ ] }),
3628
+ /* @__PURE__ */ jsx("div", { className: "max-h-80 overflow-y-auto", children: citations.map((citation, index) => {
3629
+ const domain = citation.url ? extractDomain(citation.url) : "";
3630
+ const cardContent = /* @__PURE__ */ jsxs(Fragment, { children: [
3631
+ /* @__PURE__ */ jsx("h4", { className: "text-sm font-medium leading-snug text-[var(--bichat-color-accent,theme(colors.blue.600))] dark:text-blue-400", children: citation.title || t("BiChat.Sources.SourceN", { n: String(index + 1) }) }),
3632
+ citation.excerpt && /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-xs text-gray-500 dark:text-gray-400 line-clamp-2 leading-relaxed", children: citation.excerpt }),
3633
+ domain && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 mt-1.5", children: [
3634
+ /* @__PURE__ */ jsx(
3635
+ "span",
3636
+ {
3637
+ className: "w-4 h-4 rounded-full flex items-center justify-center text-[7px] font-bold text-white flex-shrink-0 select-none",
3638
+ style: { backgroundColor: domainColor(domain) },
3639
+ "aria-hidden": "true",
3640
+ children: domain[0]?.toUpperCase()
3641
+ }
3642
+ ),
3643
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] text-gray-400 dark:text-gray-500 truncate", children: domain })
3224
3644
  ] })
3225
- },
3226
- citation.id
3227
- )) })
3228
- ] }) }) });
3645
+ ] });
3646
+ const cardClass = "block px-4 py-3 border-t border-gray-100 dark:border-gray-700/50";
3647
+ if (citation.url) {
3648
+ return /* @__PURE__ */ jsx(
3649
+ "a",
3650
+ {
3651
+ href: citation.url,
3652
+ target: "_blank",
3653
+ rel: "noopener noreferrer",
3654
+ className: `${cardClass} hover:bg-gray-50 dark:hover:bg-gray-700/40 transition-colors duration-100
3655
+ focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--bichat-primary)]/40`,
3656
+ children: cardContent
3657
+ },
3658
+ citation.id
3659
+ );
3660
+ }
3661
+ return /* @__PURE__ */ jsx("div", { className: cardClass, children: cardContent }, citation.id);
3662
+ }) })
3663
+ ] });
3229
3664
  }
3230
3665
  var MIME_BY_TYPE = {
3231
3666
  excel: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
@@ -3286,7 +3721,7 @@ function InlineQuestionForm({ pendingQuestion }) {
3286
3721
  const [currentStep, setCurrentStep] = useState(0);
3287
3722
  const [answers, setAnswers] = useState({});
3288
3723
  const [otherTexts, setOtherTexts] = useState({});
3289
- const questions = pendingQuestion.questions;
3724
+ const questions = Array.isArray(pendingQuestion.questions) ? pendingQuestion.questions : [];
3290
3725
  const currentQuestion = questions[currentStep];
3291
3726
  const isLastStep = currentStep === questions.length - 1;
3292
3727
  const isFirstStep = currentStep === 0;
@@ -3380,9 +3815,31 @@ function InlineQuestionForm({ pendingQuestion }) {
3380
3815
  e.preventDefault();
3381
3816
  handleNext();
3382
3817
  };
3383
- if (!currentQuestion) return null;
3818
+ if (!currentQuestion) {
3819
+ return /* @__PURE__ */ jsxs("div", { className: "animate-slide-up rounded-2xl border border-amber-200 dark:border-amber-700/50 bg-gradient-to-b from-amber-50/70 to-white dark:from-amber-950/20 dark:to-gray-900/80 shadow-sm overflow-hidden p-4", children: [
3820
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-gray-800 dark:text-gray-200", children: t("BiChat.Error.SomethingWentWrong") }),
3821
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500 dark:text-gray-400", children: t("BiChat.Error.UnexpectedError") }),
3822
+ /* @__PURE__ */ jsx("div", { className: "mt-3", children: /* @__PURE__ */ jsxs(
3823
+ "button",
3824
+ {
3825
+ type: "button",
3826
+ onClick: handleRejectPendingQuestion,
3827
+ disabled: loading,
3828
+ className: "cursor-pointer inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-40",
3829
+ children: [
3830
+ /* @__PURE__ */ jsx(X, { size: 14, weight: "bold" }),
3831
+ t("BiChat.InlineQuestion.Dismiss")
3832
+ ]
3833
+ }
3834
+ ) })
3835
+ ] });
3836
+ }
3384
3837
  const isMultiSelect = currentQuestion.type === "MULTIPLE_CHOICE";
3385
- const options = currentQuestion.options || [];
3838
+ const options = (currentQuestion.options || []).filter((option) => Boolean(option && typeof option.label === "string")).map((option, index) => ({
3839
+ id: option.id || `${currentQuestion.id}-option-${index}`,
3840
+ label: option.label,
3841
+ value: option.value || option.label
3842
+ }));
3386
3843
  const isOtherSelected = currentAnswer?.customText !== void 0;
3387
3844
  const canProceed = isCurrentAnswerValid();
3388
3845
  return /* @__PURE__ */ jsx("div", { className: "animate-slide-up rounded-2xl border border-gray-200 dark:border-gray-700/50 bg-gradient-to-b from-primary-50/80 to-white dark:from-primary-950/30 dark:to-gray-900/80 shadow-sm overflow-hidden", children: /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, children: [
@@ -3555,6 +4012,59 @@ function InlineQuestionForm({ pendingQuestion }) {
3555
4012
  ] }) });
3556
4013
  }
3557
4014
 
4015
+ // ui/src/bichat/components/RetryActionArea.tsx
4016
+ init_useTranslation();
4017
+ var RetryActionArea = memo(function RetryActionArea2({
4018
+ onRetry
4019
+ }) {
4020
+ const { t } = useTranslation();
4021
+ return (
4022
+ // Wrapper matches TurnBubble layout for assistant messages (justify-start = left-aligned)
4023
+ /* @__PURE__ */ jsx(
4024
+ motion.div,
4025
+ {
4026
+ initial: { opacity: 0, y: 10 },
4027
+ animate: { opacity: 1, y: 0 },
4028
+ exit: { opacity: 0, y: -10 },
4029
+ transition: { duration: 0.2 },
4030
+ className: "flex justify-start",
4031
+ children: /* @__PURE__ */ jsxs(
4032
+ "div",
4033
+ {
4034
+ className: "flex flex-col gap-3 max-w-2xl rounded-2xl px-5 py-3 shadow-sm bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700",
4035
+ role: "status",
4036
+ "aria-live": "polite",
4037
+ children: [
4038
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
4039
+ /* @__PURE__ */ jsx(
4040
+ Warning,
4041
+ {
4042
+ className: "w-5 h-5 text-amber-500 dark:text-amber-400 flex-shrink-0",
4043
+ weight: "fill"
4044
+ }
4045
+ ),
4046
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-gray-700 dark:text-gray-300", children: t("BiChat.Retry.Subtitle") })
4047
+ ] }),
4048
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsxs(
4049
+ "button",
4050
+ {
4051
+ onClick: onRetry,
4052
+ className: "cursor-pointer inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 dark:bg-primary-700 dark:hover:bg-primary-600 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-gray-800",
4053
+ "aria-label": t("BiChat.Retry.Title"),
4054
+ children: [
4055
+ /* @__PURE__ */ jsx(ArrowClockwise, { size: 16, className: "w-4 h-4" }),
4056
+ t("BiChat.Retry.Button")
4057
+ ]
4058
+ }
4059
+ ) })
4060
+ ]
4061
+ }
4062
+ )
4063
+ }
4064
+ )
4065
+ );
4066
+ });
4067
+
3558
4068
  // ui/src/bichat/utils/debugMetrics.ts
3559
4069
  function formatGenerationDuration(generationMs) {
3560
4070
  return generationMs > 1e3 ? `${(generationMs / 1e3).toFixed(2)}s` : `${generationMs}ms`;
@@ -3853,6 +4363,7 @@ var defaultClassNames2 = {
3853
4363
  bubble: "bg-white dark:bg-gray-800 rounded-2xl rounded-bl-sm px-4 py-3 shadow-sm",
3854
4364
  codeOutputs: "",
3855
4365
  charts: "mb-1 w-full",
4366
+ tables: "mb-1 flex flex-col gap-3",
3856
4367
  artifacts: "mb-1 flex flex-wrap gap-2",
3857
4368
  sources: "",
3858
4369
  explanation: "mt-4 border-t border-gray-100 dark:border-gray-700 pt-4",
@@ -3869,6 +4380,7 @@ function mergeClassNames2(defaults, overrides) {
3869
4380
  bubble: overrides.bubble ?? defaults.bubble,
3870
4381
  codeOutputs: overrides.codeOutputs ?? defaults.codeOutputs,
3871
4382
  charts: overrides.charts ?? defaults.charts,
4383
+ tables: overrides.tables ?? defaults.tables,
3872
4384
  artifacts: overrides.artifacts ?? defaults.artifacts,
3873
4385
  sources: overrides.sources ?? defaults.sources,
3874
4386
  explanation: overrides.explanation ?? defaults.explanation,
@@ -3913,6 +4425,14 @@ function AssistantMessage({
3913
4425
  const hasContent = turn.content?.trim().length > 0;
3914
4426
  const hasExplanation = !!turn.explanation?.trim();
3915
4427
  const hasPendingQuestion = !!pendingQuestion && pendingQuestion.status === "PENDING" && pendingQuestion.turnId === turnId;
4428
+ const hasCodeOutputs = !!turn.codeOutputs?.length;
4429
+ const hasChart = !!turn.chartData;
4430
+ const hasTables = !!turn.renderTables?.length;
4431
+ const hasArtifacts = !!turn.artifacts?.length;
4432
+ const hasDebug = showDebug && !!turn.debug;
4433
+ const hasRenderablePayload = hasContent || hasExplanation || hasPendingQuestion || hasCodeOutputs || hasChart || hasTables || hasArtifacts || hasDebug;
4434
+ const canRegenerate = !!onRegenerate && !!turnId && !isSystemMessage && isLastTurn;
4435
+ const showInlineRetry = !hasRenderablePayload && canRegenerate;
3916
4436
  const handleCopyClick = useCallback(async () => {
3917
4437
  try {
3918
4438
  if (onCopy) {
@@ -3954,15 +4474,18 @@ function AssistantMessage({
3954
4474
  const codeOutputsSlotProps = {
3955
4475
  outputs: turn.codeOutputs || []
3956
4476
  };
4477
+ const tablesSlotProps = {
4478
+ tables: turn.renderTables || []
4479
+ };
3957
4480
  const artifactsSlotProps = {
3958
4481
  artifacts: turn.artifacts || []
3959
4482
  };
3960
4483
  const actionsSlotProps = {
3961
4484
  onCopy: handleCopyClick,
3962
- onRegenerate: onRegenerate && turnId && !isSystemMessage && isLastTurn ? handleRegenerateClick : void 0,
4485
+ onRegenerate: canRegenerate ? handleRegenerateClick : void 0,
3963
4486
  timestamp,
3964
4487
  canCopy: hasContent,
3965
- canRegenerate: !!onRegenerate && !!turnId && !isSystemMessage && isLastTurn
4488
+ canRegenerate
3966
4489
  };
3967
4490
  const explanationSlotProps = {
3968
4491
  explanation: turn.explanation || "",
@@ -3975,14 +4498,30 @@ function AssistantMessage({
3975
4498
  return slot;
3976
4499
  };
3977
4500
  return /* @__PURE__ */ jsxs("div", { className: classes.root, children: [
3978
- !hideAvatar && /* @__PURE__ */ jsx("div", { className: avatarClassName, children: renderSlot(slots?.avatar, avatarSlotProps, isSystemMessage ? "SYS" : "AI") }),
4501
+ !hideAvatar && !showInlineRetry && /* @__PURE__ */ jsx("div", { className: avatarClassName, children: renderSlot(slots?.avatar, avatarSlotProps, isSystemMessage ? "SYS" : "AI") }),
3979
4502
  /* @__PURE__ */ jsxs("div", { className: classes.wrapper, children: [
4503
+ showInlineRetry && /* @__PURE__ */ jsx(RetryActionArea, { onRetry: () => {
4504
+ void handleRegenerateClick();
4505
+ } }),
3980
4506
  turn.codeOutputs && turn.codeOutputs.length > 0 && /* @__PURE__ */ jsx("div", { className: classes.codeOutputs, children: renderSlot(
3981
4507
  slots?.codeOutputs,
3982
4508
  codeOutputsSlotProps,
3983
4509
  /* @__PURE__ */ jsx(CodeOutputsPanel_default, { outputs: turn.codeOutputs })
3984
4510
  ) }),
3985
4511
  turn.chartData && /* @__PURE__ */ jsx("div", { className: classes.charts, children: renderSlot(slots?.charts, chartsSlotProps, /* @__PURE__ */ jsx(ChartCard, { chartData: turn.chartData })) }),
4512
+ turn.renderTables && turn.renderTables.length > 0 && /* @__PURE__ */ jsx("div", { className: classes.tables, children: renderSlot(
4513
+ slots?.tables,
4514
+ tablesSlotProps,
4515
+ turn.renderTables.map((table) => /* @__PURE__ */ jsx(
4516
+ InteractiveTableCard,
4517
+ {
4518
+ table,
4519
+ onSendMessage,
4520
+ sendDisabled: sendDisabled || isStreaming
4521
+ },
4522
+ table.id
4523
+ ))
4524
+ ) }),
3986
4525
  hasContent && /* @__PURE__ */ jsxs("div", { className: bubbleClassName, children: [
3987
4526
  renderSlot(
3988
4527
  slots?.content,
@@ -4072,7 +4611,7 @@ function AssistantMessage({
4072
4611
  children: isCopied ? /* @__PURE__ */ jsx(Check, { size: 14, weight: "bold" }) : /* @__PURE__ */ jsx(Copy, { size: 14, weight: "regular" })
4073
4612
  }
4074
4613
  ),
4075
- onRegenerate && turnId && !isSystemMessage && isLastTurn && /* @__PURE__ */ jsx(
4614
+ canRegenerate && /* @__PURE__ */ jsx(
4076
4615
  "button",
4077
4616
  {
4078
4617
  onClick: handleRegenerateClick,
@@ -4296,7 +4835,8 @@ function TurnBubble({
4296
4835
  userTurn: classNames?.userTurn ?? defaultClassNames3.userTurn,
4297
4836
  assistantTurn: classNames?.assistantTurn ?? defaultClassNames3.assistantTurn
4298
4837
  };
4299
- const isSystemSummaryTurn = turn.userTurn.content.trim() === "" && turn.assistantTurn?.role === "system";
4838
+ const userContent = typeof turn.userTurn?.content === "string" ? turn.userTurn.content : "";
4839
+ const isSystemSummaryTurn = userContent.trim() === "" && turn.assistantTurn?.role === "system";
4300
4840
  return /* @__PURE__ */ jsxs("div", { className: classes.root, "data-turn-id": turn.id, children: [
4301
4841
  !isSystemSummaryTurn && /* @__PURE__ */ jsx("div", { className: classes.userTurn, children: renderUserTurn ? renderUserTurn(turn) : /* @__PURE__ */ jsx(
4302
4842
  UserTurnView,
@@ -8074,12 +8614,63 @@ function DefaultErrorContent({
8074
8614
  ] })
8075
8615
  ] });
8076
8616
  }
8617
+ function StaticEmergencyErrorContent({
8618
+ error,
8619
+ onReset
8620
+ }) {
8621
+ return /* @__PURE__ */ jsx("div", { className: "flex flex-col items-center justify-center p-8 text-center min-h-[200px]", children: /* @__PURE__ */ jsxs("div", { className: "relative flex flex-col items-center", children: [
8622
+ /* @__PURE__ */ jsxs("div", { className: "relative mb-5", children: [
8623
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-0 rounded-full bg-red-100 scale-150 blur-md" }),
8624
+ /* @__PURE__ */ jsx("div", { className: "relative flex items-center justify-center w-14 h-14 rounded-full bg-red-50 border border-red-200/60", children: /* @__PURE__ */ jsx(WarningCircle, { size: 28, className: "text-red-500", weight: "fill" }) })
8625
+ ] }),
8626
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold text-gray-900 mb-1.5", children: "Something went wrong" }),
8627
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500 mb-5 max-w-md leading-relaxed", children: error?.message || "An unexpected UI error occurred." }),
8628
+ onReset && /* @__PURE__ */ jsxs(
8629
+ "button",
8630
+ {
8631
+ type: "button",
8632
+ onClick: onReset,
8633
+ className: "flex items-center gap-2 px-5 py-2.5 bg-red-600 hover:bg-red-700 active:bg-red-800 text-white rounded-lg transition-colors shadow-sm text-sm font-medium",
8634
+ children: [
8635
+ /* @__PURE__ */ jsx(ArrowClockwise, { size: 16, weight: "bold" }),
8636
+ "Try again"
8637
+ ]
8638
+ }
8639
+ )
8640
+ ] }) });
8641
+ }
8642
+ var FallbackGuard = class extends Component {
8643
+ constructor(props) {
8644
+ super(props);
8645
+ this.state = { fallbackFailed: false };
8646
+ }
8647
+ static getDerivedStateFromError() {
8648
+ return { fallbackFailed: true };
8649
+ }
8650
+ componentDidCatch(error, errorInfo) {
8651
+ this.props.onFallbackError?.(error, errorInfo);
8652
+ }
8653
+ render() {
8654
+ if (this.state.fallbackFailed) {
8655
+ return /* @__PURE__ */ jsx(StaticEmergencyErrorContent, { error: this.props.primaryError, onReset: this.props.onReset });
8656
+ }
8657
+ return this.props.renderFallback();
8658
+ }
8659
+ };
8077
8660
  var ErrorBoundary = class extends Component {
8078
8661
  constructor(props) {
8079
8662
  super(props);
8080
8663
  this.handleReset = () => {
8081
8664
  this.setState({ hasError: false, error: null });
8082
8665
  };
8666
+ this.handleFallbackError = (error, errorInfo) => {
8667
+ console.error("React Error Boundary fallback crashed:", {
8668
+ primaryError: this.state.error,
8669
+ fallbackError: error,
8670
+ errorInfo
8671
+ });
8672
+ this.props.onError?.(error, errorInfo);
8673
+ };
8083
8674
  this.state = { hasError: false, error: null };
8084
8675
  }
8085
8676
  static getDerivedStateFromError(error) {
@@ -8091,13 +8682,24 @@ var ErrorBoundary = class extends Component {
8091
8682
  }
8092
8683
  render() {
8093
8684
  if (this.state.hasError) {
8094
- if (this.props.fallback) {
8095
- if (typeof this.props.fallback === "function") {
8096
- return this.props.fallback(this.state.error, this.handleReset);
8097
- }
8098
- return this.props.fallback;
8099
- }
8100
- return /* @__PURE__ */ jsx(DefaultErrorContent, { error: this.state.error, onReset: this.handleReset });
8685
+ return /* @__PURE__ */ jsx(
8686
+ FallbackGuard,
8687
+ {
8688
+ primaryError: this.state.error,
8689
+ onReset: this.handleReset,
8690
+ onFallbackError: this.handleFallbackError,
8691
+ renderFallback: () => {
8692
+ if (this.props.fallback) {
8693
+ if (typeof this.props.fallback === "function") {
8694
+ return this.props.fallback(this.state.error, this.handleReset);
8695
+ }
8696
+ return this.props.fallback;
8697
+ }
8698
+ return /* @__PURE__ */ jsx(DefaultErrorContent, { error: this.state.error, onReset: this.handleReset });
8699
+ }
8700
+ },
8701
+ `${this.state.error?.name ?? "Error"}:${this.state.error?.message ?? ""}`
8702
+ );
8101
8703
  }
8102
8704
  return this.props.children;
8103
8705
  }
@@ -9131,6 +9733,10 @@ function ErrorAlert({ error }) {
9131
9733
  );
9132
9734
  }
9133
9735
  var COLLAPSE_STORAGE_KEY = "bichat-sidebar-collapsed";
9736
+ var SESSION_RECONCILE_POLL_INTERVAL_MS = 2e3;
9737
+ var SESSION_RECONCILE_MAX_POLLS = 30;
9738
+ var ACTIVE_SESSION_MISS_MAX_RETRIES = 8;
9739
+ var ACTIVE_SESSION_MISS_RETRY_DELAY_MS = 1e3;
9134
9740
  function useSidebarCollapse() {
9135
9741
  const [isCollapsed, setIsCollapsed] = useState(() => {
9136
9742
  try {
@@ -9188,7 +9794,7 @@ function Sidebar2({
9188
9794
  const shouldReduceMotion = useReducedMotion();
9189
9795
  const sessionListRef = useRef(null);
9190
9796
  const searchContainerRef = useRef(null);
9191
- const refreshForActiveSessionRef = useRef(null);
9797
+ const activeSessionMissRetriesRef = useRef({});
9192
9798
  const { isCollapsed, toggle, collapse } = useSidebarCollapse();
9193
9799
  const collapsible = !onClose;
9194
9800
  const handleSidebarClick = useCallback(
@@ -9232,6 +9838,7 @@ function Sidebar2({
9232
9838
  const [actionError, setActionError] = useState(null);
9233
9839
  const accessDenied = loadError?.isPermissionDenied === true;
9234
9840
  const [refreshKey, setRefreshKey] = useState(0);
9841
+ const [reconcilePollToken, setReconcilePollToken] = useState(0);
9235
9842
  const [showConfirm, setShowConfirm] = useState(false);
9236
9843
  const [sessionToArchive, setSessionToArchive] = useState(null);
9237
9844
  const fetchSessions = useCallback(async () => {
@@ -9252,8 +9859,13 @@ function Sidebar2({
9252
9859
  fetchSessions();
9253
9860
  }, [fetchSessions, refreshKey]);
9254
9861
  useEffect(() => {
9255
- const handleSessionsUpdated = () => {
9862
+ const handleSessionsUpdated = (event) => {
9256
9863
  setRefreshKey((k) => k + 1);
9864
+ const detail = event.detail;
9865
+ const reason = detail?.reason;
9866
+ if (!reason || reason === "session_created" || reason === "message_sent" || reason === "title_regenerate_requested") {
9867
+ setReconcilePollToken((k) => k + 1);
9868
+ }
9257
9869
  };
9258
9870
  window.addEventListener("bichat:sessions-updated", handleSessionsUpdated);
9259
9871
  return () => {
@@ -9261,31 +9873,33 @@ function Sidebar2({
9261
9873
  };
9262
9874
  }, []);
9263
9875
  useEffect(() => {
9264
- if (!activeSessionId) {
9265
- refreshForActiveSessionRef.current = null;
9266
- return;
9267
- }
9876
+ activeSessionMissRetriesRef.current = {};
9877
+ }, [activeSessionId]);
9878
+ useEffect(() => {
9879
+ if (!activeSessionId) return;
9268
9880
  if (loading) return;
9269
9881
  const hasActiveSession = sessions.some((session) => session.id === activeSessionId);
9270
9882
  if (hasActiveSession) {
9271
- if (refreshForActiveSessionRef.current === activeSessionId) {
9272
- refreshForActiveSessionRef.current = null;
9273
- }
9883
+ delete activeSessionMissRetriesRef.current[activeSessionId];
9274
9884
  return;
9275
9885
  }
9276
- if (refreshForActiveSessionRef.current !== activeSessionId) {
9277
- refreshForActiveSessionRef.current = activeSessionId;
9278
- setRefreshKey((k) => k + 1);
9886
+ const attempts = activeSessionMissRetriesRef.current[activeSessionId] ?? 0;
9887
+ if (attempts >= ACTIVE_SESSION_MISS_MAX_RETRIES) {
9888
+ return;
9279
9889
  }
9890
+ activeSessionMissRetriesRef.current[activeSessionId] = attempts + 1;
9891
+ const timeoutId = window.setTimeout(() => {
9892
+ setRefreshKey((k) => k + 1);
9893
+ setReconcilePollToken((k) => k + 1);
9894
+ }, ACTIVE_SESSION_MISS_RETRY_DELAY_MS);
9895
+ return () => window.clearTimeout(timeoutId);
9280
9896
  }, [activeSessionId, loading, sessions]);
9281
9897
  const hasPlaceholderTitles = useMemo(() => {
9282
9898
  const newChatLabel = t("BiChat.Chat.NewChat");
9283
9899
  return Array.isArray(sessions) && sessions.some((s) => s && (!s.title || s.title === newChatLabel));
9284
9900
  }, [sessions, t]);
9285
9901
  useEffect(() => {
9286
- if (!hasPlaceholderTitles) return;
9287
- const pollInterval = 2e3;
9288
- const maxPolls = 5;
9902
+ if (!hasPlaceholderTitles && reconcilePollToken === 0) return;
9289
9903
  let pollCount = 0;
9290
9904
  const intervalId = setInterval(async () => {
9291
9905
  pollCount++;
@@ -9294,12 +9908,12 @@ function Sidebar2({
9294
9908
  setSessions(result.sessions);
9295
9909
  } catch {
9296
9910
  }
9297
- if (pollCount >= maxPolls) {
9911
+ if (pollCount >= SESSION_RECONCILE_MAX_POLLS) {
9298
9912
  clearInterval(intervalId);
9299
9913
  }
9300
- }, pollInterval);
9914
+ }, SESSION_RECONCILE_POLL_INTERVAL_MS);
9301
9915
  return () => clearInterval(intervalId);
9302
- }, [hasPlaceholderTitles, dataSource]);
9916
+ }, [hasPlaceholderTitles, dataSource, reconcilePollToken]);
9303
9917
  const handleArchiveRequest = (sessionId) => {
9304
9918
  setSessionToArchive(sessionId);
9305
9919
  setShowConfirm(true);
@@ -9357,7 +9971,9 @@ function Sidebar2({
9357
9971
  try {
9358
9972
  await dataSource.regenerateSessionTitle(sessionId);
9359
9973
  toast.success(t("BiChat.Sidebar.TitleRegenerated"));
9360
- setRefreshKey((k) => k + 1);
9974
+ window.dispatchEvent(new CustomEvent("bichat:sessions-updated", {
9975
+ detail: { reason: "title_regenerate_requested", sessionId }
9976
+ }));
9361
9977
  } catch (err) {
9362
9978
  console.error("Failed to regenerate title:", err);
9363
9979
  const display = toErrorDisplay(err, t("BiChat.Sidebar.FailedToRegenerateTitle"));
@@ -10170,59 +10786,6 @@ function BiChatLayout({
10170
10786
  ] })
10171
10787
  ] });
10172
10788
  }
10173
-
10174
- // ui/src/bichat/components/RetryActionArea.tsx
10175
- init_useTranslation();
10176
- var RetryActionArea = memo(function RetryActionArea2({
10177
- onRetry
10178
- }) {
10179
- const { t } = useTranslation();
10180
- return (
10181
- // Wrapper matches TurnBubble layout for assistant messages (justify-start = left-aligned)
10182
- /* @__PURE__ */ jsx(
10183
- motion.div,
10184
- {
10185
- initial: { opacity: 0, y: 10 },
10186
- animate: { opacity: 1, y: 0 },
10187
- exit: { opacity: 0, y: -10 },
10188
- transition: { duration: 0.2 },
10189
- className: "flex justify-start",
10190
- children: /* @__PURE__ */ jsxs(
10191
- "div",
10192
- {
10193
- className: "flex flex-col gap-3 max-w-2xl rounded-2xl px-5 py-3 shadow-sm bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700",
10194
- role: "status",
10195
- "aria-live": "polite",
10196
- children: [
10197
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
10198
- /* @__PURE__ */ jsx(
10199
- Warning,
10200
- {
10201
- className: "w-5 h-5 text-amber-500 dark:text-amber-400 flex-shrink-0",
10202
- weight: "fill"
10203
- }
10204
- ),
10205
- /* @__PURE__ */ jsx("span", { className: "text-sm text-gray-700 dark:text-gray-300", children: t("BiChat.Retry.Subtitle") })
10206
- ] }),
10207
- /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsxs(
10208
- "button",
10209
- {
10210
- onClick: onRetry,
10211
- className: "cursor-pointer inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 dark:bg-primary-700 dark:hover:bg-primary-600 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-gray-800",
10212
- "aria-label": t("BiChat.Retry.Title"),
10213
- children: [
10214
- /* @__PURE__ */ jsx(ArrowClockwise, { size: 16, className: "w-4 h-4" }),
10215
- t("BiChat.Retry.Button")
10216
- ]
10217
- }
10218
- ) })
10219
- ]
10220
- }
10221
- )
10222
- }
10223
- )
10224
- );
10225
- });
10226
10789
  init_useTranslation();
10227
10790
  function MessageActions({
10228
10791
  message,
@@ -12040,6 +12603,75 @@ function toStreamEvent(chunk) {
12040
12603
  }
12041
12604
  }
12042
12605
 
12606
+ // ui/src/bichat/utils/tableSpec.ts
12607
+ function isRecord2(value) {
12608
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12609
+ }
12610
+ function readString(value) {
12611
+ if (typeof value !== "string") return null;
12612
+ const trimmed = value.trim();
12613
+ return trimmed.length > 0 ? trimmed : null;
12614
+ }
12615
+ function readPositiveInteger(value) {
12616
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
12617
+ const n = Math.floor(value);
12618
+ return n > 0 ? n : null;
12619
+ }
12620
+ function normalizeRows(value) {
12621
+ if (!Array.isArray(value)) return [];
12622
+ const rows = [];
12623
+ for (const row of value) {
12624
+ if (!Array.isArray(row)) continue;
12625
+ rows.push(row);
12626
+ }
12627
+ return rows;
12628
+ }
12629
+ function parseExport(value) {
12630
+ if (!isRecord2(value)) return void 0;
12631
+ const url = readString(value.url);
12632
+ if (!url) return void 0;
12633
+ return {
12634
+ url,
12635
+ filename: readString(value.filename) || "table_export.xlsx",
12636
+ rowCount: readPositiveInteger(value.row_count) || readPositiveInteger(value.rowCount) || void 0,
12637
+ fileSizeKB: readPositiveInteger(value.file_size_kb) || readPositiveInteger(value.fileSizeKB) || void 0
12638
+ };
12639
+ }
12640
+ function parseRenderTableDataFromJsonString(json, fallbackId) {
12641
+ const trimmed = json.trim();
12642
+ if (!trimmed) return null;
12643
+ let parsed;
12644
+ try {
12645
+ parsed = JSON.parse(trimmed);
12646
+ } catch {
12647
+ return null;
12648
+ }
12649
+ if (!isRecord2(parsed)) return null;
12650
+ const columns = Array.isArray(parsed.columns) ? parsed.columns.map((column) => readString(column)).filter((column) => column !== null) : [];
12651
+ if (columns.length === 0) return null;
12652
+ const rows = normalizeRows(parsed.rows);
12653
+ const headersRaw = Array.isArray(parsed.headers) ? parsed.headers.map((header) => readString(header)).filter((header) => header !== null) : [];
12654
+ const headers = headersRaw.length === columns.length ? headersRaw : columns;
12655
+ const totalRows = readPositiveInteger(parsed.total_rows) || readPositiveInteger(parsed.totalRows) || rows.length;
12656
+ const pageSize = readPositiveInteger(parsed.page_size) || readPositiveInteger(parsed.pageSize) || 25;
12657
+ const query = readString(parsed.query) || readString(parsed.sql);
12658
+ if (!query) return null;
12659
+ return {
12660
+ id: readString(parsed.id) || fallbackId,
12661
+ title: readString(parsed.title) || void 0,
12662
+ query,
12663
+ columns,
12664
+ headers,
12665
+ rows,
12666
+ totalRows,
12667
+ pageSize,
12668
+ truncated: parsed.truncated === true,
12669
+ truncatedReason: readString(parsed.truncated_reason) || readString(parsed.truncatedReason) || void 0,
12670
+ export: parseExport(parsed.export),
12671
+ exportPrompt: readString(parsed.export_prompt) || readString(parsed.exportPrompt) || void 0
12672
+ };
12673
+ }
12674
+
12043
12675
  // ui/src/bichat/data/HttpDataSource.ts
12044
12676
  function isSessionNotFoundError(err) {
12045
12677
  if (!(err instanceof AppletRPCException)) return false;
@@ -12056,6 +12688,7 @@ function toSessionArtifact(artifact) {
12056
12688
  id: artifact.id,
12057
12689
  sessionId: artifact.sessionId,
12058
12690
  messageId: artifact.messageId,
12691
+ uploadId: artifact.uploadId,
12059
12692
  type: artifact.type,
12060
12693
  name: artifact.name,
12061
12694
  description: artifact.description,
@@ -12066,21 +12699,313 @@ function toSessionArtifact(artifact) {
12066
12699
  createdAt: artifact.createdAt
12067
12700
  };
12068
12701
  }
12069
- function toPendingQuestion(rpc) {
12070
- if (!rpc) return null;
12071
- const questions = (rpc.questions || []).map((q) => ({
12072
- id: q.id,
12073
- text: q.text,
12074
- type: q.type,
12075
- options: (q.options || []).map((o) => ({
12076
- id: o.id,
12077
- label: o.label,
12078
- value: o.label
12079
- }))
12080
- }));
12702
+ function warnMalformedSessionPayload(message, details) {
12703
+ console.warn(`[BiChat] ${message}`, details || {});
12704
+ }
12705
+ function readString2(value, fallback = "") {
12706
+ return typeof value === "string" ? value : fallback;
12707
+ }
12708
+ function readNonEmptyString(value) {
12709
+ if (typeof value !== "string") return null;
12710
+ const trimmed = value.trim();
12711
+ return trimmed.length > 0 ? trimmed : null;
12712
+ }
12713
+ function readFiniteNumber(value, fallback = 0) {
12714
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
12715
+ }
12716
+ function readOptionalFiniteNumber(value) {
12717
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
12718
+ }
12719
+ var MIME_TO_EXTENSION = {
12720
+ "image/jpeg": "jpg",
12721
+ "image/png": "png",
12722
+ "image/gif": "gif",
12723
+ "application/pdf": "pdf"
12724
+ };
12725
+ var SAFE_AUTOCORRECT_MIME_TYPES = new Set(Object.keys(MIME_TO_EXTENSION));
12726
+ function detectMimeFromSignature(bytes) {
12727
+ if (bytes.length >= 8) {
12728
+ const isPng = bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71 && bytes[4] === 13 && bytes[5] === 10 && bytes[6] === 26 && bytes[7] === 10;
12729
+ if (isPng) return "image/png";
12730
+ }
12731
+ if (bytes.length >= 3) {
12732
+ const isJpeg = bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255;
12733
+ if (isJpeg) return "image/jpeg";
12734
+ }
12735
+ if (bytes.length >= 6) {
12736
+ const isGif = bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56 && (bytes[4] === 55 || bytes[4] === 57) && bytes[5] === 97;
12737
+ if (isGif) return "image/gif";
12738
+ }
12739
+ if (bytes.length >= 4) {
12740
+ const isPdf = bytes[0] === 37 && bytes[1] === 80 && bytes[2] === 68 && bytes[3] === 70;
12741
+ if (isPdf) return "application/pdf";
12742
+ }
12743
+ return void 0;
12744
+ }
12745
+ function normalizeFilenameForMime(filename, mimeType) {
12746
+ const expectedExt = MIME_TO_EXTENSION[mimeType];
12747
+ if (!expectedExt) return filename;
12748
+ const lower = filename.toLowerCase();
12749
+ if (mimeType === "image/jpeg" && (lower.endsWith(".jpg") || lower.endsWith(".jpeg"))) {
12750
+ return filename;
12751
+ }
12752
+ if (lower.endsWith(`.${expectedExt}`)) {
12753
+ return filename;
12754
+ }
12755
+ const dotIndex = filename.lastIndexOf(".");
12756
+ const baseName = dotIndex > 0 ? filename.slice(0, dotIndex) : filename;
12757
+ return `${baseName}.${expectedExt}`;
12758
+ }
12759
+ function normalizeQuestionType(rawType) {
12760
+ const normalized = readString2(rawType).trim().toUpperCase().replace(/[\s-]+/g, "_");
12761
+ return normalized === "MULTIPLE_CHOICE" ? "MULTIPLE_CHOICE" : "SINGLE_CHOICE";
12762
+ }
12763
+ function normalizeMessageRole(rawRole) {
12764
+ const normalized = readString2(rawRole).trim().toLowerCase();
12765
+ if (normalized === "user" /* User */) return "user" /* User */;
12766
+ if (normalized === "system" /* System */) return "system" /* System */;
12767
+ if (normalized === "tool" /* Tool */) return "tool" /* Tool */;
12768
+ return "assistant" /* Assistant */;
12769
+ }
12770
+ function sanitizeAttachment(rawAttachment, turnId, index) {
12771
+ if (!isRecord(rawAttachment)) {
12772
+ warnMalformedSessionPayload("Dropped malformed attachment entry", { turnId, index });
12773
+ return null;
12774
+ }
12775
+ const filename = readString2(rawAttachment.filename, "attachment");
12776
+ const mimeType = readString2(rawAttachment.mimeType, "application/octet-stream");
12777
+ const id = readNonEmptyString(rawAttachment.id) || void 0;
12778
+ const clientKey = readNonEmptyString(rawAttachment.clientKey) || id || `${turnId}-attachment-${index}`;
12779
+ return {
12780
+ id,
12781
+ clientKey,
12782
+ filename,
12783
+ mimeType,
12784
+ sizeBytes: readFiniteNumber(rawAttachment.sizeBytes),
12785
+ uploadId: readOptionalFiniteNumber(rawAttachment.uploadId),
12786
+ base64Data: readNonEmptyString(rawAttachment.base64Data) || void 0,
12787
+ url: readNonEmptyString(rawAttachment.url) || void 0,
12788
+ preview: readNonEmptyString(rawAttachment.preview) || void 0
12789
+ };
12790
+ }
12791
+ function sanitizeUserAttachments(rawAttachments, turnId) {
12792
+ if (!Array.isArray(rawAttachments)) return [];
12793
+ const result = [];
12794
+ for (let i = 0; i < rawAttachments.length; i++) {
12795
+ const sanitized = sanitizeAttachment(rawAttachments[i], turnId, i);
12796
+ if (sanitized) result.push(sanitized);
12797
+ }
12798
+ return result;
12799
+ }
12800
+ function sanitizeAssistantArtifacts(rawArtifacts, turnId) {
12801
+ if (!Array.isArray(rawArtifacts)) return [];
12802
+ const artifacts = [];
12803
+ for (let i = 0; i < rawArtifacts.length; i++) {
12804
+ const raw = rawArtifacts[i];
12805
+ if (!isRecord(raw)) {
12806
+ warnMalformedSessionPayload("Dropped malformed assistant artifact", { turnId, index: i });
12807
+ continue;
12808
+ }
12809
+ const type = readString2(raw.type).toLowerCase();
12810
+ if (type !== "excel" && type !== "pdf") {
12811
+ continue;
12812
+ }
12813
+ const url = readNonEmptyString(raw.url);
12814
+ if (!url) {
12815
+ warnMalformedSessionPayload("Dropped assistant artifact without url", { turnId, index: i });
12816
+ continue;
12817
+ }
12818
+ artifacts.push({
12819
+ type,
12820
+ filename: readString2(raw.filename, "download"),
12821
+ url,
12822
+ sizeReadable: readNonEmptyString(raw.sizeReadable) || void 0,
12823
+ rowCount: typeof raw.rowCount === "number" && Number.isFinite(raw.rowCount) ? raw.rowCount : void 0,
12824
+ description: readNonEmptyString(raw.description) || void 0
12825
+ });
12826
+ }
12827
+ return artifacts;
12828
+ }
12829
+ function sanitizeAssistantTurn(rawAssistantTurn, fallbackCreatedAt, turnId) {
12830
+ if (rawAssistantTurn == null) return void 0;
12831
+ if (!isRecord(rawAssistantTurn)) {
12832
+ warnMalformedSessionPayload("Dropped malformed assistant turn payload", { turnId });
12833
+ return void 0;
12834
+ }
12835
+ const assistantID = readNonEmptyString(rawAssistantTurn.id);
12836
+ if (!assistantID) {
12837
+ warnMalformedSessionPayload("Dropped assistant turn without id", { turnId });
12838
+ return void 0;
12839
+ }
12840
+ const citations = Array.isArray(rawAssistantTurn.citations) ? rawAssistantTurn.citations.filter((item) => isRecord(item)).map((item, index) => ({
12841
+ id: readString2(item.id, `${assistantID}-citation-${index}`),
12842
+ type: readString2(item.type),
12843
+ title: readString2(item.title),
12844
+ url: readString2(item.url),
12845
+ startIndex: readFiniteNumber(item.startIndex),
12846
+ endIndex: readFiniteNumber(item.endIndex),
12847
+ excerpt: readNonEmptyString(item.excerpt) || void 0
12848
+ })) : [];
12849
+ const toolCalls = Array.isArray(rawAssistantTurn.toolCalls) ? rawAssistantTurn.toolCalls.filter((item) => isRecord(item)).map((item, index) => ({
12850
+ id: readString2(item.id, `${assistantID}-tool-${index}`),
12851
+ name: readString2(item.name),
12852
+ arguments: readString2(item.arguments),
12853
+ result: readNonEmptyString(item.result) || void 0,
12854
+ error: readNonEmptyString(item.error) || void 0,
12855
+ durationMs: readFiniteNumber(item.durationMs)
12856
+ })) : [];
12857
+ const codeOutputs = Array.isArray(rawAssistantTurn.codeOutputs) ? rawAssistantTurn.codeOutputs.filter((item) => isRecord(item)).map((item) => ({
12858
+ type: (() => {
12859
+ const normalizedType = readString2(item.type, "text").toLowerCase();
12860
+ if (normalizedType === "image" || normalizedType === "error") return normalizedType;
12861
+ return "text";
12862
+ })(),
12863
+ content: readString2(item.content),
12864
+ filename: readNonEmptyString(item.filename) || void 0,
12865
+ mimeType: readNonEmptyString(item.mimeType) || void 0,
12866
+ sizeBytes: readOptionalFiniteNumber(item.sizeBytes)
12867
+ })) : [];
12868
+ const debugTrace = isRecord(rawAssistantTurn.debug) ? {
12869
+ generationMs: readOptionalFiniteNumber(rawAssistantTurn.debug.generationMs),
12870
+ usage: isRecord(rawAssistantTurn.debug.usage) ? {
12871
+ promptTokens: readFiniteNumber(rawAssistantTurn.debug.usage.promptTokens),
12872
+ completionTokens: readFiniteNumber(rawAssistantTurn.debug.usage.completionTokens),
12873
+ totalTokens: readFiniteNumber(rawAssistantTurn.debug.usage.totalTokens),
12874
+ cachedTokens: readOptionalFiniteNumber(rawAssistantTurn.debug.usage.cachedTokens),
12875
+ cost: readOptionalFiniteNumber(rawAssistantTurn.debug.usage.cost)
12876
+ } : void 0,
12877
+ tools: Array.isArray(rawAssistantTurn.debug.tools) ? rawAssistantTurn.debug.tools.filter((tool) => isRecord(tool)).map((tool) => ({
12878
+ callId: readNonEmptyString(tool.callId) || void 0,
12879
+ name: readString2(tool.name),
12880
+ arguments: readNonEmptyString(tool.arguments) || void 0,
12881
+ result: readNonEmptyString(tool.result) || void 0,
12882
+ error: readNonEmptyString(tool.error) || void 0,
12883
+ durationMs: readOptionalFiniteNumber(tool.durationMs)
12884
+ })) : []
12885
+ } : void 0;
12886
+ return {
12887
+ id: assistantID,
12888
+ role: normalizeMessageRole(rawAssistantTurn.role),
12889
+ content: readString2(rawAssistantTurn.content),
12890
+ explanation: readNonEmptyString(rawAssistantTurn.explanation) || void 0,
12891
+ citations,
12892
+ toolCalls,
12893
+ chartData: void 0,
12894
+ renderTables: void 0,
12895
+ artifacts: sanitizeAssistantArtifacts(rawAssistantTurn.artifacts, turnId),
12896
+ codeOutputs,
12897
+ debug: debugTrace,
12898
+ createdAt: readString2(rawAssistantTurn.createdAt, fallbackCreatedAt)
12899
+ };
12900
+ }
12901
+ function sanitizeConversationTurn(rawTurn, index, fallbackSessionID) {
12902
+ if (!isRecord(rawTurn)) {
12903
+ warnMalformedSessionPayload("Dropped malformed turn payload (not an object)", { index });
12904
+ return null;
12905
+ }
12906
+ if (!isRecord(rawTurn.userTurn)) {
12907
+ warnMalformedSessionPayload("Dropped malformed turn payload (missing user turn)", { index });
12908
+ return null;
12909
+ }
12910
+ const userTurnID = readNonEmptyString(rawTurn.userTurn.id);
12911
+ if (!userTurnID) {
12912
+ warnMalformedSessionPayload("Dropped malformed turn payload (missing user turn id)", { index });
12913
+ return null;
12914
+ }
12915
+ const turnID = readString2(rawTurn.id, userTurnID);
12916
+ const createdAt = readString2(
12917
+ rawTurn.createdAt,
12918
+ readString2(rawTurn.userTurn.createdAt, (/* @__PURE__ */ new Date()).toISOString())
12919
+ );
12920
+ return {
12921
+ id: turnID,
12922
+ sessionId: readString2(rawTurn.sessionId, fallbackSessionID),
12923
+ userTurn: {
12924
+ id: userTurnID,
12925
+ content: readString2(rawTurn.userTurn.content),
12926
+ attachments: sanitizeUserAttachments(rawTurn.userTurn.attachments, turnID),
12927
+ createdAt: readString2(rawTurn.userTurn.createdAt, createdAt)
12928
+ },
12929
+ assistantTurn: sanitizeAssistantTurn(rawTurn.assistantTurn, createdAt, turnID),
12930
+ createdAt
12931
+ };
12932
+ }
12933
+ function sanitizeConversationTurns(rawTurns, sessionID) {
12934
+ if (!Array.isArray(rawTurns)) {
12935
+ warnMalformedSessionPayload("Session payload contained non-array turns field", { sessionID });
12936
+ return [];
12937
+ }
12938
+ const turns = [];
12939
+ let dropped = 0;
12940
+ for (let i = 0; i < rawTurns.length; i++) {
12941
+ const sanitizedTurn = sanitizeConversationTurn(rawTurns[i], i, sessionID);
12942
+ if (sanitizedTurn) {
12943
+ turns.push(sanitizedTurn);
12944
+ } else {
12945
+ dropped++;
12946
+ }
12947
+ }
12948
+ if (dropped > 0) {
12949
+ warnMalformedSessionPayload("Dropped malformed turns from session payload", {
12950
+ sessionID,
12951
+ dropped,
12952
+ total: rawTurns.length
12953
+ });
12954
+ }
12955
+ return turns;
12956
+ }
12957
+ function sanitizePendingQuestion(rawPendingQuestion, sessionID) {
12958
+ if (!rawPendingQuestion) return null;
12959
+ const checkpointID = readNonEmptyString(rawPendingQuestion.checkpointId);
12960
+ if (!checkpointID) {
12961
+ warnMalformedSessionPayload("Dropped malformed pendingQuestion without checkpointId", { sessionID });
12962
+ return null;
12963
+ }
12964
+ if (!Array.isArray(rawPendingQuestion.questions)) {
12965
+ warnMalformedSessionPayload("Pending question had non-array questions payload", {
12966
+ sessionID,
12967
+ checkpointID
12968
+ });
12969
+ }
12970
+ const questions = Array.isArray(rawPendingQuestion.questions) ? rawPendingQuestion.questions.filter((question) => {
12971
+ if (!question || !isRecord(question)) {
12972
+ warnMalformedSessionPayload("Dropped malformed question from pendingQuestion", {
12973
+ sessionID,
12974
+ checkpointID
12975
+ });
12976
+ return false;
12977
+ }
12978
+ return true;
12979
+ }).map((question, index) => {
12980
+ const questionID = readString2(question.id, `${checkpointID}-q-${index}`);
12981
+ const options = Array.isArray(question.options) ? question.options.filter((option) => {
12982
+ if (!option || !isRecord(option)) {
12983
+ warnMalformedSessionPayload("Dropped malformed pendingQuestion option", {
12984
+ sessionID,
12985
+ checkpointID,
12986
+ questionID
12987
+ });
12988
+ return false;
12989
+ }
12990
+ return true;
12991
+ }).map((option, optionIndex) => {
12992
+ const label = readString2(option.label);
12993
+ return {
12994
+ id: readString2(option.id, `${questionID}-opt-${optionIndex}`),
12995
+ label,
12996
+ value: label
12997
+ };
12998
+ }) : [];
12999
+ return {
13000
+ id: questionID,
13001
+ text: readString2(question.text),
13002
+ type: normalizeQuestionType(question.type),
13003
+ options
13004
+ };
13005
+ }) : [];
12081
13006
  return {
12082
- id: rpc.checkpointId,
12083
- turnId: rpc.turnId || "",
13007
+ id: checkpointID,
13008
+ turnId: readString2(rawPendingQuestion.turnId),
12084
13009
  questions,
12085
13010
  status: "PENDING"
12086
13011
  };
@@ -12152,6 +13077,18 @@ function extractChartDataFromToolCalls(toolCalls) {
12152
13077
  }
12153
13078
  return void 0;
12154
13079
  }
13080
+ function extractRenderTablesFromToolCalls(toolCalls) {
13081
+ if (!toolCalls) return [];
13082
+ const tables = [];
13083
+ for (const tc of toolCalls) {
13084
+ if (tc.name !== "renderTable" || !tc.result) continue;
13085
+ const parsed = parseRenderTableDataFromJsonString(tc.result, tc.id);
13086
+ if (parsed) {
13087
+ tables.push(parsed);
13088
+ }
13089
+ }
13090
+ return tables;
13091
+ }
12155
13092
  var EXPORT_TOOL_NAMES = {
12156
13093
  export_query_to_excel: "excel",
12157
13094
  export_data_to_excel: "excel",
@@ -12187,6 +13124,7 @@ function extractDownloadArtifactsFromToolCalls(toolCalls) {
12187
13124
  function normalizeAssistantTurn(turn) {
12188
13125
  const existingArtifacts = turn.artifacts || [];
12189
13126
  const fromToolCalls = extractDownloadArtifactsFromToolCalls(turn.toolCalls);
13127
+ const renderTables = turn.renderTables || extractRenderTablesFromToolCalls(turn.toolCalls);
12190
13128
  const merged = [...existingArtifacts];
12191
13129
  for (const a of fromToolCalls) {
12192
13130
  if (!merged.some((e) => e.url === a.url && e.filename === a.filename)) {
@@ -12197,6 +13135,7 @@ function normalizeAssistantTurn(turn) {
12197
13135
  ...turn,
12198
13136
  role: turn.role || "assistant" /* Assistant */,
12199
13137
  chartData: turn.chartData || extractChartDataFromToolCalls(turn.toolCalls),
13138
+ renderTables,
12200
13139
  citations: turn.citations || [],
12201
13140
  artifacts: merged,
12202
13141
  codeOutputs: turn.codeOutputs || []
@@ -12276,6 +13215,7 @@ var HttpDataSource = class {
12276
13215
  this.abortController = null;
12277
13216
  this.config = {
12278
13217
  streamEndpoint: "/stream",
13218
+ uploadEndpoint: "/api/uploads",
12279
13219
  timeout: 12e4,
12280
13220
  ...config
12281
13221
  };
@@ -12311,6 +13251,216 @@ var HttpDataSource = class {
12311
13251
  }
12312
13252
  return headers;
12313
13253
  }
13254
+ createUploadHeaders(additionalHeaders) {
13255
+ const headers = new Headers({
13256
+ ...this.config.headers,
13257
+ ...additionalHeaders
13258
+ });
13259
+ const csrfToken = this.getCSRFToken();
13260
+ if (csrfToken) {
13261
+ headers.set("X-CSRF-Token", csrfToken);
13262
+ }
13263
+ headers.delete("Content-Type");
13264
+ return headers;
13265
+ }
13266
+ logAttachmentLifecycle(event, details) {
13267
+ const payload = {
13268
+ source: "HttpDataSource",
13269
+ event,
13270
+ ...details
13271
+ };
13272
+ if (event.endsWith("_fail")) {
13273
+ console.warn("[bichat.attachments]", payload);
13274
+ return;
13275
+ }
13276
+ }
13277
+ async normalizeAttachmentFile(attachment, file) {
13278
+ const signatureBytes = new Uint8Array(await file.slice(0, 16).arrayBuffer());
13279
+ const detectedMimeType = detectMimeFromSignature(signatureBytes);
13280
+ const declaredMimeType = (attachment.mimeType || file.type || "").trim().toLowerCase();
13281
+ let resolvedMimeType = declaredMimeType || detectedMimeType || "application/octet-stream";
13282
+ let correctedFromDeclared = false;
13283
+ if (detectedMimeType && declaredMimeType && detectedMimeType !== declaredMimeType) {
13284
+ const safeToCorrect = SAFE_AUTOCORRECT_MIME_TYPES.has(detectedMimeType) && SAFE_AUTOCORRECT_MIME_TYPES.has(declaredMimeType);
13285
+ if (!safeToCorrect) {
13286
+ throw new Error(
13287
+ `Attachment "${attachment.filename}" MIME mismatch: declared "${declaredMimeType}", detected "${detectedMimeType}"`
13288
+ );
13289
+ }
13290
+ resolvedMimeType = detectedMimeType;
13291
+ correctedFromDeclared = true;
13292
+ } else if (detectedMimeType && !declaredMimeType) {
13293
+ resolvedMimeType = detectedMimeType;
13294
+ }
13295
+ const normalizedName = normalizeFilenameForMime(attachment.filename, resolvedMimeType);
13296
+ const normalized = new File([file], normalizedName, {
13297
+ type: resolvedMimeType,
13298
+ lastModified: file.lastModified
13299
+ });
13300
+ this.logAttachmentLifecycle("attachment_decode_success", {
13301
+ attachmentKey: attachment.clientKey,
13302
+ filename: attachment.filename,
13303
+ normalizedFilename: normalized.name,
13304
+ declaredMimeType: declaredMimeType || void 0,
13305
+ detectedMimeType,
13306
+ resolvedMimeType,
13307
+ correctedFromDeclared,
13308
+ sizeBytes: normalized.size
13309
+ });
13310
+ return normalized;
13311
+ }
13312
+ async uploadFile(file) {
13313
+ const formData = new FormData();
13314
+ formData.append("file", file);
13315
+ const response = await fetch(`${this.config.baseUrl}${this.config.uploadEndpoint}`, {
13316
+ method: "POST",
13317
+ headers: this.createUploadHeaders(),
13318
+ body: formData
13319
+ });
13320
+ let payload = null;
13321
+ try {
13322
+ payload = await response.json();
13323
+ } catch {
13324
+ payload = null;
13325
+ }
13326
+ if (!response.ok) {
13327
+ const errorMessage = isRecord(payload) && typeof payload.error === "string" ? payload.error : `Upload failed: HTTP ${response.status}`;
13328
+ throw new Error(errorMessage);
13329
+ }
13330
+ if (!isRecord(payload) || typeof payload.id !== "number" || payload.id <= 0) {
13331
+ throw new Error("Upload failed: invalid response payload");
13332
+ }
13333
+ return {
13334
+ id: payload.id,
13335
+ url: typeof payload.url === "string" ? payload.url : "",
13336
+ path: typeof payload.path === "string" ? payload.path : "",
13337
+ name: typeof payload.name === "string" ? payload.name : file.name,
13338
+ mimetype: typeof payload.mimetype === "string" ? payload.mimetype : file.type,
13339
+ size: typeof payload.size === "number" && Number.isFinite(payload.size) ? payload.size : file.size
13340
+ };
13341
+ }
13342
+ async attachmentToFile(attachment) {
13343
+ if (attachment.base64Data && attachment.base64Data.trim().length > 0) {
13344
+ try {
13345
+ const base64Data = attachment.base64Data.trim();
13346
+ const dataUrl = base64Data.startsWith("data:") ? base64Data : `data:${attachment.mimeType || "application/octet-stream"};base64,${base64Data}`;
13347
+ const blob = await fetch(dataUrl).then((response) => response.blob());
13348
+ return new File([blob], attachment.filename, {
13349
+ type: attachment.mimeType || blob.type || "application/octet-stream"
13350
+ });
13351
+ } catch (err) {
13352
+ const message = err instanceof Error ? err.message : "Unknown decode error";
13353
+ throw new Error(`Attachment "${attachment.filename}" decode failed: ${message}`);
13354
+ }
13355
+ }
13356
+ if (attachment.url) {
13357
+ let parsed;
13358
+ try {
13359
+ parsed = new URL(attachment.url, window.location?.origin ?? "https://localhost");
13360
+ if (!["http:", "https:"].includes(parsed.protocol)) {
13361
+ throw new Error(`Attachment "${attachment.filename}" URL has disallowed protocol: ${parsed.protocol}`);
13362
+ }
13363
+ } catch (err) {
13364
+ if (err instanceof Error && err.message.includes("Attachment")) throw err;
13365
+ throw new Error(`Attachment "${attachment.filename}" has invalid or malformed URL`);
13366
+ }
13367
+ const response = await fetch(parsed.href);
13368
+ if (!response.ok) {
13369
+ throw new Error(`Attachment "${attachment.filename}" decode failed: source HTTP ${response.status}`);
13370
+ }
13371
+ const blob = await response.blob();
13372
+ return new File([blob], attachment.filename, {
13373
+ type: attachment.mimeType || blob.type || "application/octet-stream"
13374
+ });
13375
+ }
13376
+ throw new Error(`Attachment "${attachment.filename}" has no uploadable data`);
13377
+ }
13378
+ assertUploadReferences(uploads) {
13379
+ return uploads.map((upload, index) => {
13380
+ if (typeof upload.id !== "number" || !Number.isFinite(upload.id) || upload.id <= 0) {
13381
+ throw new Error(`Attachment upload reference is invalid at index ${index}`);
13382
+ }
13383
+ return { uploadId: upload.id };
13384
+ });
13385
+ }
13386
+ async ensureAttachmentUpload(attachment, context) {
13387
+ if (typeof attachment.uploadId === "number" && attachment.uploadId > 0) {
13388
+ this.logAttachmentLifecycle("attachment_upload_success", {
13389
+ sessionId: context.sessionId,
13390
+ attachmentIndex: context.attachmentIndex,
13391
+ attachmentKey: attachment.clientKey,
13392
+ filename: attachment.filename,
13393
+ uploadId: attachment.uploadId,
13394
+ reusedUploadId: true
13395
+ });
13396
+ return {
13397
+ id: attachment.uploadId,
13398
+ url: attachment.url || "",
13399
+ path: "",
13400
+ name: attachment.filename,
13401
+ mimetype: attachment.mimeType,
13402
+ size: attachment.sizeBytes
13403
+ };
13404
+ }
13405
+ this.logAttachmentLifecycle("attachment_decode_start", {
13406
+ sessionId: context.sessionId,
13407
+ attachmentIndex: context.attachmentIndex,
13408
+ attachmentKey: attachment.clientKey,
13409
+ filename: attachment.filename,
13410
+ hasBase64Data: Boolean(attachment.base64Data && attachment.base64Data.trim().length > 0),
13411
+ hasURL: Boolean(attachment.url)
13412
+ });
13413
+ let file;
13414
+ try {
13415
+ const rawFile = await this.attachmentToFile(attachment);
13416
+ file = await this.normalizeAttachmentFile(attachment, rawFile);
13417
+ validateAttachmentFile(file);
13418
+ } catch (err) {
13419
+ const message = err instanceof Error ? err.message : "Unknown attachment decode/validation error";
13420
+ this.logAttachmentLifecycle("attachment_decode_fail", {
13421
+ sessionId: context.sessionId,
13422
+ attachmentIndex: context.attachmentIndex,
13423
+ attachmentKey: attachment.clientKey,
13424
+ filename: attachment.filename,
13425
+ error: message
13426
+ });
13427
+ throw new Error(message);
13428
+ }
13429
+ this.logAttachmentLifecycle("attachment_upload_start", {
13430
+ sessionId: context.sessionId,
13431
+ attachmentIndex: context.attachmentIndex,
13432
+ attachmentKey: attachment.clientKey,
13433
+ filename: file.name,
13434
+ mimeType: file.type,
13435
+ sizeBytes: file.size
13436
+ });
13437
+ try {
13438
+ const upload = await this.uploadFile(file);
13439
+ attachment.uploadId = upload.id;
13440
+ attachment.mimeType = upload.mimetype || file.type;
13441
+ attachment.filename = upload.name || file.name;
13442
+ attachment.sizeBytes = upload.size || file.size;
13443
+ this.logAttachmentLifecycle("attachment_upload_success", {
13444
+ sessionId: context.sessionId,
13445
+ attachmentIndex: context.attachmentIndex,
13446
+ attachmentKey: attachment.clientKey,
13447
+ filename: attachment.filename,
13448
+ uploadId: upload.id,
13449
+ reusedUploadId: false
13450
+ });
13451
+ return upload;
13452
+ } catch (err) {
13453
+ const message = err instanceof Error ? err.message : "Unknown upload error";
13454
+ this.logAttachmentLifecycle("attachment_upload_fail", {
13455
+ sessionId: context.sessionId,
13456
+ attachmentIndex: context.attachmentIndex,
13457
+ attachmentKey: attachment.clientKey,
13458
+ filename: file.name,
13459
+ error: message
13460
+ });
13461
+ throw new Error(`Attachment "${attachment.filename}" upload failed: ${message}`);
13462
+ }
13463
+ }
12314
13464
  async callRPC(method, params) {
12315
13465
  return this.rpc.callTyped(method, params);
12316
13466
  }
@@ -12333,11 +13483,22 @@ var HttpDataSource = class {
12333
13483
  return { artifacts: [], hasMore: false, nextOffset: 0 };
12334
13484
  })
12335
13485
  ]);
12336
- const turns = attachArtifactsToTurns(normalizeTurns(data.turns), artifactsData.artifacts || []);
13486
+ const sanitizedTurns = sanitizeConversationTurns(data.turns, id);
13487
+ const turns = attachArtifactsToTurns(
13488
+ normalizeTurns(sanitizedTurns),
13489
+ artifactsData.artifacts || []
13490
+ );
13491
+ const pendingQuestion = sanitizePendingQuestion(data.pendingQuestion, id);
13492
+ if (data.pendingQuestion && pendingQuestion && pendingQuestion.questions.length === 0) {
13493
+ warnMalformedSessionPayload("Pending question normalized to zero renderable questions", {
13494
+ sessionID: id,
13495
+ checkpointID: pendingQuestion.id
13496
+ });
13497
+ }
12337
13498
  return {
12338
13499
  session: toSession(data.session),
12339
13500
  turns,
12340
- pendingQuestion: toPendingQuestion(data.pendingQuestion)
13501
+ pendingQuestion
12341
13502
  };
12342
13503
  } catch (err) {
12343
13504
  if (isSessionNotFoundError(err)) {
@@ -12369,27 +13530,12 @@ var HttpDataSource = class {
12369
13530
  return { artifacts: [] };
12370
13531
  }
12371
13532
  validateFileCount(0, files.length, 10);
12372
- const attachments = [];
12373
- for (const file of files) {
12374
- validateAttachmentFile(file);
12375
- const base64Data = await convertToBase64(file);
12376
- attachments.push({
12377
- clientKey: crypto.randomUUID(),
12378
- filename: file.name,
12379
- mimeType: file.type,
12380
- sizeBytes: file.size,
12381
- base64Data
12382
- });
12383
- }
13533
+ files.forEach((file) => validateAttachmentFile(file));
13534
+ const uploads = await Promise.all(files.map((file) => this.uploadFile(file)));
12384
13535
  const data = await this.callRPC("bichat.session.uploadArtifacts", {
12385
13536
  sessionId,
12386
- attachments: attachments.map((a) => ({
12387
- id: "",
12388
- // Backend will assign ID
12389
- filename: a.filename,
12390
- mimeType: a.mimeType,
12391
- sizeBytes: a.sizeBytes,
12392
- base64Data: a.base64Data
13537
+ attachments: uploads.map((upload) => ({
13538
+ uploadId: upload.id
12393
13539
  }))
12394
13540
  });
12395
13541
  return {
@@ -12420,22 +13566,26 @@ var HttpDataSource = class {
12420
13566
  signal.addEventListener("abort", onExternalAbort);
12421
13567
  }
12422
13568
  const url = `${this.config.baseUrl}${this.config.streamEndpoint}`;
12423
- const payload = {
12424
- sessionId,
12425
- content,
12426
- debugMode: options?.debugMode ?? false,
12427
- replaceFromMessageId: options?.replaceFromMessageID,
12428
- attachments: attachments.map((a) => ({
12429
- filename: a.filename,
12430
- mimeType: a.mimeType,
12431
- sizeBytes: a.sizeBytes,
12432
- base64Data: a.base64Data,
12433
- url: a.url
12434
- }))
12435
- };
12436
13569
  let connectionTimeoutID;
12437
13570
  let connectionTimedOut = false;
12438
13571
  try {
13572
+ const uploads = await Promise.all(
13573
+ attachments.map(
13574
+ (attachment, attachmentIndex) => this.ensureAttachmentUpload(attachment, { sessionId, attachmentIndex })
13575
+ )
13576
+ );
13577
+ const streamAttachments = this.assertUploadReferences(uploads);
13578
+ this.logAttachmentLifecycle("stream_send_with_upload_ids", {
13579
+ sessionId,
13580
+ attachmentCount: streamAttachments.length
13581
+ });
13582
+ const payload = {
13583
+ sessionId,
13584
+ content,
13585
+ debugMode: options?.debugMode ?? false,
13586
+ replaceFromMessageId: options?.replaceFromMessageID,
13587
+ attachments: streamAttachments
13588
+ };
12439
13589
  const timeoutMs = this.config.timeout ?? 0;
12440
13590
  if (timeoutMs > 0) {
12441
13591
  connectionTimeoutID = setTimeout(() => {
@@ -12603,6 +13753,6 @@ function createHttpDataSource(config) {
12603
13753
  return new HttpDataSource(config);
12604
13754
  }
12605
13755
 
12606
- export { ATTACHMENT_ACCEPT_ATTRIBUTE, ActionButton, Alert_default as Alert, AllChatsList, ArchiveBanner_default as ArchiveBanner, ArchivedChatList, AssistantMessage, AssistantTurnView, MemoizedAttachmentGrid as AttachmentGrid, AttachmentPreview_default as AttachmentPreview, AttachmentUpload_default as AttachmentUpload, Avatar, BiChatLayout, Bubble, CHART_VISUAL, ChartCard, ChatHeader, ChatMachine, ChatSession, ChatSessionProvider, MemoizedCodeBlock as CodeBlock, CodeOutputsPanel, CompactionDoodle, ConfigProvider, ConfirmModal, ConfirmationStep, DateGroupHeader, DebugPanel, DefaultErrorContent, DownloadCard, MemoizedEditableText as EditableText, MemoizedEmptyState as EmptyState, ErrorBoundary, HttpDataSource, ImageModal, InlineQuestionForm, IotaContextProvider, ListItemSkeleton, MemoizedLoadingSpinner as LoadingSpinner, MemoizedMarkdownRenderer as MarkdownRenderer, MessageActions, MessageInput, MessageList, MessageRole, PermissionGuard, QuestionForm, QuestionStep, RateLimiter, RetryActionArea, ScreenReaderAnnouncer, ScrollToBottomButton, MemoizedSearchInput as SearchInput, SessionArtifactList, SessionArtifactPreview, SessionArtifactsPanel, SessionItem_default as SessionItem, SessionSkeleton, Sidebar2 as Sidebar, MemoizedSkeleton as Skeleton, SkeletonAvatar, SkeletonCard, SkeletonGroup, SkeletonText, SkipLink, Slot, SourcesPanel, StreamError, StreamingCursor, SystemMessage, TableExportButton, TableWithExport, ThemeProvider, Toast, ToastContainer, TouchContextMenu, Turn, TurnBubble, MemoizedTypingIndicator as TypingIndicator, MemoizedUserAvatar as UserAvatar, MemoizedUserFilter as UserFilter, UserMessage, UserTurnView, WelcomeContent, addCSRFHeader, backdropVariants, buttonVariants, convertToBase64, createDataUrl, createHeadersWithCSRF, createHttpDataSource, darkTheme, dropdownVariants, errorMessageVariants, fadeInUpVariants, fadeInVariants, floatingButtonVariants, formatFileSize, getCSRFToken, getFileVisual, getValidChildren, groupSessionsByDate, hasPermission, isImageMimeType, isPermissionDeniedError, lightTheme, listItemVariants, messageContainerVariants, messageVariants, parseBichatStream, parseBichatStreamEvents, parseSSEStream, scaleFadeVariants, sessionItemVariants, staggerContainerVariants, toErrorDisplay, typingDotVariants, useActionButtonContext, useAttachments, useAutoScroll, useAvatarContext, useBubbleContext, useChatInput, useChatMessaging, useChatSession, useConfig, useFocusTrap, useImageGallery, useIotaContext, useKeyboardShortcuts, useLongPress, useMarkdownCopy, useMessageActions, useModalLock, useOptionalChatMessaging, useRequiredConfig, useScrollToBottom, useSidebarState, useStreaming, useTheme, useToast, useTranslation, useTurnContext, validateAttachmentFile, validateFileCount, validateImageFile, verbTransitionVariants };
13756
+ export { ATTACHMENT_ACCEPT_ATTRIBUTE, ActionButton, Alert_default as Alert, AllChatsList, ArchiveBanner_default as ArchiveBanner, ArchivedChatList, AssistantMessage, AssistantTurnView, MemoizedAttachmentGrid as AttachmentGrid, AttachmentPreview_default as AttachmentPreview, AttachmentUpload_default as AttachmentUpload, Avatar, BiChatLayout, Bubble, CHART_VISUAL, ChartCard, ChatHeader, ChatMachine, ChatSession, ChatSessionProvider, MemoizedCodeBlock as CodeBlock, CodeOutputsPanel, CompactionDoodle, ConfigProvider, ConfirmModal, ConfirmationStep, DateGroupHeader, DebugPanel, DefaultErrorContent, DownloadCard, MemoizedEditableText as EditableText, MemoizedEmptyState as EmptyState, ErrorBoundary, HttpDataSource, ImageModal, InlineQuestionForm, InteractiveTableCard, IotaContextProvider, ListItemSkeleton, MemoizedLoadingSpinner as LoadingSpinner, MemoizedMarkdownRenderer as MarkdownRenderer, MessageActions, MessageInput, MessageList, MessageRole, PermissionGuard, QuestionForm, QuestionStep, RateLimiter, RetryActionArea, ScreenReaderAnnouncer, ScrollToBottomButton, MemoizedSearchInput as SearchInput, SessionArtifactList, SessionArtifactPreview, SessionArtifactsPanel, SessionItem_default as SessionItem, SessionSkeleton, Sidebar2 as Sidebar, MemoizedSkeleton as Skeleton, SkeletonAvatar, SkeletonCard, SkeletonGroup, SkeletonText, SkipLink, Slot, SourcesPanel, StreamError, StreamingCursor, SystemMessage, TableExportButton, TableWithExport, ThemeProvider, Toast, ToastContainer, TouchContextMenu, Turn, TurnBubble, MemoizedTypingIndicator as TypingIndicator, MemoizedUserAvatar as UserAvatar, MemoizedUserFilter as UserFilter, UserMessage, UserTurnView, WelcomeContent, addCSRFHeader, backdropVariants, buttonVariants, convertToBase64, createDataUrl, createHeadersWithCSRF, createHttpDataSource, darkTheme, dropdownVariants, errorMessageVariants, fadeInUpVariants, fadeInVariants, floatingButtonVariants, formatFileSize, getCSRFToken, getFileVisual, getValidChildren, groupSessionsByDate, hasPermission, isImageMimeType, isPermissionDeniedError, lightTheme, listItemVariants, messageContainerVariants, messageVariants, parseBichatStream, parseBichatStreamEvents, parseSSEStream, scaleFadeVariants, sessionItemVariants, staggerContainerVariants, toErrorDisplay, typingDotVariants, useActionButtonContext, useAttachments, useAutoScroll, useAvatarContext, useBubbleContext, useChatInput, useChatMessaging, useChatSession, useConfig, useFocusTrap, useImageGallery, useIotaContext, useKeyboardShortcuts, useLongPress, useMarkdownCopy, useMessageActions, useModalLock, useOptionalChatMessaging, useRequiredConfig, useScrollToBottom, useSidebarState, useStreaming, useTheme, useToast, useTranslation, useTurnContext, validateAttachmentFile, validateFileCount, validateImageFile, verbTransitionVariants };
12607
13757
  //# sourceMappingURL=index.mjs.map
12608
13758
  //# sourceMappingURL=index.mjs.map