@iota-uz/sdk 0.4.19 → 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 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';
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"() {
@@ -1537,6 +1537,12 @@ var ChatMachine = class {
1537
1537
  streamErrorRetryable: false
1538
1538
  });
1539
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
+ }
1540
1546
  _cancel() {
1541
1547
  if (this.abortController) {
1542
1548
  this.abortController.abort();
@@ -1804,7 +1810,11 @@ var ChatMachine = class {
1804
1810
  }
1805
1811
  }
1806
1812
  const targetSessionId = createdSessionId || activeSessionId;
1813
+ if (targetSessionId && targetSessionId !== "new") {
1814
+ this._notifySessionsUpdated("message_sent", targetSessionId);
1815
+ }
1807
1816
  if (shouldNavigateAfter && targetSessionId && targetSessionId !== "new") {
1817
+ this._notifySessionsUpdated("session_created", targetSessionId);
1808
1818
  if (this.onSessionCreated) {
1809
1819
  this.onSessionCreated(targetSessionId);
1810
1820
  } else {
@@ -1994,6 +2004,8 @@ var ChatMachine = class {
1994
2004
  this._clearStreamError();
1995
2005
  const convertedAttachments = attachments.map((att) => ({
1996
2006
  clientKey: att.clientKey || crypto.randomUUID(),
2007
+ id: att.id,
2008
+ uploadId: att.uploadId,
1997
2009
  filename: att.filename,
1998
2010
  mimeType: att.mimeType,
1999
2011
  sizeBytes: att.sizeBytes,
@@ -2656,6 +2668,9 @@ var MemoizedAttachmentGrid = React.memo(AttachmentGrid);
2656
2668
  MemoizedAttachmentGrid.displayName = "AttachmentGrid";
2657
2669
  var AttachmentGrid_default = MemoizedAttachmentGrid;
2658
2670
  init_useTranslation();
2671
+ var MIN_SCALE = 0.25;
2672
+ var MAX_SCALE = 5;
2673
+ var ZOOM_STEP = 0.25;
2659
2674
  function ImageModal({
2660
2675
  isOpen,
2661
2676
  onClose,
@@ -2668,9 +2683,23 @@ function ImageModal({
2668
2683
  const [isImageLoaded, setIsImageLoaded] = useState(false);
2669
2684
  const [imageError, setImageError] = useState(false);
2670
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);
2671
2693
  const hasMultipleImages = allAttachments && allAttachments.length > 1;
2672
2694
  const canNavigatePrev = hasMultipleImages && currentIndex > 0;
2673
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]);
2674
2703
  useEffect(() => {
2675
2704
  if (!isOpen) return;
2676
2705
  const handleKeyDown = (e) => {
@@ -2678,6 +2707,14 @@ function ImageModal({
2678
2707
  onNavigate("prev");
2679
2708
  } else if (e.key === "ArrowRight" && onNavigate && canNavigateNext) {
2680
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 });
2681
2718
  }
2682
2719
  };
2683
2720
  document.addEventListener("keydown", handleKeyDown);
@@ -2686,128 +2723,246 @@ function ImageModal({
2686
2723
  useEffect(() => {
2687
2724
  setIsImageLoaded(false);
2688
2725
  setImageError(false);
2726
+ setScale(1);
2727
+ setPosition({ x: 0, y: 0 });
2689
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]);
2690
2744
  const handleRetry = useCallback(() => {
2691
2745
  setImageError(false);
2692
2746
  setIsImageLoaded(false);
2693
2747
  setRetryKey((k) => k + 1);
2694
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]);
2695
2793
  const previewUrl = attachment.preview || createDataUrl(attachment.base64Data, attachment.mimeType);
2794
+ const zoomPercent = Math.round(scale * 100);
2696
2795
  return /* @__PURE__ */ jsxs(Dialog, { open: isOpen, onClose, className: "relative", style: { zIndex: 99999 }, children: [
2697
2796
  /* @__PURE__ */ jsx(
2698
2797
  DialogBackdrop,
2699
2798
  {
2700
- className: "fixed inset-0",
2701
- 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 }
2702
2801
  }
2703
2802
  ),
2704
- /* @__PURE__ */ jsxs(DialogPanel, { className: "fixed inset-0 flex flex-col", style: { zIndex: 1e5 }, children: [
2705
- /* @__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: [
2706
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [
2707
- hasMultipleImages && /* @__PURE__ */ jsxs("span", { className: "text-xs text-gray-500 dark:text-gray-400 tabular-nums whitespace-nowrap", children: [
2708
- currentIndex + 1,
2709
- " / ",
2710
- allAttachments?.length
2711
- ] }),
2712
- /* @__PURE__ */ jsx("span", { className: "text-sm text-gray-900 dark:text-gray-200 truncate", children: attachment.filename }),
2713
- /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap", children: formatFileSize(attachment.sizeBytes) })
2714
- ] }),
2715
- /* @__PURE__ */ jsx(
2716
- "button",
2717
- {
2718
- onClick: onClose,
2719
- 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",
2720
- "aria-label": t("BiChat.Image.Close"),
2721
- type: "button",
2722
- children: /* @__PURE__ */ jsx(X, { size: 18, weight: "bold" })
2723
- }
2724
- )
2725
- ] }),
2726
- /* @__PURE__ */ jsxs(
2727
- "div",
2728
- {
2729
- className: "relative flex-1 flex items-center justify-center min-h-0",
2730
- onClick: (e) => {
2731
- if (e.target === e.currentTarget) onClose();
2732
- },
2733
- children: [
2734
- !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: [
2735
- /* @__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" }),
2736
- /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400 dark:text-gray-500", children: t("BiChat.Loading") })
2737
- ] }) }),
2738
- imageError && /* @__PURE__ */ jsxs("div", { role: "alert", className: "flex flex-col items-center justify-center text-center max-w-xs", children: [
2739
- /* @__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" }) }),
2740
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-gray-700 dark:text-gray-300 mb-1", children: t("BiChat.Image.FailedToLoad") }),
2741
- /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400 dark:text-gray-500 mb-5 truncate max-w-full", children: attachment.filename }),
2742
- /* @__PURE__ */ jsxs(
2743
- "button",
2744
- {
2745
- type: "button",
2746
- onClick: handleRetry,
2747
- 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",
2748
- "aria-label": t("BiChat.Image.Retry"),
2749
- children: [
2750
- /* @__PURE__ */ jsx(ArrowClockwise, { size: 16, weight: "bold" }),
2751
- t("BiChat.Retry.Label")
2752
- ]
2753
- }
2754
- )
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
2755
2817
  ] }),
2756
- /* @__PURE__ */ jsx(
2757
- "img",
2758
- {
2759
- src: previewUrl,
2760
- alt: attachment.filename,
2761
- className: [
2762
- "max-w-[90vw] max-h-[calc(100vh-120px)] object-contain select-none",
2763
- "transition-opacity duration-300 ease-out",
2764
- isImageLoaded ? "opacity-100" : "opacity-0"
2765
- ].join(" "),
2766
- onLoad: () => setIsImageLoaded(true),
2767
- onError: () => setImageError(true),
2768
- loading: "lazy",
2769
- draggable: false
2770
- },
2771
- retryKey
2772
- ),
2773
- hasMultipleImages && /* @__PURE__ */ jsxs(Fragment, { children: [
2774
- /* @__PURE__ */ jsx(
2775
- "button",
2776
- {
2777
- onClick: () => onNavigate?.("prev"),
2778
- disabled: !canNavigatePrev || !isImageLoaded || imageError,
2779
- className: [
2780
- "absolute left-3 top-1/2 -translate-y-1/2",
2781
- "flex items-center justify-center w-10 h-10 rounded-md",
2782
- "transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400",
2783
- 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"
2784
- ].join(" "),
2785
- "aria-label": t("BiChat.Image.Previous"),
2786
- type: "button",
2787
- children: /* @__PURE__ */ jsx(CaretLeft, { size: 20, weight: "bold" })
2788
- }
2789
- ),
2790
- /* @__PURE__ */ jsx(
2791
- "button",
2792
- {
2793
- onClick: () => onNavigate?.("next"),
2794
- disabled: !canNavigateNext || !isImageLoaded || imageError,
2795
- className: [
2796
- "absolute right-3 top-1/2 -translate-y-1/2",
2797
- "flex items-center justify-center w-10 h-10 rounded-md",
2798
- "transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400",
2799
- 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"
2800
- ].join(" "),
2801
- "aria-label": t("BiChat.Image.Next"),
2802
- type: "button",
2803
- children: /* @__PURE__ */ jsx(CaretRight, { size: 20, weight: "bold" })
2804
- }
2805
- )
2806
- ] })
2807
- ]
2808
- }
2809
- )
2810
- ] })
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
+ )
2811
2966
  ] });
2812
2967
  }
2813
2968
  var ImageModal_default = ImageModal;
@@ -2859,6 +3014,7 @@ function UserMessage({
2859
3014
  const [draftContent, setDraftContent] = useState("");
2860
3015
  const [isCopied, setIsCopied] = useState(false);
2861
3016
  const copyFeedbackTimeoutRef = useRef(null);
3017
+ const editTextareaRef = useRef(null);
2862
3018
  const classes = mergeClassNames(defaultClassNames, classNameOverrides);
2863
3019
  useEffect(() => {
2864
3020
  return () => {
@@ -2868,6 +3024,16 @@ function UserMessage({
2868
3024
  }
2869
3025
  };
2870
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]);
2871
3037
  const normalizedAttachments = turn.attachments.map((attachment) => {
2872
3038
  if (!attachment.mimeType.startsWith("image/")) {
2873
3039
  return attachment;
@@ -2952,6 +3118,21 @@ function UserMessage({
2952
3118
  onEdit(turnId, newContent);
2953
3119
  setIsEditing(false);
2954
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
+ }, []);
2955
3136
  const handleNavigate = useCallback(
2956
3137
  (direction) => {
2957
3138
  if (selectedImageIndex === null) return;
@@ -3002,36 +3183,48 @@ function UserMessage({
3002
3183
  }
3003
3184
  )
3004
3185
  ) }),
3005
- 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: [
3006
3187
  /* @__PURE__ */ jsx(
3007
3188
  "textarea",
3008
3189
  {
3190
+ ref: editTextareaRef,
3009
3191
  value: draftContent,
3010
- onChange: (e) => setDraftContent(e.target.value),
3011
- 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",
3012
- "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
3013
3197
  }
3014
3198
  ),
3015
- /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
3016
- /* @__PURE__ */ jsx(
3017
- "button",
3018
- {
3019
- type: "button",
3020
- onClick: handleEditCancel,
3021
- className: "cursor-pointer px-3 py-1.5 rounded-lg bg-white/10 hover:bg-white/15 transition-colors text-sm font-medium",
3022
- children: t("BiChat.Message.Cancel")
3023
- }
3024
- ),
3025
- /* @__PURE__ */ jsx(
3026
- "button",
3027
- {
3028
- type: "button",
3029
- onClick: handleEditSave,
3030
- 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",
3031
- disabled: !draftContent.trim() || draftContent === turn.content,
3032
- children: t("BiChat.Message.Save")
3033
- }
3034
- )
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
+ ] })
3035
3228
  ] })
3036
3229
  ] }) : renderSlot(slots?.content, contentSlotProps, turn.content) }) }),
3037
3230
  !hideActions && /* @__PURE__ */ jsx("div", { className: `${classes.actions} ${isCopied ? "opacity-100" : ""}`, children: renderSlot(
@@ -3179,67 +3372,295 @@ function StreamingCursor() {
3179
3372
  }
3180
3373
  var StreamingCursor_default = StreamingCursor;
3181
3374
 
3182
- // ui/src/bichat/components/AssistantMessage.tsx
3183
- init_ChartCard();
3375
+ // ui/src/bichat/components/AssistantMessage.tsx
3376
+ init_ChartCard();
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
+ }
3390
+ }
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
+ ] }),
3460
+ /* @__PURE__ */ jsx(
3461
+ TableExportButton,
3462
+ {
3463
+ onClick: handleExport,
3464
+ disabled: exportDisabled,
3465
+ label: t("BiChat.Table.ExportToExcel"),
3466
+ disabledTooltip: sendDisabled ? t("BiChat.Table.PleaseWait") : t("BiChat.Table.ExportUnavailable")
3467
+ }
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
+ )
3547
+ ] })
3548
+ ] }),
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
+ }
3184
3577
  function SourcesPanel({ citations }) {
3185
- if (!citations || citations.length === 0) {
3186
- return null;
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",
3590
+ {
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")
3609
+ ] })
3610
+ ]
3611
+ }
3612
+ ) });
3187
3613
  }
3188
- 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: [
3189
- /* @__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: [
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") }),
3190
3617
  /* @__PURE__ */ jsx(
3191
- "svg",
3618
+ "button",
3192
3619
  {
3193
- className: `w-4 h-4 transition-transform duration-150 ${open ? "rotate-90" : ""}`,
3194
- fill: "none",
3195
- stroke: "currentColor",
3196
- viewBox: "0 0 24 24",
3197
- children: /* @__PURE__ */ jsx(
3198
- "path",
3199
- {
3200
- strokeLinecap: "round",
3201
- strokeLinejoin: "round",
3202
- strokeWidth: 2,
3203
- d: "M9 5l7 7-7 7"
3204
- }
3205
- )
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" })
3206
3625
  }
3207
- ),
3208
- /* @__PURE__ */ jsxs("span", { children: [
3209
- citations.length,
3210
- " ",
3211
- citations.length === 1 ? "source" : "sources"
3212
- ] })
3626
+ )
3213
3627
  ] }),
3214
- /* @__PURE__ */ jsx(DisclosurePanel, { className: "mt-2 space-y-2", children: citations.map((citation, index) => /* @__PURE__ */ jsx(
3215
- "div",
3216
- {
3217
- className: "p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg text-sm",
3218
- children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
3219
- /* @__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 }),
3220
- /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
3221
- /* @__PURE__ */ jsx("div", { className: "font-medium text-gray-900 dark:text-gray-100", children: citation.title }),
3222
- citation.url && /* @__PURE__ */ jsx(
3223
- "a",
3224
- {
3225
- href: citation.url,
3226
- target: "_blank",
3227
- rel: "noopener noreferrer",
3228
- className: "text-[var(--bichat-primary)] hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50 rounded",
3229
- children: citation.url
3230
- }
3231
- ),
3232
- citation.excerpt && /* @__PURE__ */ jsxs("div", { className: "mt-1 text-gray-600 dark:text-gray-400 italic", children: [
3233
- '"',
3234
- citation.excerpt,
3235
- '"'
3236
- ] })
3237
- ] })
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 })
3238
3644
  ] })
3239
- },
3240
- citation.id
3241
- )) })
3242
- ] }) }) });
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
+ ] });
3243
3664
  }
3244
3665
  var MIME_BY_TYPE = {
3245
3666
  excel: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
@@ -3591,6 +4012,59 @@ function InlineQuestionForm({ pendingQuestion }) {
3591
4012
  ] }) });
3592
4013
  }
3593
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
+
3594
4068
  // ui/src/bichat/utils/debugMetrics.ts
3595
4069
  function formatGenerationDuration(generationMs) {
3596
4070
  return generationMs > 1e3 ? `${(generationMs / 1e3).toFixed(2)}s` : `${generationMs}ms`;
@@ -3889,6 +4363,7 @@ var defaultClassNames2 = {
3889
4363
  bubble: "bg-white dark:bg-gray-800 rounded-2xl rounded-bl-sm px-4 py-3 shadow-sm",
3890
4364
  codeOutputs: "",
3891
4365
  charts: "mb-1 w-full",
4366
+ tables: "mb-1 flex flex-col gap-3",
3892
4367
  artifacts: "mb-1 flex flex-wrap gap-2",
3893
4368
  sources: "",
3894
4369
  explanation: "mt-4 border-t border-gray-100 dark:border-gray-700 pt-4",
@@ -3905,6 +4380,7 @@ function mergeClassNames2(defaults, overrides) {
3905
4380
  bubble: overrides.bubble ?? defaults.bubble,
3906
4381
  codeOutputs: overrides.codeOutputs ?? defaults.codeOutputs,
3907
4382
  charts: overrides.charts ?? defaults.charts,
4383
+ tables: overrides.tables ?? defaults.tables,
3908
4384
  artifacts: overrides.artifacts ?? defaults.artifacts,
3909
4385
  sources: overrides.sources ?? defaults.sources,
3910
4386
  explanation: overrides.explanation ?? defaults.explanation,
@@ -3949,6 +4425,14 @@ function AssistantMessage({
3949
4425
  const hasContent = turn.content?.trim().length > 0;
3950
4426
  const hasExplanation = !!turn.explanation?.trim();
3951
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;
3952
4436
  const handleCopyClick = useCallback(async () => {
3953
4437
  try {
3954
4438
  if (onCopy) {
@@ -3990,15 +4474,18 @@ function AssistantMessage({
3990
4474
  const codeOutputsSlotProps = {
3991
4475
  outputs: turn.codeOutputs || []
3992
4476
  };
4477
+ const tablesSlotProps = {
4478
+ tables: turn.renderTables || []
4479
+ };
3993
4480
  const artifactsSlotProps = {
3994
4481
  artifacts: turn.artifacts || []
3995
4482
  };
3996
4483
  const actionsSlotProps = {
3997
4484
  onCopy: handleCopyClick,
3998
- onRegenerate: onRegenerate && turnId && !isSystemMessage && isLastTurn ? handleRegenerateClick : void 0,
4485
+ onRegenerate: canRegenerate ? handleRegenerateClick : void 0,
3999
4486
  timestamp,
4000
4487
  canCopy: hasContent,
4001
- canRegenerate: !!onRegenerate && !!turnId && !isSystemMessage && isLastTurn
4488
+ canRegenerate
4002
4489
  };
4003
4490
  const explanationSlotProps = {
4004
4491
  explanation: turn.explanation || "",
@@ -4011,14 +4498,30 @@ function AssistantMessage({
4011
4498
  return slot;
4012
4499
  };
4013
4500
  return /* @__PURE__ */ jsxs("div", { className: classes.root, children: [
4014
- !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") }),
4015
4502
  /* @__PURE__ */ jsxs("div", { className: classes.wrapper, children: [
4503
+ showInlineRetry && /* @__PURE__ */ jsx(RetryActionArea, { onRetry: () => {
4504
+ void handleRegenerateClick();
4505
+ } }),
4016
4506
  turn.codeOutputs && turn.codeOutputs.length > 0 && /* @__PURE__ */ jsx("div", { className: classes.codeOutputs, children: renderSlot(
4017
4507
  slots?.codeOutputs,
4018
4508
  codeOutputsSlotProps,
4019
4509
  /* @__PURE__ */ jsx(CodeOutputsPanel_default, { outputs: turn.codeOutputs })
4020
4510
  ) }),
4021
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
+ ) }),
4022
4525
  hasContent && /* @__PURE__ */ jsxs("div", { className: bubbleClassName, children: [
4023
4526
  renderSlot(
4024
4527
  slots?.content,
@@ -4108,7 +4611,7 @@ function AssistantMessage({
4108
4611
  children: isCopied ? /* @__PURE__ */ jsx(Check, { size: 14, weight: "bold" }) : /* @__PURE__ */ jsx(Copy, { size: 14, weight: "regular" })
4109
4612
  }
4110
4613
  ),
4111
- onRegenerate && turnId && !isSystemMessage && isLastTurn && /* @__PURE__ */ jsx(
4614
+ canRegenerate && /* @__PURE__ */ jsx(
4112
4615
  "button",
4113
4616
  {
4114
4617
  onClick: handleRegenerateClick,
@@ -9230,6 +9733,10 @@ function ErrorAlert({ error }) {
9230
9733
  );
9231
9734
  }
9232
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;
9233
9740
  function useSidebarCollapse() {
9234
9741
  const [isCollapsed, setIsCollapsed] = useState(() => {
9235
9742
  try {
@@ -9287,7 +9794,7 @@ function Sidebar2({
9287
9794
  const shouldReduceMotion = useReducedMotion();
9288
9795
  const sessionListRef = useRef(null);
9289
9796
  const searchContainerRef = useRef(null);
9290
- const refreshForActiveSessionRef = useRef(null);
9797
+ const activeSessionMissRetriesRef = useRef({});
9291
9798
  const { isCollapsed, toggle, collapse } = useSidebarCollapse();
9292
9799
  const collapsible = !onClose;
9293
9800
  const handleSidebarClick = useCallback(
@@ -9331,6 +9838,7 @@ function Sidebar2({
9331
9838
  const [actionError, setActionError] = useState(null);
9332
9839
  const accessDenied = loadError?.isPermissionDenied === true;
9333
9840
  const [refreshKey, setRefreshKey] = useState(0);
9841
+ const [reconcilePollToken, setReconcilePollToken] = useState(0);
9334
9842
  const [showConfirm, setShowConfirm] = useState(false);
9335
9843
  const [sessionToArchive, setSessionToArchive] = useState(null);
9336
9844
  const fetchSessions = useCallback(async () => {
@@ -9351,8 +9859,13 @@ function Sidebar2({
9351
9859
  fetchSessions();
9352
9860
  }, [fetchSessions, refreshKey]);
9353
9861
  useEffect(() => {
9354
- const handleSessionsUpdated = () => {
9862
+ const handleSessionsUpdated = (event) => {
9355
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
+ }
9356
9869
  };
9357
9870
  window.addEventListener("bichat:sessions-updated", handleSessionsUpdated);
9358
9871
  return () => {
@@ -9360,31 +9873,33 @@ function Sidebar2({
9360
9873
  };
9361
9874
  }, []);
9362
9875
  useEffect(() => {
9363
- if (!activeSessionId) {
9364
- refreshForActiveSessionRef.current = null;
9365
- return;
9366
- }
9876
+ activeSessionMissRetriesRef.current = {};
9877
+ }, [activeSessionId]);
9878
+ useEffect(() => {
9879
+ if (!activeSessionId) return;
9367
9880
  if (loading) return;
9368
9881
  const hasActiveSession = sessions.some((session) => session.id === activeSessionId);
9369
9882
  if (hasActiveSession) {
9370
- if (refreshForActiveSessionRef.current === activeSessionId) {
9371
- refreshForActiveSessionRef.current = null;
9372
- }
9883
+ delete activeSessionMissRetriesRef.current[activeSessionId];
9373
9884
  return;
9374
9885
  }
9375
- if (refreshForActiveSessionRef.current !== activeSessionId) {
9376
- refreshForActiveSessionRef.current = activeSessionId;
9377
- setRefreshKey((k) => k + 1);
9886
+ const attempts = activeSessionMissRetriesRef.current[activeSessionId] ?? 0;
9887
+ if (attempts >= ACTIVE_SESSION_MISS_MAX_RETRIES) {
9888
+ return;
9378
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);
9379
9896
  }, [activeSessionId, loading, sessions]);
9380
9897
  const hasPlaceholderTitles = useMemo(() => {
9381
9898
  const newChatLabel = t("BiChat.Chat.NewChat");
9382
9899
  return Array.isArray(sessions) && sessions.some((s) => s && (!s.title || s.title === newChatLabel));
9383
9900
  }, [sessions, t]);
9384
9901
  useEffect(() => {
9385
- if (!hasPlaceholderTitles) return;
9386
- const pollInterval = 2e3;
9387
- const maxPolls = 5;
9902
+ if (!hasPlaceholderTitles && reconcilePollToken === 0) return;
9388
9903
  let pollCount = 0;
9389
9904
  const intervalId = setInterval(async () => {
9390
9905
  pollCount++;
@@ -9393,12 +9908,12 @@ function Sidebar2({
9393
9908
  setSessions(result.sessions);
9394
9909
  } catch {
9395
9910
  }
9396
- if (pollCount >= maxPolls) {
9911
+ if (pollCount >= SESSION_RECONCILE_MAX_POLLS) {
9397
9912
  clearInterval(intervalId);
9398
9913
  }
9399
- }, pollInterval);
9914
+ }, SESSION_RECONCILE_POLL_INTERVAL_MS);
9400
9915
  return () => clearInterval(intervalId);
9401
- }, [hasPlaceholderTitles, dataSource]);
9916
+ }, [hasPlaceholderTitles, dataSource, reconcilePollToken]);
9402
9917
  const handleArchiveRequest = (sessionId) => {
9403
9918
  setSessionToArchive(sessionId);
9404
9919
  setShowConfirm(true);
@@ -9456,7 +9971,9 @@ function Sidebar2({
9456
9971
  try {
9457
9972
  await dataSource.regenerateSessionTitle(sessionId);
9458
9973
  toast.success(t("BiChat.Sidebar.TitleRegenerated"));
9459
- setRefreshKey((k) => k + 1);
9974
+ window.dispatchEvent(new CustomEvent("bichat:sessions-updated", {
9975
+ detail: { reason: "title_regenerate_requested", sessionId }
9976
+ }));
9460
9977
  } catch (err) {
9461
9978
  console.error("Failed to regenerate title:", err);
9462
9979
  const display = toErrorDisplay(err, t("BiChat.Sidebar.FailedToRegenerateTitle"));
@@ -10269,59 +10786,6 @@ function BiChatLayout({
10269
10786
  ] })
10270
10787
  ] });
10271
10788
  }
10272
-
10273
- // ui/src/bichat/components/RetryActionArea.tsx
10274
- init_useTranslation();
10275
- var RetryActionArea = memo(function RetryActionArea2({
10276
- onRetry
10277
- }) {
10278
- const { t } = useTranslation();
10279
- return (
10280
- // Wrapper matches TurnBubble layout for assistant messages (justify-start = left-aligned)
10281
- /* @__PURE__ */ jsx(
10282
- motion.div,
10283
- {
10284
- initial: { opacity: 0, y: 10 },
10285
- animate: { opacity: 1, y: 0 },
10286
- exit: { opacity: 0, y: -10 },
10287
- transition: { duration: 0.2 },
10288
- className: "flex justify-start",
10289
- children: /* @__PURE__ */ jsxs(
10290
- "div",
10291
- {
10292
- 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",
10293
- role: "status",
10294
- "aria-live": "polite",
10295
- children: [
10296
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
10297
- /* @__PURE__ */ jsx(
10298
- Warning,
10299
- {
10300
- className: "w-5 h-5 text-amber-500 dark:text-amber-400 flex-shrink-0",
10301
- weight: "fill"
10302
- }
10303
- ),
10304
- /* @__PURE__ */ jsx("span", { className: "text-sm text-gray-700 dark:text-gray-300", children: t("BiChat.Retry.Subtitle") })
10305
- ] }),
10306
- /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsxs(
10307
- "button",
10308
- {
10309
- onClick: onRetry,
10310
- 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",
10311
- "aria-label": t("BiChat.Retry.Title"),
10312
- children: [
10313
- /* @__PURE__ */ jsx(ArrowClockwise, { size: 16, className: "w-4 h-4" }),
10314
- t("BiChat.Retry.Button")
10315
- ]
10316
- }
10317
- ) })
10318
- ]
10319
- }
10320
- )
10321
- }
10322
- )
10323
- );
10324
- });
10325
10789
  init_useTranslation();
10326
10790
  function MessageActions({
10327
10791
  message,
@@ -12139,6 +12603,75 @@ function toStreamEvent(chunk) {
12139
12603
  }
12140
12604
  }
12141
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
+
12142
12675
  // ui/src/bichat/data/HttpDataSource.ts
12143
12676
  function isSessionNotFoundError(err) {
12144
12677
  if (!(err instanceof AppletRPCException)) return false;
@@ -12169,7 +12702,7 @@ function toSessionArtifact(artifact) {
12169
12702
  function warnMalformedSessionPayload(message, details) {
12170
12703
  console.warn(`[BiChat] ${message}`, details || {});
12171
12704
  }
12172
- function readString(value, fallback = "") {
12705
+ function readString2(value, fallback = "") {
12173
12706
  return typeof value === "string" ? value : fallback;
12174
12707
  }
12175
12708
  function readNonEmptyString(value) {
@@ -12183,12 +12716,52 @@ function readFiniteNumber(value, fallback = 0) {
12183
12716
  function readOptionalFiniteNumber(value) {
12184
12717
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
12185
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
+ }
12186
12759
  function normalizeQuestionType(rawType) {
12187
- const normalized = readString(rawType).trim().toUpperCase().replace(/[\s-]+/g, "_");
12760
+ const normalized = readString2(rawType).trim().toUpperCase().replace(/[\s-]+/g, "_");
12188
12761
  return normalized === "MULTIPLE_CHOICE" ? "MULTIPLE_CHOICE" : "SINGLE_CHOICE";
12189
12762
  }
12190
12763
  function normalizeMessageRole(rawRole) {
12191
- const normalized = readString(rawRole).trim().toLowerCase();
12764
+ const normalized = readString2(rawRole).trim().toLowerCase();
12192
12765
  if (normalized === "user" /* User */) return "user" /* User */;
12193
12766
  if (normalized === "system" /* System */) return "system" /* System */;
12194
12767
  if (normalized === "tool" /* Tool */) return "tool" /* Tool */;
@@ -12199,8 +12772,8 @@ function sanitizeAttachment(rawAttachment, turnId, index) {
12199
12772
  warnMalformedSessionPayload("Dropped malformed attachment entry", { turnId, index });
12200
12773
  return null;
12201
12774
  }
12202
- const filename = readString(rawAttachment.filename, "attachment");
12203
- const mimeType = readString(rawAttachment.mimeType, "application/octet-stream");
12775
+ const filename = readString2(rawAttachment.filename, "attachment");
12776
+ const mimeType = readString2(rawAttachment.mimeType, "application/octet-stream");
12204
12777
  const id = readNonEmptyString(rawAttachment.id) || void 0;
12205
12778
  const clientKey = readNonEmptyString(rawAttachment.clientKey) || id || `${turnId}-attachment-${index}`;
12206
12779
  return {
@@ -12233,7 +12806,7 @@ function sanitizeAssistantArtifacts(rawArtifacts, turnId) {
12233
12806
  warnMalformedSessionPayload("Dropped malformed assistant artifact", { turnId, index: i });
12234
12807
  continue;
12235
12808
  }
12236
- const type = readString(raw.type).toLowerCase();
12809
+ const type = readString2(raw.type).toLowerCase();
12237
12810
  if (type !== "excel" && type !== "pdf") {
12238
12811
  continue;
12239
12812
  }
@@ -12244,7 +12817,7 @@ function sanitizeAssistantArtifacts(rawArtifacts, turnId) {
12244
12817
  }
12245
12818
  artifacts.push({
12246
12819
  type,
12247
- filename: readString(raw.filename, "download"),
12820
+ filename: readString2(raw.filename, "download"),
12248
12821
  url,
12249
12822
  sizeReadable: readNonEmptyString(raw.sizeReadable) || void 0,
12250
12823
  rowCount: typeof raw.rowCount === "number" && Number.isFinite(raw.rowCount) ? raw.rowCount : void 0,
@@ -12265,29 +12838,29 @@ function sanitizeAssistantTurn(rawAssistantTurn, fallbackCreatedAt, turnId) {
12265
12838
  return void 0;
12266
12839
  }
12267
12840
  const citations = Array.isArray(rawAssistantTurn.citations) ? rawAssistantTurn.citations.filter((item) => isRecord(item)).map((item, index) => ({
12268
- id: readString(item.id, `${assistantID}-citation-${index}`),
12269
- type: readString(item.type),
12270
- title: readString(item.title),
12271
- url: readString(item.url),
12841
+ id: readString2(item.id, `${assistantID}-citation-${index}`),
12842
+ type: readString2(item.type),
12843
+ title: readString2(item.title),
12844
+ url: readString2(item.url),
12272
12845
  startIndex: readFiniteNumber(item.startIndex),
12273
12846
  endIndex: readFiniteNumber(item.endIndex),
12274
12847
  excerpt: readNonEmptyString(item.excerpt) || void 0
12275
12848
  })) : [];
12276
12849
  const toolCalls = Array.isArray(rawAssistantTurn.toolCalls) ? rawAssistantTurn.toolCalls.filter((item) => isRecord(item)).map((item, index) => ({
12277
- id: readString(item.id, `${assistantID}-tool-${index}`),
12278
- name: readString(item.name),
12279
- arguments: readString(item.arguments),
12850
+ id: readString2(item.id, `${assistantID}-tool-${index}`),
12851
+ name: readString2(item.name),
12852
+ arguments: readString2(item.arguments),
12280
12853
  result: readNonEmptyString(item.result) || void 0,
12281
12854
  error: readNonEmptyString(item.error) || void 0,
12282
12855
  durationMs: readFiniteNumber(item.durationMs)
12283
12856
  })) : [];
12284
12857
  const codeOutputs = Array.isArray(rawAssistantTurn.codeOutputs) ? rawAssistantTurn.codeOutputs.filter((item) => isRecord(item)).map((item) => ({
12285
12858
  type: (() => {
12286
- const normalizedType = readString(item.type, "text").toLowerCase();
12859
+ const normalizedType = readString2(item.type, "text").toLowerCase();
12287
12860
  if (normalizedType === "image" || normalizedType === "error") return normalizedType;
12288
12861
  return "text";
12289
12862
  })(),
12290
- content: readString(item.content),
12863
+ content: readString2(item.content),
12291
12864
  filename: readNonEmptyString(item.filename) || void 0,
12292
12865
  mimeType: readNonEmptyString(item.mimeType) || void 0,
12293
12866
  sizeBytes: readOptionalFiniteNumber(item.sizeBytes)
@@ -12303,7 +12876,7 @@ function sanitizeAssistantTurn(rawAssistantTurn, fallbackCreatedAt, turnId) {
12303
12876
  } : void 0,
12304
12877
  tools: Array.isArray(rawAssistantTurn.debug.tools) ? rawAssistantTurn.debug.tools.filter((tool) => isRecord(tool)).map((tool) => ({
12305
12878
  callId: readNonEmptyString(tool.callId) || void 0,
12306
- name: readString(tool.name),
12879
+ name: readString2(tool.name),
12307
12880
  arguments: readNonEmptyString(tool.arguments) || void 0,
12308
12881
  result: readNonEmptyString(tool.result) || void 0,
12309
12882
  error: readNonEmptyString(tool.error) || void 0,
@@ -12313,15 +12886,16 @@ function sanitizeAssistantTurn(rawAssistantTurn, fallbackCreatedAt, turnId) {
12313
12886
  return {
12314
12887
  id: assistantID,
12315
12888
  role: normalizeMessageRole(rawAssistantTurn.role),
12316
- content: readString(rawAssistantTurn.content),
12889
+ content: readString2(rawAssistantTurn.content),
12317
12890
  explanation: readNonEmptyString(rawAssistantTurn.explanation) || void 0,
12318
12891
  citations,
12319
12892
  toolCalls,
12320
12893
  chartData: void 0,
12894
+ renderTables: void 0,
12321
12895
  artifacts: sanitizeAssistantArtifacts(rawAssistantTurn.artifacts, turnId),
12322
12896
  codeOutputs,
12323
12897
  debug: debugTrace,
12324
- createdAt: readString(rawAssistantTurn.createdAt, fallbackCreatedAt)
12898
+ createdAt: readString2(rawAssistantTurn.createdAt, fallbackCreatedAt)
12325
12899
  };
12326
12900
  }
12327
12901
  function sanitizeConversationTurn(rawTurn, index, fallbackSessionID) {
@@ -12338,19 +12912,19 @@ function sanitizeConversationTurn(rawTurn, index, fallbackSessionID) {
12338
12912
  warnMalformedSessionPayload("Dropped malformed turn payload (missing user turn id)", { index });
12339
12913
  return null;
12340
12914
  }
12341
- const turnID = readString(rawTurn.id, userTurnID);
12342
- const createdAt = readString(
12915
+ const turnID = readString2(rawTurn.id, userTurnID);
12916
+ const createdAt = readString2(
12343
12917
  rawTurn.createdAt,
12344
- readString(rawTurn.userTurn.createdAt, (/* @__PURE__ */ new Date()).toISOString())
12918
+ readString2(rawTurn.userTurn.createdAt, (/* @__PURE__ */ new Date()).toISOString())
12345
12919
  );
12346
12920
  return {
12347
12921
  id: turnID,
12348
- sessionId: readString(rawTurn.sessionId, fallbackSessionID),
12922
+ sessionId: readString2(rawTurn.sessionId, fallbackSessionID),
12349
12923
  userTurn: {
12350
12924
  id: userTurnID,
12351
- content: readString(rawTurn.userTurn.content),
12925
+ content: readString2(rawTurn.userTurn.content),
12352
12926
  attachments: sanitizeUserAttachments(rawTurn.userTurn.attachments, turnID),
12353
- createdAt: readString(rawTurn.userTurn.createdAt, createdAt)
12927
+ createdAt: readString2(rawTurn.userTurn.createdAt, createdAt)
12354
12928
  },
12355
12929
  assistantTurn: sanitizeAssistantTurn(rawTurn.assistantTurn, createdAt, turnID),
12356
12930
  createdAt
@@ -12403,7 +12977,7 @@ function sanitizePendingQuestion(rawPendingQuestion, sessionID) {
12403
12977
  }
12404
12978
  return true;
12405
12979
  }).map((question, index) => {
12406
- const questionID = readString(question.id, `${checkpointID}-q-${index}`);
12980
+ const questionID = readString2(question.id, `${checkpointID}-q-${index}`);
12407
12981
  const options = Array.isArray(question.options) ? question.options.filter((option) => {
12408
12982
  if (!option || !isRecord(option)) {
12409
12983
  warnMalformedSessionPayload("Dropped malformed pendingQuestion option", {
@@ -12415,23 +12989,23 @@ function sanitizePendingQuestion(rawPendingQuestion, sessionID) {
12415
12989
  }
12416
12990
  return true;
12417
12991
  }).map((option, optionIndex) => {
12418
- const label = readString(option.label);
12992
+ const label = readString2(option.label);
12419
12993
  return {
12420
- id: readString(option.id, `${questionID}-opt-${optionIndex}`),
12994
+ id: readString2(option.id, `${questionID}-opt-${optionIndex}`),
12421
12995
  label,
12422
12996
  value: label
12423
12997
  };
12424
12998
  }) : [];
12425
12999
  return {
12426
13000
  id: questionID,
12427
- text: readString(question.text),
13001
+ text: readString2(question.text),
12428
13002
  type: normalizeQuestionType(question.type),
12429
13003
  options
12430
13004
  };
12431
13005
  }) : [];
12432
13006
  return {
12433
13007
  id: checkpointID,
12434
- turnId: readString(rawPendingQuestion.turnId),
13008
+ turnId: readString2(rawPendingQuestion.turnId),
12435
13009
  questions,
12436
13010
  status: "PENDING"
12437
13011
  };
@@ -12503,6 +13077,18 @@ function extractChartDataFromToolCalls(toolCalls) {
12503
13077
  }
12504
13078
  return void 0;
12505
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
+ }
12506
13092
  var EXPORT_TOOL_NAMES = {
12507
13093
  export_query_to_excel: "excel",
12508
13094
  export_data_to_excel: "excel",
@@ -12538,6 +13124,7 @@ function extractDownloadArtifactsFromToolCalls(toolCalls) {
12538
13124
  function normalizeAssistantTurn(turn) {
12539
13125
  const existingArtifacts = turn.artifacts || [];
12540
13126
  const fromToolCalls = extractDownloadArtifactsFromToolCalls(turn.toolCalls);
13127
+ const renderTables = turn.renderTables || extractRenderTablesFromToolCalls(turn.toolCalls);
12541
13128
  const merged = [...existingArtifacts];
12542
13129
  for (const a of fromToolCalls) {
12543
13130
  if (!merged.some((e) => e.url === a.url && e.filename === a.filename)) {
@@ -12548,6 +13135,7 @@ function normalizeAssistantTurn(turn) {
12548
13135
  ...turn,
12549
13136
  role: turn.role || "assistant" /* Assistant */,
12550
13137
  chartData: turn.chartData || extractChartDataFromToolCalls(turn.toolCalls),
13138
+ renderTables,
12551
13139
  citations: turn.citations || [],
12552
13140
  artifacts: merged,
12553
13141
  codeOutputs: turn.codeOutputs || []
@@ -12675,6 +13263,52 @@ var HttpDataSource = class {
12675
13263
  headers.delete("Content-Type");
12676
13264
  return headers;
12677
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
+ }
12678
13312
  async uploadFile(file) {
12679
13313
  const formData = new FormData();
12680
13314
  formData.append("file", file);
@@ -12707,17 +13341,32 @@ var HttpDataSource = class {
12707
13341
  }
12708
13342
  async attachmentToFile(attachment) {
12709
13343
  if (attachment.base64Data && attachment.base64Data.trim().length > 0) {
12710
- const base64Data = attachment.base64Data.trim();
12711
- const dataUrl = base64Data.startsWith("data:") ? base64Data : `data:${attachment.mimeType || "application/octet-stream"};base64,${base64Data}`;
12712
- const blob = await fetch(dataUrl).then((response) => response.blob());
12713
- return new File([blob], attachment.filename, {
12714
- type: attachment.mimeType || blob.type || "application/octet-stream"
12715
- });
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
+ }
12716
13355
  }
12717
13356
  if (attachment.url) {
12718
- const response = await fetch(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);
12719
13368
  if (!response.ok) {
12720
- throw new Error(`Failed to read attachment source: HTTP ${response.status}`);
13369
+ throw new Error(`Attachment "${attachment.filename}" decode failed: source HTTP ${response.status}`);
12721
13370
  }
12722
13371
  const blob = await response.blob();
12723
13372
  return new File([blob], attachment.filename, {
@@ -12726,8 +13375,24 @@ var HttpDataSource = class {
12726
13375
  }
12727
13376
  throw new Error(`Attachment "${attachment.filename}" has no uploadable data`);
12728
13377
  }
12729
- async ensureAttachmentUpload(attachment) {
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) {
12730
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
+ });
12731
13396
  return {
12732
13397
  id: attachment.uploadId,
12733
13398
  url: attachment.url || "",
@@ -12737,8 +13402,64 @@ var HttpDataSource = class {
12737
13402
  size: attachment.sizeBytes
12738
13403
  };
12739
13404
  }
12740
- const file = await this.attachmentToFile(attachment);
12741
- return this.uploadFile(file);
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
+ }
12742
13463
  }
12743
13464
  async callRPC(method, params) {
12744
13465
  return this.rpc.callTyped(method, params);
@@ -12849,16 +13570,21 @@ var HttpDataSource = class {
12849
13570
  let connectionTimedOut = false;
12850
13571
  try {
12851
13572
  const uploads = await Promise.all(
12852
- attachments.map((attachment) => this.ensureAttachmentUpload(attachment))
13573
+ attachments.map(
13574
+ (attachment, attachmentIndex) => this.ensureAttachmentUpload(attachment, { sessionId, attachmentIndex })
13575
+ )
12853
13576
  );
13577
+ const streamAttachments = this.assertUploadReferences(uploads);
13578
+ this.logAttachmentLifecycle("stream_send_with_upload_ids", {
13579
+ sessionId,
13580
+ attachmentCount: streamAttachments.length
13581
+ });
12854
13582
  const payload = {
12855
13583
  sessionId,
12856
13584
  content,
12857
13585
  debugMode: options?.debugMode ?? false,
12858
13586
  replaceFromMessageId: options?.replaceFromMessageID,
12859
- attachments: uploads.map((upload) => ({
12860
- uploadId: upload.id
12861
- }))
13587
+ attachments: streamAttachments
12862
13588
  };
12863
13589
  const timeoutMs = this.config.timeout ?? 0;
12864
13590
  if (timeoutMs > 0) {
@@ -13027,6 +13753,6 @@ function createHttpDataSource(config) {
13027
13753
  return new HttpDataSource(config);
13028
13754
  }
13029
13755
 
13030
- 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 };
13031
13757
  //# sourceMappingURL=index.mjs.map
13032
13758
  //# sourceMappingURL=index.mjs.map