@iota-uz/sdk 0.4.17 → 0.4.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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"() {
@@ -1518,6 +1518,10 @@ var ChatMachine = class {
1518
1518
  }
1519
1519
  /** Sets turns from fetch, preserving pending user-only turns if server hasn't caught up. */
1520
1520
  _setTurnsFromFetch(fetchedTurns) {
1521
+ if (!Array.isArray(fetchedTurns)) {
1522
+ console.warn("[ChatMachine] Ignoring malformed turns payload from fetchSession");
1523
+ return;
1524
+ }
1521
1525
  const prev = this.state.messaging.turns;
1522
1526
  const hasPendingUserOnly = prev.length > 0 && !prev[prev.length - 1].assistantTurn;
1523
1527
  if (hasPendingUserOnly && (!fetchedTurns || fetchedTurns.length === 0)) {
@@ -1543,6 +1547,12 @@ var ChatMachine = class {
1543
1547
  streamErrorRetryable: false
1544
1548
  });
1545
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
+ }
1546
1556
  _cancel() {
1547
1557
  if (this.abortController) {
1548
1558
  this.abortController.abort();
@@ -1810,7 +1820,11 @@ var ChatMachine = class {
1810
1820
  }
1811
1821
  }
1812
1822
  const targetSessionId = createdSessionId || activeSessionId;
1823
+ if (targetSessionId && targetSessionId !== "new") {
1824
+ this._notifySessionsUpdated("message_sent", targetSessionId);
1825
+ }
1813
1826
  if (shouldNavigateAfter && targetSessionId && targetSessionId !== "new") {
1827
+ this._notifySessionsUpdated("session_created", targetSessionId);
1814
1828
  if (this.onSessionCreated) {
1815
1829
  this.onSessionCreated(targetSessionId);
1816
1830
  } else {
@@ -1907,6 +1921,7 @@ var ChatMachine = class {
1907
1921
  const curSessionId = this.state.session.currentSessionId;
1908
1922
  const curPendingQuestion = this.state.messaging.pendingQuestion;
1909
1923
  if (!curSessionId || !curPendingQuestion) return;
1924
+ const previousTurns = this.state.messaging.turns;
1910
1925
  this._updateMessaging({ loading: true });
1911
1926
  this._updateSession({ error: null, errorRetryable: false });
1912
1927
  const previousPendingQuestion = curPendingQuestion;
@@ -1924,19 +1939,28 @@ var ChatMachine = class {
1924
1939
  const fetchResult = await this.dataSource.fetchSession(curSessionId);
1925
1940
  if (this.disposed) return;
1926
1941
  if (fetchResult) {
1927
- this._updateMessaging({
1928
- turns: fetchResult.turns,
1929
- pendingQuestion: fetchResult.pendingQuestion || null
1930
- });
1942
+ this._updateSession({ session: fetchResult.session });
1943
+ this._updateMessaging({ pendingQuestion: fetchResult.pendingQuestion || null });
1944
+ const hasMalformedRefresh = previousTurns.length > 0 && Array.isArray(fetchResult.turns) && fetchResult.turns.length === 0;
1945
+ if (hasMalformedRefresh) {
1946
+ console.warn("[ChatMachine] Preserving previous turns due to empty post-HITL refetch payload", {
1947
+ sessionId: curSessionId,
1948
+ previousTurnCount: previousTurns.length
1949
+ });
1950
+ this._updateSession({
1951
+ error: "Failed to fully refresh session. Showing last known messages.",
1952
+ errorRetryable: true
1953
+ });
1954
+ } else {
1955
+ this._setTurnsFromFetch(fetchResult.turns);
1956
+ }
1931
1957
  } else {
1932
- this._updateMessaging({ pendingQuestion: previousPendingQuestion });
1933
- this._updateSession({ error: "Failed to load updated session", errorRetryable: false });
1958
+ this._updateSession({ error: "Failed to load updated session", errorRetryable: true });
1934
1959
  }
1935
1960
  } catch (fetchErr) {
1936
1961
  if (this.disposed) return;
1937
- this._updateMessaging({ pendingQuestion: previousPendingQuestion });
1938
1962
  const normalized = normalizeRPCError(fetchErr, "Failed to load updated session");
1939
- this._updateSession({ error: normalized.userMessage, errorRetryable: normalized.retryable });
1963
+ this._updateSession({ error: normalized.userMessage, errorRetryable: true });
1940
1964
  }
1941
1965
  }
1942
1966
  } else {
@@ -1990,6 +2014,8 @@ var ChatMachine = class {
1990
2014
  this._clearStreamError();
1991
2015
  const convertedAttachments = attachments.map((att) => ({
1992
2016
  clientKey: att.clientKey || crypto.randomUUID(),
2017
+ id: att.id,
2018
+ uploadId: att.uploadId,
1993
2019
  filename: att.filename,
1994
2020
  mimeType: att.mimeType,
1995
2021
  sizeBytes: att.sizeBytes,
@@ -2652,6 +2678,9 @@ var MemoizedAttachmentGrid = React__default.default.memo(AttachmentGrid);
2652
2678
  MemoizedAttachmentGrid.displayName = "AttachmentGrid";
2653
2679
  var AttachmentGrid_default = MemoizedAttachmentGrid;
2654
2680
  init_useTranslation();
2681
+ var MIN_SCALE = 0.25;
2682
+ var MAX_SCALE = 5;
2683
+ var ZOOM_STEP = 0.25;
2655
2684
  function ImageModal({
2656
2685
  isOpen,
2657
2686
  onClose,
@@ -2664,9 +2693,23 @@ function ImageModal({
2664
2693
  const [isImageLoaded, setIsImageLoaded] = React.useState(false);
2665
2694
  const [imageError, setImageError] = React.useState(false);
2666
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);
2667
2703
  const hasMultipleImages = allAttachments && allAttachments.length > 1;
2668
2704
  const canNavigatePrev = hasMultipleImages && currentIndex > 0;
2669
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]);
2670
2713
  React.useEffect(() => {
2671
2714
  if (!isOpen) return;
2672
2715
  const handleKeyDown = (e) => {
@@ -2674,6 +2717,14 @@ function ImageModal({
2674
2717
  onNavigate("prev");
2675
2718
  } else if (e.key === "ArrowRight" && onNavigate && canNavigateNext) {
2676
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 });
2677
2728
  }
2678
2729
  };
2679
2730
  document.addEventListener("keydown", handleKeyDown);
@@ -2682,128 +2733,246 @@ function ImageModal({
2682
2733
  React.useEffect(() => {
2683
2734
  setIsImageLoaded(false);
2684
2735
  setImageError(false);
2736
+ setScale(1);
2737
+ setPosition({ x: 0, y: 0 });
2685
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]);
2686
2754
  const handleRetry = React.useCallback(() => {
2687
2755
  setImageError(false);
2688
2756
  setIsImageLoaded(false);
2689
2757
  setRetryKey((k) => k + 1);
2690
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]);
2691
2803
  const previewUrl = attachment.preview || createDataUrl(attachment.base64Data, attachment.mimeType);
2804
+ const zoomPercent = Math.round(scale * 100);
2692
2805
  return /* @__PURE__ */ jsxRuntime.jsxs(react$1.Dialog, { open: isOpen, onClose, className: "relative", style: { zIndex: 99999 }, children: [
2693
2806
  /* @__PURE__ */ jsxRuntime.jsx(
2694
2807
  react$1.DialogBackdrop,
2695
2808
  {
2696
- className: "fixed inset-0",
2697
- 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 }
2698
2811
  }
2699
2812
  ),
2700
- /* @__PURE__ */ jsxRuntime.jsxs(react$1.DialogPanel, { className: "fixed inset-0 flex flex-col", style: { zIndex: 1e5 }, children: [
2701
- /* @__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: [
2702
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [
2703
- hasMultipleImages && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-gray-500 dark:text-gray-400 tabular-nums whitespace-nowrap", children: [
2704
- currentIndex + 1,
2705
- " / ",
2706
- allAttachments?.length
2707
- ] }),
2708
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-900 dark:text-gray-200 truncate", children: attachment.filename }),
2709
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap", children: formatFileSize(attachment.sizeBytes) })
2710
- ] }),
2711
- /* @__PURE__ */ jsxRuntime.jsx(
2712
- "button",
2713
- {
2714
- onClick: onClose,
2715
- 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",
2716
- "aria-label": t("BiChat.Image.Close"),
2717
- type: "button",
2718
- children: /* @__PURE__ */ jsxRuntime.jsx(react.X, { size: 18, weight: "bold" })
2719
- }
2720
- )
2721
- ] }),
2722
- /* @__PURE__ */ jsxRuntime.jsxs(
2723
- "div",
2724
- {
2725
- className: "relative flex-1 flex items-center justify-center min-h-0",
2726
- onClick: (e) => {
2727
- if (e.target === e.currentTarget) onClose();
2728
- },
2729
- children: [
2730
- !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: [
2731
- /* @__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" }),
2732
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400 dark:text-gray-500", children: t("BiChat.Loading") })
2733
- ] }) }),
2734
- imageError && /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "alert", className: "flex flex-col items-center justify-center text-center max-w-xs", children: [
2735
- /* @__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" }) }),
2736
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-gray-700 dark:text-gray-300 mb-1", children: t("BiChat.Image.FailedToLoad") }),
2737
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400 dark:text-gray-500 mb-5 truncate max-w-full", children: attachment.filename }),
2738
- /* @__PURE__ */ jsxRuntime.jsxs(
2739
- "button",
2740
- {
2741
- type: "button",
2742
- onClick: handleRetry,
2743
- 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",
2744
- "aria-label": t("BiChat.Image.Retry"),
2745
- children: [
2746
- /* @__PURE__ */ jsxRuntime.jsx(react.ArrowClockwise, { size: 16, weight: "bold" }),
2747
- t("BiChat.Retry.Label")
2748
- ]
2749
- }
2750
- )
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
2751
2827
  ] }),
2752
- /* @__PURE__ */ jsxRuntime.jsx(
2753
- "img",
2754
- {
2755
- src: previewUrl,
2756
- alt: attachment.filename,
2757
- className: [
2758
- "max-w-[90vw] max-h-[calc(100vh-120px)] object-contain select-none",
2759
- "transition-opacity duration-300 ease-out",
2760
- isImageLoaded ? "opacity-100" : "opacity-0"
2761
- ].join(" "),
2762
- onLoad: () => setIsImageLoaded(true),
2763
- onError: () => setImageError(true),
2764
- loading: "lazy",
2765
- draggable: false
2766
- },
2767
- retryKey
2768
- ),
2769
- hasMultipleImages && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2770
- /* @__PURE__ */ jsxRuntime.jsx(
2771
- "button",
2772
- {
2773
- onClick: () => onNavigate?.("prev"),
2774
- disabled: !canNavigatePrev || !isImageLoaded || imageError,
2775
- className: [
2776
- "absolute left-3 top-1/2 -translate-y-1/2",
2777
- "flex items-center justify-center w-10 h-10 rounded-md",
2778
- "transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400",
2779
- 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"
2780
- ].join(" "),
2781
- "aria-label": t("BiChat.Image.Previous"),
2782
- type: "button",
2783
- children: /* @__PURE__ */ jsxRuntime.jsx(react.CaretLeft, { size: 20, weight: "bold" })
2784
- }
2785
- ),
2786
- /* @__PURE__ */ jsxRuntime.jsx(
2787
- "button",
2788
- {
2789
- onClick: () => onNavigate?.("next"),
2790
- disabled: !canNavigateNext || !isImageLoaded || imageError,
2791
- className: [
2792
- "absolute right-3 top-1/2 -translate-y-1/2",
2793
- "flex items-center justify-center w-10 h-10 rounded-md",
2794
- "transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400",
2795
- 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"
2796
- ].join(" "),
2797
- "aria-label": t("BiChat.Image.Next"),
2798
- type: "button",
2799
- children: /* @__PURE__ */ jsxRuntime.jsx(react.CaretRight, { size: 20, weight: "bold" })
2800
- }
2801
- )
2802
- ] })
2803
- ]
2804
- }
2805
- )
2806
- ] })
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
+ )
2807
2976
  ] });
2808
2977
  }
2809
2978
  var ImageModal_default = ImageModal;
@@ -2855,6 +3024,7 @@ function UserMessage({
2855
3024
  const [draftContent, setDraftContent] = React.useState("");
2856
3025
  const [isCopied, setIsCopied] = React.useState(false);
2857
3026
  const copyFeedbackTimeoutRef = React.useRef(null);
3027
+ const editTextareaRef = React.useRef(null);
2858
3028
  const classes = mergeClassNames(defaultClassNames, classNameOverrides);
2859
3029
  React.useEffect(() => {
2860
3030
  return () => {
@@ -2864,6 +3034,16 @@ function UserMessage({
2864
3034
  }
2865
3035
  };
2866
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]);
2867
3047
  const normalizedAttachments = turn.attachments.map((attachment) => {
2868
3048
  if (!attachment.mimeType.startsWith("image/")) {
2869
3049
  return attachment;
@@ -2948,6 +3128,21 @@ function UserMessage({
2948
3128
  onEdit(turnId, newContent);
2949
3129
  setIsEditing(false);
2950
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
+ }, []);
2951
3146
  const handleNavigate = React.useCallback(
2952
3147
  (direction) => {
2953
3148
  if (selectedImageIndex === null) return;
@@ -2998,36 +3193,48 @@ function UserMessage({
2998
3193
  }
2999
3194
  )
3000
3195
  ) }),
3001
- 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: [
3002
3197
  /* @__PURE__ */ jsxRuntime.jsx(
3003
3198
  "textarea",
3004
3199
  {
3200
+ ref: editTextareaRef,
3005
3201
  value: draftContent,
3006
- onChange: (e) => setDraftContent(e.target.value),
3007
- 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",
3008
- "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
3009
3207
  }
3010
3208
  ),
3011
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end gap-2", children: [
3012
- /* @__PURE__ */ jsxRuntime.jsx(
3013
- "button",
3014
- {
3015
- type: "button",
3016
- onClick: handleEditCancel,
3017
- className: "cursor-pointer px-3 py-1.5 rounded-lg bg-white/10 hover:bg-white/15 transition-colors text-sm font-medium",
3018
- children: t("BiChat.Message.Cancel")
3019
- }
3020
- ),
3021
- /* @__PURE__ */ jsxRuntime.jsx(
3022
- "button",
3023
- {
3024
- type: "button",
3025
- onClick: handleEditSave,
3026
- 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",
3027
- disabled: !draftContent.trim() || draftContent === turn.content,
3028
- children: t("BiChat.Message.Save")
3029
- }
3030
- )
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
+ ] })
3031
3238
  ] })
3032
3239
  ] }) : renderSlot(slots?.content, contentSlotProps, turn.content) }) }),
3033
3240
  !hideActions && /* @__PURE__ */ jsxRuntime.jsx("div", { className: `${classes.actions} ${isCopied ? "opacity-100" : ""}`, children: renderSlot(
@@ -3177,65 +3384,293 @@ var StreamingCursor_default = StreamingCursor;
3177
3384
 
3178
3385
  // ui/src/bichat/components/AssistantMessage.tsx
3179
3386
  init_ChartCard();
3180
- function SourcesPanel({ citations }) {
3181
- if (!citations || citations.length === 0) {
3182
- return null;
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
+ }
3183
3400
  }
3184
- 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: [
3185
- /* @__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: [
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
+ ] }),
3186
3470
  /* @__PURE__ */ jsxRuntime.jsx(
3187
- "svg",
3471
+ exports.TableExportButton,
3188
3472
  {
3189
- className: `w-4 h-4 transition-transform duration-150 ${open ? "rotate-90" : ""}`,
3190
- fill: "none",
3191
- stroke: "currentColor",
3192
- viewBox: "0 0 24 24",
3193
- children: /* @__PURE__ */ jsxRuntime.jsx(
3194
- "path",
3195
- {
3196
- strokeLinecap: "round",
3197
- strokeLinejoin: "round",
3198
- strokeWidth: 2,
3199
- d: "M9 5l7 7-7 7"
3200
- }
3201
- )
3473
+ onClick: handleExport,
3474
+ disabled: exportDisabled,
3475
+ label: t("BiChat.Table.ExportToExcel"),
3476
+ disabledTooltip: sendDisabled ? t("BiChat.Table.PleaseWait") : t("BiChat.Table.ExportUnavailable")
3202
3477
  }
3203
- ),
3204
- /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
3205
- citations.length,
3206
- " ",
3207
- citations.length === 1 ? "source" : "sources"
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
+ )
3208
3557
  ] })
3209
3558
  ] }),
3210
- /* @__PURE__ */ jsxRuntime.jsx(react$1.DisclosurePanel, { className: "mt-2 space-y-2", children: citations.map((citation, index) => /* @__PURE__ */ jsxRuntime.jsx(
3211
- "div",
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
+ }
3587
+ function SourcesPanel({ citations }) {
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",
3212
3600
  {
3213
- className: "p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg text-sm",
3214
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2", children: [
3215
- /* @__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 }),
3216
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
3217
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-medium text-gray-900 dark:text-gray-100", children: citation.title }),
3218
- citation.url && /* @__PURE__ */ jsxRuntime.jsx(
3219
- "a",
3220
- {
3221
- href: citation.url,
3222
- target: "_blank",
3223
- rel: "noopener noreferrer",
3224
- className: "text-[var(--bichat-primary)] hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50 rounded",
3225
- children: citation.url
3226
- }
3227
- ),
3228
- citation.excerpt && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-1 text-gray-600 dark:text-gray-400 italic", children: [
3229
- '"',
3230
- citation.excerpt,
3231
- '"'
3232
- ] })
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")
3233
3619
  ] })
3620
+ ]
3621
+ }
3622
+ ) });
3623
+ }
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") }),
3627
+ /* @__PURE__ */ jsxRuntime.jsx(
3628
+ "button",
3629
+ {
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" })
3635
+ }
3636
+ )
3637
+ ] }),
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 })
3234
3654
  ] })
3235
- },
3236
- citation.id
3237
- )) })
3238
- ] }) }) });
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
+ ] });
3239
3674
  }
3240
3675
  var MIME_BY_TYPE = {
3241
3676
  excel: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
@@ -3296,7 +3731,7 @@ function InlineQuestionForm({ pendingQuestion }) {
3296
3731
  const [currentStep, setCurrentStep] = React.useState(0);
3297
3732
  const [answers, setAnswers] = React.useState({});
3298
3733
  const [otherTexts, setOtherTexts] = React.useState({});
3299
- const questions = pendingQuestion.questions;
3734
+ const questions = Array.isArray(pendingQuestion.questions) ? pendingQuestion.questions : [];
3300
3735
  const currentQuestion = questions[currentStep];
3301
3736
  const isLastStep = currentStep === questions.length - 1;
3302
3737
  const isFirstStep = currentStep === 0;
@@ -3390,9 +3825,31 @@ function InlineQuestionForm({ pendingQuestion }) {
3390
3825
  e.preventDefault();
3391
3826
  handleNext();
3392
3827
  };
3393
- if (!currentQuestion) return null;
3828
+ if (!currentQuestion) {
3829
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "animate-slide-up rounded-2xl border border-amber-200 dark:border-amber-700/50 bg-gradient-to-b from-amber-50/70 to-white dark:from-amber-950/20 dark:to-gray-900/80 shadow-sm overflow-hidden p-4", children: [
3830
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-gray-800 dark:text-gray-200", children: t("BiChat.Error.SomethingWentWrong") }),
3831
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-xs text-gray-500 dark:text-gray-400", children: t("BiChat.Error.UnexpectedError") }),
3832
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-3", children: /* @__PURE__ */ jsxRuntime.jsxs(
3833
+ "button",
3834
+ {
3835
+ type: "button",
3836
+ onClick: handleRejectPendingQuestion,
3837
+ disabled: loading,
3838
+ className: "cursor-pointer inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-40",
3839
+ children: [
3840
+ /* @__PURE__ */ jsxRuntime.jsx(react.X, { size: 14, weight: "bold" }),
3841
+ t("BiChat.InlineQuestion.Dismiss")
3842
+ ]
3843
+ }
3844
+ ) })
3845
+ ] });
3846
+ }
3394
3847
  const isMultiSelect = currentQuestion.type === "MULTIPLE_CHOICE";
3395
- const options = currentQuestion.options || [];
3848
+ const options = (currentQuestion.options || []).filter((option) => Boolean(option && typeof option.label === "string")).map((option, index) => ({
3849
+ id: option.id || `${currentQuestion.id}-option-${index}`,
3850
+ label: option.label,
3851
+ value: option.value || option.label
3852
+ }));
3396
3853
  const isOtherSelected = currentAnswer?.customText !== void 0;
3397
3854
  const canProceed = isCurrentAnswerValid();
3398
3855
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "animate-slide-up rounded-2xl border border-gray-200 dark:border-gray-700/50 bg-gradient-to-b from-primary-50/80 to-white dark:from-primary-950/30 dark:to-gray-900/80 shadow-sm overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleSubmit, children: [
@@ -3565,6 +4022,59 @@ function InlineQuestionForm({ pendingQuestion }) {
3565
4022
  ] }) });
3566
4023
  }
3567
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
+
3568
4078
  // ui/src/bichat/utils/debugMetrics.ts
3569
4079
  function formatGenerationDuration(generationMs) {
3570
4080
  return generationMs > 1e3 ? `${(generationMs / 1e3).toFixed(2)}s` : `${generationMs}ms`;
@@ -3863,6 +4373,7 @@ var defaultClassNames2 = {
3863
4373
  bubble: "bg-white dark:bg-gray-800 rounded-2xl rounded-bl-sm px-4 py-3 shadow-sm",
3864
4374
  codeOutputs: "",
3865
4375
  charts: "mb-1 w-full",
4376
+ tables: "mb-1 flex flex-col gap-3",
3866
4377
  artifacts: "mb-1 flex flex-wrap gap-2",
3867
4378
  sources: "",
3868
4379
  explanation: "mt-4 border-t border-gray-100 dark:border-gray-700 pt-4",
@@ -3879,6 +4390,7 @@ function mergeClassNames2(defaults, overrides) {
3879
4390
  bubble: overrides.bubble ?? defaults.bubble,
3880
4391
  codeOutputs: overrides.codeOutputs ?? defaults.codeOutputs,
3881
4392
  charts: overrides.charts ?? defaults.charts,
4393
+ tables: overrides.tables ?? defaults.tables,
3882
4394
  artifacts: overrides.artifacts ?? defaults.artifacts,
3883
4395
  sources: overrides.sources ?? defaults.sources,
3884
4396
  explanation: overrides.explanation ?? defaults.explanation,
@@ -3923,6 +4435,14 @@ function AssistantMessage({
3923
4435
  const hasContent = turn.content?.trim().length > 0;
3924
4436
  const hasExplanation = !!turn.explanation?.trim();
3925
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;
3926
4446
  const handleCopyClick = React.useCallback(async () => {
3927
4447
  try {
3928
4448
  if (onCopy) {
@@ -3964,15 +4484,18 @@ function AssistantMessage({
3964
4484
  const codeOutputsSlotProps = {
3965
4485
  outputs: turn.codeOutputs || []
3966
4486
  };
4487
+ const tablesSlotProps = {
4488
+ tables: turn.renderTables || []
4489
+ };
3967
4490
  const artifactsSlotProps = {
3968
4491
  artifacts: turn.artifacts || []
3969
4492
  };
3970
4493
  const actionsSlotProps = {
3971
4494
  onCopy: handleCopyClick,
3972
- onRegenerate: onRegenerate && turnId && !isSystemMessage && isLastTurn ? handleRegenerateClick : void 0,
4495
+ onRegenerate: canRegenerate ? handleRegenerateClick : void 0,
3973
4496
  timestamp,
3974
4497
  canCopy: hasContent,
3975
- canRegenerate: !!onRegenerate && !!turnId && !isSystemMessage && isLastTurn
4498
+ canRegenerate
3976
4499
  };
3977
4500
  const explanationSlotProps = {
3978
4501
  explanation: turn.explanation || "",
@@ -3985,14 +4508,30 @@ function AssistantMessage({
3985
4508
  return slot;
3986
4509
  };
3987
4510
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classes.root, children: [
3988
- !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") }),
3989
4512
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classes.wrapper, children: [
4513
+ showInlineRetry && /* @__PURE__ */ jsxRuntime.jsx(RetryActionArea, { onRetry: () => {
4514
+ void handleRegenerateClick();
4515
+ } }),
3990
4516
  turn.codeOutputs && turn.codeOutputs.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: classes.codeOutputs, children: renderSlot(
3991
4517
  slots?.codeOutputs,
3992
4518
  codeOutputsSlotProps,
3993
4519
  /* @__PURE__ */ jsxRuntime.jsx(CodeOutputsPanel_default, { outputs: turn.codeOutputs })
3994
4520
  ) }),
3995
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
+ ) }),
3996
4535
  hasContent && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: bubbleClassName, children: [
3997
4536
  renderSlot(
3998
4537
  slots?.content,
@@ -4082,7 +4621,7 @@ function AssistantMessage({
4082
4621
  children: isCopied ? /* @__PURE__ */ jsxRuntime.jsx(react.Check, { size: 14, weight: "bold" }) : /* @__PURE__ */ jsxRuntime.jsx(react.Copy, { size: 14, weight: "regular" })
4083
4622
  }
4084
4623
  ),
4085
- onRegenerate && turnId && !isSystemMessage && isLastTurn && /* @__PURE__ */ jsxRuntime.jsx(
4624
+ canRegenerate && /* @__PURE__ */ jsxRuntime.jsx(
4086
4625
  "button",
4087
4626
  {
4088
4627
  onClick: handleRegenerateClick,
@@ -4306,7 +4845,8 @@ function TurnBubble({
4306
4845
  userTurn: classNames?.userTurn ?? defaultClassNames3.userTurn,
4307
4846
  assistantTurn: classNames?.assistantTurn ?? defaultClassNames3.assistantTurn
4308
4847
  };
4309
- const isSystemSummaryTurn = turn.userTurn.content.trim() === "" && turn.assistantTurn?.role === "system";
4848
+ const userContent = typeof turn.userTurn?.content === "string" ? turn.userTurn.content : "";
4849
+ const isSystemSummaryTurn = userContent.trim() === "" && turn.assistantTurn?.role === "system";
4310
4850
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classes.root, "data-turn-id": turn.id, children: [
4311
4851
  !isSystemSummaryTurn && /* @__PURE__ */ jsxRuntime.jsx("div", { className: classes.userTurn, children: renderUserTurn ? renderUserTurn(turn) : /* @__PURE__ */ jsxRuntime.jsx(
4312
4852
  UserTurnView,
@@ -8084,12 +8624,63 @@ function DefaultErrorContent({
8084
8624
  ] })
8085
8625
  ] });
8086
8626
  }
8627
+ function StaticEmergencyErrorContent({
8628
+ error,
8629
+ onReset
8630
+ }) {
8631
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col items-center justify-center p-8 text-center min-h-[200px]", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex flex-col items-center", children: [
8632
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative mb-5", children: [
8633
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 rounded-full bg-red-100 scale-150 blur-md" }),
8634
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative flex items-center justify-center w-14 h-14 rounded-full bg-red-50 border border-red-200/60", children: /* @__PURE__ */ jsxRuntime.jsx(react.WarningCircle, { size: 28, className: "text-red-500", weight: "fill" }) })
8635
+ ] }),
8636
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-semibold text-gray-900 mb-1.5", children: "Something went wrong" }),
8637
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500 mb-5 max-w-md leading-relaxed", children: error?.message || "An unexpected UI error occurred." }),
8638
+ onReset && /* @__PURE__ */ jsxRuntime.jsxs(
8639
+ "button",
8640
+ {
8641
+ type: "button",
8642
+ onClick: onReset,
8643
+ className: "flex items-center gap-2 px-5 py-2.5 bg-red-600 hover:bg-red-700 active:bg-red-800 text-white rounded-lg transition-colors shadow-sm text-sm font-medium",
8644
+ children: [
8645
+ /* @__PURE__ */ jsxRuntime.jsx(react.ArrowClockwise, { size: 16, weight: "bold" }),
8646
+ "Try again"
8647
+ ]
8648
+ }
8649
+ )
8650
+ ] }) });
8651
+ }
8652
+ var FallbackGuard = class extends React.Component {
8653
+ constructor(props) {
8654
+ super(props);
8655
+ this.state = { fallbackFailed: false };
8656
+ }
8657
+ static getDerivedStateFromError() {
8658
+ return { fallbackFailed: true };
8659
+ }
8660
+ componentDidCatch(error, errorInfo) {
8661
+ this.props.onFallbackError?.(error, errorInfo);
8662
+ }
8663
+ render() {
8664
+ if (this.state.fallbackFailed) {
8665
+ return /* @__PURE__ */ jsxRuntime.jsx(StaticEmergencyErrorContent, { error: this.props.primaryError, onReset: this.props.onReset });
8666
+ }
8667
+ return this.props.renderFallback();
8668
+ }
8669
+ };
8087
8670
  var ErrorBoundary = class extends React.Component {
8088
8671
  constructor(props) {
8089
8672
  super(props);
8090
8673
  this.handleReset = () => {
8091
8674
  this.setState({ hasError: false, error: null });
8092
8675
  };
8676
+ this.handleFallbackError = (error, errorInfo) => {
8677
+ console.error("React Error Boundary fallback crashed:", {
8678
+ primaryError: this.state.error,
8679
+ fallbackError: error,
8680
+ errorInfo
8681
+ });
8682
+ this.props.onError?.(error, errorInfo);
8683
+ };
8093
8684
  this.state = { hasError: false, error: null };
8094
8685
  }
8095
8686
  static getDerivedStateFromError(error) {
@@ -8101,13 +8692,24 @@ var ErrorBoundary = class extends React.Component {
8101
8692
  }
8102
8693
  render() {
8103
8694
  if (this.state.hasError) {
8104
- if (this.props.fallback) {
8105
- if (typeof this.props.fallback === "function") {
8106
- return this.props.fallback(this.state.error, this.handleReset);
8107
- }
8108
- return this.props.fallback;
8109
- }
8110
- return /* @__PURE__ */ jsxRuntime.jsx(DefaultErrorContent, { error: this.state.error, onReset: this.handleReset });
8695
+ return /* @__PURE__ */ jsxRuntime.jsx(
8696
+ FallbackGuard,
8697
+ {
8698
+ primaryError: this.state.error,
8699
+ onReset: this.handleReset,
8700
+ onFallbackError: this.handleFallbackError,
8701
+ renderFallback: () => {
8702
+ if (this.props.fallback) {
8703
+ if (typeof this.props.fallback === "function") {
8704
+ return this.props.fallback(this.state.error, this.handleReset);
8705
+ }
8706
+ return this.props.fallback;
8707
+ }
8708
+ return /* @__PURE__ */ jsxRuntime.jsx(DefaultErrorContent, { error: this.state.error, onReset: this.handleReset });
8709
+ }
8710
+ },
8711
+ `${this.state.error?.name ?? "Error"}:${this.state.error?.message ?? ""}`
8712
+ );
8111
8713
  }
8112
8714
  return this.props.children;
8113
8715
  }
@@ -9141,6 +9743,10 @@ function ErrorAlert({ error }) {
9141
9743
  );
9142
9744
  }
9143
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;
9144
9750
  function useSidebarCollapse() {
9145
9751
  const [isCollapsed, setIsCollapsed] = React.useState(() => {
9146
9752
  try {
@@ -9198,7 +9804,7 @@ function Sidebar2({
9198
9804
  const shouldReduceMotion = framerMotion.useReducedMotion();
9199
9805
  const sessionListRef = React.useRef(null);
9200
9806
  const searchContainerRef = React.useRef(null);
9201
- const refreshForActiveSessionRef = React.useRef(null);
9807
+ const activeSessionMissRetriesRef = React.useRef({});
9202
9808
  const { isCollapsed, toggle, collapse } = useSidebarCollapse();
9203
9809
  const collapsible = !onClose;
9204
9810
  const handleSidebarClick = React.useCallback(
@@ -9242,6 +9848,7 @@ function Sidebar2({
9242
9848
  const [actionError, setActionError] = React.useState(null);
9243
9849
  const accessDenied = loadError?.isPermissionDenied === true;
9244
9850
  const [refreshKey, setRefreshKey] = React.useState(0);
9851
+ const [reconcilePollToken, setReconcilePollToken] = React.useState(0);
9245
9852
  const [showConfirm, setShowConfirm] = React.useState(false);
9246
9853
  const [sessionToArchive, setSessionToArchive] = React.useState(null);
9247
9854
  const fetchSessions = React.useCallback(async () => {
@@ -9262,8 +9869,13 @@ function Sidebar2({
9262
9869
  fetchSessions();
9263
9870
  }, [fetchSessions, refreshKey]);
9264
9871
  React.useEffect(() => {
9265
- const handleSessionsUpdated = () => {
9872
+ const handleSessionsUpdated = (event) => {
9266
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
+ }
9267
9879
  };
9268
9880
  window.addEventListener("bichat:sessions-updated", handleSessionsUpdated);
9269
9881
  return () => {
@@ -9271,31 +9883,33 @@ function Sidebar2({
9271
9883
  };
9272
9884
  }, []);
9273
9885
  React.useEffect(() => {
9274
- if (!activeSessionId) {
9275
- refreshForActiveSessionRef.current = null;
9276
- return;
9277
- }
9886
+ activeSessionMissRetriesRef.current = {};
9887
+ }, [activeSessionId]);
9888
+ React.useEffect(() => {
9889
+ if (!activeSessionId) return;
9278
9890
  if (loading) return;
9279
9891
  const hasActiveSession = sessions.some((session) => session.id === activeSessionId);
9280
9892
  if (hasActiveSession) {
9281
- if (refreshForActiveSessionRef.current === activeSessionId) {
9282
- refreshForActiveSessionRef.current = null;
9283
- }
9893
+ delete activeSessionMissRetriesRef.current[activeSessionId];
9284
9894
  return;
9285
9895
  }
9286
- if (refreshForActiveSessionRef.current !== activeSessionId) {
9287
- refreshForActiveSessionRef.current = activeSessionId;
9288
- setRefreshKey((k) => k + 1);
9896
+ const attempts = activeSessionMissRetriesRef.current[activeSessionId] ?? 0;
9897
+ if (attempts >= ACTIVE_SESSION_MISS_MAX_RETRIES) {
9898
+ return;
9289
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);
9290
9906
  }, [activeSessionId, loading, sessions]);
9291
9907
  const hasPlaceholderTitles = React.useMemo(() => {
9292
9908
  const newChatLabel = t("BiChat.Chat.NewChat");
9293
9909
  return Array.isArray(sessions) && sessions.some((s) => s && (!s.title || s.title === newChatLabel));
9294
9910
  }, [sessions, t]);
9295
9911
  React.useEffect(() => {
9296
- if (!hasPlaceholderTitles) return;
9297
- const pollInterval = 2e3;
9298
- const maxPolls = 5;
9912
+ if (!hasPlaceholderTitles && reconcilePollToken === 0) return;
9299
9913
  let pollCount = 0;
9300
9914
  const intervalId = setInterval(async () => {
9301
9915
  pollCount++;
@@ -9304,12 +9918,12 @@ function Sidebar2({
9304
9918
  setSessions(result.sessions);
9305
9919
  } catch {
9306
9920
  }
9307
- if (pollCount >= maxPolls) {
9921
+ if (pollCount >= SESSION_RECONCILE_MAX_POLLS) {
9308
9922
  clearInterval(intervalId);
9309
9923
  }
9310
- }, pollInterval);
9924
+ }, SESSION_RECONCILE_POLL_INTERVAL_MS);
9311
9925
  return () => clearInterval(intervalId);
9312
- }, [hasPlaceholderTitles, dataSource]);
9926
+ }, [hasPlaceholderTitles, dataSource, reconcilePollToken]);
9313
9927
  const handleArchiveRequest = (sessionId) => {
9314
9928
  setSessionToArchive(sessionId);
9315
9929
  setShowConfirm(true);
@@ -9367,7 +9981,9 @@ function Sidebar2({
9367
9981
  try {
9368
9982
  await dataSource.regenerateSessionTitle(sessionId);
9369
9983
  toast.success(t("BiChat.Sidebar.TitleRegenerated"));
9370
- setRefreshKey((k) => k + 1);
9984
+ window.dispatchEvent(new CustomEvent("bichat:sessions-updated", {
9985
+ detail: { reason: "title_regenerate_requested", sessionId }
9986
+ }));
9371
9987
  } catch (err) {
9372
9988
  console.error("Failed to regenerate title:", err);
9373
9989
  const display = toErrorDisplay(err, t("BiChat.Sidebar.FailedToRegenerateTitle"));
@@ -10180,59 +10796,6 @@ function BiChatLayout({
10180
10796
  ] })
10181
10797
  ] });
10182
10798
  }
10183
-
10184
- // ui/src/bichat/components/RetryActionArea.tsx
10185
- init_useTranslation();
10186
- var RetryActionArea = React.memo(function RetryActionArea2({
10187
- onRetry
10188
- }) {
10189
- const { t } = useTranslation();
10190
- return (
10191
- // Wrapper matches TurnBubble layout for assistant messages (justify-start = left-aligned)
10192
- /* @__PURE__ */ jsxRuntime.jsx(
10193
- framerMotion.motion.div,
10194
- {
10195
- initial: { opacity: 0, y: 10 },
10196
- animate: { opacity: 1, y: 0 },
10197
- exit: { opacity: 0, y: -10 },
10198
- transition: { duration: 0.2 },
10199
- className: "flex justify-start",
10200
- children: /* @__PURE__ */ jsxRuntime.jsxs(
10201
- "div",
10202
- {
10203
- 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",
10204
- role: "status",
10205
- "aria-live": "polite",
10206
- children: [
10207
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
10208
- /* @__PURE__ */ jsxRuntime.jsx(
10209
- react.Warning,
10210
- {
10211
- className: "w-5 h-5 text-amber-500 dark:text-amber-400 flex-shrink-0",
10212
- weight: "fill"
10213
- }
10214
- ),
10215
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-700 dark:text-gray-300", children: t("BiChat.Retry.Subtitle") })
10216
- ] }),
10217
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsxRuntime.jsxs(
10218
- "button",
10219
- {
10220
- onClick: onRetry,
10221
- 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",
10222
- "aria-label": t("BiChat.Retry.Title"),
10223
- children: [
10224
- /* @__PURE__ */ jsxRuntime.jsx(react.ArrowClockwise, { size: 16, className: "w-4 h-4" }),
10225
- t("BiChat.Retry.Button")
10226
- ]
10227
- }
10228
- ) })
10229
- ]
10230
- }
10231
- )
10232
- }
10233
- )
10234
- );
10235
- });
10236
10799
  init_useTranslation();
10237
10800
  function MessageActions({
10238
10801
  message,
@@ -12050,6 +12613,75 @@ function toStreamEvent(chunk) {
12050
12613
  }
12051
12614
  }
12052
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
+
12053
12685
  // ui/src/bichat/data/HttpDataSource.ts
12054
12686
  function isSessionNotFoundError(err) {
12055
12687
  if (!(err instanceof AppletRPCException)) return false;
@@ -12066,6 +12698,7 @@ function toSessionArtifact(artifact) {
12066
12698
  id: artifact.id,
12067
12699
  sessionId: artifact.sessionId,
12068
12700
  messageId: artifact.messageId,
12701
+ uploadId: artifact.uploadId,
12069
12702
  type: artifact.type,
12070
12703
  name: artifact.name,
12071
12704
  description: artifact.description,
@@ -12076,21 +12709,313 @@ function toSessionArtifact(artifact) {
12076
12709
  createdAt: artifact.createdAt
12077
12710
  };
12078
12711
  }
12079
- function toPendingQuestion(rpc) {
12080
- if (!rpc) return null;
12081
- const questions = (rpc.questions || []).map((q) => ({
12082
- id: q.id,
12083
- text: q.text,
12084
- type: q.type,
12085
- options: (q.options || []).map((o) => ({
12086
- id: o.id,
12087
- label: o.label,
12088
- value: o.label
12089
- }))
12090
- }));
12712
+ function warnMalformedSessionPayload(message, details) {
12713
+ console.warn(`[BiChat] ${message}`, details || {});
12714
+ }
12715
+ function readString2(value, fallback = "") {
12716
+ return typeof value === "string" ? value : fallback;
12717
+ }
12718
+ function readNonEmptyString(value) {
12719
+ if (typeof value !== "string") return null;
12720
+ const trimmed = value.trim();
12721
+ return trimmed.length > 0 ? trimmed : null;
12722
+ }
12723
+ function readFiniteNumber(value, fallback = 0) {
12724
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
12725
+ }
12726
+ function readOptionalFiniteNumber(value) {
12727
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
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
+ }
12769
+ function normalizeQuestionType(rawType) {
12770
+ const normalized = readString2(rawType).trim().toUpperCase().replace(/[\s-]+/g, "_");
12771
+ return normalized === "MULTIPLE_CHOICE" ? "MULTIPLE_CHOICE" : "SINGLE_CHOICE";
12772
+ }
12773
+ function normalizeMessageRole(rawRole) {
12774
+ const normalized = readString2(rawRole).trim().toLowerCase();
12775
+ if (normalized === "user" /* User */) return "user" /* User */;
12776
+ if (normalized === "system" /* System */) return "system" /* System */;
12777
+ if (normalized === "tool" /* Tool */) return "tool" /* Tool */;
12778
+ return "assistant" /* Assistant */;
12779
+ }
12780
+ function sanitizeAttachment(rawAttachment, turnId, index) {
12781
+ if (!isRecord(rawAttachment)) {
12782
+ warnMalformedSessionPayload("Dropped malformed attachment entry", { turnId, index });
12783
+ return null;
12784
+ }
12785
+ const filename = readString2(rawAttachment.filename, "attachment");
12786
+ const mimeType = readString2(rawAttachment.mimeType, "application/octet-stream");
12787
+ const id = readNonEmptyString(rawAttachment.id) || void 0;
12788
+ const clientKey = readNonEmptyString(rawAttachment.clientKey) || id || `${turnId}-attachment-${index}`;
12789
+ return {
12790
+ id,
12791
+ clientKey,
12792
+ filename,
12793
+ mimeType,
12794
+ sizeBytes: readFiniteNumber(rawAttachment.sizeBytes),
12795
+ uploadId: readOptionalFiniteNumber(rawAttachment.uploadId),
12796
+ base64Data: readNonEmptyString(rawAttachment.base64Data) || void 0,
12797
+ url: readNonEmptyString(rawAttachment.url) || void 0,
12798
+ preview: readNonEmptyString(rawAttachment.preview) || void 0
12799
+ };
12800
+ }
12801
+ function sanitizeUserAttachments(rawAttachments, turnId) {
12802
+ if (!Array.isArray(rawAttachments)) return [];
12803
+ const result = [];
12804
+ for (let i = 0; i < rawAttachments.length; i++) {
12805
+ const sanitized = sanitizeAttachment(rawAttachments[i], turnId, i);
12806
+ if (sanitized) result.push(sanitized);
12807
+ }
12808
+ return result;
12809
+ }
12810
+ function sanitizeAssistantArtifacts(rawArtifacts, turnId) {
12811
+ if (!Array.isArray(rawArtifacts)) return [];
12812
+ const artifacts = [];
12813
+ for (let i = 0; i < rawArtifacts.length; i++) {
12814
+ const raw = rawArtifacts[i];
12815
+ if (!isRecord(raw)) {
12816
+ warnMalformedSessionPayload("Dropped malformed assistant artifact", { turnId, index: i });
12817
+ continue;
12818
+ }
12819
+ const type = readString2(raw.type).toLowerCase();
12820
+ if (type !== "excel" && type !== "pdf") {
12821
+ continue;
12822
+ }
12823
+ const url = readNonEmptyString(raw.url);
12824
+ if (!url) {
12825
+ warnMalformedSessionPayload("Dropped assistant artifact without url", { turnId, index: i });
12826
+ continue;
12827
+ }
12828
+ artifacts.push({
12829
+ type,
12830
+ filename: readString2(raw.filename, "download"),
12831
+ url,
12832
+ sizeReadable: readNonEmptyString(raw.sizeReadable) || void 0,
12833
+ rowCount: typeof raw.rowCount === "number" && Number.isFinite(raw.rowCount) ? raw.rowCount : void 0,
12834
+ description: readNonEmptyString(raw.description) || void 0
12835
+ });
12836
+ }
12837
+ return artifacts;
12838
+ }
12839
+ function sanitizeAssistantTurn(rawAssistantTurn, fallbackCreatedAt, turnId) {
12840
+ if (rawAssistantTurn == null) return void 0;
12841
+ if (!isRecord(rawAssistantTurn)) {
12842
+ warnMalformedSessionPayload("Dropped malformed assistant turn payload", { turnId });
12843
+ return void 0;
12844
+ }
12845
+ const assistantID = readNonEmptyString(rawAssistantTurn.id);
12846
+ if (!assistantID) {
12847
+ warnMalformedSessionPayload("Dropped assistant turn without id", { turnId });
12848
+ return void 0;
12849
+ }
12850
+ const citations = Array.isArray(rawAssistantTurn.citations) ? rawAssistantTurn.citations.filter((item) => isRecord(item)).map((item, index) => ({
12851
+ id: readString2(item.id, `${assistantID}-citation-${index}`),
12852
+ type: readString2(item.type),
12853
+ title: readString2(item.title),
12854
+ url: readString2(item.url),
12855
+ startIndex: readFiniteNumber(item.startIndex),
12856
+ endIndex: readFiniteNumber(item.endIndex),
12857
+ excerpt: readNonEmptyString(item.excerpt) || void 0
12858
+ })) : [];
12859
+ const toolCalls = Array.isArray(rawAssistantTurn.toolCalls) ? rawAssistantTurn.toolCalls.filter((item) => isRecord(item)).map((item, index) => ({
12860
+ id: readString2(item.id, `${assistantID}-tool-${index}`),
12861
+ name: readString2(item.name),
12862
+ arguments: readString2(item.arguments),
12863
+ result: readNonEmptyString(item.result) || void 0,
12864
+ error: readNonEmptyString(item.error) || void 0,
12865
+ durationMs: readFiniteNumber(item.durationMs)
12866
+ })) : [];
12867
+ const codeOutputs = Array.isArray(rawAssistantTurn.codeOutputs) ? rawAssistantTurn.codeOutputs.filter((item) => isRecord(item)).map((item) => ({
12868
+ type: (() => {
12869
+ const normalizedType = readString2(item.type, "text").toLowerCase();
12870
+ if (normalizedType === "image" || normalizedType === "error") return normalizedType;
12871
+ return "text";
12872
+ })(),
12873
+ content: readString2(item.content),
12874
+ filename: readNonEmptyString(item.filename) || void 0,
12875
+ mimeType: readNonEmptyString(item.mimeType) || void 0,
12876
+ sizeBytes: readOptionalFiniteNumber(item.sizeBytes)
12877
+ })) : [];
12878
+ const debugTrace = isRecord(rawAssistantTurn.debug) ? {
12879
+ generationMs: readOptionalFiniteNumber(rawAssistantTurn.debug.generationMs),
12880
+ usage: isRecord(rawAssistantTurn.debug.usage) ? {
12881
+ promptTokens: readFiniteNumber(rawAssistantTurn.debug.usage.promptTokens),
12882
+ completionTokens: readFiniteNumber(rawAssistantTurn.debug.usage.completionTokens),
12883
+ totalTokens: readFiniteNumber(rawAssistantTurn.debug.usage.totalTokens),
12884
+ cachedTokens: readOptionalFiniteNumber(rawAssistantTurn.debug.usage.cachedTokens),
12885
+ cost: readOptionalFiniteNumber(rawAssistantTurn.debug.usage.cost)
12886
+ } : void 0,
12887
+ tools: Array.isArray(rawAssistantTurn.debug.tools) ? rawAssistantTurn.debug.tools.filter((tool) => isRecord(tool)).map((tool) => ({
12888
+ callId: readNonEmptyString(tool.callId) || void 0,
12889
+ name: readString2(tool.name),
12890
+ arguments: readNonEmptyString(tool.arguments) || void 0,
12891
+ result: readNonEmptyString(tool.result) || void 0,
12892
+ error: readNonEmptyString(tool.error) || void 0,
12893
+ durationMs: readOptionalFiniteNumber(tool.durationMs)
12894
+ })) : []
12895
+ } : void 0;
12896
+ return {
12897
+ id: assistantID,
12898
+ role: normalizeMessageRole(rawAssistantTurn.role),
12899
+ content: readString2(rawAssistantTurn.content),
12900
+ explanation: readNonEmptyString(rawAssistantTurn.explanation) || void 0,
12901
+ citations,
12902
+ toolCalls,
12903
+ chartData: void 0,
12904
+ renderTables: void 0,
12905
+ artifacts: sanitizeAssistantArtifacts(rawAssistantTurn.artifacts, turnId),
12906
+ codeOutputs,
12907
+ debug: debugTrace,
12908
+ createdAt: readString2(rawAssistantTurn.createdAt, fallbackCreatedAt)
12909
+ };
12910
+ }
12911
+ function sanitizeConversationTurn(rawTurn, index, fallbackSessionID) {
12912
+ if (!isRecord(rawTurn)) {
12913
+ warnMalformedSessionPayload("Dropped malformed turn payload (not an object)", { index });
12914
+ return null;
12915
+ }
12916
+ if (!isRecord(rawTurn.userTurn)) {
12917
+ warnMalformedSessionPayload("Dropped malformed turn payload (missing user turn)", { index });
12918
+ return null;
12919
+ }
12920
+ const userTurnID = readNonEmptyString(rawTurn.userTurn.id);
12921
+ if (!userTurnID) {
12922
+ warnMalformedSessionPayload("Dropped malformed turn payload (missing user turn id)", { index });
12923
+ return null;
12924
+ }
12925
+ const turnID = readString2(rawTurn.id, userTurnID);
12926
+ const createdAt = readString2(
12927
+ rawTurn.createdAt,
12928
+ readString2(rawTurn.userTurn.createdAt, (/* @__PURE__ */ new Date()).toISOString())
12929
+ );
12930
+ return {
12931
+ id: turnID,
12932
+ sessionId: readString2(rawTurn.sessionId, fallbackSessionID),
12933
+ userTurn: {
12934
+ id: userTurnID,
12935
+ content: readString2(rawTurn.userTurn.content),
12936
+ attachments: sanitizeUserAttachments(rawTurn.userTurn.attachments, turnID),
12937
+ createdAt: readString2(rawTurn.userTurn.createdAt, createdAt)
12938
+ },
12939
+ assistantTurn: sanitizeAssistantTurn(rawTurn.assistantTurn, createdAt, turnID),
12940
+ createdAt
12941
+ };
12942
+ }
12943
+ function sanitizeConversationTurns(rawTurns, sessionID) {
12944
+ if (!Array.isArray(rawTurns)) {
12945
+ warnMalformedSessionPayload("Session payload contained non-array turns field", { sessionID });
12946
+ return [];
12947
+ }
12948
+ const turns = [];
12949
+ let dropped = 0;
12950
+ for (let i = 0; i < rawTurns.length; i++) {
12951
+ const sanitizedTurn = sanitizeConversationTurn(rawTurns[i], i, sessionID);
12952
+ if (sanitizedTurn) {
12953
+ turns.push(sanitizedTurn);
12954
+ } else {
12955
+ dropped++;
12956
+ }
12957
+ }
12958
+ if (dropped > 0) {
12959
+ warnMalformedSessionPayload("Dropped malformed turns from session payload", {
12960
+ sessionID,
12961
+ dropped,
12962
+ total: rawTurns.length
12963
+ });
12964
+ }
12965
+ return turns;
12966
+ }
12967
+ function sanitizePendingQuestion(rawPendingQuestion, sessionID) {
12968
+ if (!rawPendingQuestion) return null;
12969
+ const checkpointID = readNonEmptyString(rawPendingQuestion.checkpointId);
12970
+ if (!checkpointID) {
12971
+ warnMalformedSessionPayload("Dropped malformed pendingQuestion without checkpointId", { sessionID });
12972
+ return null;
12973
+ }
12974
+ if (!Array.isArray(rawPendingQuestion.questions)) {
12975
+ warnMalformedSessionPayload("Pending question had non-array questions payload", {
12976
+ sessionID,
12977
+ checkpointID
12978
+ });
12979
+ }
12980
+ const questions = Array.isArray(rawPendingQuestion.questions) ? rawPendingQuestion.questions.filter((question) => {
12981
+ if (!question || !isRecord(question)) {
12982
+ warnMalformedSessionPayload("Dropped malformed question from pendingQuestion", {
12983
+ sessionID,
12984
+ checkpointID
12985
+ });
12986
+ return false;
12987
+ }
12988
+ return true;
12989
+ }).map((question, index) => {
12990
+ const questionID = readString2(question.id, `${checkpointID}-q-${index}`);
12991
+ const options = Array.isArray(question.options) ? question.options.filter((option) => {
12992
+ if (!option || !isRecord(option)) {
12993
+ warnMalformedSessionPayload("Dropped malformed pendingQuestion option", {
12994
+ sessionID,
12995
+ checkpointID,
12996
+ questionID
12997
+ });
12998
+ return false;
12999
+ }
13000
+ return true;
13001
+ }).map((option, optionIndex) => {
13002
+ const label = readString2(option.label);
13003
+ return {
13004
+ id: readString2(option.id, `${questionID}-opt-${optionIndex}`),
13005
+ label,
13006
+ value: label
13007
+ };
13008
+ }) : [];
13009
+ return {
13010
+ id: questionID,
13011
+ text: readString2(question.text),
13012
+ type: normalizeQuestionType(question.type),
13013
+ options
13014
+ };
13015
+ }) : [];
12091
13016
  return {
12092
- id: rpc.checkpointId,
12093
- turnId: rpc.turnId || "",
13017
+ id: checkpointID,
13018
+ turnId: readString2(rawPendingQuestion.turnId),
12094
13019
  questions,
12095
13020
  status: "PENDING"
12096
13021
  };
@@ -12162,6 +13087,18 @@ function extractChartDataFromToolCalls(toolCalls) {
12162
13087
  }
12163
13088
  return void 0;
12164
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
+ }
12165
13102
  var EXPORT_TOOL_NAMES = {
12166
13103
  export_query_to_excel: "excel",
12167
13104
  export_data_to_excel: "excel",
@@ -12197,6 +13134,7 @@ function extractDownloadArtifactsFromToolCalls(toolCalls) {
12197
13134
  function normalizeAssistantTurn(turn) {
12198
13135
  const existingArtifacts = turn.artifacts || [];
12199
13136
  const fromToolCalls = extractDownloadArtifactsFromToolCalls(turn.toolCalls);
13137
+ const renderTables = turn.renderTables || extractRenderTablesFromToolCalls(turn.toolCalls);
12200
13138
  const merged = [...existingArtifacts];
12201
13139
  for (const a of fromToolCalls) {
12202
13140
  if (!merged.some((e) => e.url === a.url && e.filename === a.filename)) {
@@ -12207,6 +13145,7 @@ function normalizeAssistantTurn(turn) {
12207
13145
  ...turn,
12208
13146
  role: turn.role || "assistant" /* Assistant */,
12209
13147
  chartData: turn.chartData || extractChartDataFromToolCalls(turn.toolCalls),
13148
+ renderTables,
12210
13149
  citations: turn.citations || [],
12211
13150
  artifacts: merged,
12212
13151
  codeOutputs: turn.codeOutputs || []
@@ -12286,6 +13225,7 @@ var HttpDataSource = class {
12286
13225
  this.abortController = null;
12287
13226
  this.config = {
12288
13227
  streamEndpoint: "/stream",
13228
+ uploadEndpoint: "/api/uploads",
12289
13229
  timeout: 12e4,
12290
13230
  ...config
12291
13231
  };
@@ -12321,6 +13261,216 @@ var HttpDataSource = class {
12321
13261
  }
12322
13262
  return headers;
12323
13263
  }
13264
+ createUploadHeaders(additionalHeaders) {
13265
+ const headers = new Headers({
13266
+ ...this.config.headers,
13267
+ ...additionalHeaders
13268
+ });
13269
+ const csrfToken = this.getCSRFToken();
13270
+ if (csrfToken) {
13271
+ headers.set("X-CSRF-Token", csrfToken);
13272
+ }
13273
+ headers.delete("Content-Type");
13274
+ return headers;
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
+ }
13322
+ async uploadFile(file) {
13323
+ const formData = new FormData();
13324
+ formData.append("file", file);
13325
+ const response = await fetch(`${this.config.baseUrl}${this.config.uploadEndpoint}`, {
13326
+ method: "POST",
13327
+ headers: this.createUploadHeaders(),
13328
+ body: formData
13329
+ });
13330
+ let payload = null;
13331
+ try {
13332
+ payload = await response.json();
13333
+ } catch {
13334
+ payload = null;
13335
+ }
13336
+ if (!response.ok) {
13337
+ const errorMessage = isRecord(payload) && typeof payload.error === "string" ? payload.error : `Upload failed: HTTP ${response.status}`;
13338
+ throw new Error(errorMessage);
13339
+ }
13340
+ if (!isRecord(payload) || typeof payload.id !== "number" || payload.id <= 0) {
13341
+ throw new Error("Upload failed: invalid response payload");
13342
+ }
13343
+ return {
13344
+ id: payload.id,
13345
+ url: typeof payload.url === "string" ? payload.url : "",
13346
+ path: typeof payload.path === "string" ? payload.path : "",
13347
+ name: typeof payload.name === "string" ? payload.name : file.name,
13348
+ mimetype: typeof payload.mimetype === "string" ? payload.mimetype : file.type,
13349
+ size: typeof payload.size === "number" && Number.isFinite(payload.size) ? payload.size : file.size
13350
+ };
13351
+ }
13352
+ async attachmentToFile(attachment) {
13353
+ if (attachment.base64Data && attachment.base64Data.trim().length > 0) {
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
+ }
13365
+ }
13366
+ if (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);
13378
+ if (!response.ok) {
13379
+ throw new Error(`Attachment "${attachment.filename}" decode failed: source HTTP ${response.status}`);
13380
+ }
13381
+ const blob = await response.blob();
13382
+ return new File([blob], attachment.filename, {
13383
+ type: attachment.mimeType || blob.type || "application/octet-stream"
13384
+ });
13385
+ }
13386
+ throw new Error(`Attachment "${attachment.filename}" has no uploadable data`);
13387
+ }
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) {
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
+ });
13406
+ return {
13407
+ id: attachment.uploadId,
13408
+ url: attachment.url || "",
13409
+ path: "",
13410
+ name: attachment.filename,
13411
+ mimetype: attachment.mimeType,
13412
+ size: attachment.sizeBytes
13413
+ };
13414
+ }
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
+ }
13473
+ }
12324
13474
  async callRPC(method, params) {
12325
13475
  return this.rpc.callTyped(method, params);
12326
13476
  }
@@ -12343,11 +13493,22 @@ var HttpDataSource = class {
12343
13493
  return { artifacts: [], hasMore: false, nextOffset: 0 };
12344
13494
  })
12345
13495
  ]);
12346
- const turns = attachArtifactsToTurns(normalizeTurns(data.turns), artifactsData.artifacts || []);
13496
+ const sanitizedTurns = sanitizeConversationTurns(data.turns, id);
13497
+ const turns = attachArtifactsToTurns(
13498
+ normalizeTurns(sanitizedTurns),
13499
+ artifactsData.artifacts || []
13500
+ );
13501
+ const pendingQuestion = sanitizePendingQuestion(data.pendingQuestion, id);
13502
+ if (data.pendingQuestion && pendingQuestion && pendingQuestion.questions.length === 0) {
13503
+ warnMalformedSessionPayload("Pending question normalized to zero renderable questions", {
13504
+ sessionID: id,
13505
+ checkpointID: pendingQuestion.id
13506
+ });
13507
+ }
12347
13508
  return {
12348
13509
  session: toSession(data.session),
12349
13510
  turns,
12350
- pendingQuestion: toPendingQuestion(data.pendingQuestion)
13511
+ pendingQuestion
12351
13512
  };
12352
13513
  } catch (err) {
12353
13514
  if (isSessionNotFoundError(err)) {
@@ -12379,27 +13540,12 @@ var HttpDataSource = class {
12379
13540
  return { artifacts: [] };
12380
13541
  }
12381
13542
  validateFileCount(0, files.length, 10);
12382
- const attachments = [];
12383
- for (const file of files) {
12384
- validateAttachmentFile(file);
12385
- const base64Data = await convertToBase64(file);
12386
- attachments.push({
12387
- clientKey: crypto.randomUUID(),
12388
- filename: file.name,
12389
- mimeType: file.type,
12390
- sizeBytes: file.size,
12391
- base64Data
12392
- });
12393
- }
13543
+ files.forEach((file) => validateAttachmentFile(file));
13544
+ const uploads = await Promise.all(files.map((file) => this.uploadFile(file)));
12394
13545
  const data = await this.callRPC("bichat.session.uploadArtifacts", {
12395
13546
  sessionId,
12396
- attachments: attachments.map((a) => ({
12397
- id: "",
12398
- // Backend will assign ID
12399
- filename: a.filename,
12400
- mimeType: a.mimeType,
12401
- sizeBytes: a.sizeBytes,
12402
- base64Data: a.base64Data
13547
+ attachments: uploads.map((upload) => ({
13548
+ uploadId: upload.id
12403
13549
  }))
12404
13550
  });
12405
13551
  return {
@@ -12430,22 +13576,26 @@ var HttpDataSource = class {
12430
13576
  signal.addEventListener("abort", onExternalAbort);
12431
13577
  }
12432
13578
  const url = `${this.config.baseUrl}${this.config.streamEndpoint}`;
12433
- const payload = {
12434
- sessionId,
12435
- content,
12436
- debugMode: options?.debugMode ?? false,
12437
- replaceFromMessageId: options?.replaceFromMessageID,
12438
- attachments: attachments.map((a) => ({
12439
- filename: a.filename,
12440
- mimeType: a.mimeType,
12441
- sizeBytes: a.sizeBytes,
12442
- base64Data: a.base64Data,
12443
- url: a.url
12444
- }))
12445
- };
12446
13579
  let connectionTimeoutID;
12447
13580
  let connectionTimedOut = false;
12448
13581
  try {
13582
+ const uploads = await Promise.all(
13583
+ attachments.map(
13584
+ (attachment, attachmentIndex) => this.ensureAttachmentUpload(attachment, { sessionId, attachmentIndex })
13585
+ )
13586
+ );
13587
+ const streamAttachments = this.assertUploadReferences(uploads);
13588
+ this.logAttachmentLifecycle("stream_send_with_upload_ids", {
13589
+ sessionId,
13590
+ attachmentCount: streamAttachments.length
13591
+ });
13592
+ const payload = {
13593
+ sessionId,
13594
+ content,
13595
+ debugMode: options?.debugMode ?? false,
13596
+ replaceFromMessageId: options?.replaceFromMessageID,
13597
+ attachments: streamAttachments
13598
+ };
12449
13599
  const timeoutMs = this.config.timeout ?? 0;
12450
13600
  if (timeoutMs > 0) {
12451
13601
  connectionTimeoutID = setTimeout(() => {
@@ -12648,6 +13798,7 @@ exports.ErrorBoundary = ErrorBoundary;
12648
13798
  exports.HttpDataSource = HttpDataSource;
12649
13799
  exports.ImageModal = ImageModal;
12650
13800
  exports.InlineQuestionForm = InlineQuestionForm;
13801
+ exports.InteractiveTableCard = InteractiveTableCard;
12651
13802
  exports.IotaContextProvider = IotaContextProvider;
12652
13803
  exports.ListItemSkeleton = ListItemSkeleton;
12653
13804
  exports.LoadingSpinner = MemoizedLoadingSpinner;