@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.
@@ -190,6 +190,37 @@ var init_ChartCard = __esm({
190
190
  DEFAULT_COLORS = ["#6366f1", "#06b6d4", "#f59e0b", "#ef4444", "#8b5cf6"];
191
191
  }
192
192
  });
193
+ exports.TableExportButton = void 0;
194
+ var init_TableExportButton = __esm({
195
+ "ui/src/bichat/components/TableExportButton.tsx"() {
196
+ init_useTranslation();
197
+ exports.TableExportButton = React.memo(function TableExportButton2({
198
+ onClick,
199
+ disabled = false,
200
+ label,
201
+ disabledTooltip
202
+ }) {
203
+ const { t } = useTranslation();
204
+ const resolvedLabel = label ?? t("BiChat.Export");
205
+ const resolvedDisabledTooltip = disabledTooltip ?? t("BiChat.Common.PleaseWait");
206
+ return /* @__PURE__ */ jsxRuntime.jsxs(
207
+ "button",
208
+ {
209
+ type: "button",
210
+ onClick,
211
+ disabled,
212
+ 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",
213
+ "aria-label": resolvedLabel,
214
+ title: disabled ? resolvedDisabledTooltip : resolvedLabel,
215
+ children: [
216
+ /* @__PURE__ */ jsxRuntime.jsx(react.FileXls, { size: 16, weight: "fill" }),
217
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: resolvedLabel })
218
+ ]
219
+ }
220
+ );
221
+ });
222
+ }
223
+ });
193
224
 
194
225
  // ui/src/bichat/utils/citationProcessor.ts
195
226
  function processCitations(content, citations) {
@@ -302,37 +333,6 @@ var init_chartSpec = __esm({
302
333
  ]);
303
334
  }
304
335
  });
305
- exports.TableExportButton = void 0;
306
- var init_TableExportButton = __esm({
307
- "ui/src/bichat/components/TableExportButton.tsx"() {
308
- init_useTranslation();
309
- exports.TableExportButton = React.memo(function TableExportButton2({
310
- onClick,
311
- disabled = false,
312
- label,
313
- disabledTooltip
314
- }) {
315
- const { t } = useTranslation();
316
- const resolvedLabel = label ?? t("BiChat.Export");
317
- const resolvedDisabledTooltip = disabledTooltip ?? t("BiChat.Common.PleaseWait");
318
- return /* @__PURE__ */ jsxRuntime.jsxs(
319
- "button",
320
- {
321
- type: "button",
322
- onClick,
323
- disabled,
324
- 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",
325
- "aria-label": resolvedLabel,
326
- title: disabled ? resolvedDisabledTooltip : resolvedLabel,
327
- children: [
328
- /* @__PURE__ */ jsxRuntime.jsx(react.FileXls, { size: 16, weight: "fill" }),
329
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: resolvedLabel })
330
- ]
331
- }
332
- );
333
- });
334
- }
335
- });
336
336
  exports.TableWithExport = void 0;
337
337
  var init_TableWithExport = __esm({
338
338
  "ui/src/bichat/components/TableWithExport.tsx"() {
@@ -1547,6 +1547,12 @@ var ChatMachine = class {
1547
1547
  streamErrorRetryable: false
1548
1548
  });
1549
1549
  }
1550
+ _notifySessionsUpdated(reason, sessionId) {
1551
+ if (typeof window === "undefined") return;
1552
+ window.dispatchEvent(new CustomEvent("bichat:sessions-updated", {
1553
+ detail: { reason, sessionId }
1554
+ }));
1555
+ }
1550
1556
  _cancel() {
1551
1557
  if (this.abortController) {
1552
1558
  this.abortController.abort();
@@ -1814,7 +1820,11 @@ var ChatMachine = class {
1814
1820
  }
1815
1821
  }
1816
1822
  const targetSessionId = createdSessionId || activeSessionId;
1823
+ if (targetSessionId && targetSessionId !== "new") {
1824
+ this._notifySessionsUpdated("message_sent", targetSessionId);
1825
+ }
1817
1826
  if (shouldNavigateAfter && targetSessionId && targetSessionId !== "new") {
1827
+ this._notifySessionsUpdated("session_created", targetSessionId);
1818
1828
  if (this.onSessionCreated) {
1819
1829
  this.onSessionCreated(targetSessionId);
1820
1830
  } else {
@@ -2004,6 +2014,8 @@ var ChatMachine = class {
2004
2014
  this._clearStreamError();
2005
2015
  const convertedAttachments = attachments.map((att) => ({
2006
2016
  clientKey: att.clientKey || crypto.randomUUID(),
2017
+ id: att.id,
2018
+ uploadId: att.uploadId,
2007
2019
  filename: att.filename,
2008
2020
  mimeType: att.mimeType,
2009
2021
  sizeBytes: att.sizeBytes,
@@ -2666,6 +2678,9 @@ var MemoizedAttachmentGrid = React__default.default.memo(AttachmentGrid);
2666
2678
  MemoizedAttachmentGrid.displayName = "AttachmentGrid";
2667
2679
  var AttachmentGrid_default = MemoizedAttachmentGrid;
2668
2680
  init_useTranslation();
2681
+ var MIN_SCALE = 0.25;
2682
+ var MAX_SCALE = 5;
2683
+ var ZOOM_STEP = 0.25;
2669
2684
  function ImageModal({
2670
2685
  isOpen,
2671
2686
  onClose,
@@ -2678,9 +2693,23 @@ function ImageModal({
2678
2693
  const [isImageLoaded, setIsImageLoaded] = React.useState(false);
2679
2694
  const [imageError, setImageError] = React.useState(false);
2680
2695
  const [retryKey, setRetryKey] = React.useState(0);
2696
+ const [scale, setScale] = React.useState(1);
2697
+ const [position, setPosition] = React.useState({ x: 0, y: 0 });
2698
+ const [isDragging, setIsDragging] = React.useState(false);
2699
+ const dragStartRef = React.useRef({ x: 0, y: 0 });
2700
+ const positionRef = React.useRef({ x: 0, y: 0 });
2701
+ const scaleRef = React.useRef(1);
2702
+ const imageAreaRef = React.useRef(null);
2681
2703
  const hasMultipleImages = allAttachments && allAttachments.length > 1;
2682
2704
  const canNavigatePrev = hasMultipleImages && currentIndex > 0;
2683
2705
  const canNavigateNext = hasMultipleImages && currentIndex < (allAttachments?.length || 1) - 1;
2706
+ const isZoomed = scale > 1;
2707
+ React.useEffect(() => {
2708
+ scaleRef.current = scale;
2709
+ }, [scale]);
2710
+ React.useEffect(() => {
2711
+ positionRef.current = position;
2712
+ }, [position]);
2684
2713
  React.useEffect(() => {
2685
2714
  if (!isOpen) return;
2686
2715
  const handleKeyDown = (e) => {
@@ -2688,6 +2717,14 @@ function ImageModal({
2688
2717
  onNavigate("prev");
2689
2718
  } else if (e.key === "ArrowRight" && onNavigate && canNavigateNext) {
2690
2719
  onNavigate("next");
2720
+ } else if (e.key === "+" || e.key === "=") {
2721
+ setScale((s) => Math.min(s + ZOOM_STEP, MAX_SCALE));
2722
+ } else if (e.key === "-") {
2723
+ setScale((s) => Math.max(s - ZOOM_STEP, MIN_SCALE));
2724
+ if (scaleRef.current - ZOOM_STEP <= 1) setPosition({ x: 0, y: 0 });
2725
+ } else if (e.key === "0") {
2726
+ setScale(1);
2727
+ setPosition({ x: 0, y: 0 });
2691
2728
  }
2692
2729
  };
2693
2730
  document.addEventListener("keydown", handleKeyDown);
@@ -2696,128 +2733,246 @@ function ImageModal({
2696
2733
  React.useEffect(() => {
2697
2734
  setIsImageLoaded(false);
2698
2735
  setImageError(false);
2736
+ setScale(1);
2737
+ setPosition({ x: 0, y: 0 });
2699
2738
  }, [attachment]);
2739
+ React.useEffect(() => {
2740
+ const el = imageAreaRef.current;
2741
+ if (!el || !isOpen) return;
2742
+ const handler = (e) => {
2743
+ const delta = e.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
2744
+ const current = scaleRef.current;
2745
+ const newScale = Math.min(Math.max(current + delta, MIN_SCALE), MAX_SCALE);
2746
+ if (newScale === current) return;
2747
+ e.preventDefault();
2748
+ setScale(newScale);
2749
+ if (newScale <= 1) setPosition({ x: 0, y: 0 });
2750
+ };
2751
+ el.addEventListener("wheel", handler, { passive: false });
2752
+ return () => el.removeEventListener("wheel", handler);
2753
+ }, [isOpen]);
2700
2754
  const handleRetry = React.useCallback(() => {
2701
2755
  setImageError(false);
2702
2756
  setIsImageLoaded(false);
2703
2757
  setRetryKey((k) => k + 1);
2704
2758
  }, []);
2759
+ const zoomIn = React.useCallback(() => {
2760
+ setScale((s) => Math.min(s + ZOOM_STEP, MAX_SCALE));
2761
+ }, []);
2762
+ const zoomOut = React.useCallback(() => {
2763
+ setScale((s) => Math.max(s - ZOOM_STEP, MIN_SCALE));
2764
+ if (scaleRef.current - ZOOM_STEP <= 1) setPosition({ x: 0, y: 0 });
2765
+ }, []);
2766
+ const resetZoom = React.useCallback(() => {
2767
+ setScale(1);
2768
+ setPosition({ x: 0, y: 0 });
2769
+ }, []);
2770
+ const handleDoubleClick = React.useCallback(() => {
2771
+ const current = scaleRef.current;
2772
+ if (current !== 1) {
2773
+ setScale(1);
2774
+ setPosition({ x: 0, y: 0 });
2775
+ } else {
2776
+ setScale(2);
2777
+ }
2778
+ }, []);
2779
+ const handleMouseDown = React.useCallback((e) => {
2780
+ if (scaleRef.current <= 1) return;
2781
+ e.preventDefault();
2782
+ setIsDragging(true);
2783
+ dragStartRef.current = {
2784
+ x: e.clientX - positionRef.current.x,
2785
+ y: e.clientY - positionRef.current.y
2786
+ };
2787
+ }, []);
2788
+ const handleMouseMove = React.useCallback((e) => {
2789
+ if (!isDragging) return;
2790
+ setPosition({
2791
+ x: e.clientX - dragStartRef.current.x,
2792
+ y: e.clientY - dragStartRef.current.y
2793
+ });
2794
+ }, [isDragging]);
2795
+ const handleMouseUp = React.useCallback(() => {
2796
+ setIsDragging(false);
2797
+ }, []);
2798
+ const handleBackdropClick = React.useCallback((e) => {
2799
+ if (e.target === e.currentTarget && !isZoomed) {
2800
+ onClose();
2801
+ }
2802
+ }, [isZoomed, onClose]);
2705
2803
  const previewUrl = attachment.preview || createDataUrl(attachment.base64Data, attachment.mimeType);
2804
+ const zoomPercent = Math.round(scale * 100);
2706
2805
  return /* @__PURE__ */ jsxRuntime.jsxs(react$1.Dialog, { open: isOpen, onClose, className: "relative", style: { zIndex: 99999 }, children: [
2707
2806
  /* @__PURE__ */ jsxRuntime.jsx(
2708
2807
  react$1.DialogBackdrop,
2709
2808
  {
2710
- className: "fixed inset-0",
2711
- style: { zIndex: 99999, backgroundColor: "rgba(0, 0, 0, 0.85)" }
2809
+ className: "fixed inset-0 bg-black/90 backdrop-blur-sm",
2810
+ style: { zIndex: 99999 }
2712
2811
  }
2713
2812
  ),
2714
- /* @__PURE__ */ jsxRuntime.jsxs(react$1.DialogPanel, { className: "fixed inset-0 flex flex-col", style: { zIndex: 1e5 }, children: [
2715
- /* @__PURE__ */ jsxRuntime.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: [
2716
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [
2717
- hasMultipleImages && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-gray-500 dark:text-gray-400 tabular-nums whitespace-nowrap", children: [
2718
- currentIndex + 1,
2719
- " / ",
2720
- allAttachments?.length
2721
- ] }),
2722
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-900 dark:text-gray-200 truncate", children: attachment.filename }),
2723
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap", children: formatFileSize(attachment.sizeBytes) })
2724
- ] }),
2725
- /* @__PURE__ */ jsxRuntime.jsx(
2726
- "button",
2727
- {
2728
- onClick: onClose,
2729
- 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",
2730
- "aria-label": t("BiChat.Image.Close"),
2731
- type: "button",
2732
- children: /* @__PURE__ */ jsxRuntime.jsx(react.X, { size: 18, weight: "bold" })
2733
- }
2734
- )
2735
- ] }),
2736
- /* @__PURE__ */ jsxRuntime.jsxs(
2737
- "div",
2738
- {
2739
- className: "relative flex-1 flex items-center justify-center min-h-0",
2740
- onClick: (e) => {
2741
- if (e.target === e.currentTarget) onClose();
2742
- },
2743
- children: [
2744
- !isImageLoaded && !imageError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 flex items-center justify-center pointer-events-none", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-3", children: [
2745
- /* @__PURE__ */ jsxRuntime.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" }),
2746
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400 dark:text-gray-500", children: t("BiChat.Loading") })
2747
- ] }) }),
2748
- imageError && /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "alert", className: "flex flex-col items-center justify-center text-center max-w-xs", children: [
2749
- /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(react.ImageBroken, { size: 28, className: "text-gray-400 dark:text-gray-500", weight: "duotone" }) }),
2750
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-gray-700 dark:text-gray-300 mb-1", children: t("BiChat.Image.FailedToLoad") }),
2751
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400 dark:text-gray-500 mb-5 truncate max-w-full", children: attachment.filename }),
2752
- /* @__PURE__ */ jsxRuntime.jsxs(
2753
- "button",
2754
- {
2755
- type: "button",
2756
- onClick: handleRetry,
2757
- 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",
2758
- "aria-label": t("BiChat.Image.Retry"),
2759
- children: [
2760
- /* @__PURE__ */ jsxRuntime.jsx(react.ArrowClockwise, { size: 16, weight: "bold" }),
2761
- t("BiChat.Retry.Label")
2762
- ]
2763
- }
2764
- )
2813
+ /* @__PURE__ */ jsxRuntime.jsxs(
2814
+ react$1.DialogPanel,
2815
+ {
2816
+ className: "fixed inset-0 flex flex-col",
2817
+ style: { zIndex: 1e5 },
2818
+ onMouseMove: handleMouseMove,
2819
+ onMouseUp: handleMouseUp,
2820
+ onMouseLeave: handleMouseUp,
2821
+ children: [
2822
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center px-5 py-3 shrink-0", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [
2823
+ hasMultipleImages && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-white/50 tabular-nums whitespace-nowrap font-medium", children: [
2824
+ currentIndex + 1,
2825
+ " / ",
2826
+ allAttachments?.length
2765
2827
  ] }),
2766
- /* @__PURE__ */ jsxRuntime.jsx(
2767
- "img",
2768
- {
2769
- src: previewUrl,
2770
- alt: attachment.filename,
2771
- className: [
2772
- "max-w-[90vw] max-h-[calc(100vh-120px)] object-contain select-none",
2773
- "transition-opacity duration-300 ease-out",
2774
- isImageLoaded ? "opacity-100" : "opacity-0"
2775
- ].join(" "),
2776
- onLoad: () => setIsImageLoaded(true),
2777
- onError: () => setImageError(true),
2778
- loading: "lazy",
2779
- draggable: false
2780
- },
2781
- retryKey
2782
- ),
2783
- hasMultipleImages && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2784
- /* @__PURE__ */ jsxRuntime.jsx(
2785
- "button",
2786
- {
2787
- onClick: () => onNavigate?.("prev"),
2788
- disabled: !canNavigatePrev || !isImageLoaded || imageError,
2789
- className: [
2790
- "absolute left-3 top-1/2 -translate-y-1/2",
2791
- "flex items-center justify-center w-10 h-10 rounded-md",
2792
- "transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400",
2793
- 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"
2794
- ].join(" "),
2795
- "aria-label": t("BiChat.Image.Previous"),
2796
- type: "button",
2797
- children: /* @__PURE__ */ jsxRuntime.jsx(react.CaretLeft, { size: 20, weight: "bold" })
2798
- }
2799
- ),
2800
- /* @__PURE__ */ jsxRuntime.jsx(
2801
- "button",
2802
- {
2803
- onClick: () => onNavigate?.("next"),
2804
- disabled: !canNavigateNext || !isImageLoaded || imageError,
2805
- className: [
2806
- "absolute right-3 top-1/2 -translate-y-1/2",
2807
- "flex items-center justify-center w-10 h-10 rounded-md",
2808
- "transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400",
2809
- 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"
2810
- ].join(" "),
2811
- "aria-label": t("BiChat.Image.Next"),
2812
- type: "button",
2813
- children: /* @__PURE__ */ jsxRuntime.jsx(react.CaretRight, { size: 20, weight: "bold" })
2814
- }
2815
- )
2816
- ] })
2817
- ]
2818
- }
2819
- )
2820
- ] })
2828
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-white/90 truncate font-medium", children: attachment.filename }),
2829
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-white/40 whitespace-nowrap", children: formatFileSize(attachment.sizeBytes) })
2830
+ ] }) }),
2831
+ /* @__PURE__ */ jsxRuntime.jsxs(
2832
+ "div",
2833
+ {
2834
+ ref: imageAreaRef,
2835
+ className: "relative flex-1 flex items-center justify-center min-h-0 px-4 pb-4",
2836
+ onClick: handleBackdropClick,
2837
+ style: { cursor: isZoomed ? isDragging ? "grabbing" : "grab" : "default" },
2838
+ children: [
2839
+ /* @__PURE__ */ jsxRuntime.jsx(
2840
+ "button",
2841
+ {
2842
+ onClick: onClose,
2843
+ 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",
2844
+ "aria-label": t("BiChat.Image.Close"),
2845
+ type: "button",
2846
+ children: /* @__PURE__ */ jsxRuntime.jsx(react.X, { size: 20, weight: "bold" })
2847
+ }
2848
+ ),
2849
+ !isImageLoaded && !imageError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 flex items-center justify-center pointer-events-none", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-3", children: [
2850
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-8 h-8 border-2 border-white/20 border-t-white/60 rounded-full animate-spin" }),
2851
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-white/40", children: t("BiChat.Loading") })
2852
+ ] }) }),
2853
+ imageError && /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "alert", className: "flex flex-col items-center justify-center text-center max-w-xs", children: [
2854
+ /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(react.ImageBroken, { size: 28, className: "text-white/30", weight: "duotone" }) }),
2855
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-white/70 mb-1", children: t("BiChat.Image.FailedToLoad") }),
2856
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-white/30 mb-5 truncate max-w-full", children: attachment.filename }),
2857
+ /* @__PURE__ */ jsxRuntime.jsxs(
2858
+ "button",
2859
+ {
2860
+ type: "button",
2861
+ onClick: handleRetry,
2862
+ 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",
2863
+ "aria-label": t("BiChat.Image.Retry"),
2864
+ children: [
2865
+ /* @__PURE__ */ jsxRuntime.jsx(react.ArrowClockwise, { size: 16, weight: "bold" }),
2866
+ t("BiChat.Retry.Label")
2867
+ ]
2868
+ }
2869
+ )
2870
+ ] }),
2871
+ /* @__PURE__ */ jsxRuntime.jsx(
2872
+ "img",
2873
+ {
2874
+ src: previewUrl,
2875
+ alt: attachment.filename,
2876
+ className: [
2877
+ "relative z-0 max-w-[85vw] max-h-[calc(100vh-160px)] object-contain select-none rounded-lg",
2878
+ "transition-opacity duration-300 ease-out",
2879
+ isImageLoaded ? "opacity-100" : "opacity-0"
2880
+ ].join(" "),
2881
+ style: {
2882
+ transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`,
2883
+ transformOrigin: "center center",
2884
+ transition: isDragging ? "opacity 0.3s ease-out" : "transform 0.2s ease-out, opacity 0.3s ease-out"
2885
+ },
2886
+ onLoad: () => setIsImageLoaded(true),
2887
+ onError: () => setImageError(true),
2888
+ onMouseDown: handleMouseDown,
2889
+ onDoubleClick: handleDoubleClick,
2890
+ loading: "lazy",
2891
+ draggable: false
2892
+ },
2893
+ retryKey
2894
+ ),
2895
+ hasMultipleImages && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2896
+ /* @__PURE__ */ jsxRuntime.jsx(
2897
+ "button",
2898
+ {
2899
+ onClick: () => onNavigate?.("prev"),
2900
+ disabled: !canNavigatePrev || !isImageLoaded || imageError,
2901
+ className: [
2902
+ "absolute left-4 top-1/2 -translate-y-1/2 z-20",
2903
+ "flex items-center justify-center w-11 h-11 rounded-full",
2904
+ "transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30",
2905
+ 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"
2906
+ ].join(" "),
2907
+ "aria-label": t("BiChat.Image.Previous"),
2908
+ type: "button",
2909
+ children: /* @__PURE__ */ jsxRuntime.jsx(react.CaretLeft, { size: 20, weight: "bold" })
2910
+ }
2911
+ ),
2912
+ /* @__PURE__ */ jsxRuntime.jsx(
2913
+ "button",
2914
+ {
2915
+ onClick: () => onNavigate?.("next"),
2916
+ disabled: !canNavigateNext || !isImageLoaded || imageError,
2917
+ className: [
2918
+ "absolute right-4 top-1/2 -translate-y-1/2 z-20",
2919
+ "flex items-center justify-center w-11 h-11 rounded-full",
2920
+ "transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30",
2921
+ 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"
2922
+ ].join(" "),
2923
+ "aria-label": t("BiChat.Image.Next"),
2924
+ type: "button",
2925
+ children: /* @__PURE__ */ jsxRuntime.jsx(react.CaretRight, { size: 20, weight: "bold" })
2926
+ }
2927
+ )
2928
+ ] }),
2929
+ isImageLoaded && !imageError && /* @__PURE__ */ jsxRuntime.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: [
2930
+ /* @__PURE__ */ jsxRuntime.jsx(
2931
+ "button",
2932
+ {
2933
+ type: "button",
2934
+ onClick: zoomOut,
2935
+ disabled: scale <= MIN_SCALE,
2936
+ 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",
2937
+ "aria-label": t("BiChat.Image.ZoomOut"),
2938
+ children: /* @__PURE__ */ jsxRuntime.jsx(react.MagnifyingGlassMinus, { size: 16, weight: "bold" })
2939
+ }
2940
+ ),
2941
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-white/60 tabular-nums font-medium min-w-[3.5rem] text-center select-none", children: [
2942
+ zoomPercent,
2943
+ "%"
2944
+ ] }),
2945
+ /* @__PURE__ */ jsxRuntime.jsx(
2946
+ "button",
2947
+ {
2948
+ type: "button",
2949
+ onClick: zoomIn,
2950
+ disabled: scale >= MAX_SCALE,
2951
+ 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",
2952
+ "aria-label": t("BiChat.Image.ZoomIn"),
2953
+ children: /* @__PURE__ */ jsxRuntime.jsx(react.MagnifyingGlassPlus, { size: 16, weight: "bold" })
2954
+ }
2955
+ ),
2956
+ isZoomed && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2957
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-px h-4 bg-white/15 mx-1" }),
2958
+ /* @__PURE__ */ jsxRuntime.jsx(
2959
+ "button",
2960
+ {
2961
+ type: "button",
2962
+ onClick: resetZoom,
2963
+ 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",
2964
+ "aria-label": t("BiChat.Image.ResetZoom"),
2965
+ children: /* @__PURE__ */ jsxRuntime.jsx(react.ArrowsIn, { size: 16, weight: "bold" })
2966
+ }
2967
+ )
2968
+ ] })
2969
+ ] })
2970
+ ]
2971
+ }
2972
+ )
2973
+ ]
2974
+ }
2975
+ )
2821
2976
  ] });
2822
2977
  }
2823
2978
  var ImageModal_default = ImageModal;
@@ -2869,6 +3024,7 @@ function UserMessage({
2869
3024
  const [draftContent, setDraftContent] = React.useState("");
2870
3025
  const [isCopied, setIsCopied] = React.useState(false);
2871
3026
  const copyFeedbackTimeoutRef = React.useRef(null);
3027
+ const editTextareaRef = React.useRef(null);
2872
3028
  const classes = mergeClassNames(defaultClassNames, classNameOverrides);
2873
3029
  React.useEffect(() => {
2874
3030
  return () => {
@@ -2878,6 +3034,16 @@ function UserMessage({
2878
3034
  }
2879
3035
  };
2880
3036
  }, []);
3037
+ React.useEffect(() => {
3038
+ if (isEditing && editTextareaRef.current) {
3039
+ const textarea = editTextareaRef.current;
3040
+ textarea.focus();
3041
+ textarea.selectionStart = textarea.value.length;
3042
+ textarea.selectionEnd = textarea.value.length;
3043
+ textarea.style.height = "auto";
3044
+ textarea.style.height = `${Math.min(textarea.scrollHeight, 300)}px`;
3045
+ }
3046
+ }, [isEditing]);
2881
3047
  const normalizedAttachments = turn.attachments.map((attachment) => {
2882
3048
  if (!attachment.mimeType.startsWith("image/")) {
2883
3049
  return attachment;
@@ -2962,6 +3128,21 @@ function UserMessage({
2962
3128
  onEdit(turnId, newContent);
2963
3129
  setIsEditing(false);
2964
3130
  }, [onEdit, turnId, draftContent, turn.content]);
3131
+ const handleEditKeyDown = React.useCallback((e) => {
3132
+ if (e.key === "Escape") {
3133
+ e.preventDefault();
3134
+ handleEditCancel();
3135
+ } else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
3136
+ e.preventDefault();
3137
+ handleEditSave();
3138
+ }
3139
+ }, [handleEditCancel, handleEditSave]);
3140
+ const handleDraftChange = React.useCallback((e) => {
3141
+ setDraftContent(e.target.value);
3142
+ const el = e.target;
3143
+ el.style.height = "auto";
3144
+ el.style.height = `${Math.min(el.scrollHeight, 300)}px`;
3145
+ }, []);
2965
3146
  const handleNavigate = React.useCallback(
2966
3147
  (direction) => {
2967
3148
  if (selectedImageIndex === null) return;
@@ -3012,36 +3193,48 @@ function UserMessage({
3012
3193
  }
3013
3194
  )
3014
3195
  ) }),
3015
- turn.content && /* @__PURE__ */ jsxRuntime.jsx("div", { className: classes.bubble, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: classes.content, children: isEditing ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
3196
+ turn.content && /* @__PURE__ */ jsxRuntime.jsx("div", { className: classes.bubble, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: classes.content, children: isEditing ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
3016
3197
  /* @__PURE__ */ jsxRuntime.jsx(
3017
3198
  "textarea",
3018
3199
  {
3200
+ ref: editTextareaRef,
3019
3201
  value: draftContent,
3020
- onChange: (e) => setDraftContent(e.target.value),
3021
- 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",
3022
- "aria-label": t("BiChat.Message.EditMessage")
3202
+ onChange: handleDraftChange,
3203
+ onKeyDown: handleEditKeyDown,
3204
+ 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",
3205
+ "aria-label": t("BiChat.Message.EditMessage"),
3206
+ rows: 1
3023
3207
  }
3024
3208
  ),
3025
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end gap-2", children: [
3026
- /* @__PURE__ */ jsxRuntime.jsx(
3027
- "button",
3028
- {
3029
- type: "button",
3030
- onClick: handleEditCancel,
3031
- className: "cursor-pointer px-3 py-1.5 rounded-lg bg-white/10 hover:bg-white/15 transition-colors text-sm font-medium",
3032
- children: t("BiChat.Message.Cancel")
3033
- }
3034
- ),
3035
- /* @__PURE__ */ jsxRuntime.jsx(
3036
- "button",
3037
- {
3038
- type: "button",
3039
- onClick: handleEditSave,
3040
- 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",
3041
- disabled: !draftContent.trim() || draftContent === turn.content,
3042
- children: t("BiChat.Message.Save")
3043
- }
3044
- )
3209
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3", children: [
3210
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-[11px] text-white/30 select-none hidden sm:inline", children: [
3211
+ "Esc \xB7 ",
3212
+ typeof navigator !== "undefined" && /mac|iphone|ipad/i.test(
3213
+ navigator.userAgentData?.platform ?? navigator?.platform ?? ""
3214
+ ) ? "\u2318" : "Ctrl",
3215
+ "+Enter"
3216
+ ] }),
3217
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 ml-auto", children: [
3218
+ /* @__PURE__ */ jsxRuntime.jsx(
3219
+ "button",
3220
+ {
3221
+ type: "button",
3222
+ onClick: handleEditCancel,
3223
+ className: "cursor-pointer px-3 py-1.5 rounded-lg text-white/60 hover:text-white hover:bg-white/10 transition-colors text-sm",
3224
+ children: t("BiChat.Message.Cancel")
3225
+ }
3226
+ ),
3227
+ /* @__PURE__ */ jsxRuntime.jsx(
3228
+ "button",
3229
+ {
3230
+ type: "button",
3231
+ onClick: handleEditSave,
3232
+ 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",
3233
+ disabled: !draftContent.trim() || draftContent === turn.content,
3234
+ children: t("BiChat.Message.Save")
3235
+ }
3236
+ )
3237
+ ] })
3045
3238
  ] })
3046
3239
  ] }) : renderSlot(slots?.content, contentSlotProps, turn.content) }) }),
3047
3240
  !hideActions && /* @__PURE__ */ jsxRuntime.jsx("div", { className: `${classes.actions} ${isCopied ? "opacity-100" : ""}`, children: renderSlot(
@@ -3189,67 +3382,295 @@ function StreamingCursor() {
3189
3382
  }
3190
3383
  var StreamingCursor_default = StreamingCursor;
3191
3384
 
3192
- // ui/src/bichat/components/AssistantMessage.tsx
3193
- init_ChartCard();
3385
+ // ui/src/bichat/components/AssistantMessage.tsx
3386
+ init_ChartCard();
3387
+
3388
+ // ui/src/bichat/components/InteractiveTableCard.tsx
3389
+ init_useTranslation();
3390
+ init_TableExportButton();
3391
+ var PAGE_SIZE_OPTIONS = [10, 25, 50, 100, 200];
3392
+ function formatCell(value) {
3393
+ if (value === null || value === void 0) return "NULL";
3394
+ if (typeof value === "object") {
3395
+ try {
3396
+ return JSON.stringify(value);
3397
+ } catch {
3398
+ return String(value);
3399
+ }
3400
+ }
3401
+ return String(value);
3402
+ }
3403
+ var InteractiveTableCard = React.memo(function InteractiveTableCard2({
3404
+ table,
3405
+ onSendMessage,
3406
+ sendDisabled = false
3407
+ }) {
3408
+ const { t } = useTranslation();
3409
+ const defaultPageSize = Math.min(Math.max(table.pageSize || 25, 1), 200);
3410
+ const [page, setPage] = React.useState(1);
3411
+ const [pageSize, setPageSize] = React.useState(defaultPageSize);
3412
+ React.useEffect(() => {
3413
+ const nextPageSize = Math.min(Math.max(table.pageSize || 25, 1), 200);
3414
+ setPage(1);
3415
+ setPageSize(nextPageSize);
3416
+ }, [table.id, table.pageSize]);
3417
+ const totalRows = table.rows.length;
3418
+ const totalPages = Math.max(1, Math.ceil(totalRows / pageSize));
3419
+ React.useEffect(() => {
3420
+ if (page > totalPages) {
3421
+ setPage(totalPages);
3422
+ }
3423
+ }, [page, totalPages]);
3424
+ const pageRows = React.useMemo(() => {
3425
+ const start = (page - 1) * pageSize;
3426
+ return table.rows.slice(start, start + pageSize);
3427
+ }, [page, pageSize, table.rows]);
3428
+ const pageSizeOptions = React.useMemo(() => {
3429
+ const set = /* @__PURE__ */ new Set([...PAGE_SIZE_OPTIONS, defaultPageSize]);
3430
+ return [...set].sort((a, b) => a - b);
3431
+ }, [defaultPageSize]);
3432
+ const canExportViaPrompt = !!onSendMessage && !!table.exportPrompt;
3433
+ const exportDisabled = sendDisabled || !table.export?.url && !canExportViaPrompt;
3434
+ const handleExport = React.useCallback(() => {
3435
+ if (table.export?.url) {
3436
+ try {
3437
+ const parsed = new URL(table.export.url, window.location.origin);
3438
+ if (!["http:", "https:", "blob:"].includes(parsed.protocol)) {
3439
+ console.warn("[InteractiveTableCard] Blocked export URL with unsafe protocol:", parsed.protocol);
3440
+ return;
3441
+ }
3442
+ } catch {
3443
+ console.warn("[InteractiveTableCard] Blocked malformed export URL");
3444
+ return;
3445
+ }
3446
+ const link = document.createElement("a");
3447
+ link.href = table.export.url;
3448
+ link.download = table.export.filename || "table_export.xlsx";
3449
+ link.rel = "noopener noreferrer";
3450
+ document.body.appendChild(link);
3451
+ link.click();
3452
+ document.body.removeChild(link);
3453
+ return;
3454
+ }
3455
+ if (canExportViaPrompt && table.exportPrompt) {
3456
+ onSendMessage?.(table.exportPrompt);
3457
+ }
3458
+ }, [canExportViaPrompt, onSendMessage, table.export, table.exportPrompt]);
3459
+ const from = totalRows === 0 ? 0 : (page - 1) * pageSize + 1;
3460
+ const to = Math.min(page * pageSize, totalRows);
3461
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "w-full rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900/40", children: [
3462
+ /* @__PURE__ */ jsxRuntime.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: [
3463
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0", children: [
3464
+ /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "truncate text-sm font-semibold text-gray-900 dark:text-gray-100", children: table.title || t("BiChat.Table.QueryResults") }),
3465
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: [
3466
+ totalRows === 1 ? t("BiChat.Table.OneRowLoaded") : t("BiChat.Table.RowsLoaded", { count: String(totalRows) }),
3467
+ table.truncated ? ` ${t("BiChat.Table.TruncatedSuffix")}` : ""
3468
+ ] })
3469
+ ] }),
3470
+ /* @__PURE__ */ jsxRuntime.jsx(
3471
+ exports.TableExportButton,
3472
+ {
3473
+ onClick: handleExport,
3474
+ disabled: exportDisabled,
3475
+ label: t("BiChat.Table.ExportToExcel"),
3476
+ disabledTooltip: sendDisabled ? t("BiChat.Table.PleaseWait") : t("BiChat.Table.ExportUnavailable")
3477
+ }
3478
+ )
3479
+ ] }),
3480
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "max-h-[420px] overflow-auto", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "w-full border-collapse text-sm", children: [
3481
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { className: "sticky top-0 bg-gray-100 dark:bg-gray-800 z-10", children: /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "border-b border-gray-200 dark:border-gray-700", children: table.headers.map((header, index) => /* @__PURE__ */ jsxRuntime.jsx(
3482
+ "th",
3483
+ {
3484
+ className: "px-3 py-2 text-left font-semibold text-gray-700 dark:text-gray-200 whitespace-nowrap",
3485
+ children: header
3486
+ },
3487
+ `${table.id}-header-${index}`
3488
+ )) }) }),
3489
+ /* @__PURE__ */ jsxRuntime.jsxs("tbody", { children: [
3490
+ pageRows.map((row, rowIndex) => /* @__PURE__ */ jsxRuntime.jsx(
3491
+ "tr",
3492
+ {
3493
+ className: "border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-800/40",
3494
+ children: table.columns.map((_, columnIndex) => /* @__PURE__ */ jsxRuntime.jsx(
3495
+ "td",
3496
+ {
3497
+ className: "px-3 py-2 text-gray-700 dark:text-gray-300 align-top",
3498
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block max-w-[420px] truncate", title: formatCell(row[columnIndex]), children: formatCell(row[columnIndex]) })
3499
+ },
3500
+ `${table.id}-cell-${rowIndex}-${columnIndex}`
3501
+ ))
3502
+ },
3503
+ `${table.id}-row-${rowIndex}`
3504
+ )),
3505
+ pageRows.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(
3506
+ "td",
3507
+ {
3508
+ colSpan: table.columns.length,
3509
+ className: "px-3 py-6 text-center text-sm text-gray-500 dark:text-gray-400",
3510
+ children: t("BiChat.Table.NoRows")
3511
+ }
3512
+ ) })
3513
+ ] })
3514
+ ] }) }),
3515
+ /* @__PURE__ */ jsxRuntime.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: [
3516
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-500 dark:text-gray-400", children: t("BiChat.Table.Showing", {
3517
+ from: String(from),
3518
+ to: String(to),
3519
+ total: String(totalRows)
3520
+ }) }),
3521
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
3522
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs text-gray-500 dark:text-gray-400", htmlFor: `${table.id}-page-size`, children: t("BiChat.Table.RowsLabel") }),
3523
+ /* @__PURE__ */ jsxRuntime.jsx(
3524
+ "select",
3525
+ {
3526
+ id: `${table.id}-page-size`,
3527
+ value: pageSize,
3528
+ onChange: (event) => {
3529
+ setPageSize(Number(event.target.value));
3530
+ setPage(1);
3531
+ },
3532
+ 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",
3533
+ children: pageSizeOptions.map((option) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: option, children: option }, `${table.id}-size-${option}`))
3534
+ }
3535
+ ),
3536
+ /* @__PURE__ */ jsxRuntime.jsx(
3537
+ "button",
3538
+ {
3539
+ type: "button",
3540
+ onClick: () => setPage((current) => Math.max(1, current - 1)),
3541
+ disabled: page <= 1,
3542
+ 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",
3543
+ children: t("BiChat.Table.Prev")
3544
+ }
3545
+ ),
3546
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-500 dark:text-gray-400", children: t("BiChat.Table.PageOf", { page: String(page), total: String(totalPages) }) }),
3547
+ /* @__PURE__ */ jsxRuntime.jsx(
3548
+ "button",
3549
+ {
3550
+ type: "button",
3551
+ onClick: () => setPage((current) => Math.min(totalPages, current + 1)),
3552
+ disabled: page >= totalPages,
3553
+ 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",
3554
+ children: t("BiChat.Table.Next")
3555
+ }
3556
+ )
3557
+ ] })
3558
+ ] }),
3559
+ table.truncated && /* @__PURE__ */ jsxRuntime.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") })
3560
+ ] });
3561
+ });
3562
+
3563
+ // ui/src/bichat/components/SourcesPanel.tsx
3564
+ init_useTranslation();
3565
+ function extractDomain(url) {
3566
+ try {
3567
+ return new URL(url).hostname.replace(/^www\./, "");
3568
+ } catch {
3569
+ return "";
3570
+ }
3571
+ }
3572
+ var PALETTE = [
3573
+ "#c0392b",
3574
+ "#d35400",
3575
+ "#f39c12",
3576
+ "#27ae60",
3577
+ "#16a085",
3578
+ "#2980b9",
3579
+ "#8e44ad",
3580
+ "#d63384"
3581
+ ];
3582
+ function domainColor(domain) {
3583
+ let h = 0;
3584
+ for (let i = 0; i < domain.length; i++) h = domain.charCodeAt(i) + ((h << 5) - h);
3585
+ return PALETTE[Math.abs(h) % PALETTE.length];
3586
+ }
3194
3587
  function SourcesPanel({ citations }) {
3195
- if (!citations || citations.length === 0) {
3196
- return null;
3588
+ const { t } = useTranslation();
3589
+ const [isOpen, setIsOpen] = React.useState(false);
3590
+ const open = React.useCallback(() => setIsOpen(true), []);
3591
+ const close = React.useCallback(() => setIsOpen(false), []);
3592
+ if (!citations?.length) return null;
3593
+ const domains = [...new Set(
3594
+ citations.filter((c) => c.url).map((c) => extractDomain(c.url)).filter(Boolean)
3595
+ )];
3596
+ const previewDomains = domains.slice(0, 5);
3597
+ if (!isOpen) {
3598
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-3", children: /* @__PURE__ */ jsxRuntime.jsxs(
3599
+ "button",
3600
+ {
3601
+ type: "button",
3602
+ onClick: open,
3603
+ 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",
3604
+ children: [
3605
+ previewDomains.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex -space-x-1.5", children: previewDomains.map((domain, i) => /* @__PURE__ */ jsxRuntime.jsx(
3606
+ "span",
3607
+ {
3608
+ 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",
3609
+ style: { backgroundColor: domainColor(domain), zIndex: previewDomains.length - i },
3610
+ "aria-hidden": "true",
3611
+ children: domain[0]?.toUpperCase()
3612
+ },
3613
+ domain
3614
+ )) }),
3615
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs font-medium text-gray-600 dark:text-gray-300 tabular-nums", children: [
3616
+ citations.length,
3617
+ " ",
3618
+ t(citations.length === 1 ? "BiChat.Sources.Source" : "BiChat.Sources.Sources")
3619
+ ] })
3620
+ ]
3621
+ }
3622
+ ) });
3197
3623
  }
3198
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-4 border-t border-[var(--bichat-border)] pt-3", children: /* @__PURE__ */ jsxRuntime.jsx(react$1.Disclosure, { children: ({ open }) => /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3199
- /* @__PURE__ */ jsxRuntime.jsxs(react$1.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: [
3624
+ return /* @__PURE__ */ jsxRuntime.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: [
3625
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-4 py-3", children: [
3626
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: t("BiChat.Sources.Title") }),
3200
3627
  /* @__PURE__ */ jsxRuntime.jsx(
3201
- "svg",
3628
+ "button",
3202
3629
  {
3203
- className: `w-4 h-4 transition-transform duration-150 ${open ? "rotate-90" : ""}`,
3204
- fill: "none",
3205
- stroke: "currentColor",
3206
- viewBox: "0 0 24 24",
3207
- children: /* @__PURE__ */ jsxRuntime.jsx(
3208
- "path",
3209
- {
3210
- strokeLinecap: "round",
3211
- strokeLinejoin: "round",
3212
- strokeWidth: 2,
3213
- d: "M9 5l7 7-7 7"
3214
- }
3215
- )
3630
+ type: "button",
3631
+ onClick: close,
3632
+ 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",
3633
+ "aria-label": t("BiChat.Sources.Close"),
3634
+ children: /* @__PURE__ */ jsxRuntime.jsx(react.X, { size: 14, weight: "bold" })
3216
3635
  }
3217
- ),
3218
- /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
3219
- citations.length,
3220
- " ",
3221
- citations.length === 1 ? "source" : "sources"
3222
- ] })
3636
+ )
3223
3637
  ] }),
3224
- /* @__PURE__ */ jsxRuntime.jsx(react$1.DisclosurePanel, { className: "mt-2 space-y-2", children: citations.map((citation, index) => /* @__PURE__ */ jsxRuntime.jsx(
3225
- "div",
3226
- {
3227
- className: "p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg text-sm",
3228
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2", children: [
3229
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-shrink-0 w-5 h-5 bg-[var(--bichat-primary)] text-white rounded-full flex items-center justify-center text-xs", children: index + 1 }),
3230
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
3231
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-medium text-gray-900 dark:text-gray-100", children: citation.title }),
3232
- citation.url && /* @__PURE__ */ jsxRuntime.jsx(
3233
- "a",
3234
- {
3235
- href: citation.url,
3236
- target: "_blank",
3237
- rel: "noopener noreferrer",
3238
- className: "text-[var(--bichat-primary)] hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50 rounded",
3239
- children: citation.url
3240
- }
3241
- ),
3242
- citation.excerpt && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-1 text-gray-600 dark:text-gray-400 italic", children: [
3243
- '"',
3244
- citation.excerpt,
3245
- '"'
3246
- ] })
3247
- ] })
3638
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "max-h-80 overflow-y-auto", children: citations.map((citation, index) => {
3639
+ const domain = citation.url ? extractDomain(citation.url) : "";
3640
+ const cardContent = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3641
+ /* @__PURE__ */ jsxRuntime.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) }) }),
3642
+ citation.excerpt && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 text-xs text-gray-500 dark:text-gray-400 line-clamp-2 leading-relaxed", children: citation.excerpt }),
3643
+ domain && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 mt-1.5", children: [
3644
+ /* @__PURE__ */ jsxRuntime.jsx(
3645
+ "span",
3646
+ {
3647
+ className: "w-4 h-4 rounded-full flex items-center justify-center text-[7px] font-bold text-white flex-shrink-0 select-none",
3648
+ style: { backgroundColor: domainColor(domain) },
3649
+ "aria-hidden": "true",
3650
+ children: domain[0]?.toUpperCase()
3651
+ }
3652
+ ),
3653
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[11px] text-gray-400 dark:text-gray-500 truncate", children: domain })
3248
3654
  ] })
3249
- },
3250
- citation.id
3251
- )) })
3252
- ] }) }) });
3655
+ ] });
3656
+ const cardClass = "block px-4 py-3 border-t border-gray-100 dark:border-gray-700/50";
3657
+ if (citation.url) {
3658
+ return /* @__PURE__ */ jsxRuntime.jsx(
3659
+ "a",
3660
+ {
3661
+ href: citation.url,
3662
+ target: "_blank",
3663
+ rel: "noopener noreferrer",
3664
+ className: `${cardClass} hover:bg-gray-50 dark:hover:bg-gray-700/40 transition-colors duration-100
3665
+ focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--bichat-primary)]/40`,
3666
+ children: cardContent
3667
+ },
3668
+ citation.id
3669
+ );
3670
+ }
3671
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cardClass, children: cardContent }, citation.id);
3672
+ }) })
3673
+ ] });
3253
3674
  }
3254
3675
  var MIME_BY_TYPE = {
3255
3676
  excel: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
@@ -3601,6 +4022,59 @@ function InlineQuestionForm({ pendingQuestion }) {
3601
4022
  ] }) });
3602
4023
  }
3603
4024
 
4025
+ // ui/src/bichat/components/RetryActionArea.tsx
4026
+ init_useTranslation();
4027
+ var RetryActionArea = React.memo(function RetryActionArea2({
4028
+ onRetry
4029
+ }) {
4030
+ const { t } = useTranslation();
4031
+ return (
4032
+ // Wrapper matches TurnBubble layout for assistant messages (justify-start = left-aligned)
4033
+ /* @__PURE__ */ jsxRuntime.jsx(
4034
+ framerMotion.motion.div,
4035
+ {
4036
+ initial: { opacity: 0, y: 10 },
4037
+ animate: { opacity: 1, y: 0 },
4038
+ exit: { opacity: 0, y: -10 },
4039
+ transition: { duration: 0.2 },
4040
+ className: "flex justify-start",
4041
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
4042
+ "div",
4043
+ {
4044
+ 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",
4045
+ role: "status",
4046
+ "aria-live": "polite",
4047
+ children: [
4048
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
4049
+ /* @__PURE__ */ jsxRuntime.jsx(
4050
+ react.Warning,
4051
+ {
4052
+ className: "w-5 h-5 text-amber-500 dark:text-amber-400 flex-shrink-0",
4053
+ weight: "fill"
4054
+ }
4055
+ ),
4056
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-700 dark:text-gray-300", children: t("BiChat.Retry.Subtitle") })
4057
+ ] }),
4058
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsxRuntime.jsxs(
4059
+ "button",
4060
+ {
4061
+ onClick: onRetry,
4062
+ 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",
4063
+ "aria-label": t("BiChat.Retry.Title"),
4064
+ children: [
4065
+ /* @__PURE__ */ jsxRuntime.jsx(react.ArrowClockwise, { size: 16, className: "w-4 h-4" }),
4066
+ t("BiChat.Retry.Button")
4067
+ ]
4068
+ }
4069
+ ) })
4070
+ ]
4071
+ }
4072
+ )
4073
+ }
4074
+ )
4075
+ );
4076
+ });
4077
+
3604
4078
  // ui/src/bichat/utils/debugMetrics.ts
3605
4079
  function formatGenerationDuration(generationMs) {
3606
4080
  return generationMs > 1e3 ? `${(generationMs / 1e3).toFixed(2)}s` : `${generationMs}ms`;
@@ -3899,6 +4373,7 @@ var defaultClassNames2 = {
3899
4373
  bubble: "bg-white dark:bg-gray-800 rounded-2xl rounded-bl-sm px-4 py-3 shadow-sm",
3900
4374
  codeOutputs: "",
3901
4375
  charts: "mb-1 w-full",
4376
+ tables: "mb-1 flex flex-col gap-3",
3902
4377
  artifacts: "mb-1 flex flex-wrap gap-2",
3903
4378
  sources: "",
3904
4379
  explanation: "mt-4 border-t border-gray-100 dark:border-gray-700 pt-4",
@@ -3915,6 +4390,7 @@ function mergeClassNames2(defaults, overrides) {
3915
4390
  bubble: overrides.bubble ?? defaults.bubble,
3916
4391
  codeOutputs: overrides.codeOutputs ?? defaults.codeOutputs,
3917
4392
  charts: overrides.charts ?? defaults.charts,
4393
+ tables: overrides.tables ?? defaults.tables,
3918
4394
  artifacts: overrides.artifacts ?? defaults.artifacts,
3919
4395
  sources: overrides.sources ?? defaults.sources,
3920
4396
  explanation: overrides.explanation ?? defaults.explanation,
@@ -3959,6 +4435,14 @@ function AssistantMessage({
3959
4435
  const hasContent = turn.content?.trim().length > 0;
3960
4436
  const hasExplanation = !!turn.explanation?.trim();
3961
4437
  const hasPendingQuestion = !!pendingQuestion && pendingQuestion.status === "PENDING" && pendingQuestion.turnId === turnId;
4438
+ const hasCodeOutputs = !!turn.codeOutputs?.length;
4439
+ const hasChart = !!turn.chartData;
4440
+ const hasTables = !!turn.renderTables?.length;
4441
+ const hasArtifacts = !!turn.artifacts?.length;
4442
+ const hasDebug = showDebug && !!turn.debug;
4443
+ const hasRenderablePayload = hasContent || hasExplanation || hasPendingQuestion || hasCodeOutputs || hasChart || hasTables || hasArtifacts || hasDebug;
4444
+ const canRegenerate = !!onRegenerate && !!turnId && !isSystemMessage && isLastTurn;
4445
+ const showInlineRetry = !hasRenderablePayload && canRegenerate;
3962
4446
  const handleCopyClick = React.useCallback(async () => {
3963
4447
  try {
3964
4448
  if (onCopy) {
@@ -4000,15 +4484,18 @@ function AssistantMessage({
4000
4484
  const codeOutputsSlotProps = {
4001
4485
  outputs: turn.codeOutputs || []
4002
4486
  };
4487
+ const tablesSlotProps = {
4488
+ tables: turn.renderTables || []
4489
+ };
4003
4490
  const artifactsSlotProps = {
4004
4491
  artifacts: turn.artifacts || []
4005
4492
  };
4006
4493
  const actionsSlotProps = {
4007
4494
  onCopy: handleCopyClick,
4008
- onRegenerate: onRegenerate && turnId && !isSystemMessage && isLastTurn ? handleRegenerateClick : void 0,
4495
+ onRegenerate: canRegenerate ? handleRegenerateClick : void 0,
4009
4496
  timestamp,
4010
4497
  canCopy: hasContent,
4011
- canRegenerate: !!onRegenerate && !!turnId && !isSystemMessage && isLastTurn
4498
+ canRegenerate
4012
4499
  };
4013
4500
  const explanationSlotProps = {
4014
4501
  explanation: turn.explanation || "",
@@ -4021,14 +4508,30 @@ function AssistantMessage({
4021
4508
  return slot;
4022
4509
  };
4023
4510
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classes.root, children: [
4024
- !hideAvatar && /* @__PURE__ */ jsxRuntime.jsx("div", { className: avatarClassName, children: renderSlot(slots?.avatar, avatarSlotProps, isSystemMessage ? "SYS" : "AI") }),
4511
+ !hideAvatar && !showInlineRetry && /* @__PURE__ */ jsxRuntime.jsx("div", { className: avatarClassName, children: renderSlot(slots?.avatar, avatarSlotProps, isSystemMessage ? "SYS" : "AI") }),
4025
4512
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classes.wrapper, children: [
4513
+ showInlineRetry && /* @__PURE__ */ jsxRuntime.jsx(RetryActionArea, { onRetry: () => {
4514
+ void handleRegenerateClick();
4515
+ } }),
4026
4516
  turn.codeOutputs && turn.codeOutputs.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: classes.codeOutputs, children: renderSlot(
4027
4517
  slots?.codeOutputs,
4028
4518
  codeOutputsSlotProps,
4029
4519
  /* @__PURE__ */ jsxRuntime.jsx(CodeOutputsPanel_default, { outputs: turn.codeOutputs })
4030
4520
  ) }),
4031
4521
  turn.chartData && /* @__PURE__ */ jsxRuntime.jsx("div", { className: classes.charts, children: renderSlot(slots?.charts, chartsSlotProps, /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { chartData: turn.chartData })) }),
4522
+ turn.renderTables && turn.renderTables.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: classes.tables, children: renderSlot(
4523
+ slots?.tables,
4524
+ tablesSlotProps,
4525
+ turn.renderTables.map((table) => /* @__PURE__ */ jsxRuntime.jsx(
4526
+ InteractiveTableCard,
4527
+ {
4528
+ table,
4529
+ onSendMessage,
4530
+ sendDisabled: sendDisabled || isStreaming
4531
+ },
4532
+ table.id
4533
+ ))
4534
+ ) }),
4032
4535
  hasContent && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: bubbleClassName, children: [
4033
4536
  renderSlot(
4034
4537
  slots?.content,
@@ -4118,7 +4621,7 @@ function AssistantMessage({
4118
4621
  children: isCopied ? /* @__PURE__ */ jsxRuntime.jsx(react.Check, { size: 14, weight: "bold" }) : /* @__PURE__ */ jsxRuntime.jsx(react.Copy, { size: 14, weight: "regular" })
4119
4622
  }
4120
4623
  ),
4121
- onRegenerate && turnId && !isSystemMessage && isLastTurn && /* @__PURE__ */ jsxRuntime.jsx(
4624
+ canRegenerate && /* @__PURE__ */ jsxRuntime.jsx(
4122
4625
  "button",
4123
4626
  {
4124
4627
  onClick: handleRegenerateClick,
@@ -9240,6 +9743,10 @@ function ErrorAlert({ error }) {
9240
9743
  );
9241
9744
  }
9242
9745
  var COLLAPSE_STORAGE_KEY = "bichat-sidebar-collapsed";
9746
+ var SESSION_RECONCILE_POLL_INTERVAL_MS = 2e3;
9747
+ var SESSION_RECONCILE_MAX_POLLS = 30;
9748
+ var ACTIVE_SESSION_MISS_MAX_RETRIES = 8;
9749
+ var ACTIVE_SESSION_MISS_RETRY_DELAY_MS = 1e3;
9243
9750
  function useSidebarCollapse() {
9244
9751
  const [isCollapsed, setIsCollapsed] = React.useState(() => {
9245
9752
  try {
@@ -9297,7 +9804,7 @@ function Sidebar2({
9297
9804
  const shouldReduceMotion = framerMotion.useReducedMotion();
9298
9805
  const sessionListRef = React.useRef(null);
9299
9806
  const searchContainerRef = React.useRef(null);
9300
- const refreshForActiveSessionRef = React.useRef(null);
9807
+ const activeSessionMissRetriesRef = React.useRef({});
9301
9808
  const { isCollapsed, toggle, collapse } = useSidebarCollapse();
9302
9809
  const collapsible = !onClose;
9303
9810
  const handleSidebarClick = React.useCallback(
@@ -9341,6 +9848,7 @@ function Sidebar2({
9341
9848
  const [actionError, setActionError] = React.useState(null);
9342
9849
  const accessDenied = loadError?.isPermissionDenied === true;
9343
9850
  const [refreshKey, setRefreshKey] = React.useState(0);
9851
+ const [reconcilePollToken, setReconcilePollToken] = React.useState(0);
9344
9852
  const [showConfirm, setShowConfirm] = React.useState(false);
9345
9853
  const [sessionToArchive, setSessionToArchive] = React.useState(null);
9346
9854
  const fetchSessions = React.useCallback(async () => {
@@ -9361,8 +9869,13 @@ function Sidebar2({
9361
9869
  fetchSessions();
9362
9870
  }, [fetchSessions, refreshKey]);
9363
9871
  React.useEffect(() => {
9364
- const handleSessionsUpdated = () => {
9872
+ const handleSessionsUpdated = (event) => {
9365
9873
  setRefreshKey((k) => k + 1);
9874
+ const detail = event.detail;
9875
+ const reason = detail?.reason;
9876
+ if (!reason || reason === "session_created" || reason === "message_sent" || reason === "title_regenerate_requested") {
9877
+ setReconcilePollToken((k) => k + 1);
9878
+ }
9366
9879
  };
9367
9880
  window.addEventListener("bichat:sessions-updated", handleSessionsUpdated);
9368
9881
  return () => {
@@ -9370,31 +9883,33 @@ function Sidebar2({
9370
9883
  };
9371
9884
  }, []);
9372
9885
  React.useEffect(() => {
9373
- if (!activeSessionId) {
9374
- refreshForActiveSessionRef.current = null;
9375
- return;
9376
- }
9886
+ activeSessionMissRetriesRef.current = {};
9887
+ }, [activeSessionId]);
9888
+ React.useEffect(() => {
9889
+ if (!activeSessionId) return;
9377
9890
  if (loading) return;
9378
9891
  const hasActiveSession = sessions.some((session) => session.id === activeSessionId);
9379
9892
  if (hasActiveSession) {
9380
- if (refreshForActiveSessionRef.current === activeSessionId) {
9381
- refreshForActiveSessionRef.current = null;
9382
- }
9893
+ delete activeSessionMissRetriesRef.current[activeSessionId];
9383
9894
  return;
9384
9895
  }
9385
- if (refreshForActiveSessionRef.current !== activeSessionId) {
9386
- refreshForActiveSessionRef.current = activeSessionId;
9387
- setRefreshKey((k) => k + 1);
9896
+ const attempts = activeSessionMissRetriesRef.current[activeSessionId] ?? 0;
9897
+ if (attempts >= ACTIVE_SESSION_MISS_MAX_RETRIES) {
9898
+ return;
9388
9899
  }
9900
+ activeSessionMissRetriesRef.current[activeSessionId] = attempts + 1;
9901
+ const timeoutId = window.setTimeout(() => {
9902
+ setRefreshKey((k) => k + 1);
9903
+ setReconcilePollToken((k) => k + 1);
9904
+ }, ACTIVE_SESSION_MISS_RETRY_DELAY_MS);
9905
+ return () => window.clearTimeout(timeoutId);
9389
9906
  }, [activeSessionId, loading, sessions]);
9390
9907
  const hasPlaceholderTitles = React.useMemo(() => {
9391
9908
  const newChatLabel = t("BiChat.Chat.NewChat");
9392
9909
  return Array.isArray(sessions) && sessions.some((s) => s && (!s.title || s.title === newChatLabel));
9393
9910
  }, [sessions, t]);
9394
9911
  React.useEffect(() => {
9395
- if (!hasPlaceholderTitles) return;
9396
- const pollInterval = 2e3;
9397
- const maxPolls = 5;
9912
+ if (!hasPlaceholderTitles && reconcilePollToken === 0) return;
9398
9913
  let pollCount = 0;
9399
9914
  const intervalId = setInterval(async () => {
9400
9915
  pollCount++;
@@ -9403,12 +9918,12 @@ function Sidebar2({
9403
9918
  setSessions(result.sessions);
9404
9919
  } catch {
9405
9920
  }
9406
- if (pollCount >= maxPolls) {
9921
+ if (pollCount >= SESSION_RECONCILE_MAX_POLLS) {
9407
9922
  clearInterval(intervalId);
9408
9923
  }
9409
- }, pollInterval);
9924
+ }, SESSION_RECONCILE_POLL_INTERVAL_MS);
9410
9925
  return () => clearInterval(intervalId);
9411
- }, [hasPlaceholderTitles, dataSource]);
9926
+ }, [hasPlaceholderTitles, dataSource, reconcilePollToken]);
9412
9927
  const handleArchiveRequest = (sessionId) => {
9413
9928
  setSessionToArchive(sessionId);
9414
9929
  setShowConfirm(true);
@@ -9466,7 +9981,9 @@ function Sidebar2({
9466
9981
  try {
9467
9982
  await dataSource.regenerateSessionTitle(sessionId);
9468
9983
  toast.success(t("BiChat.Sidebar.TitleRegenerated"));
9469
- setRefreshKey((k) => k + 1);
9984
+ window.dispatchEvent(new CustomEvent("bichat:sessions-updated", {
9985
+ detail: { reason: "title_regenerate_requested", sessionId }
9986
+ }));
9470
9987
  } catch (err) {
9471
9988
  console.error("Failed to regenerate title:", err);
9472
9989
  const display = toErrorDisplay(err, t("BiChat.Sidebar.FailedToRegenerateTitle"));
@@ -10279,59 +10796,6 @@ function BiChatLayout({
10279
10796
  ] })
10280
10797
  ] });
10281
10798
  }
10282
-
10283
- // ui/src/bichat/components/RetryActionArea.tsx
10284
- init_useTranslation();
10285
- var RetryActionArea = React.memo(function RetryActionArea2({
10286
- onRetry
10287
- }) {
10288
- const { t } = useTranslation();
10289
- return (
10290
- // Wrapper matches TurnBubble layout for assistant messages (justify-start = left-aligned)
10291
- /* @__PURE__ */ jsxRuntime.jsx(
10292
- framerMotion.motion.div,
10293
- {
10294
- initial: { opacity: 0, y: 10 },
10295
- animate: { opacity: 1, y: 0 },
10296
- exit: { opacity: 0, y: -10 },
10297
- transition: { duration: 0.2 },
10298
- className: "flex justify-start",
10299
- children: /* @__PURE__ */ jsxRuntime.jsxs(
10300
- "div",
10301
- {
10302
- 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",
10303
- role: "status",
10304
- "aria-live": "polite",
10305
- children: [
10306
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
10307
- /* @__PURE__ */ jsxRuntime.jsx(
10308
- react.Warning,
10309
- {
10310
- className: "w-5 h-5 text-amber-500 dark:text-amber-400 flex-shrink-0",
10311
- weight: "fill"
10312
- }
10313
- ),
10314
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-700 dark:text-gray-300", children: t("BiChat.Retry.Subtitle") })
10315
- ] }),
10316
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsxRuntime.jsxs(
10317
- "button",
10318
- {
10319
- onClick: onRetry,
10320
- 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",
10321
- "aria-label": t("BiChat.Retry.Title"),
10322
- children: [
10323
- /* @__PURE__ */ jsxRuntime.jsx(react.ArrowClockwise, { size: 16, className: "w-4 h-4" }),
10324
- t("BiChat.Retry.Button")
10325
- ]
10326
- }
10327
- ) })
10328
- ]
10329
- }
10330
- )
10331
- }
10332
- )
10333
- );
10334
- });
10335
10799
  init_useTranslation();
10336
10800
  function MessageActions({
10337
10801
  message,
@@ -12149,6 +12613,75 @@ function toStreamEvent(chunk) {
12149
12613
  }
12150
12614
  }
12151
12615
 
12616
+ // ui/src/bichat/utils/tableSpec.ts
12617
+ function isRecord2(value) {
12618
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12619
+ }
12620
+ function readString(value) {
12621
+ if (typeof value !== "string") return null;
12622
+ const trimmed = value.trim();
12623
+ return trimmed.length > 0 ? trimmed : null;
12624
+ }
12625
+ function readPositiveInteger(value) {
12626
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
12627
+ const n = Math.floor(value);
12628
+ return n > 0 ? n : null;
12629
+ }
12630
+ function normalizeRows(value) {
12631
+ if (!Array.isArray(value)) return [];
12632
+ const rows = [];
12633
+ for (const row of value) {
12634
+ if (!Array.isArray(row)) continue;
12635
+ rows.push(row);
12636
+ }
12637
+ return rows;
12638
+ }
12639
+ function parseExport(value) {
12640
+ if (!isRecord2(value)) return void 0;
12641
+ const url = readString(value.url);
12642
+ if (!url) return void 0;
12643
+ return {
12644
+ url,
12645
+ filename: readString(value.filename) || "table_export.xlsx",
12646
+ rowCount: readPositiveInteger(value.row_count) || readPositiveInteger(value.rowCount) || void 0,
12647
+ fileSizeKB: readPositiveInteger(value.file_size_kb) || readPositiveInteger(value.fileSizeKB) || void 0
12648
+ };
12649
+ }
12650
+ function parseRenderTableDataFromJsonString(json, fallbackId) {
12651
+ const trimmed = json.trim();
12652
+ if (!trimmed) return null;
12653
+ let parsed;
12654
+ try {
12655
+ parsed = JSON.parse(trimmed);
12656
+ } catch {
12657
+ return null;
12658
+ }
12659
+ if (!isRecord2(parsed)) return null;
12660
+ const columns = Array.isArray(parsed.columns) ? parsed.columns.map((column) => readString(column)).filter((column) => column !== null) : [];
12661
+ if (columns.length === 0) return null;
12662
+ const rows = normalizeRows(parsed.rows);
12663
+ const headersRaw = Array.isArray(parsed.headers) ? parsed.headers.map((header) => readString(header)).filter((header) => header !== null) : [];
12664
+ const headers = headersRaw.length === columns.length ? headersRaw : columns;
12665
+ const totalRows = readPositiveInteger(parsed.total_rows) || readPositiveInteger(parsed.totalRows) || rows.length;
12666
+ const pageSize = readPositiveInteger(parsed.page_size) || readPositiveInteger(parsed.pageSize) || 25;
12667
+ const query = readString(parsed.query) || readString(parsed.sql);
12668
+ if (!query) return null;
12669
+ return {
12670
+ id: readString(parsed.id) || fallbackId,
12671
+ title: readString(parsed.title) || void 0,
12672
+ query,
12673
+ columns,
12674
+ headers,
12675
+ rows,
12676
+ totalRows,
12677
+ pageSize,
12678
+ truncated: parsed.truncated === true,
12679
+ truncatedReason: readString(parsed.truncated_reason) || readString(parsed.truncatedReason) || void 0,
12680
+ export: parseExport(parsed.export),
12681
+ exportPrompt: readString(parsed.export_prompt) || readString(parsed.exportPrompt) || void 0
12682
+ };
12683
+ }
12684
+
12152
12685
  // ui/src/bichat/data/HttpDataSource.ts
12153
12686
  function isSessionNotFoundError(err) {
12154
12687
  if (!(err instanceof AppletRPCException)) return false;
@@ -12179,7 +12712,7 @@ function toSessionArtifact(artifact) {
12179
12712
  function warnMalformedSessionPayload(message, details) {
12180
12713
  console.warn(`[BiChat] ${message}`, details || {});
12181
12714
  }
12182
- function readString(value, fallback = "") {
12715
+ function readString2(value, fallback = "") {
12183
12716
  return typeof value === "string" ? value : fallback;
12184
12717
  }
12185
12718
  function readNonEmptyString(value) {
@@ -12193,12 +12726,52 @@ function readFiniteNumber(value, fallback = 0) {
12193
12726
  function readOptionalFiniteNumber(value) {
12194
12727
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
12195
12728
  }
12729
+ var MIME_TO_EXTENSION = {
12730
+ "image/jpeg": "jpg",
12731
+ "image/png": "png",
12732
+ "image/gif": "gif",
12733
+ "application/pdf": "pdf"
12734
+ };
12735
+ var SAFE_AUTOCORRECT_MIME_TYPES = new Set(Object.keys(MIME_TO_EXTENSION));
12736
+ function detectMimeFromSignature(bytes) {
12737
+ if (bytes.length >= 8) {
12738
+ 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;
12739
+ if (isPng) return "image/png";
12740
+ }
12741
+ if (bytes.length >= 3) {
12742
+ const isJpeg = bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255;
12743
+ if (isJpeg) return "image/jpeg";
12744
+ }
12745
+ if (bytes.length >= 6) {
12746
+ const isGif = bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56 && (bytes[4] === 55 || bytes[4] === 57) && bytes[5] === 97;
12747
+ if (isGif) return "image/gif";
12748
+ }
12749
+ if (bytes.length >= 4) {
12750
+ const isPdf = bytes[0] === 37 && bytes[1] === 80 && bytes[2] === 68 && bytes[3] === 70;
12751
+ if (isPdf) return "application/pdf";
12752
+ }
12753
+ return void 0;
12754
+ }
12755
+ function normalizeFilenameForMime(filename, mimeType) {
12756
+ const expectedExt = MIME_TO_EXTENSION[mimeType];
12757
+ if (!expectedExt) return filename;
12758
+ const lower = filename.toLowerCase();
12759
+ if (mimeType === "image/jpeg" && (lower.endsWith(".jpg") || lower.endsWith(".jpeg"))) {
12760
+ return filename;
12761
+ }
12762
+ if (lower.endsWith(`.${expectedExt}`)) {
12763
+ return filename;
12764
+ }
12765
+ const dotIndex = filename.lastIndexOf(".");
12766
+ const baseName = dotIndex > 0 ? filename.slice(0, dotIndex) : filename;
12767
+ return `${baseName}.${expectedExt}`;
12768
+ }
12196
12769
  function normalizeQuestionType(rawType) {
12197
- const normalized = readString(rawType).trim().toUpperCase().replace(/[\s-]+/g, "_");
12770
+ const normalized = readString2(rawType).trim().toUpperCase().replace(/[\s-]+/g, "_");
12198
12771
  return normalized === "MULTIPLE_CHOICE" ? "MULTIPLE_CHOICE" : "SINGLE_CHOICE";
12199
12772
  }
12200
12773
  function normalizeMessageRole(rawRole) {
12201
- const normalized = readString(rawRole).trim().toLowerCase();
12774
+ const normalized = readString2(rawRole).trim().toLowerCase();
12202
12775
  if (normalized === "user" /* User */) return "user" /* User */;
12203
12776
  if (normalized === "system" /* System */) return "system" /* System */;
12204
12777
  if (normalized === "tool" /* Tool */) return "tool" /* Tool */;
@@ -12209,8 +12782,8 @@ function sanitizeAttachment(rawAttachment, turnId, index) {
12209
12782
  warnMalformedSessionPayload("Dropped malformed attachment entry", { turnId, index });
12210
12783
  return null;
12211
12784
  }
12212
- const filename = readString(rawAttachment.filename, "attachment");
12213
- const mimeType = readString(rawAttachment.mimeType, "application/octet-stream");
12785
+ const filename = readString2(rawAttachment.filename, "attachment");
12786
+ const mimeType = readString2(rawAttachment.mimeType, "application/octet-stream");
12214
12787
  const id = readNonEmptyString(rawAttachment.id) || void 0;
12215
12788
  const clientKey = readNonEmptyString(rawAttachment.clientKey) || id || `${turnId}-attachment-${index}`;
12216
12789
  return {
@@ -12243,7 +12816,7 @@ function sanitizeAssistantArtifacts(rawArtifacts, turnId) {
12243
12816
  warnMalformedSessionPayload("Dropped malformed assistant artifact", { turnId, index: i });
12244
12817
  continue;
12245
12818
  }
12246
- const type = readString(raw.type).toLowerCase();
12819
+ const type = readString2(raw.type).toLowerCase();
12247
12820
  if (type !== "excel" && type !== "pdf") {
12248
12821
  continue;
12249
12822
  }
@@ -12254,7 +12827,7 @@ function sanitizeAssistantArtifacts(rawArtifacts, turnId) {
12254
12827
  }
12255
12828
  artifacts.push({
12256
12829
  type,
12257
- filename: readString(raw.filename, "download"),
12830
+ filename: readString2(raw.filename, "download"),
12258
12831
  url,
12259
12832
  sizeReadable: readNonEmptyString(raw.sizeReadable) || void 0,
12260
12833
  rowCount: typeof raw.rowCount === "number" && Number.isFinite(raw.rowCount) ? raw.rowCount : void 0,
@@ -12275,29 +12848,29 @@ function sanitizeAssistantTurn(rawAssistantTurn, fallbackCreatedAt, turnId) {
12275
12848
  return void 0;
12276
12849
  }
12277
12850
  const citations = Array.isArray(rawAssistantTurn.citations) ? rawAssistantTurn.citations.filter((item) => isRecord(item)).map((item, index) => ({
12278
- id: readString(item.id, `${assistantID}-citation-${index}`),
12279
- type: readString(item.type),
12280
- title: readString(item.title),
12281
- url: readString(item.url),
12851
+ id: readString2(item.id, `${assistantID}-citation-${index}`),
12852
+ type: readString2(item.type),
12853
+ title: readString2(item.title),
12854
+ url: readString2(item.url),
12282
12855
  startIndex: readFiniteNumber(item.startIndex),
12283
12856
  endIndex: readFiniteNumber(item.endIndex),
12284
12857
  excerpt: readNonEmptyString(item.excerpt) || void 0
12285
12858
  })) : [];
12286
12859
  const toolCalls = Array.isArray(rawAssistantTurn.toolCalls) ? rawAssistantTurn.toolCalls.filter((item) => isRecord(item)).map((item, index) => ({
12287
- id: readString(item.id, `${assistantID}-tool-${index}`),
12288
- name: readString(item.name),
12289
- arguments: readString(item.arguments),
12860
+ id: readString2(item.id, `${assistantID}-tool-${index}`),
12861
+ name: readString2(item.name),
12862
+ arguments: readString2(item.arguments),
12290
12863
  result: readNonEmptyString(item.result) || void 0,
12291
12864
  error: readNonEmptyString(item.error) || void 0,
12292
12865
  durationMs: readFiniteNumber(item.durationMs)
12293
12866
  })) : [];
12294
12867
  const codeOutputs = Array.isArray(rawAssistantTurn.codeOutputs) ? rawAssistantTurn.codeOutputs.filter((item) => isRecord(item)).map((item) => ({
12295
12868
  type: (() => {
12296
- const normalizedType = readString(item.type, "text").toLowerCase();
12869
+ const normalizedType = readString2(item.type, "text").toLowerCase();
12297
12870
  if (normalizedType === "image" || normalizedType === "error") return normalizedType;
12298
12871
  return "text";
12299
12872
  })(),
12300
- content: readString(item.content),
12873
+ content: readString2(item.content),
12301
12874
  filename: readNonEmptyString(item.filename) || void 0,
12302
12875
  mimeType: readNonEmptyString(item.mimeType) || void 0,
12303
12876
  sizeBytes: readOptionalFiniteNumber(item.sizeBytes)
@@ -12313,7 +12886,7 @@ function sanitizeAssistantTurn(rawAssistantTurn, fallbackCreatedAt, turnId) {
12313
12886
  } : void 0,
12314
12887
  tools: Array.isArray(rawAssistantTurn.debug.tools) ? rawAssistantTurn.debug.tools.filter((tool) => isRecord(tool)).map((tool) => ({
12315
12888
  callId: readNonEmptyString(tool.callId) || void 0,
12316
- name: readString(tool.name),
12889
+ name: readString2(tool.name),
12317
12890
  arguments: readNonEmptyString(tool.arguments) || void 0,
12318
12891
  result: readNonEmptyString(tool.result) || void 0,
12319
12892
  error: readNonEmptyString(tool.error) || void 0,
@@ -12323,15 +12896,16 @@ function sanitizeAssistantTurn(rawAssistantTurn, fallbackCreatedAt, turnId) {
12323
12896
  return {
12324
12897
  id: assistantID,
12325
12898
  role: normalizeMessageRole(rawAssistantTurn.role),
12326
- content: readString(rawAssistantTurn.content),
12899
+ content: readString2(rawAssistantTurn.content),
12327
12900
  explanation: readNonEmptyString(rawAssistantTurn.explanation) || void 0,
12328
12901
  citations,
12329
12902
  toolCalls,
12330
12903
  chartData: void 0,
12904
+ renderTables: void 0,
12331
12905
  artifacts: sanitizeAssistantArtifacts(rawAssistantTurn.artifacts, turnId),
12332
12906
  codeOutputs,
12333
12907
  debug: debugTrace,
12334
- createdAt: readString(rawAssistantTurn.createdAt, fallbackCreatedAt)
12908
+ createdAt: readString2(rawAssistantTurn.createdAt, fallbackCreatedAt)
12335
12909
  };
12336
12910
  }
12337
12911
  function sanitizeConversationTurn(rawTurn, index, fallbackSessionID) {
@@ -12348,19 +12922,19 @@ function sanitizeConversationTurn(rawTurn, index, fallbackSessionID) {
12348
12922
  warnMalformedSessionPayload("Dropped malformed turn payload (missing user turn id)", { index });
12349
12923
  return null;
12350
12924
  }
12351
- const turnID = readString(rawTurn.id, userTurnID);
12352
- const createdAt = readString(
12925
+ const turnID = readString2(rawTurn.id, userTurnID);
12926
+ const createdAt = readString2(
12353
12927
  rawTurn.createdAt,
12354
- readString(rawTurn.userTurn.createdAt, (/* @__PURE__ */ new Date()).toISOString())
12928
+ readString2(rawTurn.userTurn.createdAt, (/* @__PURE__ */ new Date()).toISOString())
12355
12929
  );
12356
12930
  return {
12357
12931
  id: turnID,
12358
- sessionId: readString(rawTurn.sessionId, fallbackSessionID),
12932
+ sessionId: readString2(rawTurn.sessionId, fallbackSessionID),
12359
12933
  userTurn: {
12360
12934
  id: userTurnID,
12361
- content: readString(rawTurn.userTurn.content),
12935
+ content: readString2(rawTurn.userTurn.content),
12362
12936
  attachments: sanitizeUserAttachments(rawTurn.userTurn.attachments, turnID),
12363
- createdAt: readString(rawTurn.userTurn.createdAt, createdAt)
12937
+ createdAt: readString2(rawTurn.userTurn.createdAt, createdAt)
12364
12938
  },
12365
12939
  assistantTurn: sanitizeAssistantTurn(rawTurn.assistantTurn, createdAt, turnID),
12366
12940
  createdAt
@@ -12413,7 +12987,7 @@ function sanitizePendingQuestion(rawPendingQuestion, sessionID) {
12413
12987
  }
12414
12988
  return true;
12415
12989
  }).map((question, index) => {
12416
- const questionID = readString(question.id, `${checkpointID}-q-${index}`);
12990
+ const questionID = readString2(question.id, `${checkpointID}-q-${index}`);
12417
12991
  const options = Array.isArray(question.options) ? question.options.filter((option) => {
12418
12992
  if (!option || !isRecord(option)) {
12419
12993
  warnMalformedSessionPayload("Dropped malformed pendingQuestion option", {
@@ -12425,23 +12999,23 @@ function sanitizePendingQuestion(rawPendingQuestion, sessionID) {
12425
12999
  }
12426
13000
  return true;
12427
13001
  }).map((option, optionIndex) => {
12428
- const label = readString(option.label);
13002
+ const label = readString2(option.label);
12429
13003
  return {
12430
- id: readString(option.id, `${questionID}-opt-${optionIndex}`),
13004
+ id: readString2(option.id, `${questionID}-opt-${optionIndex}`),
12431
13005
  label,
12432
13006
  value: label
12433
13007
  };
12434
13008
  }) : [];
12435
13009
  return {
12436
13010
  id: questionID,
12437
- text: readString(question.text),
13011
+ text: readString2(question.text),
12438
13012
  type: normalizeQuestionType(question.type),
12439
13013
  options
12440
13014
  };
12441
13015
  }) : [];
12442
13016
  return {
12443
13017
  id: checkpointID,
12444
- turnId: readString(rawPendingQuestion.turnId),
13018
+ turnId: readString2(rawPendingQuestion.turnId),
12445
13019
  questions,
12446
13020
  status: "PENDING"
12447
13021
  };
@@ -12513,6 +13087,18 @@ function extractChartDataFromToolCalls(toolCalls) {
12513
13087
  }
12514
13088
  return void 0;
12515
13089
  }
13090
+ function extractRenderTablesFromToolCalls(toolCalls) {
13091
+ if (!toolCalls) return [];
13092
+ const tables = [];
13093
+ for (const tc of toolCalls) {
13094
+ if (tc.name !== "renderTable" || !tc.result) continue;
13095
+ const parsed = parseRenderTableDataFromJsonString(tc.result, tc.id);
13096
+ if (parsed) {
13097
+ tables.push(parsed);
13098
+ }
13099
+ }
13100
+ return tables;
13101
+ }
12516
13102
  var EXPORT_TOOL_NAMES = {
12517
13103
  export_query_to_excel: "excel",
12518
13104
  export_data_to_excel: "excel",
@@ -12548,6 +13134,7 @@ function extractDownloadArtifactsFromToolCalls(toolCalls) {
12548
13134
  function normalizeAssistantTurn(turn) {
12549
13135
  const existingArtifacts = turn.artifacts || [];
12550
13136
  const fromToolCalls = extractDownloadArtifactsFromToolCalls(turn.toolCalls);
13137
+ const renderTables = turn.renderTables || extractRenderTablesFromToolCalls(turn.toolCalls);
12551
13138
  const merged = [...existingArtifacts];
12552
13139
  for (const a of fromToolCalls) {
12553
13140
  if (!merged.some((e) => e.url === a.url && e.filename === a.filename)) {
@@ -12558,6 +13145,7 @@ function normalizeAssistantTurn(turn) {
12558
13145
  ...turn,
12559
13146
  role: turn.role || "assistant" /* Assistant */,
12560
13147
  chartData: turn.chartData || extractChartDataFromToolCalls(turn.toolCalls),
13148
+ renderTables,
12561
13149
  citations: turn.citations || [],
12562
13150
  artifacts: merged,
12563
13151
  codeOutputs: turn.codeOutputs || []
@@ -12685,6 +13273,52 @@ var HttpDataSource = class {
12685
13273
  headers.delete("Content-Type");
12686
13274
  return headers;
12687
13275
  }
13276
+ logAttachmentLifecycle(event, details) {
13277
+ const payload = {
13278
+ source: "HttpDataSource",
13279
+ event,
13280
+ ...details
13281
+ };
13282
+ if (event.endsWith("_fail")) {
13283
+ console.warn("[bichat.attachments]", payload);
13284
+ return;
13285
+ }
13286
+ }
13287
+ async normalizeAttachmentFile(attachment, file) {
13288
+ const signatureBytes = new Uint8Array(await file.slice(0, 16).arrayBuffer());
13289
+ const detectedMimeType = detectMimeFromSignature(signatureBytes);
13290
+ const declaredMimeType = (attachment.mimeType || file.type || "").trim().toLowerCase();
13291
+ let resolvedMimeType = declaredMimeType || detectedMimeType || "application/octet-stream";
13292
+ let correctedFromDeclared = false;
13293
+ if (detectedMimeType && declaredMimeType && detectedMimeType !== declaredMimeType) {
13294
+ const safeToCorrect = SAFE_AUTOCORRECT_MIME_TYPES.has(detectedMimeType) && SAFE_AUTOCORRECT_MIME_TYPES.has(declaredMimeType);
13295
+ if (!safeToCorrect) {
13296
+ throw new Error(
13297
+ `Attachment "${attachment.filename}" MIME mismatch: declared "${declaredMimeType}", detected "${detectedMimeType}"`
13298
+ );
13299
+ }
13300
+ resolvedMimeType = detectedMimeType;
13301
+ correctedFromDeclared = true;
13302
+ } else if (detectedMimeType && !declaredMimeType) {
13303
+ resolvedMimeType = detectedMimeType;
13304
+ }
13305
+ const normalizedName = normalizeFilenameForMime(attachment.filename, resolvedMimeType);
13306
+ const normalized = new File([file], normalizedName, {
13307
+ type: resolvedMimeType,
13308
+ lastModified: file.lastModified
13309
+ });
13310
+ this.logAttachmentLifecycle("attachment_decode_success", {
13311
+ attachmentKey: attachment.clientKey,
13312
+ filename: attachment.filename,
13313
+ normalizedFilename: normalized.name,
13314
+ declaredMimeType: declaredMimeType || void 0,
13315
+ detectedMimeType,
13316
+ resolvedMimeType,
13317
+ correctedFromDeclared,
13318
+ sizeBytes: normalized.size
13319
+ });
13320
+ return normalized;
13321
+ }
12688
13322
  async uploadFile(file) {
12689
13323
  const formData = new FormData();
12690
13324
  formData.append("file", file);
@@ -12717,17 +13351,32 @@ var HttpDataSource = class {
12717
13351
  }
12718
13352
  async attachmentToFile(attachment) {
12719
13353
  if (attachment.base64Data && attachment.base64Data.trim().length > 0) {
12720
- const base64Data = attachment.base64Data.trim();
12721
- const dataUrl = base64Data.startsWith("data:") ? base64Data : `data:${attachment.mimeType || "application/octet-stream"};base64,${base64Data}`;
12722
- const blob = await fetch(dataUrl).then((response) => response.blob());
12723
- return new File([blob], attachment.filename, {
12724
- type: attachment.mimeType || blob.type || "application/octet-stream"
12725
- });
13354
+ try {
13355
+ const base64Data = attachment.base64Data.trim();
13356
+ const dataUrl = base64Data.startsWith("data:") ? base64Data : `data:${attachment.mimeType || "application/octet-stream"};base64,${base64Data}`;
13357
+ const blob = await fetch(dataUrl).then((response) => response.blob());
13358
+ return new File([blob], attachment.filename, {
13359
+ type: attachment.mimeType || blob.type || "application/octet-stream"
13360
+ });
13361
+ } catch (err) {
13362
+ const message = err instanceof Error ? err.message : "Unknown decode error";
13363
+ throw new Error(`Attachment "${attachment.filename}" decode failed: ${message}`);
13364
+ }
12726
13365
  }
12727
13366
  if (attachment.url) {
12728
- const response = await fetch(attachment.url);
13367
+ let parsed;
13368
+ try {
13369
+ parsed = new URL(attachment.url, window.location?.origin ?? "https://localhost");
13370
+ if (!["http:", "https:"].includes(parsed.protocol)) {
13371
+ throw new Error(`Attachment "${attachment.filename}" URL has disallowed protocol: ${parsed.protocol}`);
13372
+ }
13373
+ } catch (err) {
13374
+ if (err instanceof Error && err.message.includes("Attachment")) throw err;
13375
+ throw new Error(`Attachment "${attachment.filename}" has invalid or malformed URL`);
13376
+ }
13377
+ const response = await fetch(parsed.href);
12729
13378
  if (!response.ok) {
12730
- throw new Error(`Failed to read attachment source: HTTP ${response.status}`);
13379
+ throw new Error(`Attachment "${attachment.filename}" decode failed: source HTTP ${response.status}`);
12731
13380
  }
12732
13381
  const blob = await response.blob();
12733
13382
  return new File([blob], attachment.filename, {
@@ -12736,8 +13385,24 @@ var HttpDataSource = class {
12736
13385
  }
12737
13386
  throw new Error(`Attachment "${attachment.filename}" has no uploadable data`);
12738
13387
  }
12739
- async ensureAttachmentUpload(attachment) {
13388
+ assertUploadReferences(uploads) {
13389
+ return uploads.map((upload, index) => {
13390
+ if (typeof upload.id !== "number" || !Number.isFinite(upload.id) || upload.id <= 0) {
13391
+ throw new Error(`Attachment upload reference is invalid at index ${index}`);
13392
+ }
13393
+ return { uploadId: upload.id };
13394
+ });
13395
+ }
13396
+ async ensureAttachmentUpload(attachment, context) {
12740
13397
  if (typeof attachment.uploadId === "number" && attachment.uploadId > 0) {
13398
+ this.logAttachmentLifecycle("attachment_upload_success", {
13399
+ sessionId: context.sessionId,
13400
+ attachmentIndex: context.attachmentIndex,
13401
+ attachmentKey: attachment.clientKey,
13402
+ filename: attachment.filename,
13403
+ uploadId: attachment.uploadId,
13404
+ reusedUploadId: true
13405
+ });
12741
13406
  return {
12742
13407
  id: attachment.uploadId,
12743
13408
  url: attachment.url || "",
@@ -12747,8 +13412,64 @@ var HttpDataSource = class {
12747
13412
  size: attachment.sizeBytes
12748
13413
  };
12749
13414
  }
12750
- const file = await this.attachmentToFile(attachment);
12751
- return this.uploadFile(file);
13415
+ this.logAttachmentLifecycle("attachment_decode_start", {
13416
+ sessionId: context.sessionId,
13417
+ attachmentIndex: context.attachmentIndex,
13418
+ attachmentKey: attachment.clientKey,
13419
+ filename: attachment.filename,
13420
+ hasBase64Data: Boolean(attachment.base64Data && attachment.base64Data.trim().length > 0),
13421
+ hasURL: Boolean(attachment.url)
13422
+ });
13423
+ let file;
13424
+ try {
13425
+ const rawFile = await this.attachmentToFile(attachment);
13426
+ file = await this.normalizeAttachmentFile(attachment, rawFile);
13427
+ validateAttachmentFile(file);
13428
+ } catch (err) {
13429
+ const message = err instanceof Error ? err.message : "Unknown attachment decode/validation error";
13430
+ this.logAttachmentLifecycle("attachment_decode_fail", {
13431
+ sessionId: context.sessionId,
13432
+ attachmentIndex: context.attachmentIndex,
13433
+ attachmentKey: attachment.clientKey,
13434
+ filename: attachment.filename,
13435
+ error: message
13436
+ });
13437
+ throw new Error(message);
13438
+ }
13439
+ this.logAttachmentLifecycle("attachment_upload_start", {
13440
+ sessionId: context.sessionId,
13441
+ attachmentIndex: context.attachmentIndex,
13442
+ attachmentKey: attachment.clientKey,
13443
+ filename: file.name,
13444
+ mimeType: file.type,
13445
+ sizeBytes: file.size
13446
+ });
13447
+ try {
13448
+ const upload = await this.uploadFile(file);
13449
+ attachment.uploadId = upload.id;
13450
+ attachment.mimeType = upload.mimetype || file.type;
13451
+ attachment.filename = upload.name || file.name;
13452
+ attachment.sizeBytes = upload.size || file.size;
13453
+ this.logAttachmentLifecycle("attachment_upload_success", {
13454
+ sessionId: context.sessionId,
13455
+ attachmentIndex: context.attachmentIndex,
13456
+ attachmentKey: attachment.clientKey,
13457
+ filename: attachment.filename,
13458
+ uploadId: upload.id,
13459
+ reusedUploadId: false
13460
+ });
13461
+ return upload;
13462
+ } catch (err) {
13463
+ const message = err instanceof Error ? err.message : "Unknown upload error";
13464
+ this.logAttachmentLifecycle("attachment_upload_fail", {
13465
+ sessionId: context.sessionId,
13466
+ attachmentIndex: context.attachmentIndex,
13467
+ attachmentKey: attachment.clientKey,
13468
+ filename: file.name,
13469
+ error: message
13470
+ });
13471
+ throw new Error(`Attachment "${attachment.filename}" upload failed: ${message}`);
13472
+ }
12752
13473
  }
12753
13474
  async callRPC(method, params) {
12754
13475
  return this.rpc.callTyped(method, params);
@@ -12859,16 +13580,21 @@ var HttpDataSource = class {
12859
13580
  let connectionTimedOut = false;
12860
13581
  try {
12861
13582
  const uploads = await Promise.all(
12862
- attachments.map((attachment) => this.ensureAttachmentUpload(attachment))
13583
+ attachments.map(
13584
+ (attachment, attachmentIndex) => this.ensureAttachmentUpload(attachment, { sessionId, attachmentIndex })
13585
+ )
12863
13586
  );
13587
+ const streamAttachments = this.assertUploadReferences(uploads);
13588
+ this.logAttachmentLifecycle("stream_send_with_upload_ids", {
13589
+ sessionId,
13590
+ attachmentCount: streamAttachments.length
13591
+ });
12864
13592
  const payload = {
12865
13593
  sessionId,
12866
13594
  content,
12867
13595
  debugMode: options?.debugMode ?? false,
12868
13596
  replaceFromMessageId: options?.replaceFromMessageID,
12869
- attachments: uploads.map((upload) => ({
12870
- uploadId: upload.id
12871
- }))
13597
+ attachments: streamAttachments
12872
13598
  };
12873
13599
  const timeoutMs = this.config.timeout ?? 0;
12874
13600
  if (timeoutMs > 0) {
@@ -13072,6 +13798,7 @@ exports.ErrorBoundary = ErrorBoundary;
13072
13798
  exports.HttpDataSource = HttpDataSource;
13073
13799
  exports.ImageModal = ImageModal;
13074
13800
  exports.InlineQuestionForm = InlineQuestionForm;
13801
+ exports.InteractiveTableCard = InteractiveTableCard;
13075
13802
  exports.IotaContextProvider = IotaContextProvider;
13076
13803
  exports.ListItemSkeleton = ListItemSkeleton;
13077
13804
  exports.LoadingSpinner = MemoizedLoadingSpinner;