@iota-uz/sdk 0.4.12 → 0.4.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,17 @@
1
- import React, { createContext, lazy, memo, forwardRef, useState, useRef, useMemo, useEffect, useImperativeHandle, useCallback, isValidElement, cloneElement, useContext, useId, Suspense, Component, Children } from 'react';
1
+ import React, { createContext, lazy, memo, forwardRef, useState, useRef, useMemo, useEffect, useImperativeHandle, useCallback, isValidElement, cloneElement, useContext, useId, useSyncExternalStore, Suspense, Component, Children } from 'react';
2
2
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
3
  import ReactApexChart from 'react-apexcharts';
4
4
  import ApexCharts from 'apexcharts';
5
- import { X, Bug, ArrowUp, ArrowDown, Stack, Paperclip, PaperPlaneRight, CircleNotch, ArrowUUpLeft, PencilSimple, Check, Bookmark, ArrowsClockwise, Archive, Trash, DotsThree, Warning, ArrowClockwise, Image, ImageBroken, CaretLeft, CaretRight, MagnifyingGlass, WarningCircle, CaretDown, Info, CheckCircle, XCircle, Spinner, Copy, FilePdf, FileXls, FileCsv, FileDoc, FileCode, FileText, File, ChartBar, DownloadSimple, Download, ChatCircleDots, PencilSimpleLine, ArrowLeft, PaperPlaneTilt, ArrowRight, Timer, Lightning, Database, Wrench, ClockCounterClockwise, Lightbulb, Package, Plus, Gear, Users, List, CaretLineLeft, CaretLineRight, ArrowsCounterClockwise, Code, ArrowSquareOut, SpinnerGap, FloppyDisk, Sidebar } from '@phosphor-icons/react';
5
+ import { X, Bug, ArrowUp, ArrowDown, Stack, Paperclip, PaperPlaneRight, CircleNotch, ArrowUUpLeft, PencilSimple, Check, Bookmark, ArrowsClockwise, Archive, Trash, DotsThree, Warning, ArrowClockwise, Image, ArrowCounterClockwise, ImageBroken, CaretLeft, CaretRight, Info, CheckCircle, XCircle, Spinner, MagnifyingGlass, WarningCircle, CaretDown, Copy, FilePdf, FileXls, FileCsv, FileDoc, FileCode, FileText, File, ChartBar, DownloadSimple, Download, ChatCircleDots, PencilSimpleLine, ArrowLeft, PaperPlaneTilt, ArrowRight, Timer, Lightning, Database, Wrench, ClockCounterClockwise, Lightbulb, Package, Plus, ArrowsCounterClockwise, Gear, Users, List, CaretLineLeft, CaretLineRight, Code, ArrowSquareOut, SpinnerGap, FloppyDisk, Sidebar } from '@phosphor-icons/react';
6
6
  import { Prism } from 'react-syntax-highlighter';
7
7
  import { vscDarkPlus, vs } from 'react-syntax-highlighter/dist/esm/styles/prism';
8
8
  import ReactMarkdown from 'react-markdown';
9
9
  import remarkGfm from 'remark-gfm';
10
10
  import { useMotionValue, useTransform, motion, AnimatePresence, useReducedMotion } from 'framer-motion';
11
- import { formatDistanceToNow, startOfDay, differenceInDays } from 'date-fns';
12
- import { Menu, MenuButton, MenuItems, MenuItem, Dialog, DialogBackdrop, DialogPanel, DialogTitle, Description, Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react';
13
- import { createPortal } from 'react-dom';
14
11
  import 'react-dom/client';
12
+ import { startOfDay, differenceInDays, differenceInMinutes, differenceInHours, format } from 'date-fns';
13
+ import { Menu, MenuButton, MenuItems, MenuItem, Dialog, DialogBackdrop, DialogPanel, DialogTitle, Description, Disclosure, DisclosureButton, DisclosurePanel, Transition } from '@headlessui/react';
14
+ import { createPortal } from 'react-dom';
15
15
 
16
16
  var __defProp = Object.defineProperty;
17
17
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -22,12 +22,12 @@ var __export = (target, all) => {
22
22
  for (var name in all)
23
23
  __defProp(target, name, { get: all[name], enumerable: true });
24
24
  };
25
- function IotaContextProvider({ children }) {
26
- const initialContext = window.__BICHAT_CONTEXT__;
27
- if (!initialContext) {
28
- throw new Error("BICHAT_CONTEXT not found. Ensure server injected context into window object.");
25
+ function IotaContextProvider({ context, children }) {
26
+ const resolved = context ?? (typeof window !== "undefined" ? window.__APPLET_CONTEXT__ : void 0);
27
+ if (!resolved) {
28
+ throw new Error("APPLET_CONTEXT not found. Pass a `context` prop or ensure the server injected context into window.__APPLET_CONTEXT__.");
29
29
  }
30
- return /* @__PURE__ */ jsx(IotaContext.Provider, { value: initialContext, children });
30
+ return /* @__PURE__ */ jsx(IotaContext.Provider, { value: resolved, children });
31
31
  }
32
32
  function useIotaContext() {
33
33
  const context = useContext(IotaContext);
@@ -37,7 +37,7 @@ function useIotaContext() {
37
37
  return context;
38
38
  }
39
39
  function hasPermission(permission) {
40
- const context = window.__BICHAT_CONTEXT__;
40
+ const context = typeof window !== "undefined" ? window.__APPLET_CONTEXT__ : void 0;
41
41
  if (!context) {
42
42
  return false;
43
43
  }
@@ -294,12 +294,16 @@ var init_chartSpec = __esm({
294
294
  var TableExportButton;
295
295
  var init_TableExportButton = __esm({
296
296
  "ui/src/bichat/components/TableExportButton.tsx"() {
297
+ init_useTranslation();
297
298
  TableExportButton = memo(function TableExportButton2({
298
299
  onClick,
299
300
  disabled = false,
300
- label = "Export",
301
- disabledTooltip = "Please wait..."
301
+ label,
302
+ disabledTooltip
302
303
  }) {
304
+ const { t } = useTranslation();
305
+ const resolvedLabel = label ?? t("BiChat.Export");
306
+ const resolvedDisabledTooltip = disabledTooltip ?? t("BiChat.Common.PleaseWait");
303
307
  return /* @__PURE__ */ jsxs(
304
308
  "button",
305
309
  {
@@ -307,35 +311,38 @@ var init_TableExportButton = __esm({
307
311
  onClick,
308
312
  disabled,
309
313
  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",
310
- "aria-label": label,
311
- title: disabled ? disabledTooltip : label,
314
+ "aria-label": resolvedLabel,
315
+ title: disabled ? resolvedDisabledTooltip : resolvedLabel,
312
316
  children: [
313
317
  /* @__PURE__ */ jsx(FileXls, { size: 16, weight: "fill" }),
314
- /* @__PURE__ */ jsx("span", { children: label })
318
+ /* @__PURE__ */ jsx("span", { children: resolvedLabel })
315
319
  ]
316
320
  }
317
321
  );
318
322
  });
319
323
  }
320
324
  });
321
- var DEFAULT_EXPORT_MESSAGE, TableWithExport;
325
+ var TableWithExport;
322
326
  var init_TableWithExport = __esm({
323
327
  "ui/src/bichat/components/TableWithExport.tsx"() {
324
328
  init_TableExportButton();
325
- DEFAULT_EXPORT_MESSAGE = "Export the table above to Excel";
329
+ init_useTranslation();
326
330
  TableWithExport = memo(function TableWithExport2({
327
331
  children,
328
332
  sendMessage,
329
333
  disabled = false,
330
- exportMessage = DEFAULT_EXPORT_MESSAGE,
331
- exportLabel = "Export"
334
+ exportMessage,
335
+ exportLabel
332
336
  }) {
337
+ const { t } = useTranslation();
338
+ const resolvedExportMessage = exportMessage ?? t("BiChat.ExportTableToExcel");
339
+ const resolvedExportLabel = exportLabel ?? t("BiChat.Export");
333
340
  const handleExport = useCallback(() => {
334
- sendMessage?.(exportMessage);
335
- }, [sendMessage, exportMessage]);
341
+ sendMessage?.(resolvedExportMessage);
342
+ }, [sendMessage, resolvedExportMessage]);
336
343
  return /* @__PURE__ */ jsxs(Fragment, { children: [
337
344
  /* @__PURE__ */ jsx("div", { className: "markdown-table-wrapper overflow-x-auto", children: /* @__PURE__ */ jsx("table", { className: "markdown-table w-full border-collapse", children }) }),
338
- sendMessage && /* @__PURE__ */ jsx("div", { className: "flex justify-end mt-1", children: /* @__PURE__ */ jsx(TableExportButton, { onClick: handleExport, disabled, label: exportLabel }) })
345
+ sendMessage && /* @__PURE__ */ jsx("div", { className: "flex justify-end mt-1", children: /* @__PURE__ */ jsx(TableExportButton, { onClick: handleExport, disabled, label: resolvedExportLabel }) })
339
346
  ] });
340
347
  });
341
348
  }
@@ -355,9 +362,12 @@ function CodeBlock({
355
362
  language,
356
363
  value,
357
364
  inline,
358
- copyLabel = "Copy",
359
- copiedLabel = "Copied!"
365
+ copyLabel,
366
+ copiedLabel
360
367
  }) {
368
+ const { t } = useTranslation();
369
+ const resolvedCopyLabel = copyLabel ?? t("BiChat.Message.Copy");
370
+ const resolvedCopiedLabel = copiedLabel ?? t("BiChat.Message.Copied");
361
371
  const [copied, setCopied] = useState(false);
362
372
  const [copyFailed, setCopyFailed] = useState(false);
363
373
  const [isDarkMode, setIsDarkMode] = useState(getInitialDarkMode);
@@ -433,14 +443,14 @@ function CodeBlock({
433
443
  {
434
444
  onClick: handleCopy,
435
445
  className: `text-xs transition-colors flex items-center gap-1.5 ${copyFailed ? "text-red-500 dark:text-red-400" : "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"}`,
436
- title: copyLabel,
446
+ title: resolvedCopyLabel,
437
447
  "aria-live": "polite",
438
448
  children: copied ? /* @__PURE__ */ jsxs(Fragment, { children: [
439
449
  /* @__PURE__ */ jsx(Check, { size: 16, className: "w-4 h-4" }),
440
- /* @__PURE__ */ jsx("span", { children: copiedLabel })
450
+ /* @__PURE__ */ jsx("span", { children: resolvedCopiedLabel })
441
451
  ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
442
452
  /* @__PURE__ */ jsx(Copy, { size: 16, className: "w-4 h-4" }),
443
- /* @__PURE__ */ jsx("span", { children: copyFailed ? "Failed" : copyLabel })
453
+ /* @__PURE__ */ jsx("span", { children: copyFailed ? t("BiChat.Message.CopyFailed") : resolvedCopyLabel })
444
454
  ] })
445
455
  }
446
456
  )
@@ -472,6 +482,7 @@ function CodeBlock({
472
482
  var getInitialDarkMode, languageMap, MemoizedCodeBlock, CodeBlock_default;
473
483
  var init_CodeBlock = __esm({
474
484
  "ui/src/bichat/components/CodeBlock.tsx"() {
485
+ init_useTranslation();
475
486
  getInitialDarkMode = () => {
476
487
  if (typeof document === "undefined") return false;
477
488
  return document.documentElement.classList.contains("dark");
@@ -530,10 +541,14 @@ function MarkdownRenderer({
530
541
  citations,
531
542
  sendMessage,
532
543
  sendDisabled = false,
533
- copyLabel = "Copy",
534
- copiedLabel = "Copied!",
535
- exportLabel = "Export"
544
+ copyLabel,
545
+ copiedLabel,
546
+ exportLabel
536
547
  }) {
548
+ const { t } = useTranslation();
549
+ const resolvedCopyLabel = copyLabel ?? t("BiChat.Message.Copy");
550
+ const resolvedCopiedLabel = copiedLabel ?? t("BiChat.Message.Copied");
551
+ const resolvedExportLabel = exportLabel ?? t("BiChat.Export");
537
552
  const processed = useMemo(() => {
538
553
  return processCitations(content, citations);
539
554
  }, [content, citations]);
@@ -568,8 +583,8 @@ function MarkdownRenderer({
568
583
  language,
569
584
  value,
570
585
  inline: false,
571
- copyLabel,
572
- copiedLabel
586
+ copyLabel: resolvedCopyLabel,
587
+ copiedLabel: resolvedCopiedLabel
573
588
  }
574
589
  )
575
590
  }
@@ -606,7 +621,7 @@ function MarkdownRenderer({
606
621
  {
607
622
  sendMessage,
608
623
  disabled: sendDisabled,
609
- exportLabel,
624
+ exportLabel: resolvedExportLabel,
610
625
  children
611
626
  }
612
627
  ),
@@ -628,6 +643,7 @@ var init_MarkdownRenderer = __esm({
628
643
  init_chartSpec();
629
644
  init_TableWithExport();
630
645
  init_ChartCard();
646
+ init_useTranslation();
631
647
  CodeBlock2 = lazy(() => Promise.resolve().then(() => (init_CodeBlock(), CodeBlock_exports)).then((module) => ({ default: module.CodeBlock })));
632
648
  INLINE_TAGS = /* @__PURE__ */ new Set([
633
649
  "a",
@@ -696,43 +712,300 @@ var RateLimiter = class {
696
712
  }
697
713
  };
698
714
 
699
- // ui/src/bichat/utils/debugTrace.ts
700
- function hasMeaningfulUsage(trace) {
701
- if (!trace) return false;
702
- return trace.promptTokens > 0 || trace.completionTokens > 0 || trace.totalTokens > 0 || (trace.cachedTokens ?? 0) > 0 || (trace.cost ?? 0) > 0;
715
+ // ui/src/applet-devtools/enabled.ts
716
+ function shouldEnableAppletDevtools() {
717
+ if (typeof window === "undefined") return false;
718
+ const url = new URL(window.location.href);
719
+ if (url.searchParams.get("appletDebug") === "1") return true;
720
+ try {
721
+ return window.localStorage.getItem("iotaAppletDevtools") === "1";
722
+ } catch {
723
+ return false;
724
+ }
703
725
  }
704
- function hasDebugTrace(trace) {
705
- return trace.tools.length > 0 || hasMeaningfulUsage(trace.usage) || !!trace.generationMs;
726
+
727
+ // ui/src/applet-host/rpc.ts
728
+ var AppletRPCException = class extends Error {
729
+ constructor(args) {
730
+ super(args.message);
731
+ this.name = "AppletRPCException";
732
+ this.code = args.code;
733
+ this.details = args.details;
734
+ this.cause = args.cause;
735
+ }
736
+ };
737
+ function createAppletRPCClient(options) {
738
+ const fetcher = options.fetcher ?? fetch;
739
+ const timeoutMs = typeof options.timeoutMs === "number" && options.timeoutMs > 0 ? options.timeoutMs : 0;
740
+ async function call(method, params) {
741
+ const req = { id: crypto.randomUUID(), method, params };
742
+ const startedAt = typeof performance !== "undefined" ? performance.now() : Date.now();
743
+ const abortController = timeoutMs > 0 ? new AbortController() : void 0;
744
+ let timeoutHandle;
745
+ let timedOut = false;
746
+ maybeDispatchRPCEvent({
747
+ id: req.id,
748
+ method: req.method,
749
+ status: "start"
750
+ });
751
+ try {
752
+ if (abortController) {
753
+ timeoutHandle = setTimeout(() => {
754
+ timedOut = true;
755
+ abortController.abort();
756
+ }, timeoutMs);
757
+ }
758
+ const resp = await fetcher(options.endpoint, {
759
+ method: "POST",
760
+ headers: { "Content-Type": "application/json" },
761
+ body: JSON.stringify(req),
762
+ signal: abortController?.signal
763
+ });
764
+ if (!resp.ok) {
765
+ throw new AppletRPCException({
766
+ code: "http_error",
767
+ message: `HTTP ${resp.status}`,
768
+ details: { status: resp.status }
769
+ });
770
+ }
771
+ const json = await resp.json();
772
+ if (json.error) {
773
+ throw new AppletRPCException({
774
+ code: json.error.code,
775
+ message: json.error.message,
776
+ details: json.error.details
777
+ });
778
+ }
779
+ if (json.result === void 0) {
780
+ throw new AppletRPCException({
781
+ code: "invalid_response",
782
+ message: "Missing result in successful response"
783
+ });
784
+ }
785
+ maybeDispatchRPCEvent({
786
+ id: req.id,
787
+ method: req.method,
788
+ status: "success",
789
+ durationMs: elapsedMs(startedAt)
790
+ });
791
+ return json.result;
792
+ } catch (err) {
793
+ let rpcErr = err;
794
+ if (err instanceof Error && err.name === "AbortError") {
795
+ rpcErr = new AppletRPCException({
796
+ code: timedOut ? "timeout" : "aborted",
797
+ message: timedOut ? `RPC request timed out after ${timeoutMs}ms` : "RPC request was aborted",
798
+ cause: err
799
+ });
800
+ }
801
+ maybeDispatchRPCEvent({
802
+ id: req.id,
803
+ method: req.method,
804
+ status: "error",
805
+ durationMs: elapsedMs(startedAt),
806
+ error: rpcErr
807
+ });
808
+ throw rpcErr;
809
+ } finally {
810
+ if (timeoutHandle !== void 0) {
811
+ clearTimeout(timeoutHandle);
812
+ }
813
+ }
814
+ }
815
+ async function callTyped(method, params) {
816
+ return call(method, params);
817
+ }
818
+ return { call, callTyped };
706
819
  }
707
- function getSessionDebugUsage(turns) {
708
- let promptTokens = 0;
709
- let completionTokens = 0;
710
- let totalTokens = 0;
711
- let turnsWithUsage = 0;
712
- let latestPromptTokens = 0;
713
- let latestCompletionTokens = 0;
714
- let latestTotalTokens = 0;
715
- for (const turn of turns) {
716
- const usage = turn.assistantTurn?.debug?.usage;
717
- if (!hasMeaningfulUsage(usage) || !usage) {
718
- continue;
820
+ function maybeDispatchRPCEvent(detail) {
821
+ if (typeof window === "undefined") return;
822
+ if (!shouldEnableAppletDevtools()) return;
823
+ window.dispatchEvent(new CustomEvent("iota:applet-rpc", { detail }));
824
+ }
825
+ function elapsedMs(startedAt) {
826
+ const now = typeof performance !== "undefined" ? performance.now() : Date.now();
827
+ return Math.max(0, Math.round(now - startedAt));
828
+ }
829
+
830
+ // ui/src/bichat/utils/errorDisplay.ts
831
+ function isPermissionDeniedError(error) {
832
+ if (!error) return false;
833
+ if (error instanceof Error) {
834
+ const msg = error.message.toLowerCase();
835
+ if (msg.includes("forbidden") || msg.includes("permission denied")) return true;
836
+ }
837
+ if (typeof error === "object" && error !== null) {
838
+ const obj = error;
839
+ if (obj.code === "forbidden" || obj.code === 403) return true;
840
+ if (obj.status === 403) return true;
841
+ if (obj.statusCode === 403) return true;
842
+ if (typeof obj.response === "object" && obj.response !== null) {
843
+ const resp = obj.response;
844
+ if (resp.status === 403) return true;
719
845
  }
720
- turnsWithUsage++;
721
- promptTokens += usage.promptTokens;
722
- completionTokens += usage.completionTokens;
723
- totalTokens += usage.totalTokens;
724
- latestPromptTokens = usage.promptTokens;
725
- latestCompletionTokens = usage.completionTokens;
726
- latestTotalTokens = usage.totalTokens;
846
+ }
847
+ if (typeof error === "string") {
848
+ const lower = error.toLowerCase();
849
+ if (lower.includes("forbidden") || lower.includes("permission denied")) return true;
850
+ }
851
+ return false;
852
+ }
853
+ function extractStatus(error) {
854
+ if (!error || typeof error !== "object") return void 0;
855
+ const obj = error;
856
+ if (typeof obj.status === "number") return obj.status;
857
+ if (typeof obj.statusCode === "number") return obj.statusCode;
858
+ if (typeof obj.details === "object" && obj.details !== null) {
859
+ const details = obj.details;
860
+ if (typeof details.status === "number") return details.status;
861
+ if (typeof details.statusCode === "number") return details.statusCode;
862
+ }
863
+ if (typeof obj.response === "object" && obj.response !== null) {
864
+ const response = obj.response;
865
+ if (typeof response.status === "number") return response.status;
866
+ }
867
+ return void 0;
868
+ }
869
+ function isOfflineNow() {
870
+ return typeof navigator !== "undefined" && navigator.onLine === false;
871
+ }
872
+ function inferErrorCode(error) {
873
+ if (isOfflineNow()) return "offline";
874
+ if (error instanceof AppletRPCException) {
875
+ const code = String(error.code || "").toLowerCase().trim();
876
+ if (code) return code;
877
+ }
878
+ if (typeof error === "object" && error !== null) {
879
+ const obj = error;
880
+ if (typeof obj.code === "string" && obj.code.trim() !== "") {
881
+ return obj.code.toLowerCase();
882
+ }
883
+ if (typeof obj.error === "string" && obj.error.trim() !== "") {
884
+ return obj.error.toLowerCase();
885
+ }
886
+ }
887
+ const status = extractStatus(error);
888
+ if (status === 401 || status === 403) return "forbidden";
889
+ if (status === 404) return "not_found";
890
+ if (status === 408) return "timeout";
891
+ if (status === 413) return "payload_too_large";
892
+ if (status === 429) return "rate_limited";
893
+ if (status && status >= 500) return "server_error";
894
+ if (status && status >= 400) return "bad_request";
895
+ let message = "";
896
+ if (error instanceof Error) message = error.message;
897
+ if (!message && typeof error === "string") message = error;
898
+ const lower = message.toLowerCase();
899
+ if (lower.includes("timeout") || lower.includes("timed out")) return "timeout";
900
+ if (lower.includes("abort") || lower.includes("cancel")) return "aborted";
901
+ if (lower.includes("forbidden") || lower.includes("permission denied")) return "forbidden";
902
+ if (lower.includes("not found") || lower.includes("session not found")) return "not_found";
903
+ if (lower.includes("payload too large") || lower.includes("request too large") || lower.includes("413")) return "payload_too_large";
904
+ if (lower.includes("network") || lower.includes("failed to fetch")) return "network_error";
905
+ return "unknown";
906
+ }
907
+ function describeCode(code, fallbackTitle) {
908
+ switch (code) {
909
+ case "offline":
910
+ return {
911
+ title: "You are offline",
912
+ description: "Check your internet connection and try again.",
913
+ retryable: true
914
+ };
915
+ case "timeout":
916
+ return {
917
+ title: "Request timed out",
918
+ description: "The request took too long. Please try again.",
919
+ retryable: true
920
+ };
921
+ case "aborted":
922
+ return {
923
+ title: "Request canceled",
924
+ description: "The request was canceled before completion.",
925
+ retryable: true
926
+ };
927
+ case "forbidden":
928
+ return {
929
+ title: "Access denied",
930
+ description: "Your account does not have permission for this action.",
931
+ retryable: false
932
+ };
933
+ case "not_found":
934
+ return {
935
+ title: "Not found",
936
+ description: "The requested resource could not be found.",
937
+ retryable: false
938
+ };
939
+ case "payload_too_large":
940
+ return {
941
+ title: "Attachment too large",
942
+ description: "The uploaded payload exceeds allowed limits. Reduce file size and retry.",
943
+ retryable: false
944
+ };
945
+ case "invalid_request":
946
+ case "validation":
947
+ case "bad_request":
948
+ return {
949
+ title: "Invalid request",
950
+ description: "The request could not be processed. Review the input and try again.",
951
+ retryable: false
952
+ };
953
+ case "rate_limited":
954
+ return {
955
+ title: "Too many requests",
956
+ description: "Please wait a moment before trying again.",
957
+ retryable: true
958
+ };
959
+ case "http_error":
960
+ case "server_error":
961
+ return {
962
+ title: "Server error",
963
+ description: "The server failed to process this request. Please retry shortly.",
964
+ retryable: true
965
+ };
966
+ case "network_error":
967
+ return {
968
+ title: "Network error",
969
+ description: "A network issue interrupted the request. Please try again.",
970
+ retryable: true
971
+ };
972
+ default:
973
+ return {
974
+ title: fallbackTitle,
975
+ description: "Something went wrong. Please try again.",
976
+ retryable: true
977
+ };
978
+ }
979
+ }
980
+ function normalizeRPCError(error, fallbackTitle) {
981
+ const code = inferErrorCode(error);
982
+ const base = describeCode(code, fallbackTitle);
983
+ const permissionDenied = code === "forbidden" || isPermissionDeniedError(error);
984
+ let description = base.description;
985
+ if (error instanceof AppletRPCException && typeof error.message === "string" && error.message.trim() !== "" && base.title === fallbackTitle) {
986
+ description = error.message;
987
+ } else if (error instanceof Error && error.message && code === "unknown") {
988
+ description = error.message;
727
989
  }
728
990
  return {
729
- promptTokens,
730
- completionTokens,
731
- totalTokens,
732
- turnsWithUsage,
733
- latestPromptTokens,
734
- latestCompletionTokens,
735
- latestTotalTokens
991
+ code,
992
+ title: base.title,
993
+ description,
994
+ userMessage: description || base.title,
995
+ retryable: base.retryable,
996
+ isPermissionDenied: permissionDenied,
997
+ isTimeout: code === "timeout",
998
+ isOffline: code === "offline",
999
+ isCanceled: code === "aborted",
1000
+ isNotFound: code === "not_found"
1001
+ };
1002
+ }
1003
+ function toErrorDisplay(error, fallbackTitle) {
1004
+ const normalized = normalizeRPCError(error, fallbackTitle);
1005
+ return {
1006
+ title: normalized.title,
1007
+ description: normalized.description,
1008
+ isPermissionDenied: normalized.isPermissionDenied
736
1009
  };
737
1010
  }
738
1011
 
@@ -785,8 +1058,45 @@ function loadQueue(sessionId) {
785
1058
  }
786
1059
  }
787
1060
 
788
- // ui/src/bichat/context/ChatContext.tsx
789
- init_IotaContext();
1061
+ // ui/src/bichat/utils/debugTrace.ts
1062
+ function hasMeaningfulUsage(trace) {
1063
+ if (!trace) return false;
1064
+ return trace.promptTokens > 0 || trace.completionTokens > 0 || trace.totalTokens > 0 || (trace.cachedTokens ?? 0) > 0 || (trace.cost ?? 0) > 0;
1065
+ }
1066
+ function hasDebugTrace(trace) {
1067
+ return trace.tools.length > 0 || hasMeaningfulUsage(trace.usage) || !!trace.generationMs;
1068
+ }
1069
+ function getSessionDebugUsage(turns) {
1070
+ let promptTokens = 0;
1071
+ let completionTokens = 0;
1072
+ let totalTokens = 0;
1073
+ let turnsWithUsage = 0;
1074
+ let latestPromptTokens = 0;
1075
+ let latestCompletionTokens = 0;
1076
+ let latestTotalTokens = 0;
1077
+ for (const turn of turns) {
1078
+ const usage = turn.assistantTurn?.debug?.usage;
1079
+ if (!hasMeaningfulUsage(usage) || !usage) {
1080
+ continue;
1081
+ }
1082
+ turnsWithUsage++;
1083
+ promptTokens += usage.promptTokens;
1084
+ completionTokens += usage.completionTokens;
1085
+ totalTokens += usage.totalTokens;
1086
+ latestPromptTokens = usage.promptTokens;
1087
+ latestCompletionTokens = usage.completionTokens;
1088
+ latestTotalTokens = usage.totalTokens;
1089
+ }
1090
+ return {
1091
+ promptTokens,
1092
+ completionTokens,
1093
+ totalTokens,
1094
+ turnsWithUsage,
1095
+ latestPromptTokens,
1096
+ latestCompletionTokens,
1097
+ latestTotalTokens
1098
+ };
1099
+ }
790
1100
 
791
1101
  // ui/src/bichat/types/index.ts
792
1102
  var MessageRole = /* @__PURE__ */ ((MessageRole2) => {
@@ -863,7 +1173,7 @@ function readDebugLimitsFromGlobalContext() {
863
1173
  if (typeof window === "undefined") {
864
1174
  return null;
865
1175
  }
866
- const limits = window.__BICHAT_CONTEXT__?.extensions?.debug?.limits;
1176
+ const limits = window.__APPLET_CONTEXT__?.extensions?.debug?.limits;
867
1177
  if (!limits) {
868
1178
  return null;
869
1179
  }
@@ -883,580 +1193,924 @@ function readDebugLimitsFromGlobalContext() {
883
1193
  completionReserveTokens
884
1194
  };
885
1195
  }
886
- var SessionCtx = createContext(null);
887
- var MessagingCtx = createContext(null);
888
- var InputCtx = createContext(null);
889
- var DEFAULT_RATE_LIMIT_CONFIG = {
890
- maxRequests: 20,
891
- windowMs: 6e4
892
- };
893
- function ChatSessionProvider({
894
- dataSource,
895
- sessionId,
896
- rateLimiter: externalRateLimiter,
897
- rateLimitConfig,
898
- children
899
- }) {
900
- const [currentSessionId, setCurrentSessionId] = useState(sessionId);
901
- const [session, setSession] = useState(null);
902
- const [fetching, setFetching] = useState(false);
903
- const [error, setError] = useState(null);
904
- const [debugModeBySession, setDebugModeBySession] = useState({});
905
- const debugSessionKey = currentSessionId || "new";
906
- const debugMode = debugModeBySession[debugSessionKey] ?? false;
907
- const debugLimits = useMemo(() => readDebugLimitsFromGlobalContext(), []);
908
- const [turns, setTurns] = useState([]);
909
- const [loading, setLoading] = useState(false);
910
- const [streamingContent, setStreamingContent] = useState("");
911
- const [isStreaming, setIsStreaming] = useState(false);
912
- const [pendingQuestion, setPendingQuestion] = useState(null);
913
- const [codeOutputs, setCodeOutputs] = useState([]);
914
- const [isCompacting, setIsCompacting] = useState(false);
915
- const [compactionSummary, setCompactionSummary] = useState(null);
916
- const [artifactsInvalidationTrigger, setArtifactsInvalidationTrigger] = useState(0);
917
- const abortControllerRef = useRef(null);
918
- const [message, setMessage] = useState("");
919
- const [inputError, setInputError] = useState(null);
920
- const [messageQueue, setMessageQueue] = useState([]);
921
- const rateLimiterRef = useRef(
922
- externalRateLimiter || new RateLimiter(rateLimitConfig || DEFAULT_RATE_LIMIT_CONFIG)
923
- );
924
- const sessionRef = useRef({ currentSessionId, debugMode, debugSessionKey });
925
- sessionRef.current = { currentSessionId, debugMode, debugSessionKey };
926
- const messagingRef = useRef({ turns, pendingQuestion, loading });
927
- messagingRef.current = { turns, pendingQuestion, loading };
928
- const sessionDebugUsage = useMemo(() => getSessionDebugUsage(turns), [turns]);
929
- useEffect(() => {
930
- setCurrentSessionId(sessionId);
931
- }, [sessionId]);
932
- useEffect(() => {
933
- const sid = currentSessionId;
934
- if (!sid || sid === "new") {
935
- setMessageQueue([]);
936
- return;
937
- }
938
- setMessageQueue(loadQueue(sid));
939
- }, [currentSessionId]);
940
- useEffect(() => {
941
- const sid = currentSessionId;
942
- if (!sid || sid === "new") return;
943
- saveQueue(sid, messageQueue);
944
- }, [currentSessionId, messageQueue]);
945
- useEffect(() => {
946
- if (!currentSessionId || currentSessionId === "new") {
947
- setSession(null);
948
- setTurns([]);
949
- setPendingQuestion(null);
950
- setFetching(false);
951
- setInputError(null);
1196
+
1197
+ // ui/src/bichat/machine/selectors.ts
1198
+ function deriveDebugMode(state) {
1199
+ const key2 = state.session.currentSessionId || "new";
1200
+ return state.session.debugModeBySession[key2] ?? false;
1201
+ }
1202
+ function deriveSessionSnapshot(state, methods) {
1203
+ return {
1204
+ session: state.session.session,
1205
+ currentSessionId: state.session.currentSessionId,
1206
+ fetching: state.session.fetching,
1207
+ error: state.session.error,
1208
+ errorRetryable: state.session.errorRetryable,
1209
+ debugMode: deriveDebugMode(state),
1210
+ sessionDebugUsage: getSessionDebugUsage(state.messaging.turns),
1211
+ debugLimits: state.session.debugLimits,
1212
+ setError: methods.setError,
1213
+ retryFetchSession: methods.retryFetchSession
1214
+ };
1215
+ }
1216
+ function deriveMessagingSnapshot(state, methods) {
1217
+ return {
1218
+ turns: state.messaging.turns,
1219
+ streamingContent: state.messaging.streamingContent,
1220
+ isStreaming: state.messaging.isStreaming,
1221
+ streamError: state.messaging.streamError,
1222
+ streamErrorRetryable: state.messaging.streamErrorRetryable,
1223
+ loading: state.messaging.loading,
1224
+ pendingQuestion: state.messaging.pendingQuestion,
1225
+ codeOutputs: state.messaging.codeOutputs,
1226
+ isCompacting: state.messaging.isCompacting,
1227
+ compactionSummary: null,
1228
+ artifactsInvalidationTrigger: state.messaging.artifactsInvalidationTrigger,
1229
+ ...methods
1230
+ };
1231
+ }
1232
+ function deriveInputSnapshot(state, methods) {
1233
+ return {
1234
+ message: state.input.message,
1235
+ inputError: state.input.inputError,
1236
+ messageQueue: state.input.messageQueue,
1237
+ ...methods
1238
+ };
1239
+ }
1240
+
1241
+ // ui/src/bichat/machine/ChatMachine.ts
1242
+ var MAX_QUEUE_SIZE = 5;
1243
+ var ChatMachine = class {
1244
+ constructor(config) {
1245
+ // ── Refs (mutable, no subscription) ─────────────────────────────────────
1246
+ this.abortController = null;
1247
+ this.lastSendAttempt = null;
1248
+ /** Prevents fetchSession effect from clobbering state while stream is active. */
1249
+ this.sendingSessionId = null;
1250
+ this.fetchCancelled = false;
1251
+ this.disposed = false;
1252
+ /** Memoized sessionDebugUsage avoids unnecessary session re-renders during streaming. */
1253
+ this.lastSessionDebugUsage = null;
1254
+ // ── Listeners ───────────────────────────────────────────────────────────
1255
+ this.sessionListeners = /* @__PURE__ */ new Set();
1256
+ this.messagingListeners = /* @__PURE__ */ new Set();
1257
+ this.inputListeners = /* @__PURE__ */ new Set();
1258
+ // ── Snapshot caches (for useSyncExternalStore identity stability) ───────
1259
+ this.cachedSessionSnapshot = null;
1260
+ this.cachedMessagingSnapshot = null;
1261
+ this.cachedInputSnapshot = null;
1262
+ this.sessionSnapshotVersion = 0;
1263
+ this.messagingSnapshotVersion = 0;
1264
+ this.inputSnapshotVersion = 0;
1265
+ this.lastSessionSnapshotVersion = -1;
1266
+ this.lastMessagingSnapshotVersion = -1;
1267
+ this.lastInputSnapshotVersion = -1;
1268
+ // =====================================================================
1269
+ // Subscribe / getSnapshot (for useSyncExternalStore)
1270
+ // =====================================================================
1271
+ this.subscribeSession = (listener) => {
1272
+ this.sessionListeners.add(listener);
1273
+ return () => {
1274
+ this.sessionListeners.delete(listener);
1275
+ };
1276
+ };
1277
+ this.getSessionSnapshot = () => {
1278
+ if (this.lastSessionSnapshotVersion !== this.sessionSnapshotVersion) {
1279
+ this.cachedSessionSnapshot = deriveSessionSnapshot(this.state, {
1280
+ setError: this.setError,
1281
+ retryFetchSession: this.retryFetchSession
1282
+ });
1283
+ this.lastSessionSnapshotVersion = this.sessionSnapshotVersion;
1284
+ }
1285
+ return this.cachedSessionSnapshot;
1286
+ };
1287
+ this.subscribeMessaging = (listener) => {
1288
+ this.messagingListeners.add(listener);
1289
+ return () => {
1290
+ this.messagingListeners.delete(listener);
1291
+ };
1292
+ };
1293
+ this.getMessagingSnapshot = () => {
1294
+ if (this.lastMessagingSnapshotVersion !== this.messagingSnapshotVersion) {
1295
+ this.cachedMessagingSnapshot = deriveMessagingSnapshot(this.state, {
1296
+ sendMessage: this.sendMessage,
1297
+ handleRegenerate: this.handleRegenerate,
1298
+ handleEdit: this.handleEdit,
1299
+ handleCopy: this.handleCopy,
1300
+ handleSubmitQuestionAnswers: this.handleSubmitQuestionAnswers,
1301
+ handleRejectPendingQuestion: this.handleRejectPendingQuestion,
1302
+ retryLastMessage: this.retryLastMessage,
1303
+ clearStreamError: this.clearStreamError,
1304
+ cancel: this.cancel,
1305
+ setCodeOutputs: this.setCodeOutputs
1306
+ });
1307
+ this.lastMessagingSnapshotVersion = this.messagingSnapshotVersion;
1308
+ }
1309
+ return this.cachedMessagingSnapshot;
1310
+ };
1311
+ this.subscribeInput = (listener) => {
1312
+ this.inputListeners.add(listener);
1313
+ return () => {
1314
+ this.inputListeners.delete(listener);
1315
+ };
1316
+ };
1317
+ this.getInputSnapshot = () => {
1318
+ if (this.lastInputSnapshotVersion !== this.inputSnapshotVersion) {
1319
+ this.cachedInputSnapshot = deriveInputSnapshot(this.state, {
1320
+ setMessage: this.setMessage,
1321
+ setInputError: this.setInputError,
1322
+ handleSubmit: this.handleSubmit,
1323
+ handleUnqueue: this.handleUnqueue,
1324
+ enqueueMessage: this.enqueueMessage,
1325
+ removeQueueItem: this.removeQueueItem,
1326
+ updateQueueItem: this.updateQueueItem
1327
+ });
1328
+ this.lastInputSnapshotVersion = this.inputSnapshotVersion;
1329
+ }
1330
+ return this.cachedInputSnapshot;
1331
+ };
1332
+ this.dataSource = config.dataSource;
1333
+ this.rateLimiter = config.rateLimiter;
1334
+ this.onSessionCreated = config.onSessionCreated;
1335
+ this.state = {
1336
+ session: {
1337
+ currentSessionId: void 0,
1338
+ session: null,
1339
+ fetching: false,
1340
+ error: null,
1341
+ errorRetryable: false,
1342
+ debugModeBySession: {},
1343
+ debugLimits: readDebugLimitsFromGlobalContext()
1344
+ },
1345
+ messaging: {
1346
+ turns: [],
1347
+ streamingContent: "",
1348
+ isStreaming: false,
1349
+ streamError: null,
1350
+ streamErrorRetryable: false,
1351
+ loading: false,
1352
+ pendingQuestion: null,
1353
+ codeOutputs: [],
1354
+ isCompacting: false,
1355
+ artifactsInvalidationTrigger: 0
1356
+ },
1357
+ input: {
1358
+ message: "",
1359
+ inputError: null,
1360
+ messageQueue: []
1361
+ }
1362
+ };
1363
+ this.setError = this._setError.bind(this);
1364
+ this.retryFetchSession = this._retryFetchSession.bind(this);
1365
+ this.sendMessage = this._sendMessage.bind(this);
1366
+ this.handleRegenerate = this._handleRegenerate.bind(this);
1367
+ this.handleEdit = this._handleEdit.bind(this);
1368
+ this.handleCopy = this._handleCopy.bind(this);
1369
+ this.handleSubmitQuestionAnswers = this._handleSubmitQuestionAnswers.bind(this);
1370
+ this.handleRejectPendingQuestion = this._handleRejectPendingQuestion.bind(this);
1371
+ this.retryLastMessage = this._retryLastMessage.bind(this);
1372
+ this.clearStreamError = this._clearStreamError.bind(this);
1373
+ this.cancel = this._cancel.bind(this);
1374
+ this.setCodeOutputs = this._setCodeOutputs.bind(this);
1375
+ this.setMessage = this._setMessage.bind(this);
1376
+ this.setInputError = this._setInputError.bind(this);
1377
+ this.handleSubmit = this._handleSubmit.bind(this);
1378
+ this.handleUnqueue = this._handleUnqueue.bind(this);
1379
+ this.enqueueMessage = this._enqueueMessage.bind(this);
1380
+ this.removeQueueItem = this._removeQueueItem.bind(this);
1381
+ this.updateQueueItem = this._updateQueueItem.bind(this);
1382
+ }
1383
+ // =====================================================================
1384
+ // Lifecycle
1385
+ // =====================================================================
1386
+ /**
1387
+ * Set the active session ID. Triggers fetch when transitioning to a real
1388
+ * session, or resets state for 'new'/undefined.
1389
+ */
1390
+ setSessionId(id) {
1391
+ if (this.disposed) return;
1392
+ const prev = this.state.session.currentSessionId;
1393
+ if (id === prev) return;
1394
+ this.state.session.currentSessionId = id;
1395
+ this._notifySession();
1396
+ if (!id || id === "new") {
1397
+ this._updateInput({ messageQueue: [] });
1398
+ } else {
1399
+ this._updateInput({ messageQueue: loadQueue(id) });
1400
+ }
1401
+ this._fetchSessionIfNeeded();
1402
+ }
1403
+ /**
1404
+ * Update mutable config that may change across parent re-renders.
1405
+ * Called from the React provider's useEffect to keep the machine in sync.
1406
+ */
1407
+ updateConfig(config) {
1408
+ this.dataSource = config.dataSource;
1409
+ this.onSessionCreated = config.onSessionCreated;
1410
+ }
1411
+ dispose() {
1412
+ this.disposed = true;
1413
+ this.fetchCancelled = true;
1414
+ this.abortController?.abort();
1415
+ this.sessionListeners.clear();
1416
+ this.messagingListeners.clear();
1417
+ this.inputListeners.clear();
1418
+ }
1419
+ // =====================================================================
1420
+ // Private — state updates + notification
1421
+ // =====================================================================
1422
+ _updateSession(patch) {
1423
+ Object.assign(this.state.session, patch);
1424
+ this._notifySession();
1425
+ }
1426
+ _notifySession() {
1427
+ this.sessionSnapshotVersion++;
1428
+ for (const fn of this.sessionListeners) fn();
1429
+ }
1430
+ _updateMessaging(patch) {
1431
+ Object.assign(this.state.messaging, patch);
1432
+ this._notifyMessaging();
1433
+ if ("turns" in patch) {
1434
+ const newUsage = getSessionDebugUsage(this.state.messaging.turns);
1435
+ if (!sessionDebugUsageEqual(this.lastSessionDebugUsage, newUsage)) {
1436
+ this.lastSessionDebugUsage = newUsage;
1437
+ this._notifySession();
1438
+ }
1439
+ }
1440
+ }
1441
+ _notifyMessaging() {
1442
+ this.messagingSnapshotVersion++;
1443
+ for (const fn of this.messagingListeners) fn();
1444
+ }
1445
+ _updateInput(patch) {
1446
+ Object.assign(this.state.input, patch);
1447
+ if ("messageQueue" in patch) {
1448
+ this._persistQueue();
1449
+ }
1450
+ this._notifyInput();
1451
+ }
1452
+ _notifyInput() {
1453
+ this.inputSnapshotVersion++;
1454
+ for (const fn of this.inputListeners) fn();
1455
+ }
1456
+ _persistQueue() {
1457
+ const sid = this.state.session.currentSessionId;
1458
+ if (!sid || sid === "new") return;
1459
+ saveQueue(sid, this.state.input.messageQueue);
1460
+ }
1461
+ // =====================================================================
1462
+ // Private — session fetch
1463
+ // =====================================================================
1464
+ _fetchSessionIfNeeded() {
1465
+ const id = this.state.session.currentSessionId;
1466
+ if (!id || id === "new") {
1467
+ this._updateSession({
1468
+ session: null,
1469
+ fetching: false,
1470
+ error: null,
1471
+ errorRetryable: false
1472
+ });
1473
+ this._updateMessaging({
1474
+ turns: [],
1475
+ pendingQuestion: null
1476
+ });
1477
+ this._updateInput({ inputError: null });
952
1478
  return;
953
1479
  }
954
- let cancelled = false;
955
- setFetching(true);
956
- setError(null);
957
- setInputError(null);
958
- dataSource.fetchSession(currentSessionId).then((state) => {
959
- if (cancelled) return;
960
- if (state) {
961
- setSession(state.session);
962
- setTurns((prev) => {
963
- const hasPendingUserOnly = prev.length > 0 && !prev[prev.length - 1].assistantTurn;
964
- if (hasPendingUserOnly && (!state.turns || state.turns.length === 0)) {
965
- return prev;
966
- }
967
- return state.turns ?? prev;
968
- });
969
- setPendingQuestion(state.pendingQuestion || null);
1480
+ if (this.sendingSessionId === id) return;
1481
+ this.fetchCancelled = false;
1482
+ this._updateSession({ fetching: true, error: null, errorRetryable: false });
1483
+ this._updateInput({ inputError: null });
1484
+ const fetchId = id;
1485
+ this.dataSource.fetchSession(fetchId).then((result) => {
1486
+ if (this.fetchCancelled || this.disposed) return;
1487
+ if (this.state.session.currentSessionId !== fetchId) return;
1488
+ if (this.sendingSessionId === fetchId) return;
1489
+ if (result) {
1490
+ this._updateSession({ session: result.session, fetching: false });
1491
+ this._setTurnsFromFetch(result.turns);
1492
+ this._updateMessaging({ pendingQuestion: result.pendingQuestion || null });
970
1493
  } else {
971
- setError("Session not found");
1494
+ this._updateSession({ error: "Session not found", fetching: false });
972
1495
  }
973
- setFetching(false);
974
1496
  }).catch((err) => {
975
- if (cancelled) return;
976
- setError(err.message || "Failed to load session");
977
- setFetching(false);
1497
+ if (this.fetchCancelled || this.disposed) return;
1498
+ if (this.state.session.currentSessionId !== fetchId) return;
1499
+ if (this.sendingSessionId === fetchId) return;
1500
+ const normalized = normalizeRPCError(err, "Failed to load session");
1501
+ this._updateSession({
1502
+ error: normalized.userMessage,
1503
+ errorRetryable: normalized.retryable,
1504
+ fetching: false
1505
+ });
978
1506
  });
979
- return () => {
980
- cancelled = true;
981
- };
982
- }, [dataSource, currentSessionId]);
983
- const handleCopy = useCallback(async (text) => {
984
- await navigator.clipboard.writeText(text);
985
- }, []);
986
- const executeSlashCommand = useCallback(
987
- async (command) => {
988
- if (command.hasArgs) {
989
- setInputError("slash.error.noArguments");
990
- return true;
991
- }
992
- setError(null);
993
- setInputError(null);
994
- if (command.name === "/debug") {
995
- if (!hasPermission("bichat.export")) {
996
- setInputError("slash.error.debugUnauthorized");
997
- return true;
998
- }
999
- const curDebugMode = sessionRef.current.debugMode;
1000
- const curDebugSessionKey = sessionRef.current.debugSessionKey;
1001
- const curSessionId2 = sessionRef.current.currentSessionId;
1002
- const nextDebugMode = !curDebugMode;
1003
- setDebugModeBySession((prev) => ({
1004
- ...prev,
1005
- [curDebugSessionKey]: nextDebugMode
1006
- }));
1007
- if (nextDebugMode && curSessionId2 && curSessionId2 !== "new") {
1008
- try {
1009
- const state = await dataSource.fetchSession(curSessionId2);
1010
- if (state) {
1011
- setSession(state.session);
1012
- setTurns(state.turns);
1013
- setPendingQuestion(state.pendingQuestion || null);
1014
- }
1015
- } catch (err) {
1016
- console.error("Failed to refresh session for debug mode:", err);
1017
- }
1507
+ }
1508
+ /** Sets turns from fetch, preserving pending user-only turns if server hasn't caught up. */
1509
+ _setTurnsFromFetch(fetchedTurns) {
1510
+ const prev = this.state.messaging.turns;
1511
+ const hasPendingUserOnly = prev.length > 0 && !prev[prev.length - 1].assistantTurn;
1512
+ if (hasPendingUserOnly && (!fetchedTurns || fetchedTurns.length === 0)) {
1513
+ return;
1514
+ }
1515
+ this._updateMessaging({ turns: fetchedTurns ?? prev });
1516
+ }
1517
+ // =====================================================================
1518
+ // Private — actions
1519
+ // =====================================================================
1520
+ _setError(error) {
1521
+ this._updateSession({
1522
+ error,
1523
+ errorRetryable: false
1524
+ });
1525
+ }
1526
+ _retryFetchSession() {
1527
+ this._fetchSessionIfNeeded();
1528
+ }
1529
+ _clearStreamError() {
1530
+ this._updateMessaging({
1531
+ streamError: null,
1532
+ streamErrorRetryable: false
1533
+ });
1534
+ }
1535
+ _cancel() {
1536
+ if (this.abortController) {
1537
+ this.abortController.abort();
1538
+ this.abortController = null;
1539
+ this._updateMessaging({ isStreaming: false, loading: false });
1540
+ }
1541
+ }
1542
+ _setCodeOutputs(outputs) {
1543
+ this._updateMessaging({ codeOutputs: outputs });
1544
+ }
1545
+ _setMessage(message) {
1546
+ this._updateInput({ message });
1547
+ }
1548
+ _setInputError(error) {
1549
+ this._updateInput({ inputError: error });
1550
+ }
1551
+ // ── Slash commands ──────────────────────────────────────────────────────
1552
+ async _executeSlashCommand(command) {
1553
+ if (command.hasArgs) {
1554
+ this._updateInput({ inputError: "BiChat.Slash.ErrorNoArguments" });
1555
+ return true;
1556
+ }
1557
+ this._updateSession({ error: null, errorRetryable: false });
1558
+ this._updateInput({ inputError: null });
1559
+ this._clearStreamError();
1560
+ if (command.name === "/debug") {
1561
+ const debugMode = deriveDebugMode(this.state);
1562
+ const key2 = this.state.session.currentSessionId || "new";
1563
+ const nextDebugMode = !debugMode;
1564
+ this._updateSession({
1565
+ debugModeBySession: {
1566
+ ...this.state.session.debugModeBySession,
1567
+ [key2]: nextDebugMode
1018
1568
  }
1019
- setMessage("");
1020
- return true;
1021
- }
1022
- const curSessionId = sessionRef.current.currentSessionId;
1023
- if (!curSessionId || curSessionId === "new") {
1024
- setInputError("slash.error.sessionRequired");
1025
- return true;
1026
- }
1027
- if (command.name === "/clear") {
1028
- setLoading(true);
1029
- setStreamingContent("");
1569
+ });
1570
+ if (nextDebugMode && this.state.session.currentSessionId && this.state.session.currentSessionId !== "new") {
1030
1571
  try {
1031
- await dataSource.clearSessionHistory(curSessionId);
1032
- const state = await dataSource.fetchSession(curSessionId);
1033
- if (state) {
1034
- setSession(state.session);
1035
- setTurns(state.turns);
1036
- setPendingQuestion(state.pendingQuestion || null);
1037
- } else {
1038
- setTurns([]);
1572
+ const result = await this.dataSource.fetchSession(this.state.session.currentSessionId);
1573
+ if (result) {
1574
+ this._updateSession({ session: result.session });
1575
+ this._updateMessaging({
1576
+ turns: result.turns,
1577
+ pendingQuestion: result.pendingQuestion || null
1578
+ });
1039
1579
  }
1040
- setCompactionSummary(null);
1041
- setCodeOutputs([]);
1042
- setMessage("");
1043
1580
  } catch (err) {
1044
- setInputError(err instanceof Error ? err.message : "slash.error.clearFailed");
1045
- } finally {
1046
- setLoading(false);
1047
- setIsStreaming(false);
1581
+ console.error("Failed to refresh session for debug mode:", err);
1048
1582
  }
1049
- return true;
1050
1583
  }
1051
- if (command.name === "/compact") {
1052
- setLoading(true);
1053
- setIsCompacting(true);
1054
- setCompactionSummary(null);
1055
- setStreamingContent("");
1056
- try {
1057
- const result = await dataSource.compactSessionHistory(curSessionId);
1058
- const summary = result.summary || "";
1059
- setTurns([createCompactedSystemTurn(curSessionId, summary)]);
1060
- setCompactionSummary(null);
1061
- const state = await dataSource.fetchSession(curSessionId);
1062
- if (state) {
1063
- setSession(state.session);
1064
- setTurns(state.turns);
1065
- setPendingQuestion(state.pendingQuestion || null);
1066
- } else {
1067
- setTurns([]);
1068
- }
1069
- setCodeOutputs([]);
1070
- setMessage("");
1071
- } catch (err) {
1072
- setInputError(err instanceof Error ? err.message : "slash.error.compactFailed");
1073
- } finally {
1074
- setIsCompacting(false);
1075
- setLoading(false);
1076
- setIsStreaming(false);
1584
+ this._updateInput({ message: "" });
1585
+ return true;
1586
+ }
1587
+ const curSessionId = this.state.session.currentSessionId;
1588
+ if (!curSessionId || curSessionId === "new") {
1589
+ this._updateInput({ inputError: "BiChat.Slash.ErrorSessionRequired" });
1590
+ return true;
1591
+ }
1592
+ if (command.name === "/clear") {
1593
+ this._updateInput({ message: "" });
1594
+ this._updateMessaging({ loading: true, streamingContent: "" });
1595
+ try {
1596
+ await this.dataSource.clearSessionHistory(curSessionId);
1597
+ const result = await this.dataSource.fetchSession(curSessionId);
1598
+ if (result) {
1599
+ this._updateSession({ session: result.session });
1600
+ this._updateMessaging({
1601
+ turns: result.turns,
1602
+ pendingQuestion: result.pendingQuestion || null
1603
+ });
1604
+ } else {
1605
+ this._updateMessaging({ turns: [] });
1077
1606
  }
1078
- return true;
1607
+ this._updateMessaging({ codeOutputs: [] });
1608
+ } catch (err) {
1609
+ const normalized = normalizeRPCError(err, "Failed to clear session history");
1610
+ this._updateInput({ inputError: normalized.userMessage });
1611
+ } finally {
1612
+ this._updateMessaging({ loading: false, isStreaming: false });
1079
1613
  }
1080
- setInputError("slash.error.unknownCommand");
1081
1614
  return true;
1082
- },
1083
- [dataSource]
1084
- );
1085
- const sendMessageDirect = useCallback(
1086
- async (content, attachments = [], options) => {
1087
- if (!content.trim() || messagingRef.current.loading) return;
1088
- const trimmedContent = content.trim();
1089
- if (trimmedContent.startsWith("/")) {
1090
- const maybeCommand = parseSlashCommand(content);
1091
- if (!maybeCommand) {
1092
- setInputError("slash.error.unknownCommand");
1093
- return;
1094
- }
1095
- if (attachments.length > 0) {
1096
- setInputError("slash.error.noAttachments");
1097
- return;
1615
+ }
1616
+ if (command.name === "/compact") {
1617
+ this._updateInput({ message: "" });
1618
+ this._updateMessaging({
1619
+ loading: true,
1620
+ isCompacting: true,
1621
+ streamingContent: ""
1622
+ });
1623
+ try {
1624
+ const compactResult = await this.dataSource.compactSessionHistory(curSessionId);
1625
+ const summary = compactResult.summary || "";
1626
+ this._updateMessaging({
1627
+ turns: [createCompactedSystemTurn(curSessionId, summary)]
1628
+ });
1629
+ const result = await this.dataSource.fetchSession(curSessionId);
1630
+ if (result) {
1631
+ this._updateSession({ session: result.session });
1632
+ this._updateMessaging({
1633
+ turns: result.turns,
1634
+ pendingQuestion: result.pendingQuestion || null
1635
+ });
1636
+ } else {
1637
+ this._updateMessaging({ turns: [] });
1098
1638
  }
1099
- await executeSlashCommand(maybeCommand);
1639
+ this._updateMessaging({ codeOutputs: [] });
1640
+ } catch (err) {
1641
+ const normalized = normalizeRPCError(err, "Failed to compact session history");
1642
+ this._updateInput({ inputError: normalized.userMessage });
1643
+ } finally {
1644
+ this._updateMessaging({ isCompacting: false, loading: false, isStreaming: false });
1645
+ }
1646
+ return true;
1647
+ }
1648
+ this._updateInput({ inputError: "BiChat.Slash.ErrorUnknownCommand" });
1649
+ return true;
1650
+ }
1651
+ // ── Send message ────────────────────────────────────────────────────────
1652
+ /**
1653
+ * Public entry point (no options). Calls _sendMessageCore internally.
1654
+ */
1655
+ async _sendMessage(content, attachments = []) {
1656
+ return this._sendMessageCore(content, attachments);
1657
+ }
1658
+ /**
1659
+ * Internal entry point with options (for regenerate/edit).
1660
+ */
1661
+ async _sendMessageDirect(content, attachments, options) {
1662
+ return this._sendMessageCore(content, attachments, options);
1663
+ }
1664
+ /**
1665
+ * Core send-message logic. Handles slash commands, rate limiting, streaming,
1666
+ * session creation, optimistic turns, and auto-queue-drain.
1667
+ */
1668
+ async _sendMessageCore(content, attachments = [], options) {
1669
+ if (this.disposed) return;
1670
+ if (!content.trim() || this.state.messaging.loading) return;
1671
+ const trimmedContent = content.trim();
1672
+ if (trimmedContent.startsWith("/")) {
1673
+ const maybeCommand = parseSlashCommand(content);
1674
+ if (!maybeCommand) {
1675
+ this._updateInput({ inputError: "BiChat.Slash.ErrorUnknownCommand" });
1100
1676
  return;
1101
1677
  }
1102
- if (!rateLimiterRef.current.canMakeRequest()) {
1103
- const timeUntilNext = rateLimiterRef.current.getTimeUntilNextRequest();
1104
- const seconds = Math.ceil(timeUntilNext / 1e3);
1105
- setError(`Rate limit exceeded. Please wait ${seconds} seconds before sending another message.`);
1678
+ if (attachments.length > 0) {
1679
+ this._updateInput({ inputError: "BiChat.Slash.ErrorNoAttachments" });
1106
1680
  return;
1107
1681
  }
1108
- setMessage("");
1109
- setLoading(true);
1110
- setError(null);
1111
- setInputError(null);
1112
- setStreamingContent("");
1113
- setCompactionSummary(null);
1114
- abortControllerRef.current = new AbortController();
1115
- const curSessionId = sessionRef.current.currentSessionId;
1116
- const curDebugMode = sessionRef.current.debugMode;
1117
- const tempTurn = createPendingTurn(curSessionId || "new", content, attachments);
1118
- const replaceFromMessageID = options?.replaceFromMessageID;
1119
- setTurns((prev) => {
1120
- if (!replaceFromMessageID) {
1121
- return [...prev, tempTurn];
1122
- }
1123
- const replaceIndex = prev.findIndex((turn) => turn.userTurn.id === replaceFromMessageID);
1124
- if (replaceIndex === -1) {
1125
- console.warn(
1126
- `[ChatContext] replaceFromMessageID "${replaceFromMessageID}" not found in turns; appending as new turn`
1127
- );
1128
- return [...prev, tempTurn];
1129
- }
1130
- return [...prev.slice(0, replaceIndex), tempTurn];
1682
+ await this._executeSlashCommand(maybeCommand);
1683
+ return;
1684
+ }
1685
+ if (!this.rateLimiter.canMakeRequest()) {
1686
+ const timeUntilNext = this.rateLimiter.getTimeUntilNextRequest();
1687
+ const seconds = Math.ceil(timeUntilNext / 1e3);
1688
+ this._updateInput({
1689
+ inputError: `Rate limit exceeded. Please wait ${seconds} seconds before sending another message.`
1131
1690
  });
1132
- try {
1133
- let activeSessionId = curSessionId;
1134
- let shouldNavigateAfter = false;
1135
- if (!activeSessionId || activeSessionId === "new") {
1136
- const result = await dataSource.createSession();
1137
- if (result) {
1138
- const createdSessionID = result.id;
1139
- activeSessionId = createdSessionID;
1140
- setCurrentSessionId(createdSessionID);
1141
- setDebugModeBySession((prev) => {
1142
- if (!curDebugMode) return prev;
1143
- return { ...prev, [createdSessionID]: true };
1691
+ return;
1692
+ }
1693
+ this._updateInput({ message: "", inputError: null });
1694
+ this._updateSession({ error: null, errorRetryable: false });
1695
+ this._clearStreamError();
1696
+ this._updateMessaging({
1697
+ loading: true,
1698
+ streamingContent: ""
1699
+ });
1700
+ this.abortController = new AbortController();
1701
+ const curSessionId = this.state.session.currentSessionId;
1702
+ const curDebugMode = deriveDebugMode(this.state);
1703
+ const replaceFromMessageID = options?.replaceFromMessageID;
1704
+ const tempTurn = createPendingTurn(curSessionId || "new", content, attachments);
1705
+ this.lastSendAttempt = { content, attachments, options };
1706
+ const prevTurns = this.state.messaging.turns;
1707
+ if (!replaceFromMessageID) {
1708
+ this._updateMessaging({ turns: [...prevTurns, tempTurn] });
1709
+ } else {
1710
+ const idx = prevTurns.findIndex((t) => t.userTurn.id === replaceFromMessageID);
1711
+ if (idx === -1) {
1712
+ console.warn(`[ChatMachine] replaceFromMessageID "${replaceFromMessageID}" not found; appending as new turn`);
1713
+ this._updateMessaging({ turns: [...prevTurns, tempTurn] });
1714
+ } else {
1715
+ this._updateMessaging({ turns: [...prevTurns.slice(0, idx), tempTurn] });
1716
+ }
1717
+ }
1718
+ let shouldDrainQueue = true;
1719
+ try {
1720
+ let activeSessionId = curSessionId;
1721
+ let shouldNavigateAfter = false;
1722
+ if (!activeSessionId || activeSessionId === "new") {
1723
+ const result = await this.dataSource.createSession();
1724
+ if (result) {
1725
+ const createdSessionID = result.id;
1726
+ activeSessionId = createdSessionID;
1727
+ this._updateSession({ currentSessionId: createdSessionID });
1728
+ if (curDebugMode) {
1729
+ this._updateSession({
1730
+ debugModeBySession: {
1731
+ ...this.state.session.debugModeBySession,
1732
+ [createdSessionID]: true
1733
+ }
1144
1734
  });
1145
- shouldNavigateAfter = true;
1146
1735
  }
1736
+ shouldNavigateAfter = true;
1147
1737
  }
1148
- let accumulatedContent = "";
1149
- let createdSessionId;
1150
- let sessionFetched = false;
1151
- setIsStreaming(true);
1152
- for await (const chunk of dataSource.sendMessage(
1153
- activeSessionId || "new",
1154
- content,
1155
- attachments,
1156
- abortControllerRef.current?.signal,
1157
- {
1158
- debugMode: curDebugMode,
1159
- replaceFromMessageID
1160
- }
1161
- )) {
1162
- if (abortControllerRef.current?.signal.aborted) {
1163
- break;
1738
+ }
1739
+ this.sendingSessionId = activeSessionId || null;
1740
+ let accumulatedContent = "";
1741
+ let createdSessionId;
1742
+ let sessionFetched = false;
1743
+ this._updateMessaging({ isStreaming: true });
1744
+ for await (const chunk of this.dataSource.sendMessage(
1745
+ activeSessionId || "new",
1746
+ content,
1747
+ attachments,
1748
+ this.abortController?.signal,
1749
+ {
1750
+ debugMode: curDebugMode,
1751
+ replaceFromMessageID
1752
+ }
1753
+ )) {
1754
+ if (this.abortController?.signal.aborted) break;
1755
+ if ((chunk.type === "chunk" || chunk.type === "content") && chunk.content) {
1756
+ accumulatedContent += chunk.content;
1757
+ this._updateMessaging({ streamingContent: accumulatedContent });
1758
+ } else if (chunk.type === "error") {
1759
+ throw new Error(chunk.error || "Stream error");
1760
+ } else if (chunk.type === "interrupt" || chunk.type === "done") {
1761
+ if (chunk.sessionId) {
1762
+ createdSessionId = chunk.sessionId;
1164
1763
  }
1165
- if ((chunk.type === "chunk" || chunk.type === "content") && chunk.content) {
1166
- accumulatedContent += chunk.content;
1167
- setStreamingContent(accumulatedContent);
1168
- } else if (chunk.type === "error") {
1169
- throw new Error(chunk.error || "Stream error");
1170
- } else if (chunk.type === "interrupt" || chunk.type === "done") {
1171
- if (chunk.sessionId) {
1172
- createdSessionId = chunk.sessionId;
1173
- }
1174
- if (!sessionFetched) {
1175
- sessionFetched = true;
1176
- const finalSessionId = createdSessionId || activeSessionId;
1177
- if (finalSessionId && finalSessionId !== "new") {
1178
- const state = await dataSource.fetchSession(finalSessionId);
1179
- if (state) {
1180
- setSession(state.session);
1181
- setTurns((prev) => {
1182
- const hasPendingUserOnly = prev.length > 0 && !prev[prev.length - 1].assistantTurn;
1183
- if (hasPendingUserOnly && (!state.turns || state.turns.length === 0)) {
1184
- return prev;
1185
- }
1186
- return state.turns ?? prev;
1187
- });
1188
- setPendingQuestion(state.pendingQuestion || null);
1189
- }
1764
+ if (!sessionFetched) {
1765
+ sessionFetched = true;
1766
+ const finalSessionId = createdSessionId || activeSessionId;
1767
+ if (finalSessionId && finalSessionId !== "new") {
1768
+ const fetchResult = await this.dataSource.fetchSession(finalSessionId);
1769
+ if (fetchResult) {
1770
+ this._updateSession({ session: fetchResult.session });
1771
+ this._setTurnsFromFetch(fetchResult.turns);
1772
+ this._updateMessaging({ pendingQuestion: fetchResult.pendingQuestion || null });
1190
1773
  }
1191
1774
  }
1192
- } else if (chunk.type === "user_message" && chunk.sessionId) {
1193
- createdSessionId = chunk.sessionId;
1194
- } else if (chunk.type === "tool_end" && chunk.tool?.name && ARTIFACT_TOOL_NAMES.has(chunk.tool.name)) {
1195
- setArtifactsInvalidationTrigger((n) => n + 1);
1196
1775
  }
1776
+ } else if (chunk.type === "user_message" && chunk.sessionId) {
1777
+ createdSessionId = chunk.sessionId;
1778
+ } else if (chunk.type === "tool_end" && chunk.tool?.name && ARTIFACT_TOOL_NAMES.has(chunk.tool.name)) {
1779
+ this._updateMessaging({
1780
+ artifactsInvalidationTrigger: this.state.messaging.artifactsInvalidationTrigger + 1
1781
+ });
1197
1782
  }
1198
- const targetSessionId = createdSessionId || activeSessionId;
1199
- if (shouldNavigateAfter && targetSessionId && targetSessionId !== "new") {
1200
- dataSource.navigateToSession?.(targetSessionId);
1201
- }
1202
- } catch (err) {
1203
- if (err instanceof Error && err.name === "AbortError") {
1204
- setMessage(content);
1205
- return;
1206
- }
1207
- setTurns((prev) => prev.filter((t) => t.id !== tempTurn.id));
1208
- const errorMessage = err instanceof Error ? err.message : "Error.NetworkError";
1209
- setInputError(errorMessage);
1210
- console.error("Send message error:", err);
1211
- } finally {
1212
- setLoading(false);
1213
- setStreamingContent("");
1214
- setIsStreaming(false);
1215
- abortControllerRef.current = null;
1216
1783
  }
1217
- },
1218
- [dataSource, executeSlashCommand]
1219
- );
1220
- const cancelStream = useCallback(() => {
1221
- if (abortControllerRef.current) {
1222
- abortControllerRef.current.abort();
1223
- abortControllerRef.current = null;
1224
- setIsStreaming(false);
1225
- setLoading(false);
1226
- }
1227
- }, []);
1228
- const handleSubmit = useCallback(
1229
- (e, attachments = []) => {
1230
- e.preventDefault();
1231
- if (!message.trim() && attachments.length === 0) return;
1232
- setInputError(null);
1233
- const convertedAttachments = attachments.map((att) => ({
1234
- clientKey: att.clientKey || crypto.randomUUID(),
1235
- filename: att.filename,
1236
- mimeType: att.mimeType,
1237
- sizeBytes: att.sizeBytes,
1238
- base64Data: att.base64Data,
1239
- url: att.url,
1240
- preview: att.preview
1241
- }));
1242
- sendMessageDirect(message, convertedAttachments);
1243
- },
1244
- [message, sendMessageDirect]
1245
- );
1246
- const handleUnqueue = useCallback(() => {
1247
- if (messageQueue.length === 0) {
1248
- return null;
1249
- }
1250
- const lastQueued = messageQueue[messageQueue.length - 1];
1251
- setMessageQueue((prev) => prev.slice(0, -1));
1252
- return {
1253
- content: lastQueued.content,
1254
- attachments: lastQueued.attachments
1255
- };
1256
- }, [messageQueue]);
1257
- const handleRegenerate = useCallback(
1258
- async (turnId) => {
1259
- const curSessionId = sessionRef.current.currentSessionId;
1260
- if (!curSessionId || curSessionId === "new") return;
1261
- const turn = messagingRef.current.turns.find((t) => t.id === turnId);
1262
- if (!turn) return;
1263
- setError(null);
1264
- try {
1265
- await sendMessageDirect(turn.userTurn.content, turn.userTurn.attachments, {
1266
- replaceFromMessageID: turn.userTurn.id
1267
- });
1268
- } catch (err) {
1269
- const errorMessage = err instanceof Error ? err.message : "Failed to regenerate response";
1270
- setError(errorMessage);
1271
- console.error("Regenerate error:", err);
1784
+ if (!sessionFetched) {
1785
+ const finalSessionId = createdSessionId || activeSessionId;
1786
+ if (finalSessionId && finalSessionId !== "new") {
1787
+ try {
1788
+ const fetchResult = await this.dataSource.fetchSession(finalSessionId);
1789
+ if (fetchResult) {
1790
+ this._updateSession({ session: fetchResult.session });
1791
+ this._updateMessaging({
1792
+ turns: fetchResult.turns ?? [],
1793
+ pendingQuestion: fetchResult.pendingQuestion || null
1794
+ });
1795
+ }
1796
+ } catch (fetchErr) {
1797
+ console.error("Failed to fetch session after stream:", fetchErr);
1798
+ }
1799
+ }
1272
1800
  }
1273
- },
1274
- [sendMessageDirect]
1275
- );
1276
- const handleEdit = useCallback(
1277
- async (turnId, newContent) => {
1278
- const curSessionId = sessionRef.current.currentSessionId;
1279
- if (!curSessionId || curSessionId === "new") {
1280
- setMessage(newContent);
1281
- setTurns((prev) => prev.filter((t) => t.id !== turnId));
1282
- return;
1801
+ const targetSessionId = createdSessionId || activeSessionId;
1802
+ if (shouldNavigateAfter && targetSessionId && targetSessionId !== "new") {
1803
+ if (this.onSessionCreated) {
1804
+ this.onSessionCreated(targetSessionId);
1805
+ } else {
1806
+ this.dataSource.navigateToSession?.(targetSessionId);
1807
+ }
1283
1808
  }
1284
- const turn = messagingRef.current.turns.find((t) => t.id === turnId);
1285
- if (!turn) {
1286
- setError("Failed to edit message");
1809
+ this._clearStreamError();
1810
+ this.lastSendAttempt = null;
1811
+ } catch (err) {
1812
+ if (err instanceof Error && err.name === "AbortError") {
1813
+ this._updateInput({ message: content });
1814
+ this._clearStreamError();
1815
+ shouldDrainQueue = false;
1287
1816
  return;
1288
1817
  }
1289
- setError(null);
1290
- try {
1291
- await sendMessageDirect(newContent, turn.userTurn.attachments, {
1292
- replaceFromMessageID: turn.userTurn.id
1293
- });
1294
- } catch (err) {
1295
- const errorMessage = err instanceof Error ? err.message : "Failed to edit message";
1296
- setError(errorMessage);
1297
- console.error("Edit error:", err);
1818
+ this._updateMessaging({
1819
+ turns: this.state.messaging.turns.filter((t) => t.id !== tempTurn.id)
1820
+ });
1821
+ const normalized = normalizeRPCError(err, "Failed to send message");
1822
+ this._updateInput({ inputError: normalized.userMessage });
1823
+ this._updateMessaging({
1824
+ streamError: normalized.userMessage,
1825
+ streamErrorRetryable: normalized.retryable
1826
+ });
1827
+ console.error("Send message error:", err);
1828
+ shouldDrainQueue = false;
1829
+ } finally {
1830
+ this._updateMessaging({
1831
+ loading: false,
1832
+ streamingContent: "",
1833
+ isStreaming: false
1834
+ });
1835
+ this.abortController = null;
1836
+ this.sendingSessionId = null;
1837
+ if (shouldDrainQueue) {
1838
+ const queue = this.state.input.messageQueue;
1839
+ if (queue.length > 0) {
1840
+ const next = queue[0];
1841
+ this._updateInput({ messageQueue: queue.slice(1) });
1842
+ setTimeout(() => {
1843
+ this._sendMessageCore(next.content, next.attachments);
1844
+ }, 0);
1845
+ }
1298
1846
  }
1299
- },
1300
- [sendMessageDirect]
1301
- );
1302
- const handleSubmitQuestionAnswers = useCallback(
1303
- (answers) => {
1304
- const curSessionId = sessionRef.current.currentSessionId;
1305
- const curPendingQuestion = messagingRef.current.pendingQuestion;
1306
- if (!curSessionId || !curPendingQuestion) return;
1307
- setLoading(true);
1308
- setError(null);
1309
- const previousPendingQuestion = curPendingQuestion;
1310
- setPendingQuestion(null);
1311
- (async () => {
1312
- try {
1313
- const result = await dataSource.submitQuestionAnswers(
1314
- curSessionId,
1315
- previousPendingQuestion.id,
1316
- answers
1317
- );
1318
- if (result.success) {
1319
- if (curSessionId !== "new") {
1320
- try {
1321
- const state = await dataSource.fetchSession(curSessionId);
1322
- if (state) {
1323
- setTurns(state.turns);
1324
- setPendingQuestion(state.pendingQuestion || null);
1325
- } else {
1326
- setPendingQuestion(previousPendingQuestion);
1327
- setError("Failed to load updated session");
1328
- }
1329
- } catch (fetchErr) {
1330
- setPendingQuestion(previousPendingQuestion);
1331
- const errorMessage = fetchErr instanceof Error ? fetchErr.message : "Failed to load updated session";
1332
- setError(errorMessage);
1333
- }
1847
+ }
1848
+ }
1849
+ // ── Retry ───────────────────────────────────────────────────────────────
1850
+ async _retryLastMessage() {
1851
+ const lastAttempt = this.lastSendAttempt;
1852
+ if (!lastAttempt || this.state.messaging.loading) return;
1853
+ this._clearStreamError();
1854
+ this._updateInput({ inputError: null });
1855
+ await this._sendMessageDirect(lastAttempt.content, lastAttempt.attachments, lastAttempt.options);
1856
+ }
1857
+ // ── Regenerate / Edit ───────────────────────────────────────────────────
1858
+ async _handleRegenerate(turnId) {
1859
+ const curSessionId = this.state.session.currentSessionId;
1860
+ if (!curSessionId || curSessionId === "new") return;
1861
+ const turn = this.state.messaging.turns.find((t) => t.id === turnId);
1862
+ if (!turn) return;
1863
+ this._updateSession({ error: null, errorRetryable: false });
1864
+ await this._sendMessageDirect(turn.userTurn.content, turn.userTurn.attachments, {
1865
+ replaceFromMessageID: turn.userTurn.id
1866
+ });
1867
+ }
1868
+ async _handleEdit(turnId, newContent) {
1869
+ const curSessionId = this.state.session.currentSessionId;
1870
+ if (!curSessionId || curSessionId === "new") {
1871
+ this._updateInput({ message: newContent });
1872
+ this._updateMessaging({
1873
+ turns: this.state.messaging.turns.filter((t) => t.id !== turnId)
1874
+ });
1875
+ return;
1876
+ }
1877
+ const turn = this.state.messaging.turns.find((t) => t.id === turnId);
1878
+ if (!turn) {
1879
+ this._updateSession({ error: "Failed to edit message", errorRetryable: false });
1880
+ return;
1881
+ }
1882
+ this._updateSession({ error: null, errorRetryable: false });
1883
+ await this._sendMessageDirect(newContent, turn.userTurn.attachments, {
1884
+ replaceFromMessageID: turn.userTurn.id
1885
+ });
1886
+ }
1887
+ async _handleCopy(text) {
1888
+ if (typeof navigator === "undefined" || !navigator.clipboard) return;
1889
+ try {
1890
+ await navigator.clipboard.writeText(text);
1891
+ } catch {
1892
+ }
1893
+ }
1894
+ // ── HITL ────────────────────────────────────────────────────────────────
1895
+ async _handleSubmitQuestionAnswers(answers) {
1896
+ const curSessionId = this.state.session.currentSessionId;
1897
+ const curPendingQuestion = this.state.messaging.pendingQuestion;
1898
+ if (!curSessionId || !curPendingQuestion) return;
1899
+ this._updateMessaging({ loading: true });
1900
+ this._updateSession({ error: null, errorRetryable: false });
1901
+ const previousPendingQuestion = curPendingQuestion;
1902
+ this._updateMessaging({ pendingQuestion: null });
1903
+ try {
1904
+ const result = await this.dataSource.submitQuestionAnswers(
1905
+ curSessionId,
1906
+ previousPendingQuestion.id,
1907
+ answers
1908
+ );
1909
+ if (this.disposed) return;
1910
+ if (result.success) {
1911
+ if (curSessionId !== "new") {
1912
+ try {
1913
+ const fetchResult = await this.dataSource.fetchSession(curSessionId);
1914
+ if (this.disposed) return;
1915
+ if (fetchResult) {
1916
+ this._updateMessaging({
1917
+ turns: fetchResult.turns,
1918
+ pendingQuestion: fetchResult.pendingQuestion || null
1919
+ });
1920
+ } else {
1921
+ this._updateMessaging({ pendingQuestion: previousPendingQuestion });
1922
+ this._updateSession({ error: "Failed to load updated session", errorRetryable: false });
1334
1923
  }
1335
- } else {
1336
- setPendingQuestion(previousPendingQuestion);
1337
- setError(result.error || "Failed to submit answers");
1924
+ } catch (fetchErr) {
1925
+ if (this.disposed) return;
1926
+ this._updateMessaging({ pendingQuestion: previousPendingQuestion });
1927
+ const normalized = normalizeRPCError(fetchErr, "Failed to load updated session");
1928
+ this._updateSession({ error: normalized.userMessage, errorRetryable: normalized.retryable });
1338
1929
  }
1339
- } catch (err) {
1340
- setPendingQuestion(previousPendingQuestion);
1341
- const errorMessage = err instanceof Error ? err.message : "Failed to submit answers";
1342
- setError(errorMessage);
1343
- } finally {
1344
- setLoading(false);
1345
1930
  }
1346
- })();
1347
- },
1348
- [dataSource]
1349
- );
1350
- const handleRejectPendingQuestion = useCallback(async () => {
1351
- const curSessionId = sessionRef.current.currentSessionId;
1352
- const curPendingQuestion = messagingRef.current.pendingQuestion;
1931
+ } else {
1932
+ this._updateMessaging({ pendingQuestion: previousPendingQuestion });
1933
+ this._updateSession({ error: result.error || "Failed to submit answers", errorRetryable: false });
1934
+ }
1935
+ } catch (err) {
1936
+ if (this.disposed) return;
1937
+ this._updateMessaging({ pendingQuestion: previousPendingQuestion });
1938
+ const normalized = normalizeRPCError(err, "Failed to submit answers");
1939
+ this._updateSession({ error: normalized.userMessage, errorRetryable: normalized.retryable });
1940
+ } finally {
1941
+ if (!this.disposed) {
1942
+ this._updateMessaging({ loading: false });
1943
+ }
1944
+ }
1945
+ }
1946
+ async _handleRejectPendingQuestion() {
1947
+ const curSessionId = this.state.session.currentSessionId;
1948
+ const curPendingQuestion = this.state.messaging.pendingQuestion;
1353
1949
  if (!curSessionId || !curPendingQuestion) return;
1354
1950
  try {
1355
- const result = await dataSource.rejectPendingQuestion(curSessionId);
1951
+ const result = await this.dataSource.rejectPendingQuestion(curSessionId);
1952
+ if (this.disposed) return;
1356
1953
  if (result.success) {
1357
- setPendingQuestion(null);
1954
+ this._updateMessaging({ pendingQuestion: null });
1358
1955
  if (curSessionId !== "new") {
1359
- const state = await dataSource.fetchSession(curSessionId);
1360
- if (state) {
1361
- setSession(state.session);
1362
- setTurns((prev) => {
1363
- const hasPendingUserOnly = prev.length > 0 && !prev[prev.length - 1].assistantTurn;
1364
- if (hasPendingUserOnly && (!state.turns || state.turns.length === 0)) {
1365
- return prev;
1366
- }
1367
- return state.turns ?? prev;
1368
- });
1369
- setPendingQuestion(state.pendingQuestion || null);
1956
+ const fetchResult = await this.dataSource.fetchSession(curSessionId);
1957
+ if (this.disposed) return;
1958
+ if (fetchResult) {
1959
+ this._updateSession({ session: fetchResult.session });
1960
+ this._setTurnsFromFetch(fetchResult.turns);
1961
+ this._updateMessaging({ pendingQuestion: fetchResult.pendingQuestion || null });
1370
1962
  }
1371
1963
  }
1372
1964
  } else {
1373
- setError(result.error || "Failed to reject question");
1965
+ this._updateSession({ error: result.error || "Failed to reject question", errorRetryable: false });
1374
1966
  }
1375
1967
  } catch (err) {
1376
- const errorMessage = err instanceof Error ? err.message : "Failed to reject question";
1377
- setError(errorMessage);
1968
+ if (this.disposed) return;
1969
+ const normalized = normalizeRPCError(err, "Failed to reject question");
1970
+ this._updateSession({ error: normalized.userMessage, errorRetryable: normalized.retryable });
1378
1971
  }
1379
- }, [dataSource]);
1380
- const sessionValue = useMemo(() => ({
1381
- session,
1382
- currentSessionId,
1383
- fetching,
1384
- error,
1385
- debugMode,
1386
- sessionDebugUsage,
1387
- debugLimits,
1388
- setError
1389
- }), [session, currentSessionId, fetching, error, debugMode, sessionDebugUsage, debugLimits]);
1390
- const messagingValue = useMemo(() => ({
1391
- turns,
1392
- streamingContent,
1393
- isStreaming,
1394
- loading,
1395
- pendingQuestion,
1396
- codeOutputs,
1397
- isCompacting,
1398
- compactionSummary,
1399
- artifactsInvalidationTrigger,
1400
- sendMessage: sendMessageDirect,
1401
- handleRegenerate,
1402
- handleEdit,
1403
- handleCopy,
1404
- handleSubmitQuestionAnswers,
1405
- handleRejectPendingQuestion,
1406
- cancel: cancelStream,
1407
- setCodeOutputs
1408
- }), [
1409
- turns,
1410
- streamingContent,
1411
- isStreaming,
1412
- loading,
1413
- pendingQuestion,
1414
- codeOutputs,
1415
- isCompacting,
1416
- compactionSummary,
1417
- artifactsInvalidationTrigger,
1418
- sendMessageDirect,
1419
- handleRegenerate,
1420
- handleEdit,
1421
- handleCopy,
1422
- handleSubmitQuestionAnswers,
1423
- handleRejectPendingQuestion,
1424
- cancelStream
1425
- ]);
1426
- const inputValue = useMemo(() => ({
1427
- message,
1428
- inputError,
1429
- messageQueue,
1430
- setMessage,
1431
- setInputError,
1432
- handleSubmit,
1433
- handleUnqueue
1434
- }), [message, inputError, messageQueue, handleSubmit, handleUnqueue]);
1435
- return /* @__PURE__ */ jsx(SessionCtx.Provider, { value: sessionValue, children: /* @__PURE__ */ jsx(MessagingCtx.Provider, { value: messagingValue, children: /* @__PURE__ */ jsx(InputCtx.Provider, { value: inputValue, children }) }) });
1972
+ }
1973
+ // ── Input / queue ───────────────────────────────────────────────────────
1974
+ _handleSubmit(e, attachments = []) {
1975
+ e.preventDefault();
1976
+ const msg = this.state.input.message;
1977
+ if (!msg.trim() && attachments.length === 0) return;
1978
+ this._updateInput({ inputError: null });
1979
+ this._clearStreamError();
1980
+ const convertedAttachments = attachments.map((att) => ({
1981
+ clientKey: att.clientKey || crypto.randomUUID(),
1982
+ filename: att.filename,
1983
+ mimeType: att.mimeType,
1984
+ sizeBytes: att.sizeBytes,
1985
+ base64Data: att.base64Data,
1986
+ url: att.url,
1987
+ preview: att.preview
1988
+ }));
1989
+ if (this.state.messaging.loading) {
1990
+ const ok = this._enqueueMessage(msg.trim(), convertedAttachments);
1991
+ if (ok) {
1992
+ this._updateInput({ message: "" });
1993
+ }
1994
+ return;
1995
+ }
1996
+ this._sendMessage(msg.trim(), convertedAttachments);
1997
+ }
1998
+ _handleUnqueue() {
1999
+ const queue = this.state.input.messageQueue;
2000
+ if (queue.length === 0) return null;
2001
+ const last = queue[queue.length - 1];
2002
+ this._updateInput({ messageQueue: queue.slice(0, -1) });
2003
+ return { content: last.content, attachments: last.attachments };
2004
+ }
2005
+ _enqueueMessage(content, attachments) {
2006
+ if (this.state.input.messageQueue.length >= MAX_QUEUE_SIZE) {
2007
+ this._updateInput({ inputError: "BiChat.Input.QueueFull" });
2008
+ return false;
2009
+ }
2010
+ this._updateInput({
2011
+ messageQueue: [...this.state.input.messageQueue, { content, attachments }]
2012
+ });
2013
+ return true;
2014
+ }
2015
+ _removeQueueItem(index) {
2016
+ this._updateInput({
2017
+ messageQueue: this.state.input.messageQueue.filter((_, i) => i !== index)
2018
+ });
2019
+ }
2020
+ _updateQueueItem(index, content) {
2021
+ this._updateInput({
2022
+ messageQueue: this.state.input.messageQueue.map(
2023
+ (item, i) => i === index ? { ...item, content } : item
2024
+ )
2025
+ });
2026
+ }
2027
+ };
2028
+ function sessionDebugUsageEqual(a, b) {
2029
+ if (!a) return false;
2030
+ return a.promptTokens === b.promptTokens && a.completionTokens === b.completionTokens && a.totalTokens === b.totalTokens && a.turnsWithUsage === b.turnsWithUsage && a.latestPromptTokens === b.latestPromptTokens && a.latestCompletionTokens === b.latestCompletionTokens && a.latestTotalTokens === b.latestTotalTokens;
1436
2031
  }
1437
- function useChatSession() {
1438
- const context = useContext(SessionCtx);
1439
- if (!context) {
1440
- throw new Error("useChatSession must be used within ChatSessionProvider");
2032
+ var MachineCtx = createContext(null);
2033
+ var DEFAULT_RATE_LIMIT_CONFIG = {
2034
+ maxRequests: 20,
2035
+ windowMs: 6e4
2036
+ };
2037
+ function ChatSessionProvider({
2038
+ dataSource,
2039
+ sessionId,
2040
+ rateLimiter: externalRateLimiter,
2041
+ rateLimitConfig,
2042
+ onSessionCreated,
2043
+ children
2044
+ }) {
2045
+ const machineRef = useRef(null);
2046
+ if (!machineRef.current) {
2047
+ machineRef.current = new ChatMachine({
2048
+ dataSource,
2049
+ rateLimiter: externalRateLimiter || new RateLimiter(rateLimitConfig || DEFAULT_RATE_LIMIT_CONFIG),
2050
+ onSessionCreated
2051
+ });
1441
2052
  }
1442
- return context;
2053
+ const machine = machineRef.current;
2054
+ useEffect(() => {
2055
+ machine.updateConfig({ dataSource, onSessionCreated });
2056
+ }, [machine, dataSource, onSessionCreated]);
2057
+ useEffect(() => {
2058
+ machine.setSessionId(sessionId);
2059
+ }, [machine, sessionId]);
2060
+ useEffect(() => {
2061
+ return () => {
2062
+ machine.dispose();
2063
+ };
2064
+ }, [machine]);
2065
+ return /* @__PURE__ */ jsx(MachineCtx.Provider, { value: machine, children });
1443
2066
  }
1444
- function useChatMessaging() {
1445
- const context = useContext(MessagingCtx);
1446
- if (!context) {
1447
- throw new Error("useChatMessaging must be used within ChatSessionProvider");
2067
+ function useMachine() {
2068
+ const machine = useContext(MachineCtx);
2069
+ if (!machine) {
2070
+ throw new Error("Chat hooks must be used within ChatSessionProvider");
1448
2071
  }
1449
- return context;
2072
+ return machine;
2073
+ }
2074
+ function useChatSession() {
2075
+ const machine = useMachine();
2076
+ return useSyncExternalStore(
2077
+ machine.subscribeSession,
2078
+ machine.getSessionSnapshot,
2079
+ machine.getSessionSnapshot
2080
+ // SSR fallback
2081
+ );
2082
+ }
2083
+ function useChatMessaging() {
2084
+ const machine = useMachine();
2085
+ return useSyncExternalStore(
2086
+ machine.subscribeMessaging,
2087
+ machine.getMessagingSnapshot,
2088
+ machine.getMessagingSnapshot
2089
+ );
1450
2090
  }
1451
2091
  function useOptionalChatMessaging() {
1452
- return useContext(MessagingCtx);
2092
+ const machine = useContext(MachineCtx);
2093
+ const snapshot = useSyncExternalStore(
2094
+ machine ? machine.subscribeMessaging : noopSubscribe,
2095
+ machine ? machine.getMessagingSnapshot : nullSnapshot,
2096
+ machine ? machine.getMessagingSnapshot : nullSnapshot
2097
+ );
2098
+ return machine ? snapshot : null;
2099
+ }
2100
+ function useChatInput() {
2101
+ const machine = useMachine();
2102
+ return useSyncExternalStore(
2103
+ machine.subscribeInput,
2104
+ machine.getInputSnapshot,
2105
+ machine.getInputSnapshot
2106
+ );
2107
+ }
2108
+ function noopSubscribe() {
2109
+ return () => {
2110
+ };
1453
2111
  }
1454
- function useChatInput() {
1455
- const context = useContext(InputCtx);
1456
- if (!context) {
1457
- throw new Error("useChatInput must be used within ChatSessionProvider");
1458
- }
1459
- return context;
2112
+ function nullSnapshot() {
2113
+ return null;
1460
2114
  }
1461
2115
 
1462
2116
  // ui/src/bichat/components/ChatHeader.tsx
@@ -1553,6 +2207,26 @@ function ChatHeader({ session, onBack, readOnly, logoSlot, actionsSlot }) {
1553
2207
  ] })
1554
2208
  ] }) });
1555
2209
  }
2210
+ function formatRelativeTime(date, t) {
2211
+ const messageDate = new Date(date);
2212
+ const now = /* @__PURE__ */ new Date();
2213
+ const diffMins = differenceInMinutes(now, messageDate);
2214
+ const diffHours = differenceInHours(now, messageDate);
2215
+ const diffDays = differenceInDays(now, messageDate);
2216
+ if (diffMins < 1) {
2217
+ return t ? t("BiChat.RelativeTime.JustNow") : "Just now";
2218
+ }
2219
+ if (diffMins < 60) {
2220
+ return t ? t("BiChat.RelativeTime.MinutesAgo", { count: diffMins }) : `${diffMins}m ago`;
2221
+ }
2222
+ if (diffHours < 24) {
2223
+ return t ? t("BiChat.RelativeTime.HoursAgo", { count: diffHours }) : `${diffHours}h ago`;
2224
+ }
2225
+ if (diffDays <= 7) {
2226
+ return t ? t("BiChat.RelativeTime.DaysAgo", { count: diffDays }) : `${diffDays}d ago`;
2227
+ }
2228
+ return format(messageDate, "HH:mm");
2229
+ }
1556
2230
  var MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024;
1557
2231
  var ALLOWED_MIME_TYPES = /* @__PURE__ */ new Set([
1558
2232
  "image/jpeg",
@@ -2274,7 +2948,7 @@ function UserMessage({
2274
2948
  [selectedImageIndex, imageAttachments.length]
2275
2949
  );
2276
2950
  const currentAttachment = selectedImageIndex !== null ? imageAttachments[selectedImageIndex] : null;
2277
- const timestamp = formatDistanceToNow(new Date(turn.createdAt), { addSuffix: true });
2951
+ const timestamp = formatRelativeTime(turn.createdAt, t);
2278
2952
  const avatarSlotProps = { initials };
2279
2953
  const contentSlotProps = { content: turn.content };
2280
2954
  const attachmentsSlotProps = {
@@ -2319,7 +2993,7 @@ function UserMessage({
2319
2993
  value: draftContent,
2320
2994
  onChange: (e) => setDraftContent(e.target.value),
2321
2995
  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",
2322
- "aria-label": "Edit message"
2996
+ "aria-label": t("BiChat.Message.EditMessage")
2323
2997
  }
2324
2998
  ),
2325
2999
  /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
@@ -2329,7 +3003,7 @@ function UserMessage({
2329
3003
  type: "button",
2330
3004
  onClick: handleEditCancel,
2331
3005
  className: "cursor-pointer px-3 py-1.5 rounded-lg bg-white/10 hover:bg-white/15 transition-colors text-sm font-medium",
2332
- children: "Cancel"
3006
+ children: t("BiChat.Message.Cancel")
2333
3007
  }
2334
3008
  ),
2335
3009
  /* @__PURE__ */ jsx(
@@ -2339,7 +3013,7 @@ function UserMessage({
2339
3013
  onClick: handleEditSave,
2340
3014
  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",
2341
3015
  disabled: !draftContent.trim() || draftContent === turn.content,
2342
- children: "Save"
3016
+ children: t("BiChat.Message.Save")
2343
3017
  }
2344
3018
  )
2345
3019
  ] })
@@ -2354,7 +3028,7 @@ function UserMessage({
2354
3028
  {
2355
3029
  onClick: handleCopyClick,
2356
3030
  className: `cursor-pointer ${classes.actionButton} ${isCopied ? "text-green-600 dark:text-green-400" : ""}`,
2357
- "aria-label": "Copy message",
3031
+ "aria-label": t("BiChat.Message.CopyMessage"),
2358
3032
  title: isCopied ? t("BiChat.Message.Copied") : t("BiChat.Message.Copy"),
2359
3033
  children: isCopied ? /* @__PURE__ */ jsx(Check, { size: 14, weight: "bold" }) : /* @__PURE__ */ jsx(Copy, { size: 14, weight: "regular" })
2360
3034
  }
@@ -2364,8 +3038,8 @@ function UserMessage({
2364
3038
  {
2365
3039
  onClick: handleEditClick,
2366
3040
  className: `cursor-pointer ${classes.actionButton}`,
2367
- "aria-label": "Edit message",
2368
- title: "Edit",
3041
+ "aria-label": t("BiChat.Message.EditMessage"),
3042
+ title: t("BiChat.Message.EditMessage"),
2369
3043
  disabled: isEditing,
2370
3044
  children: /* @__PURE__ */ jsx(PencilSimple, { size: 14, weight: "regular" })
2371
3045
  }
@@ -2415,6 +3089,7 @@ function UserTurnView({
2415
3089
  }
2416
3090
  );
2417
3091
  }
3092
+ init_useTranslation();
2418
3093
  function toBase64(str) {
2419
3094
  const bytes = new TextEncoder().encode(str);
2420
3095
  let binary = "";
@@ -2424,16 +3099,17 @@ function toBase64(str) {
2424
3099
  return btoa(binary);
2425
3100
  }
2426
3101
  function CodeOutputsPanel({ outputs }) {
3102
+ const { t } = useTranslation();
2427
3103
  if (!outputs || outputs.length === 0) return null;
2428
3104
  return /* @__PURE__ */ jsxs("div", { className: "mb-2 p-3 bg-gray-50 dark:bg-gray-900/50 rounded-lg border border-gray-200 dark:border-gray-700", children: [
2429
- /* @__PURE__ */ jsx("div", { className: "text-xs font-semibold text-gray-600 dark:text-gray-400 mb-2", children: "Code Output" }),
3105
+ /* @__PURE__ */ jsx("div", { className: "text-xs font-semibold text-gray-600 dark:text-gray-400 mb-2", children: t("BiChat.CodeOutput.Title") }),
2430
3106
  /* @__PURE__ */ jsx("div", { className: "space-y-2", children: outputs.map((output, index) => /* @__PURE__ */ jsxs("div", { children: [
2431
3107
  output.type === "image" && /* @__PURE__ */ jsxs("div", { className: "relative group", children: [
2432
3108
  /* @__PURE__ */ jsx(
2433
3109
  "img",
2434
3110
  {
2435
3111
  src: output.content.startsWith("data:") ? output.content : `data:${output.mimeType || "image/png"};base64,${output.content}`,
2436
- alt: output.filename || "Code output",
3112
+ alt: output.filename || t("BiChat.CodeOutput.CodeOutput"),
2437
3113
  className: "max-w-full rounded border border-gray-300 dark:border-gray-600"
2438
3114
  }
2439
3115
  ),
@@ -2465,19 +3141,23 @@ function CodeOutputsPanel({ outputs }) {
2465
3141
  ] })
2466
3142
  ] }),
2467
3143
  output.type === "error" && /* @__PURE__ */ jsxs("div", { className: "text-xs text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20 p-2 rounded border border-red-200 dark:border-red-800", children: [
2468
- /* @__PURE__ */ jsx("div", { className: "font-semibold mb-1", children: "Error" }),
3144
+ /* @__PURE__ */ jsx("div", { className: "font-semibold mb-1", children: t("BiChat.Error.Label") }),
2469
3145
  /* @__PURE__ */ jsx("pre", { className: "whitespace-pre-wrap", children: output.content })
2470
3146
  ] })
2471
3147
  ] }, index)) })
2472
3148
  ] });
2473
3149
  }
2474
3150
  var CodeOutputsPanel_default = CodeOutputsPanel;
3151
+
3152
+ // ui/src/bichat/components/StreamingCursor.tsx
3153
+ init_useTranslation();
2475
3154
  function StreamingCursor() {
3155
+ const { t } = useTranslation();
2476
3156
  return /* @__PURE__ */ jsx(
2477
3157
  "span",
2478
3158
  {
2479
3159
  className: "inline-block w-1.5 h-4 ml-0.5 bg-primary-600 dark:bg-primary-500 animate-pulse",
2480
- "aria-label": "AI is typing"
3160
+ "aria-label": t("BiChat.Common.AITyping")
2481
3161
  }
2482
3162
  );
2483
3163
  }
@@ -3256,7 +3936,7 @@ function AssistantMessage({
3256
3936
  await onRegenerate(turnId);
3257
3937
  }
3258
3938
  }, [onRegenerate, turnId]);
3259
- const timestamp = formatDistanceToNow(new Date(turn.createdAt), { addSuffix: true });
3939
+ const timestamp = formatRelativeTime(turn.createdAt, t);
3260
3940
  const avatarSlotProps = { text: isSystemMessage ? "SYS" : "AI" };
3261
3941
  const contentSlotProps = {
3262
3942
  content: turn.content,
@@ -3310,7 +3990,7 @@ function AssistantMessage({
3310
3990
  {
3311
3991
  fallback: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm text-gray-400 dark:text-gray-500", children: [
3312
3992
  /* @__PURE__ */ jsx("div", { className: "w-4 h-4 border-2 border-gray-300 dark:border-gray-600 border-t-transparent rounded-full animate-spin" }),
3313
- "Loading..."
3993
+ t("BiChat.Common.Loading")
3314
3994
  ] }),
3315
3995
  children: /* @__PURE__ */ jsx(
3316
3996
  MarkdownRenderer2,
@@ -3364,7 +4044,7 @@ function AssistantMessage({
3364
4044
  ]
3365
4045
  }
3366
4046
  ),
3367
- explanationExpanded && /* @__PURE__ */ jsx("div", { className: "pt-3 text-sm text-gray-600 dark:text-gray-400", children: /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx("div", { children: "Loading..." }), children: /* @__PURE__ */ jsx(MarkdownRenderer2, { content: turn.explanation }) }) })
4047
+ explanationExpanded && /* @__PURE__ */ jsx("div", { className: "pt-3 text-sm text-gray-600 dark:text-gray-400", children: /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx("div", { children: t("BiChat.Common.Loading") }), children: /* @__PURE__ */ jsx(MarkdownRenderer2, { content: turn.explanation }) }) })
3368
4048
  ] })
3369
4049
  ) }),
3370
4050
  showDebug && /* @__PURE__ */ jsx(DebugPanel, { trace: turn.debug })
@@ -3385,7 +4065,7 @@ function AssistantMessage({
3385
4065
  {
3386
4066
  onClick: handleCopyClick,
3387
4067
  className: `cursor-pointer ${classes.actionButton} ${isCopied ? "text-green-600 dark:text-green-400" : ""}`,
3388
- "aria-label": "Copy message",
4068
+ "aria-label": t("BiChat.Message.CopyMessage"),
3389
4069
  title: isCopied ? t("BiChat.Message.Copied") : t("BiChat.Message.Copy"),
3390
4070
  children: isCopied ? /* @__PURE__ */ jsx(Check, { size: 14, weight: "bold" }) : /* @__PURE__ */ jsx(Copy, { size: 14, weight: "regular" })
3391
4071
  }
@@ -3395,8 +4075,8 @@ function AssistantMessage({
3395
4075
  {
3396
4076
  onClick: handleRegenerateClick,
3397
4077
  className: `cursor-pointer ${classes.actionButton}`,
3398
- "aria-label": "Regenerate response",
3399
- title: "Regenerate",
4078
+ "aria-label": t("BiChat.Message.Regenerate"),
4079
+ title: t("BiChat.Message.Regenerate"),
3400
4080
  children: /* @__PURE__ */ jsx(ArrowsClockwise, { size: 14, weight: "regular" })
3401
4081
  }
3402
4082
  )
@@ -3405,8 +4085,6 @@ function AssistantMessage({
3405
4085
  ] })
3406
4086
  ] });
3407
4087
  }
3408
-
3409
- // ui/src/bichat/components/SystemMessage.tsx
3410
4088
  init_useTranslation();
3411
4089
  var MarkdownRenderer3 = lazy(
3412
4090
  () => Promise.resolve().then(() => (init_MarkdownRenderer(), MarkdownRenderer_exports)).then((module) => ({ default: module.MarkdownRenderer }))
@@ -3473,7 +4151,7 @@ function SystemMessage({
3473
4151
  setIsCopied(false);
3474
4152
  }
3475
4153
  }, [content, onCopy]);
3476
- const timestamp = formatDistanceToNow(new Date(createdAt), { addSuffix: true });
4154
+ const timestamp = formatRelativeTime(createdAt, t);
3477
4155
  const resolvedHeight = isExpanded ? contentHeight : COLLAPSED_HEIGHT;
3478
4156
  return /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx("div", { className: "w-full max-w-3xl px-2 sm:px-4", children: /* @__PURE__ */ jsxs("div", { className: "relative overflow-hidden rounded-xl border border-gray-200/80 dark:border-gray-700/60 bg-gradient-to-b from-gray-50/80 to-gray-100/40 dark:from-gray-800/40 dark:to-gray-900/30 shadow-[0_1px_3px_rgba(0,0,0,0.04)] dark:shadow-[0_1px_3px_rgba(0,0,0,0.2)]", children: [
3479
4157
  /* @__PURE__ */ jsx("div", { className: "absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-gray-300/40 dark:via-gray-600/20 to-transparent" }),
@@ -3493,7 +4171,7 @@ function SystemMessage({
3493
4171
  focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50
3494
4172
  ${isCopied ? "text-green-600 dark:text-green-400" : "text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-200/50 dark:hover:bg-gray-700/40"}
3495
4173
  `,
3496
- "aria-label": "Copy message",
4174
+ "aria-label": t("BiChat.Message.CopyMessage"),
3497
4175
  title: isCopied ? t("BiChat.Message.Copied") : t("BiChat.Message.Copy"),
3498
4176
  children: isCopied ? /* @__PURE__ */ jsx(Check, { size: 13, weight: "bold" }) : /* @__PURE__ */ jsx(Copy, { size: 13, weight: "regular" })
3499
4177
  }
@@ -3833,23 +4511,6 @@ var dropdownVariants = {
3833
4511
  transition: { duration: 0.1 }
3834
4512
  }
3835
4513
  };
3836
- var toastVariants = {
3837
- initial: { opacity: 0, y: -8 },
3838
- animate: {
3839
- opacity: 1,
3840
- y: 0,
3841
- transition: {
3842
- duration: prefersReducedMotion() ? 0 : 0.2
3843
- }
3844
- },
3845
- exit: {
3846
- opacity: 0,
3847
- y: -8,
3848
- transition: {
3849
- duration: prefersReducedMotion() ? 0 : 0.15
3850
- }
3851
- }
3852
- };
3853
4514
  var sessionItemVariants = {
3854
4515
  initial: { opacity: 0, x: -20 },
3855
4516
  animate: {
@@ -3911,7 +4572,7 @@ var prefersReducedMotion2 = () => {
3911
4572
  var getRandomVerb = (verbs, current) => {
3912
4573
  const available = verbs.filter((v) => v !== current);
3913
4574
  if (available.length === 0) {
3914
- return current || verbs[0] || "Thinking";
4575
+ return current || verbs[0] || "";
3915
4576
  }
3916
4577
  return available[Math.floor(Math.random() * available.length)];
3917
4578
  };
@@ -3953,7 +4614,7 @@ function TypingIndicator({
3953
4614
  animate: "animate",
3954
4615
  exit: "exit",
3955
4616
  className: "text-sm bichat-thinking-shimmer block",
3956
- "aria-label": `AI is ${verb}`,
4617
+ "aria-label": t("BiChat.Thinking.AriaLabel", { verb }),
3957
4618
  children: [
3958
4619
  verb,
3959
4620
  "..."
@@ -3967,6 +4628,9 @@ function TypingIndicator({
3967
4628
  }
3968
4629
  var MemoizedTypingIndicator = memo(TypingIndicator);
3969
4630
  MemoizedTypingIndicator.displayName = "TypingIndicator";
4631
+
4632
+ // ui/src/bichat/components/ScrollToBottomButton.tsx
4633
+ init_useTranslation();
3970
4634
  function ScrollToBottomButton({
3971
4635
  show,
3972
4636
  onClick,
@@ -3974,6 +4638,7 @@ function ScrollToBottomButton({
3974
4638
  disabled = false,
3975
4639
  label
3976
4640
  }) {
4641
+ const { t } = useTranslation();
3977
4642
  return /* @__PURE__ */ jsx(AnimatePresence, { children: show && /* @__PURE__ */ jsx(
3978
4643
  "div",
3979
4644
  {
@@ -3989,7 +4654,7 @@ function ScrollToBottomButton({
3989
4654
  onClick: disabled ? void 0 : onClick,
3990
4655
  disabled,
3991
4656
  className: `pointer-events-auto cursor-pointer shadow-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 active:bg-gray-100 dark:active:bg-gray-600 transition-colors duration-150 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-900 disabled:opacity-40 disabled:cursor-not-allowed ${label ? "flex items-center gap-1.5 px-4 py-2 rounded-full bg-primary-600 dark:bg-primary-500 border-primary-600 dark:border-primary-500 hover:bg-primary-700 dark:hover:bg-primary-600 active:bg-primary-800 dark:active:bg-primary-700" : "p-2.5 rounded-full bg-white dark:bg-gray-800"}`,
3992
- "aria-label": label || "Scroll to bottom",
4657
+ "aria-label": label || t("BiChat.Common.ScrollToBottom"),
3993
4658
  children: label ? /* @__PURE__ */ jsxs(Fragment, { children: [
3994
4659
  /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-white", children: label }),
3995
4660
  /* @__PURE__ */ jsx(ArrowDown, { size: 16, weight: "bold", className: "text-white" })
@@ -4003,23 +4668,6 @@ function ScrollToBottomButton({
4003
4668
  ) });
4004
4669
  }
4005
4670
  var ScrollToBottomButton_default = ScrollToBottomButton;
4006
- function CompactionDoodle({ title, subtitle }) {
4007
- return /* @__PURE__ */ jsx("div", { className: "rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
4008
- /* @__PURE__ */ jsxs("div", { className: "relative w-10 h-10", children: [
4009
- /* @__PURE__ */ jsx("div", { className: "absolute inset-0 rounded-full bg-primary-500/20 animate-pulse motion-reduce:animate-none" }),
4010
- /* @__PURE__ */ jsx("div", { className: "absolute inset-1 rounded-full bg-primary-500/40 animate-pulse motion-reduce:animate-none" }),
4011
- /* @__PURE__ */ jsx("div", { className: "absolute inset-3 rounded-full bg-primary-600" })
4012
- ] }),
4013
- /* @__PURE__ */ jsxs("div", { children: [
4014
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-gray-900 dark:text-gray-100", children: title }),
4015
- /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: subtitle })
4016
- ] })
4017
- ] }) });
4018
- }
4019
- var CompactionDoodle_default = CompactionDoodle;
4020
-
4021
- // ui/src/bichat/components/MessageList.tsx
4022
- init_useTranslation();
4023
4671
 
4024
4672
  // ui/src/bichat/utils/markdownStream.ts
4025
4673
  function normalizeStreamingMarkdown(text) {
@@ -4048,7 +4696,6 @@ var MarkdownRenderer4 = lazy(
4048
4696
  () => Promise.resolve().then(() => (init_MarkdownRenderer(), MarkdownRenderer_exports)).then((m) => ({ default: m.MarkdownRenderer }))
4049
4697
  );
4050
4698
  function MessageList({ renderUserTurn, renderAssistantTurn, thinkingVerbs, readOnly }) {
4051
- const { t } = useTranslation();
4052
4699
  const { currentSessionId, fetching } = useChatSession();
4053
4700
  const { turns, streamingContent, isStreaming, loading, isCompacting } = useChatMessaging();
4054
4701
  const messagesEndRef = useRef(null);
@@ -4106,13 +4753,6 @@ function MessageList({ renderUserTurn, renderAssistantTurn, thinkingVerbs, readO
4106
4753
  );
4107
4754
  return /* @__PURE__ */ jsxs("div", { className: "relative flex-1 min-h-0", children: [
4108
4755
  /* @__PURE__ */ jsx("div", { ref: containerRef, className: "h-full overflow-y-auto px-4 py-6", children: /* @__PURE__ */ jsxs("div", { className: "mx-auto space-y-6", children: [
4109
- isCompacting && /* @__PURE__ */ jsx(
4110
- CompactionDoodle_default,
4111
- {
4112
- title: t("BiChat.Slash.CompactingTitle"),
4113
- subtitle: t("BiChat.Slash.CompactingSubtitle")
4114
- }
4115
- ),
4116
4756
  fetching && turns.length === 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-6", "aria-hidden": "true", children: [
4117
4757
  /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxs("div", { className: "w-3/5 max-w-md rounded-2xl bg-gray-100 dark:bg-gray-800 p-4 space-y-2", children: [
4118
4758
  /* @__PURE__ */ jsx("div", { className: "h-3 w-full rounded bg-gray-200 dark:bg-gray-700 animate-pulse" }),
@@ -4175,6 +4815,128 @@ function MessageList({ renderUserTurn, renderAssistantTurn, thinkingVerbs, readO
4175
4815
  )
4176
4816
  ] });
4177
4817
  }
4818
+
4819
+ // ui/src/bichat/components/MessageQueueList.tsx
4820
+ init_useTranslation();
4821
+ function MessageQueueList({ queue, onRemove, onUpdate }) {
4822
+ const { t } = useTranslation();
4823
+ const [editingIndex, setEditingIndex] = useState(null);
4824
+ const [editValue, setEditValue] = useState("");
4825
+ const startEdit = useCallback((index) => {
4826
+ setEditingIndex(index);
4827
+ setEditValue(queue[index].content);
4828
+ }, [queue]);
4829
+ const saveEdit = useCallback(() => {
4830
+ if (editingIndex === null) return;
4831
+ const trimmed = editValue.trim();
4832
+ if (trimmed) {
4833
+ onUpdate(editingIndex, trimmed);
4834
+ }
4835
+ setEditingIndex(null);
4836
+ setEditValue("");
4837
+ }, [editingIndex, editValue, onUpdate]);
4838
+ const cancelEdit = useCallback(() => {
4839
+ setEditingIndex(null);
4840
+ setEditValue("");
4841
+ }, []);
4842
+ if (queue.length === 0) return null;
4843
+ return /* @__PURE__ */ jsxs("div", { className: "mb-3 space-y-1.5", children: [
4844
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400", children: /* @__PURE__ */ jsx("span", { className: "font-medium", children: t("BiChat.Input.QueuedMessages", { count: queue.length }) }) }),
4845
+ /* @__PURE__ */ jsx(AnimatePresence, { initial: false, children: queue.map((item, index) => /* @__PURE__ */ jsx(
4846
+ motion.div,
4847
+ {
4848
+ initial: { opacity: 0, height: 0 },
4849
+ animate: { opacity: 1, height: "auto" },
4850
+ exit: { opacity: 0, height: 0 },
4851
+ transition: { duration: 0.15 },
4852
+ className: "overflow-hidden",
4853
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-2 text-sm", children: [
4854
+ /* @__PURE__ */ jsx("span", { className: "flex-shrink-0 mt-0.5 w-5 h-5 rounded-full bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 flex items-center justify-center text-[10px] font-bold", children: index + 1 }),
4855
+ editingIndex === index ? /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0 flex flex-col gap-1.5", children: [
4856
+ /* @__PURE__ */ jsx(
4857
+ "textarea",
4858
+ {
4859
+ value: editValue,
4860
+ onChange: (e) => setEditValue(e.target.value),
4861
+ onKeyDown: (e) => {
4862
+ if (e.key === "Enter" && !e.shiftKey) {
4863
+ e.preventDefault();
4864
+ saveEdit();
4865
+ }
4866
+ if (e.key === "Escape") cancelEdit();
4867
+ },
4868
+ className: "w-full resize-none rounded border border-primary-300 dark:border-primary-600 bg-transparent px-2 py-1 text-sm text-gray-900 dark:text-white outline-none focus:ring-1 focus:ring-primary-500",
4869
+ rows: 1,
4870
+ autoFocus: true
4871
+ }
4872
+ ),
4873
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
4874
+ /* @__PURE__ */ jsxs(
4875
+ "button",
4876
+ {
4877
+ type: "button",
4878
+ onClick: saveEdit,
4879
+ className: "cursor-pointer inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium text-primary-700 dark:text-primary-300 bg-primary-50 dark:bg-primary-900/30 hover:bg-primary-100 dark:hover:bg-primary-900/50 rounded transition-colors",
4880
+ children: [
4881
+ /* @__PURE__ */ jsx(Check, { size: 12, weight: "bold" }),
4882
+ t("BiChat.Message.Save")
4883
+ ]
4884
+ }
4885
+ ),
4886
+ /* @__PURE__ */ jsxs(
4887
+ "button",
4888
+ {
4889
+ type: "button",
4890
+ onClick: cancelEdit,
4891
+ className: "cursor-pointer inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors",
4892
+ children: [
4893
+ /* @__PURE__ */ jsx(ArrowCounterClockwise, { size: 12 }),
4894
+ t("BiChat.Message.Cancel")
4895
+ ]
4896
+ }
4897
+ )
4898
+ ] })
4899
+ ] }) : /* @__PURE__ */ jsxs("p", { className: "flex-1 min-w-0 text-gray-700 dark:text-gray-300 truncate", children: [
4900
+ item.content,
4901
+ item.attachments.length > 0 && /* @__PURE__ */ jsxs("span", { className: "ml-1.5 text-gray-400 dark:text-gray-500", children: [
4902
+ "+",
4903
+ item.attachments.length,
4904
+ " ",
4905
+ t("BiChat.Input.AttachFiles").toLowerCase()
4906
+ ] })
4907
+ ] }),
4908
+ editingIndex !== index && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 flex-shrink-0", children: [
4909
+ /* @__PURE__ */ jsx(
4910
+ "button",
4911
+ {
4912
+ type: "button",
4913
+ onClick: () => startEdit(index),
4914
+ className: "cursor-pointer p-1 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors",
4915
+ "aria-label": t("BiChat.Input.EditQueueItem"),
4916
+ title: t("BiChat.Input.EditQueueItem"),
4917
+ children: /* @__PURE__ */ jsx(PencilSimple, { size: 14 })
4918
+ }
4919
+ ),
4920
+ /* @__PURE__ */ jsx(
4921
+ "button",
4922
+ {
4923
+ type: "button",
4924
+ onClick: () => onRemove(index),
4925
+ className: "cursor-pointer p-1 text-gray-400 dark:text-gray-500 hover:text-red-500 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors",
4926
+ "aria-label": t("BiChat.Input.RemoveQueueItem"),
4927
+ title: t("BiChat.Input.RemoveQueueItem"),
4928
+ children: /* @__PURE__ */ jsx(X, { size: 14 })
4929
+ }
4930
+ )
4931
+ ] })
4932
+ ] })
4933
+ },
4934
+ `queue-${index}-${item.content.slice(0, 20)}`
4935
+ )) })
4936
+ ] });
4937
+ }
4938
+
4939
+ // ui/src/bichat/components/MessageInput.tsx
4178
4940
  init_useTranslation();
4179
4941
  var MAX_FILES_DEFAULT = 10;
4180
4942
  var MAX_FILE_SIZE_DEFAULT = 20 * 1024 * 1024;
@@ -4194,6 +4956,8 @@ var MessageInput = forwardRef(
4194
4956
  onMessageChange,
4195
4957
  onSubmit,
4196
4958
  onUnqueue,
4959
+ onRemoveQueueItem,
4960
+ onUpdateQueueItem,
4197
4961
  placeholder: placeholderOverride,
4198
4962
  maxFiles = MAX_FILES_DEFAULT,
4199
4963
  maxFileSize = MAX_FILE_SIZE_DEFAULT,
@@ -4417,14 +5181,22 @@ var MessageInput = forwardRef(
4417
5181
  return;
4418
5182
  }
4419
5183
  if (isCommandListVisible) {
4420
- if (e.key === "ArrowDown" || e.key === "Tab" && !e.shiftKey) {
5184
+ if (e.key === "Tab") {
5185
+ e.preventDefault();
5186
+ if (filteredCommands.length > 0) {
5187
+ onMessageChange(filteredCommands[activeCommandIndex].name);
5188
+ setCommandListDismissed(true);
5189
+ }
5190
+ return;
5191
+ }
5192
+ if (e.key === "ArrowDown") {
4421
5193
  e.preventDefault();
4422
5194
  if (filteredCommands.length > 0) {
4423
5195
  setActiveCommandIndex((prev) => (prev + 1) % filteredCommands.length);
4424
5196
  }
4425
5197
  return;
4426
5198
  }
4427
- if (e.key === "ArrowUp" || e.key === "Tab" && e.shiftKey) {
5199
+ if (e.key === "ArrowUp") {
4428
5200
  e.preventDefault();
4429
5201
  if (filteredCommands.length > 0) {
4430
5202
  setActiveCommandIndex(
@@ -4450,7 +5222,7 @@ var MessageInput = forwardRef(
4450
5222
  }
4451
5223
  if (e.key === "Enter" && !e.shiftKey) {
4452
5224
  e.preventDefault();
4453
- if (!loading && (message.trim() || attachments.length > 0)) {
5225
+ if (message.trim() || attachments.length > 0) {
4454
5226
  handleFormSubmit(e);
4455
5227
  }
4456
5228
  }
@@ -4474,7 +5246,7 @@ var MessageInput = forwardRef(
4474
5246
  const handleFormSubmit = (e) => {
4475
5247
  e.preventDefault();
4476
5248
  if (isComposing) return;
4477
- if (loading || disabled || !message.trim() && attachments.length === 0) {
5249
+ if (disabled || !message.trim() && attachments.length === 0) {
4478
5250
  return;
4479
5251
  }
4480
5252
  setCommandListDismissed(true);
@@ -4482,7 +5254,7 @@ var MessageInput = forwardRef(
4482
5254
  setAttachments([]);
4483
5255
  setError(null);
4484
5256
  };
4485
- const canSubmit = !loading && !disabled && (message.trim() || attachments.length > 0);
5257
+ const canSubmit = !disabled && (message.trim() || attachments.length > 0);
4486
5258
  const visibleError = error || commandError;
4487
5259
  const visibleErrorText = visibleError ? t(visibleError) : "";
4488
5260
  const defaultContainerClassName = "shrink-0 px-4 pt-4 pb-6";
@@ -4503,8 +5275,9 @@ var MessageInput = forwardRef(
4503
5275
  ref: containerRef,
4504
5276
  className: containerClassName ?? defaultContainerClassName,
4505
5277
  children: /* @__PURE__ */ jsxs("form", { ref: formRef, onSubmit: handleFormSubmit, className: formClassName ?? "mx-auto", children: [
4506
- visibleError && /* @__PURE__ */ jsxs("div", { className: "mb-3 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg text-sm text-red-600 dark:text-red-400 flex items-center justify-between", children: [
4507
- /* @__PURE__ */ jsx("span", { children: visibleErrorText }),
5278
+ visibleError && /* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-start gap-2.5 px-3 py-2.5 bg-red-50 dark:bg-red-950/40 border border-red-200/80 dark:border-red-900/60 rounded-xl text-sm shadow-sm", children: [
5279
+ /* @__PURE__ */ jsx("div", { className: "flex-shrink-0 mt-0.5 flex items-center justify-center w-5 h-5 rounded-full bg-red-100 dark:bg-red-900/40", children: /* @__PURE__ */ jsx(X, { size: 10, className: "text-red-600 dark:text-red-400", weight: "bold" }) }),
5280
+ /* @__PURE__ */ jsx("span", { className: "flex-1 text-red-700 dark:text-red-300 text-xs leading-relaxed", children: visibleErrorText }),
4508
5281
  /* @__PURE__ */ jsx(
4509
5282
  "button",
4510
5283
  {
@@ -4513,13 +5286,20 @@ var MessageInput = forwardRef(
4513
5286
  setError(null);
4514
5287
  onClearCommandError?.();
4515
5288
  },
4516
- className: "cursor-pointer ml-2 p-1 hover:bg-red-100 dark:hover:bg-red-800 rounded transition-colors",
5289
+ className: "cursor-pointer flex-shrink-0 p-0.5 text-red-400 dark:text-red-500 hover:text-red-600 dark:hover:text-red-300 hover:bg-red-100 dark:hover:bg-red-900/40 rounded-md transition-colors",
4517
5290
  "aria-label": t("BiChat.Input.DismissError"),
4518
5291
  children: /* @__PURE__ */ jsx(X, { size: 14 })
4519
5292
  }
4520
5293
  )
4521
5294
  ] }),
4522
- messageQueue.length > 0 && /* @__PURE__ */ jsx("div", { className: "mb-3 text-xs text-gray-500 dark:text-gray-400", children: /* @__PURE__ */ jsx("span", { className: "px-2.5 py-1 bg-primary-50 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 rounded font-medium", children: t("BiChat.Input.MessagesQueued", { count: messageQueue.length }) }) }),
5295
+ messageQueue.length > 0 && onRemoveQueueItem && onUpdateQueueItem && /* @__PURE__ */ jsx(
5296
+ MessageQueueList,
5297
+ {
5298
+ queue: messageQueue,
5299
+ onRemove: onRemoveQueueItem,
5300
+ onUpdate: onUpdateQueueItem
5301
+ }
5302
+ ),
4523
5303
  debugMode && /* @__PURE__ */ jsxs("div", { className: "mb-3 space-y-2 text-xs", children: [
4524
5304
  /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5 px-2.5 py-1 bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-300 rounded-full font-medium text-[11px]", children: [
4525
5305
  /* @__PURE__ */ jsxs("span", { className: "relative flex h-1.5 w-1.5", "aria-hidden": "true", children: [
@@ -4676,7 +5456,7 @@ var MessageInput = forwardRef(
4676
5456
  className: "resize-none bg-transparent border-none outline-none px-1 py-2 w-full text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 text-sm leading-relaxed",
4677
5457
  style: { maxHeight: `${MAX_HEIGHT}px` },
4678
5458
  rows: 1,
4679
- disabled: loading || disabled,
5459
+ disabled,
4680
5460
  "aria-busy": loading,
4681
5461
  "aria-label": t("BiChat.Input.MessageInput")
4682
5462
  }
@@ -4742,13 +5522,36 @@ var MessageInput = forwardRef(
4742
5522
  }
4743
5523
  );
4744
5524
  MessageInput.displayName = "MessageInput";
5525
+ function CompactionDoodle({ title, subtitle }) {
5526
+ return /* @__PURE__ */ jsxs(
5527
+ motion.div,
5528
+ {
5529
+ initial: { opacity: 0, y: 8, scale: 0.96 },
5530
+ animate: { opacity: 1, y: 0, scale: 1 },
5531
+ exit: { opacity: 0, y: 4, scale: 0.98 },
5532
+ transition: { type: "spring", stiffness: 400, damping: 28 },
5533
+ className: "flex items-center gap-2.5 rounded-xl border border-gray-200/70 bg-white/95 px-3.5 py-2 shadow-sm backdrop-blur-sm dark:border-gray-700/50 dark:bg-gray-800/95",
5534
+ children: [
5535
+ /* @__PURE__ */ jsxs("div", { className: "relative flex h-5 w-5 items-center justify-center", children: [
5536
+ /* @__PURE__ */ jsx("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-primary-400/30" }),
5537
+ /* @__PURE__ */ jsx("span", { className: "relative inline-flex h-2 w-2 rounded-full bg-primary-500" })
5538
+ ] }),
5539
+ /* @__PURE__ */ jsxs("div", { className: "flex items-baseline gap-1.5", children: [
5540
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-gray-700 dark:text-gray-200", children: title }),
5541
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] text-gray-400 dark:text-gray-500", children: subtitle })
5542
+ ] })
5543
+ ]
5544
+ }
5545
+ );
5546
+ }
5547
+ var CompactionDoodle_default = CompactionDoodle;
4745
5548
 
4746
5549
  // ui/src/bichat/components/WelcomeContent.tsx
4747
5550
  init_useTranslation();
4748
5551
  var PROMPT_DEFS = [
4749
- { categoryKey: "Welcome.Prompt1Category", textKey: "Welcome.Prompt1Text", icon: ChartBar, defaultCategory: "OSAGO Portfolio", defaultText: "What is the total amount of accrued OSAGO premiums for the reporting period?" },
4750
- { categoryKey: "Welcome.Prompt2Category", textKey: "Welcome.Prompt2Text", icon: FileText, defaultCategory: "Regional Analysis", defaultText: "Show me the top 5 regions by collected insurance premiums" },
4751
- { categoryKey: "Welcome.Prompt3Category", textKey: "Welcome.Prompt3Text", icon: Lightbulb, defaultCategory: "Loss Analysis", defaultText: "Calculate the loss ratio across the entire OSAGO portfolio" }
5552
+ { categoryKey: "BiChat.Welcome.Prompt1Category", textKey: "BiChat.Welcome.Prompt1Text", icon: ChartBar, defaultCategory: "Data Analysis", defaultText: "Show me a summary of key metrics" },
5553
+ { categoryKey: "BiChat.Welcome.Prompt2Category", textKey: "BiChat.Welcome.Prompt2Text", icon: FileText, defaultCategory: "Reports", defaultText: "Generate a report for the current period" },
5554
+ { categoryKey: "BiChat.Welcome.Prompt3Category", textKey: "BiChat.Welcome.Prompt3Text", icon: Lightbulb, defaultCategory: "Insights", defaultText: "What trends can you identify in the data?" }
4752
5555
  ];
4753
5556
  var PROMPT_STYLES = [
4754
5557
  {
@@ -4898,11 +5701,172 @@ function WelcomeContent({
4898
5701
  );
4899
5702
  }) })
4900
5703
  ] })
4901
- ]
4902
- }
4903
- );
5704
+ ]
5705
+ }
5706
+ );
5707
+ }
5708
+ var WelcomeContent_default = WelcomeContent;
5709
+ init_useTranslation();
5710
+ var variantStyles = {
5711
+ error: {
5712
+ container: "border-red-200 bg-red-50 dark:bg-red-900/20",
5713
+ title: "text-red-800 dark:text-red-300",
5714
+ message: "text-red-700 dark:text-red-400",
5715
+ icon: "text-red-600 dark:text-red-400",
5716
+ button: "text-red-400 hover:text-red-600 dark:hover:text-red-300",
5717
+ retryButton: "bg-red-600 dark:bg-red-700 hover:bg-red-700 dark:hover:bg-red-800 text-white",
5718
+ Icon: XCircle
5719
+ },
5720
+ success: {
5721
+ container: "border-green-200 bg-green-50 dark:bg-green-900/20",
5722
+ title: "text-green-800 dark:text-green-300",
5723
+ message: "text-green-700 dark:text-green-400",
5724
+ icon: "text-green-600 dark:text-green-400",
5725
+ button: "text-green-400 hover:text-green-600 dark:hover:text-green-300",
5726
+ retryButton: "bg-green-600 dark:bg-green-700 hover:bg-green-700 dark:hover:bg-green-800 text-white",
5727
+ Icon: CheckCircle
5728
+ },
5729
+ warning: {
5730
+ container: "border-yellow-200 bg-yellow-50 dark:bg-yellow-900/20",
5731
+ title: "text-yellow-800 dark:text-yellow-300",
5732
+ message: "text-yellow-700 dark:text-yellow-400",
5733
+ icon: "text-yellow-600 dark:text-yellow-400",
5734
+ button: "text-yellow-400 hover:text-yellow-600 dark:hover:text-yellow-300",
5735
+ retryButton: "bg-yellow-600 dark:bg-yellow-700 hover:bg-yellow-700 dark:hover:bg-yellow-800 text-white",
5736
+ Icon: Warning
5737
+ },
5738
+ info: {
5739
+ container: "border-blue-200 bg-blue-50 dark:bg-blue-900/20",
5740
+ title: "text-blue-800 dark:text-blue-300",
5741
+ message: "text-blue-700 dark:text-blue-400",
5742
+ icon: "text-blue-600 dark:text-blue-400",
5743
+ button: "text-blue-400 hover:text-blue-600 dark:hover:text-blue-300",
5744
+ retryButton: "bg-blue-600 dark:bg-blue-700 hover:bg-blue-700 dark:hover:bg-blue-800 text-white",
5745
+ Icon: Info
5746
+ }
5747
+ };
5748
+ function Alert({
5749
+ variant = "info",
5750
+ message,
5751
+ title,
5752
+ onDismiss,
5753
+ onRetry,
5754
+ show = true,
5755
+ dismissible = true
5756
+ }) {
5757
+ const { t } = useTranslation();
5758
+ const styles = variantStyles[variant];
5759
+ const IconComponent = styles.Icon;
5760
+ return /* @__PURE__ */ jsx(AnimatePresence, { children: show && /* @__PURE__ */ jsx(
5761
+ motion.div,
5762
+ {
5763
+ variants: errorMessageVariants,
5764
+ initial: "initial",
5765
+ animate: "animate",
5766
+ exit: "exit",
5767
+ className: `border-t border ${styles.container} px-4 py-3`,
5768
+ role: "alert",
5769
+ "aria-live": "assertive",
5770
+ children: /* @__PURE__ */ jsxs("div", { className: "w-full flex items-start justify-between px-4", children: [
5771
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 flex-1", children: [
5772
+ /* @__PURE__ */ jsx(IconComponent, { size: 20, className: `w-5 h-5 ${styles.icon} flex-shrink-0 mt-0.5` }),
5773
+ /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
5774
+ title && /* @__PURE__ */ jsx("p", { className: `text-sm ${styles.title} font-medium`, children: title }),
5775
+ /* @__PURE__ */ jsx("p", { className: `text-sm ${styles.message} ${title ? "mt-1" : ""}`, children: message }),
5776
+ onRetry && /* @__PURE__ */ jsx(
5777
+ "button",
5778
+ {
5779
+ onClick: onRetry,
5780
+ className: `mt-2 text-xs px-3 py-1.5 rounded ${styles.retryButton} transition-colors font-medium`,
5781
+ children: t("BiChat.Chat.Retry")
5782
+ }
5783
+ )
5784
+ ] })
5785
+ ] }),
5786
+ dismissible && onDismiss && /* @__PURE__ */ jsx(
5787
+ "button",
5788
+ {
5789
+ onClick: onDismiss,
5790
+ className: `${styles.button} transition-colors flex-shrink-0`,
5791
+ "aria-label": t("BiChat.Chat.DismissNotification"),
5792
+ children: /* @__PURE__ */ jsx(X, { size: 20, className: "w-5 h-5" })
5793
+ }
5794
+ )
5795
+ ] })
5796
+ }
5797
+ ) });
5798
+ }
5799
+ var Alert_default = memo(Alert);
5800
+
5801
+ // ui/src/bichat/components/ArchiveBanner.tsx
5802
+ init_useTranslation();
5803
+ function ArchiveBanner({
5804
+ show = true,
5805
+ onRestore,
5806
+ restoring = false,
5807
+ onRestoreComplete
5808
+ }) {
5809
+ const { t } = useTranslation();
5810
+ const [error, setError] = useState(null);
5811
+ const handleRestore = async () => {
5812
+ try {
5813
+ setError(null);
5814
+ if (onRestore) {
5815
+ await onRestore();
5816
+ }
5817
+ if (onRestoreComplete) {
5818
+ onRestoreComplete();
5819
+ }
5820
+ } catch (err) {
5821
+ const message = err instanceof Error ? err.message : t("BiChat.Archive.RestoreFailed");
5822
+ setError(message);
5823
+ }
5824
+ };
5825
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
5826
+ /* @__PURE__ */ jsx(AnimatePresence, { children: show && /* @__PURE__ */ jsx(
5827
+ motion.div,
5828
+ {
5829
+ variants: errorMessageVariants,
5830
+ initial: "initial",
5831
+ animate: "animate",
5832
+ exit: "exit",
5833
+ className: "border-t border border-blue-200 bg-blue-50 dark:bg-blue-900/20 px-4 py-3",
5834
+ role: "region",
5835
+ "aria-label": t("BiChat.Archive.Banner"),
5836
+ children: /* @__PURE__ */ jsxs("div", { className: "w-full flex items-start justify-between px-4", children: [
5837
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 flex-1", children: [
5838
+ /* @__PURE__ */ jsx(Archive, { size: 20, className: "w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" }),
5839
+ /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-blue-700 dark:text-blue-400", children: t("BiChat.Archive.Archived") }) })
5840
+ ] }),
5841
+ /* @__PURE__ */ jsx(
5842
+ "button",
5843
+ {
5844
+ onClick: handleRestore,
5845
+ disabled: restoring,
5846
+ className: "ml-2 flex-shrink-0 px-3 py-1.5 text-xs font-medium bg-blue-600 dark:bg-blue-700 hover:bg-blue-700 dark:hover:bg-blue-800 text-white rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5",
5847
+ "aria-label": t("BiChat.Archive.Restore"),
5848
+ children: restoring ? /* @__PURE__ */ jsxs(Fragment, { children: [
5849
+ /* @__PURE__ */ jsx(Spinner, { size: 16, className: "w-4 h-4 animate-spin" }),
5850
+ t("BiChat.Archive.Restoring")
5851
+ ] }) : t("BiChat.Archive.Restore")
5852
+ }
5853
+ )
5854
+ ] })
5855
+ }
5856
+ ) }),
5857
+ error && /* @__PURE__ */ jsx(
5858
+ Alert_default,
5859
+ {
5860
+ variant: "error",
5861
+ message: error,
5862
+ title: t("BiChat.Archive.RestoreFailed"),
5863
+ onDismiss: () => setError(null),
5864
+ dismissible: true
5865
+ }
5866
+ )
5867
+ ] });
4904
5868
  }
4905
- var WelcomeContent_default = WelcomeContent;
5869
+ var ArchiveBanner_default = memo(ArchiveBanner);
4906
5870
 
4907
5871
  // ui/src/bichat/components/ChatSession.tsx
4908
5872
  init_useTranslation();
@@ -4913,11 +5877,11 @@ init_useTranslation();
4913
5877
  // ui/src/bichat/components/SessionArtifactList.tsx
4914
5878
  init_useTranslation();
4915
5879
  var TYPE_LABEL_KEYS = {
4916
- chart: "Artifacts.GroupCharts",
4917
- code_output: "Artifacts.GroupCodeOutputs",
4918
- export: "Artifacts.GroupExports",
4919
- attachment: "Artifacts.GroupAttachments",
4920
- other: "Artifacts.GroupOther"
5880
+ chart: "BiChat.Artifacts.GroupCharts",
5881
+ code_output: "BiChat.Artifacts.GroupCodeOutputs",
5882
+ export: "BiChat.Artifacts.GroupExports",
5883
+ attachment: "BiChat.Artifacts.GroupAttachments",
5884
+ other: "BiChat.Artifacts.GroupOther"
4921
5885
  };
4922
5886
  function getGroupIcon(type) {
4923
5887
  const cls = "h-3.5 w-3.5";
@@ -4993,7 +5957,8 @@ function SessionArtifactList({
4993
5957
  /* @__PURE__ */ jsx("div", { className: "flex h-12 w-12 items-center justify-center rounded-xl bg-gray-100 dark:bg-gray-800", children: /* @__PURE__ */ jsx(Package, { className: "h-6 w-6 text-gray-400 dark:text-gray-500", weight: "duotone" }) }),
4994
5958
  /* @__PURE__ */ jsxs("div", { children: [
4995
5959
  /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-gray-500 dark:text-gray-400", children: t("BiChat.Artifacts.Empty") }),
4996
- /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-xs text-gray-400 dark:text-gray-500", children: t("BiChat.Artifacts.EmptySubtitle") })
5960
+ /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-xs text-gray-400 dark:text-gray-500", children: t("BiChat.Artifacts.EmptySubtitle") }),
5961
+ /* @__PURE__ */ jsx("p", { className: "mt-2 text-xs text-gray-400 dark:text-gray-500", children: t("BiChat.Artifacts.EmptyHint") })
4997
5962
  ] })
4998
5963
  ] });
4999
5964
  }
@@ -5685,6 +6650,29 @@ function SessionArtifactsPanel({
5685
6650
  },
5686
6651
  [canDeleteArtifact, dataSource]
5687
6652
  );
6653
+ const fileInputRef = useRef(null);
6654
+ const handleAttachClick = useCallback(() => {
6655
+ fileInputRef.current?.click();
6656
+ }, []);
6657
+ const handleFileInputChange = useCallback(
6658
+ async (e) => {
6659
+ if (!dataSource.uploadSessionArtifacts || !e.target.files?.length) return;
6660
+ const files = Array.from(e.target.files);
6661
+ try {
6662
+ const result = await dataSource.uploadSessionArtifacts(sessionId, files);
6663
+ if ((result.artifacts || []).length > 0) {
6664
+ setDropSuccessState();
6665
+ void fetchArtifacts({ reset: true, manual: false });
6666
+ }
6667
+ setError(null);
6668
+ } catch (err) {
6669
+ setError(err instanceof Error ? err.message : tRef.current("BiChat.Artifacts.FailedToLoad"));
6670
+ } finally {
6671
+ e.target.value = "";
6672
+ }
6673
+ },
6674
+ [dataSource, fetchArtifacts, sessionId, setDropSuccessState]
6675
+ );
5688
6676
  return /* @__PURE__ */ jsxs(
5689
6677
  "aside",
5690
6678
  {
@@ -5712,12 +6700,38 @@ function SessionArtifactsPanel({
5712
6700
  ]
5713
6701
  }
5714
6702
  ) }),
5715
- /* @__PURE__ */ jsx("header", { className: "flex items-center justify-between border-b border-gray-200 px-3 py-2 dark:border-gray-700/80", children: /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsxs("h2", { className: "truncate text-sm font-semibold text-gray-900 dark:text-gray-100", children: [
5716
- t("BiChat.Artifacts.Title"),
5717
- " (",
5718
- artifacts.length,
5719
- ")"
5720
- ] }) }) }),
6703
+ /* @__PURE__ */ jsxs("header", { className: "flex items-center justify-between border-b border-gray-200 px-3 py-2 dark:border-gray-700/80", children: [
6704
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsxs("h2", { className: "truncate text-sm font-semibold text-gray-900 dark:text-gray-100", children: [
6705
+ t("BiChat.Artifacts.Title"),
6706
+ " (",
6707
+ artifacts.length,
6708
+ ")"
6709
+ ] }) }),
6710
+ canDropFiles && /* @__PURE__ */ jsxs(Fragment, { children: [
6711
+ /* @__PURE__ */ jsx(
6712
+ "button",
6713
+ {
6714
+ type: "button",
6715
+ onClick: handleAttachClick,
6716
+ className: "ml-2 flex-shrink-0 rounded-md p-1.5 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50 dark:text-gray-500 dark:hover:bg-gray-800 dark:hover:text-gray-300",
6717
+ "aria-label": t("BiChat.Artifacts.AttachFiles"),
6718
+ title: t("BiChat.Artifacts.AttachFiles"),
6719
+ children: /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4", weight: "bold" })
6720
+ }
6721
+ ),
6722
+ /* @__PURE__ */ jsx(
6723
+ "input",
6724
+ {
6725
+ ref: fileInputRef,
6726
+ type: "file",
6727
+ multiple: true,
6728
+ className: "hidden",
6729
+ onChange: handleFileInputChange,
6730
+ "aria-label": t("BiChat.Artifacts.AttachFiles")
6731
+ }
6732
+ )
6733
+ ] })
6734
+ ] }),
5721
6735
  /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-3 py-3", children: fetching ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center text-sm text-gray-500 dark:text-gray-400", children: t("BiChat.Artifacts.Loading") }) : error ? /* @__PURE__ */ jsxs("div", { className: "space-y-3 rounded-lg border border-red-200 bg-red-50 p-3 dark:border-red-900/70 dark:bg-red-950/30", children: [
5722
6736
  /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-red-800 dark:text-red-300", children: t("BiChat.Artifacts.FailedToLoad") }),
5723
6737
  /* @__PURE__ */ jsx("p", { className: "text-xs text-red-700 dark:text-red-400", children: error }),
@@ -5770,6 +6784,77 @@ function SessionArtifactsPanel({
5770
6784
  }
5771
6785
  );
5772
6786
  }
6787
+
6788
+ // ui/src/bichat/components/StreamError.tsx
6789
+ init_useTranslation();
6790
+ function StreamError({
6791
+ error,
6792
+ onRetry,
6793
+ onRegenerate,
6794
+ onDismiss,
6795
+ compact = false
6796
+ }) {
6797
+ const { t } = useTranslation();
6798
+ return /* @__PURE__ */ jsxs(
6799
+ motion.div,
6800
+ {
6801
+ initial: { opacity: 0, y: 10 },
6802
+ animate: { opacity: 1, y: 0 },
6803
+ exit: { opacity: 0, y: -10 },
6804
+ className: `flex items-start gap-3 ${compact ? "px-3 py-2.5" : "px-4 py-3"} bg-red-50 dark:bg-red-950/40 border border-red-200/80 dark:border-red-900/60 rounded-xl shadow-sm`,
6805
+ role: "alert",
6806
+ children: [
6807
+ /* @__PURE__ */ jsx("div", { className: "flex-shrink-0 mt-0.5 flex items-center justify-center w-7 h-7 rounded-full bg-red-100 dark:bg-red-900/40", children: /* @__PURE__ */ jsx(
6808
+ Warning,
6809
+ {
6810
+ className: "w-4 h-4 text-red-600 dark:text-red-400",
6811
+ weight: "fill"
6812
+ }
6813
+ ) }),
6814
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
6815
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-red-800 dark:text-red-200 leading-snug", children: t("BiChat.Error.Generic") }),
6816
+ /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-xs text-red-600/80 dark:text-red-400/70 break-words leading-relaxed", children: error }),
6817
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mt-2", children: [
6818
+ onRetry && /* @__PURE__ */ jsxs(
6819
+ "button",
6820
+ {
6821
+ onClick: onRetry,
6822
+ className: "cursor-pointer inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-red-600 hover:bg-red-700 active:bg-red-800 dark:bg-red-700 dark:hover:bg-red-600 rounded-lg transition-colors shadow-sm",
6823
+ type: "button",
6824
+ children: [
6825
+ /* @__PURE__ */ jsx(ArrowClockwise, { className: "w-3.5 h-3.5" }),
6826
+ t("BiChat.StreamError.Retry")
6827
+ ]
6828
+ }
6829
+ ),
6830
+ onRegenerate && /* @__PURE__ */ jsxs(
6831
+ "button",
6832
+ {
6833
+ onClick: onRegenerate,
6834
+ className: "cursor-pointer inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg transition-colors shadow-sm",
6835
+ type: "button",
6836
+ children: [
6837
+ /* @__PURE__ */ jsx(ArrowsCounterClockwise, { className: "w-3.5 h-3.5" }),
6838
+ t("BiChat.StreamError.Regenerate")
6839
+ ]
6840
+ }
6841
+ )
6842
+ ] })
6843
+ ] }),
6844
+ onDismiss && /* @__PURE__ */ jsx(
6845
+ "button",
6846
+ {
6847
+ onClick: onDismiss,
6848
+ className: "cursor-pointer flex-shrink-0 mt-0.5 inline-flex items-center justify-center w-6 h-6 text-red-400 dark:text-red-500 hover:text-red-600 dark:hover:text-red-300 hover:bg-red-100 dark:hover:bg-red-900/40 rounded-md transition-colors",
6849
+ type: "button",
6850
+ "aria-label": t("BiChat.Chat.DismissNotification"),
6851
+ children: /* @__PURE__ */ jsx(X, { className: "w-3.5 h-3.5" })
6852
+ }
6853
+ )
6854
+ ]
6855
+ }
6856
+ );
6857
+ }
5773
6858
  var ARTIFACTS_PANEL_WIDTH_DEFAULT = 352;
5774
6859
  var ARTIFACTS_PANEL_WIDTH_MIN = 280;
5775
6860
  var ARTIFACTS_PANEL_WIDTH_MAX = 560;
@@ -5786,13 +6871,34 @@ function ChatSessionCore({
5786
6871
  actionsSlot,
5787
6872
  onBack,
5788
6873
  thinkingVerbs,
6874
+ onSessionRestored,
5789
6875
  showArtifactsPanel = false,
5790
6876
  artifactsPanelDefaultExpanded = false,
5791
6877
  artifactsPanelStorageKey = "bichat.artifacts-panel.expanded"
5792
6878
  }) {
5793
6879
  const { t } = useTranslation();
5794
- const { session, fetching, error, debugMode, sessionDebugUsage, debugLimits, currentSessionId } = useChatSession();
5795
- const { turns, loading, isStreaming } = useChatMessaging();
6880
+ const {
6881
+ session,
6882
+ fetching,
6883
+ error,
6884
+ errorRetryable,
6885
+ debugMode,
6886
+ sessionDebugUsage,
6887
+ debugLimits,
6888
+ currentSessionId,
6889
+ setError,
6890
+ retryFetchSession
6891
+ } = useChatSession();
6892
+ const {
6893
+ turns,
6894
+ loading,
6895
+ isStreaming,
6896
+ streamError,
6897
+ streamErrorRetryable,
6898
+ isCompacting,
6899
+ retryLastMessage,
6900
+ clearStreamError
6901
+ } = useChatMessaging();
5796
6902
  const {
5797
6903
  inputError,
5798
6904
  message,
@@ -5800,9 +6906,23 @@ function ChatSessionCore({
5800
6906
  setInputError,
5801
6907
  handleSubmit,
5802
6908
  messageQueue,
5803
- handleUnqueue
6909
+ handleUnqueue,
6910
+ removeQueueItem,
6911
+ updateQueueItem
5804
6912
  } = useChatInput();
5805
- const effectiveReadOnly = Boolean(readOnly ?? isReadOnly);
6913
+ const isArchived = session?.status === "archived";
6914
+ const effectiveReadOnly = Boolean(readOnly ?? isReadOnly) || isArchived;
6915
+ const [restoring, setRestoring] = useState(false);
6916
+ const handleRestore = useCallback(async () => {
6917
+ if (!session?.id) return;
6918
+ setRestoring(true);
6919
+ try {
6920
+ await dataSource.unarchiveSession(session.id);
6921
+ onSessionRestored?.(session.id);
6922
+ } finally {
6923
+ setRestoring(false);
6924
+ }
6925
+ }, [dataSource, session?.id, onSessionRestored]);
5806
6926
  const [artifactsPanelExpanded, setArtifactsPanelExpanded] = useState(
5807
6927
  artifactsPanelDefaultExpanded
5808
6928
  );
@@ -5874,16 +6994,9 @@ function ChatSessionCore({
5874
6994
  document.body.style.userSelect = "";
5875
6995
  };
5876
6996
  }, [isResizingArtifactsPanel, artifactsPanelStorageKey]);
5877
- if (fetching) {
6997
+ if (fetching && turns.length === 0 && !session) {
5878
6998
  return /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ jsx("div", { className: "text-gray-500 dark:text-gray-400", children: t("BiChat.Input.Processing") }) });
5879
6999
  }
5880
- if (error) {
5881
- return /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ jsxs("div", { className: "text-red-500 dark:text-red-400", children: [
5882
- t("BiChat.Error.Generic"),
5883
- ": ",
5884
- error
5885
- ] }) });
5886
- }
5887
7000
  const showWelcome = !session && turns.length === 0;
5888
7001
  const activeSessionId = session?.id || (currentSessionId && currentSessionId !== "new" ? currentSessionId : void 0);
5889
7002
  const supportsArtifactsPanel = typeof dataSource.fetchSessionArtifacts === "function";
@@ -5940,6 +7053,16 @@ function ChatSessionCore({
5940
7053
  actionsSlot: headerActions
5941
7054
  }
5942
7055
  ),
7056
+ error && /* @__PURE__ */ jsx(
7057
+ Alert_default,
7058
+ {
7059
+ variant: errorRetryable ? "warning" : "error",
7060
+ title: t("BiChat.Error.Generic"),
7061
+ message: error,
7062
+ onDismiss: () => setError(null),
7063
+ onRetry: errorRetryable ? retryFetchSession : void 0
7064
+ }
7065
+ ),
5943
7066
  /* @__PURE__ */ jsxs(
5944
7067
  "div",
5945
7068
  {
@@ -5948,6 +7071,15 @@ function ChatSessionCore({
5948
7071
  children: [
5949
7072
  /* @__PURE__ */ jsx("div", { className: "flex min-h-0 flex-1 flex-col", children: showWelcome ? /* @__PURE__ */ jsx("div", { className: "flex flex-1 flex-col overflow-auto", children: /* @__PURE__ */ jsx("div", { className: "flex flex-1 items-center justify-center px-4 py-8", children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-5xl", children: [
5950
7073
  welcomeSlot || /* @__PURE__ */ jsx(WelcomeContent_default, { onPromptSelect: handlePromptSelect, disabled: loading }),
7074
+ streamError && /* @__PURE__ */ jsx("div", { className: "px-6 pt-4", children: /* @__PURE__ */ jsx(
7075
+ StreamError,
7076
+ {
7077
+ error: streamError,
7078
+ compact: true,
7079
+ onRetry: streamErrorRetryable ? () => void retryLastMessage() : void 0,
7080
+ onDismiss: clearStreamError
7081
+ }
7082
+ ) }),
5951
7083
  !effectiveReadOnly && /* @__PURE__ */ jsx(
5952
7084
  MessageInput,
5953
7085
  {
@@ -5963,12 +7095,22 @@ function ChatSessionCore({
5963
7095
  onSubmit: handleSubmit,
5964
7096
  messageQueue,
5965
7097
  onUnqueue: handleUnqueue,
7098
+ onRemoveQueueItem: removeQueueItem,
7099
+ onUpdateQueueItem: updateQueueItem,
5966
7100
  containerClassName: "pt-6 px-6",
5967
7101
  formClassName: "mx-auto"
5968
7102
  }
5969
7103
  ),
5970
7104
  /* @__PURE__ */ jsx("p", { className: "mt-4 pb-1 text-center text-xs text-gray-500 dark:text-gray-400", children: t("BiChat.Welcome.Disclaimer") })
5971
7105
  ] }) }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
7106
+ isArchived && /* @__PURE__ */ jsx(
7107
+ ArchiveBanner_default,
7108
+ {
7109
+ show: true,
7110
+ onRestore: handleRestore,
7111
+ restoring
7112
+ }
7113
+ ),
5972
7114
  /* @__PURE__ */ jsx(
5973
7115
  MessageList,
5974
7116
  {
@@ -5978,6 +7120,22 @@ function ChatSessionCore({
5978
7120
  readOnly: effectiveReadOnly
5979
7121
  }
5980
7122
  ),
7123
+ /* @__PURE__ */ jsx(AnimatePresence, { children: isCompacting && /* @__PURE__ */ jsx("div", { className: "flex justify-center px-4 pb-2", children: /* @__PURE__ */ jsx(
7124
+ CompactionDoodle_default,
7125
+ {
7126
+ title: t("BiChat.Slash.CompactingTitle"),
7127
+ subtitle: t("BiChat.Slash.CompactingSubtitle")
7128
+ }
7129
+ ) }) }),
7130
+ streamError && /* @__PURE__ */ jsx("div", { className: "px-4 pb-2", children: /* @__PURE__ */ jsx(
7131
+ StreamError,
7132
+ {
7133
+ error: streamError,
7134
+ compact: true,
7135
+ onRetry: streamErrorRetryable ? () => void retryLastMessage() : void 0,
7136
+ onDismiss: clearStreamError
7137
+ }
7138
+ ) }),
5981
7139
  !effectiveReadOnly && /* @__PURE__ */ jsx(
5982
7140
  MessageInput,
5983
7141
  {
@@ -5992,7 +7150,9 @@ function ChatSessionCore({
5992
7150
  onMessageChange: setMessage,
5993
7151
  onSubmit: handleSubmit,
5994
7152
  messageQueue,
5995
- onUnqueue: handleUnqueue
7153
+ onUnqueue: handleUnqueue,
7154
+ onRemoveQueueItem: removeQueueItem,
7155
+ onUpdateQueueItem: updateQueueItem
5996
7156
  }
5997
7157
  )
5998
7158
  ] }) }),
@@ -6083,8 +7243,17 @@ function ChatSessionCore({
6083
7243
  );
6084
7244
  }
6085
7245
  function ChatSession(props) {
6086
- const { dataSource, sessionId, rateLimiter, ...coreProps } = props;
6087
- return /* @__PURE__ */ jsx(ChatSessionProvider, { dataSource, sessionId, rateLimiter, children: /* @__PURE__ */ jsx(ChatSessionCore, { dataSource, ...coreProps }) });
7246
+ const { dataSource, sessionId, rateLimiter, onSessionCreated, ...coreProps } = props;
7247
+ return /* @__PURE__ */ jsx(
7248
+ ChatSessionProvider,
7249
+ {
7250
+ dataSource,
7251
+ sessionId,
7252
+ rateLimiter,
7253
+ onSessionCreated,
7254
+ children: /* @__PURE__ */ jsx(ChatSessionCore, { dataSource, ...coreProps })
7255
+ }
7256
+ );
6088
7257
  }
6089
7258
 
6090
7259
  // ui/src/bichat/index.ts
@@ -6186,20 +7355,19 @@ var EditableText = forwardRef(
6186
7355
  onSave,
6187
7356
  maxLength = 100,
6188
7357
  isLoading = false,
6189
- placeholder = "Untitled",
7358
+ placeholder,
6190
7359
  className = "",
6191
7360
  inputClassName = "",
6192
7361
  size = "sm"
6193
7362
  }, ref) => {
6194
7363
  const { t } = useTranslation();
7364
+ const resolvedPlaceholder = placeholder ?? t("BiChat.Common.Untitled");
6195
7365
  const [isEditing, setIsEditing] = useState(false);
6196
7366
  const [editValue, setEditValue] = useState(value);
6197
7367
  const inputRef = useRef(null);
6198
7368
  useImperativeHandle(ref, () => ({
6199
7369
  startEditing: () => {
6200
- if (!isLoading) {
6201
- setIsEditing(true);
6202
- }
7370
+ setIsEditing(true);
6203
7371
  },
6204
7372
  cancelEditing: () => {
6205
7373
  setEditValue(value);
@@ -6241,9 +7409,7 @@ var EditableText = forwardRef(
6241
7409
  }
6242
7410
  };
6243
7411
  const handleDoubleClick = () => {
6244
- if (!isLoading) {
6245
- setIsEditing(true);
6246
- }
7412
+ setIsEditing(true);
6247
7413
  };
6248
7414
  const handleBlur = () => {
6249
7415
  handleSave();
@@ -6265,7 +7431,7 @@ var EditableText = forwardRef(
6265
7431
  onKeyDown: handleKeyDown,
6266
7432
  onBlur: handleBlur,
6267
7433
  maxLength,
6268
- placeholder,
7434
+ placeholder: resolvedPlaceholder,
6269
7435
  className: `flex-1 px-2 py-1 ${sizeClass} bg-white dark:bg-gray-700 border border-primary-500 dark:border-primary-600 rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50 dark:focus-visible:ring-primary-600/30 text-gray-900 dark:text-white ${inputClassName}`,
6270
7436
  "aria-label": t("BiChat.EditableText.AriaLabel")
6271
7437
  }
@@ -6273,7 +7439,7 @@ var EditableText = forwardRef(
6273
7439
  }
6274
7440
  );
6275
7441
  }
6276
- const displayValue = value || placeholder;
7442
+ const displayValue = value || resolvedPlaceholder;
6277
7443
  return /* @__PURE__ */ jsx(
6278
7444
  "span",
6279
7445
  {
@@ -6321,16 +7487,18 @@ var sizeClasses3 = {
6321
7487
  function SearchInput({
6322
7488
  value,
6323
7489
  onChange,
6324
- placeholder = "Search...",
7490
+ placeholder,
6325
7491
  autoFocus = false,
6326
7492
  onSubmit,
6327
7493
  onEscape,
6328
7494
  className = "",
6329
7495
  size = "md",
6330
7496
  disabled = false,
6331
- ariaLabel = "Search"
7497
+ ariaLabel
6332
7498
  }) {
6333
7499
  const { t } = useTranslation();
7500
+ const resolvedPlaceholder = placeholder ?? t("BiChat.Common.Search");
7501
+ const resolvedAriaLabel = ariaLabel ?? t("BiChat.Common.Search");
6334
7502
  const inputRef = useRef(null);
6335
7503
  const sizes = sizeClasses3[size];
6336
7504
  useEffect(() => {
@@ -6373,10 +7541,10 @@ function SearchInput({
6373
7541
  value,
6374
7542
  onChange: (e) => onChange(e.target.value),
6375
7543
  onKeyDown: handleKeyDown,
6376
- placeholder,
7544
+ placeholder: resolvedPlaceholder,
6377
7545
  disabled,
6378
7546
  className: `w-full ${sizes.container} bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-gray-700/50 rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50 focus-visible:border-primary-400 dark:focus-visible:border-primary-600 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed`,
6379
- "aria-label": ariaLabel
7547
+ "aria-label": resolvedAriaLabel
6380
7548
  }
6381
7549
  ),
6382
7550
  value && !disabled && /* @__PURE__ */ jsx(
@@ -6576,26 +7744,37 @@ var LoadingSpinner_default = MemoizedLoadingSpinner;
6576
7744
  // ui/src/bichat/index.ts
6577
7745
  init_TableExportButton();
6578
7746
  init_TableWithExport();
7747
+
7748
+ // ui/src/bichat/components/Toast.tsx
7749
+ init_useTranslation();
6579
7750
  var typeConfig = {
6580
7751
  success: {
6581
- bgColor: "bg-green-700",
6582
- darkBgColor: "dark:bg-green-800",
6583
- icon: /* @__PURE__ */ jsx(CheckCircle, { size: 20, className: "w-5 h-5 flex-shrink-0", weight: "fill" })
7752
+ accent: "text-emerald-600 dark:text-emerald-400",
7753
+ bg: "bg-emerald-50 dark:bg-emerald-950/40 border-emerald-200/80 dark:border-emerald-800/50",
7754
+ icon: "bg-emerald-100 dark:bg-emerald-900/50",
7755
+ progress: "bg-emerald-500 dark:bg-emerald-400",
7756
+ iconEl: CheckCircle
6584
7757
  },
6585
7758
  error: {
6586
- bgColor: "bg-red-700",
6587
- darkBgColor: "dark:bg-red-800",
6588
- icon: /* @__PURE__ */ jsx(XCircle, { size: 20, className: "w-5 h-5 flex-shrink-0", weight: "fill" })
7759
+ accent: "text-red-600 dark:text-red-400",
7760
+ bg: "bg-red-50 dark:bg-red-950/40 border-red-200/80 dark:border-red-800/50",
7761
+ icon: "bg-red-100 dark:bg-red-900/50",
7762
+ progress: "bg-red-500 dark:bg-red-400",
7763
+ iconEl: XCircle
6589
7764
  },
6590
7765
  info: {
6591
- bgColor: "bg-blue-700",
6592
- darkBgColor: "dark:bg-blue-800",
6593
- icon: /* @__PURE__ */ jsx(Info, { size: 20, className: "w-5 h-5 flex-shrink-0", weight: "fill" })
7766
+ accent: "text-blue-600 dark:text-blue-400",
7767
+ bg: "bg-blue-50 dark:bg-blue-950/40 border-blue-200/80 dark:border-blue-800/50",
7768
+ icon: "bg-blue-100 dark:bg-blue-900/50",
7769
+ progress: "bg-blue-500 dark:bg-blue-400",
7770
+ iconEl: Info
6594
7771
  },
6595
7772
  warning: {
6596
- bgColor: "bg-amber-700",
6597
- darkBgColor: "dark:bg-amber-800",
6598
- icon: /* @__PURE__ */ jsx(Warning, { size: 20, className: "w-5 h-5 flex-shrink-0", weight: "fill" })
7773
+ accent: "text-amber-600 dark:text-amber-400",
7774
+ bg: "bg-amber-50 dark:bg-amber-950/40 border-amber-200/80 dark:border-amber-800/50",
7775
+ icon: "bg-amber-100 dark:bg-amber-900/50",
7776
+ progress: "bg-amber-500 dark:bg-amber-400",
7777
+ iconEl: Warning
6599
7778
  }
6600
7779
  };
6601
7780
  function Toast({
@@ -6604,55 +7783,117 @@ function Toast({
6604
7783
  message,
6605
7784
  duration = 5e3,
6606
7785
  onDismiss,
6607
- dismissLabel = "Dismiss notification"
7786
+ dismissLabel
6608
7787
  }) {
7788
+ const { t } = useTranslation();
7789
+ const resolvedDismissLabel = dismissLabel ?? t("BiChat.Chat.DismissNotification");
6609
7790
  const config = typeConfig[type];
7791
+ const Icon = config.iconEl;
7792
+ const [show, setShow] = useState(false);
7793
+ const [paused, setPaused] = useState(false);
7794
+ const remainingRef = useRef(duration);
7795
+ const startRef = useRef(Date.now());
7796
+ useEffect(() => {
7797
+ const frame = requestAnimationFrame(() => setShow(true));
7798
+ return () => cancelAnimationFrame(frame);
7799
+ }, []);
7800
+ useEffect(() => {
7801
+ if (paused) return;
7802
+ startRef.current = Date.now();
7803
+ const timer = setTimeout(() => {
7804
+ setShow(false);
7805
+ setTimeout(() => onDismiss(id), 200);
7806
+ }, remainingRef.current);
7807
+ return () => {
7808
+ const elapsed = Date.now() - startRef.current;
7809
+ remainingRef.current = Math.max(0, remainingRef.current - elapsed);
7810
+ clearTimeout(timer);
7811
+ };
7812
+ }, [id, paused, onDismiss]);
7813
+ const handleDismiss = useCallback(() => {
7814
+ setShow(false);
7815
+ setTimeout(() => onDismiss(id), 200);
7816
+ }, [id, onDismiss]);
6610
7817
  const ariaLive = type === "error" ? "assertive" : "polite";
6611
7818
  const role = type === "error" || type === "warning" ? "alert" : "status";
6612
- useEffect(() => {
6613
- const timer = setTimeout(() => onDismiss(id), duration);
6614
- return () => clearTimeout(timer);
6615
- }, [id, duration, onDismiss]);
6616
- return /* @__PURE__ */ jsxs(
6617
- motion.div,
7819
+ return /* @__PURE__ */ jsx(
7820
+ Transition,
6618
7821
  {
6619
- initial: { opacity: 0, y: -50, scale: 0.95 },
6620
- animate: { opacity: 1, y: 0, scale: 1 },
6621
- exit: { opacity: 0, scale: 0.95 },
6622
- transition: { duration: 0.2 },
6623
- className: `flex items-center gap-3 px-4 py-3 rounded-lg shadow-lg backdrop-blur-sm min-w-[300px] max-w-[400px] text-white ${config.bgColor} ${config.darkBgColor}`,
6624
- role,
6625
- "aria-live": ariaLive,
6626
- "aria-atomic": "true",
6627
- children: [
6628
- config.icon,
6629
- /* @__PURE__ */ jsx("p", { className: "flex-1 text-sm font-medium", children: message }),
6630
- /* @__PURE__ */ jsx(
6631
- "button",
6632
- {
6633
- onClick: () => onDismiss(id),
6634
- className: "cursor-pointer ml-2 text-white hover:bg-white/20 p-1 rounded transition-colors flex-shrink-0",
6635
- "aria-label": dismissLabel,
6636
- children: /* @__PURE__ */ jsx(X, { size: 16, className: "w-4 h-4", weight: "bold" })
6637
- }
6638
- )
6639
- ]
7822
+ show,
7823
+ enter: "transition duration-200 ease-out",
7824
+ enterFrom: "-translate-y-2 opacity-0 scale-95",
7825
+ enterTo: "translate-y-0 opacity-100 scale-100",
7826
+ leave: "transition duration-150 ease-in",
7827
+ leaveFrom: "translate-y-0 opacity-100 scale-100",
7828
+ leaveTo: "-translate-y-2 opacity-0 scale-95",
7829
+ children: /* @__PURE__ */ jsxs(
7830
+ "div",
7831
+ {
7832
+ className: `relative flex items-start gap-3 rounded-xl border px-4 py-3 shadow-lg shadow-black/5 dark:shadow-black/20 backdrop-blur-sm min-w-[320px] max-w-[420px] overflow-hidden ${config.bg}`,
7833
+ role,
7834
+ "aria-live": ariaLive,
7835
+ "aria-atomic": "true",
7836
+ onMouseEnter: () => setPaused(true),
7837
+ onMouseLeave: () => setPaused(false),
7838
+ children: [
7839
+ /* @__PURE__ */ jsx("div", { className: `mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-lg ${config.icon}`, children: /* @__PURE__ */ jsx(Icon, { size: 16, className: config.accent, weight: "fill" }) }),
7840
+ /* @__PURE__ */ jsx("p", { className: "flex-1 pt-0.5 text-sm font-medium leading-snug text-gray-800 dark:text-gray-100", children: message }),
7841
+ /* @__PURE__ */ jsx(
7842
+ "button",
7843
+ {
7844
+ onClick: handleDismiss,
7845
+ className: "mt-0.5 -mr-1 cursor-pointer shrink-0 rounded-lg p-1 text-gray-400 hover:text-gray-600 hover:bg-gray-200/60 dark:text-gray-500 dark:hover:text-gray-300 dark:hover:bg-gray-700/60 transition-colors duration-100",
7846
+ "aria-label": resolvedDismissLabel,
7847
+ children: /* @__PURE__ */ jsx(X, { size: 14, weight: "bold" })
7848
+ }
7849
+ ),
7850
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-x-0 bottom-0 h-0.5 bg-black/5 dark:bg-white/5", children: /* @__PURE__ */ jsx(
7851
+ "div",
7852
+ {
7853
+ className: `h-full ${config.progress} origin-left`,
7854
+ style: {
7855
+ animation: `bichat-toast-progress ${duration}ms linear forwards`,
7856
+ animationPlayState: paused ? "paused" : "running"
7857
+ }
7858
+ }
7859
+ ) })
7860
+ ]
7861
+ }
7862
+ )
6640
7863
  }
6641
7864
  );
6642
7865
  }
7866
+
7867
+ // ui/src/bichat/components/ToastContainer.tsx
7868
+ init_useTranslation();
6643
7869
  function ToastContainer({ toasts, onDismiss, dismissLabel }) {
6644
- return /* @__PURE__ */ jsx("div", { className: "fixed top-4 right-4 sm:top-6 sm:right-6 z-50 flex flex-col gap-2 pointer-events-none", children: /* @__PURE__ */ jsx(AnimatePresence, { children: toasts.map((toast) => /* @__PURE__ */ jsx("div", { className: "pointer-events-auto", children: /* @__PURE__ */ jsx(Toast, { ...toast, onDismiss, dismissLabel }) }, toast.id)) }) });
7870
+ const { t } = useTranslation();
7871
+ if (toasts.length === 0) return null;
7872
+ return /* @__PURE__ */ jsx(
7873
+ "div",
7874
+ {
7875
+ "aria-label": t("BiChat.Common.Notifications"),
7876
+ className: "fixed top-6 right-6 z-[var(--bichat-z-toast,60)] flex flex-col gap-2 pointer-events-none",
7877
+ children: toasts.map((toast) => /* @__PURE__ */ jsx("div", { className: "pointer-events-auto", children: /* @__PURE__ */ jsx(Toast, { ...toast, onDismiss, dismissLabel }) }, toast.id))
7878
+ }
7879
+ );
6645
7880
  }
7881
+
7882
+ // ui/src/bichat/components/ConfirmModal.tsx
7883
+ init_useTranslation();
6646
7884
  function ConfirmModalBase({
6647
7885
  isOpen,
6648
7886
  title,
6649
7887
  message,
6650
7888
  onConfirm,
6651
7889
  onCancel,
6652
- confirmText = "Confirm",
6653
- cancelText = "Cancel",
7890
+ confirmText,
7891
+ cancelText,
6654
7892
  isDanger = false
6655
7893
  }) {
7894
+ const { t } = useTranslation();
7895
+ const resolvedConfirmText = confirmText ?? t("BiChat.Common.Confirm");
7896
+ const resolvedCancelText = cancelText ?? t("BiChat.Common.Cancel");
6656
7897
  return /* @__PURE__ */ jsxs(Dialog, { open: isOpen, onClose: onCancel, className: "relative z-40", children: [
6657
7898
  /* @__PURE__ */ jsx(DialogBackdrop, { className: "fixed inset-0 bg-black/40 dark:bg-black/60 backdrop-blur-sm transition-opacity duration-200" }),
6658
7899
  /* @__PURE__ */ jsx("div", { className: "fixed inset-0 flex items-center justify-center z-50 p-4", children: /* @__PURE__ */ jsxs(DialogPanel, { className: "bg-white dark:bg-gray-800 rounded-2xl shadow-xl dark:shadow-2xl dark:shadow-black/30 max-w-sm w-full overflow-hidden", children: [
@@ -6669,9 +7910,9 @@ function ConfirmModalBase({
6669
7910
  {
6670
7911
  onClick: onCancel,
6671
7912
  className: "cursor-pointer px-4 py-2 text-sm font-medium rounded-xl text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700/60 hover:bg-gray-200 dark:hover:bg-gray-700 active:bg-gray-250 dark:active:bg-gray-600 transition-colors duration-150 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",
6672
- "aria-label": `Cancel ${title.toLowerCase()}`,
7913
+ "aria-label": resolvedCancelText,
6673
7914
  "data-testid": "confirm-modal-cancel",
6674
- children: cancelText
7915
+ children: resolvedCancelText
6675
7916
  }
6676
7917
  ),
6677
7918
  /* @__PURE__ */ jsx(
@@ -6684,9 +7925,9 @@ function ConfirmModalBase({
6684
7925
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-gray-800",
6685
7926
  isDanger ? "bg-red-600 hover:bg-red-700 active:bg-red-800 focus-visible:ring-red-500/50" : "bg-primary-600 hover:bg-primary-700 active:bg-primary-800 focus-visible:ring-primary-500/50"
6686
7927
  ].join(" "),
6687
- "aria-label": `Confirm ${title.toLowerCase()}`,
7928
+ "aria-label": resolvedConfirmText,
6688
7929
  "data-testid": "confirm-modal-confirm",
6689
- children: confirmText
7930
+ children: resolvedConfirmText
6690
7931
  }
6691
7932
  )
6692
7933
  ] })
@@ -6776,28 +8017,44 @@ function PermissionGuard({
6776
8017
  const permitted = mode === "all" ? permissions.every((p) => hasPermission2(p)) : permissions.some((p) => hasPermission2(p));
6777
8018
  return permitted ? /* @__PURE__ */ jsx(Fragment, { children }) : /* @__PURE__ */ jsx(Fragment, { children: fallback });
6778
8019
  }
8020
+
8021
+ // ui/src/bichat/components/ErrorBoundary.tsx
8022
+ init_useTranslation();
6779
8023
  function DefaultErrorContent({
6780
8024
  error,
6781
8025
  onReset,
6782
- resetLabel = "Try Again",
6783
- errorTitle = "Something went wrong"
8026
+ resetLabel,
8027
+ errorTitle
6784
8028
  }) {
8029
+ const { t } = useTranslation();
8030
+ const resolvedResetLabel = resetLabel ?? t("BiChat.Common.TryAgain");
8031
+ const resolvedErrorTitle = errorTitle ?? t("BiChat.Error.SomethingWentWrong");
6785
8032
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center p-8 text-center min-h-[200px]", children: [
6786
- /* @__PURE__ */ jsx(WarningCircle, { size: 48, className: "text-red-500 mb-4", weight: "fill" }),
6787
- /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold text-gray-900 dark:text-white mb-2", children: errorTitle }),
6788
- /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-600 dark:text-gray-400 mb-4 max-w-md", children: error?.message || "An unexpected error occurred. Please try again." }),
6789
- onReset && /* @__PURE__ */ jsxs(
6790
- "button",
6791
- {
6792
- type: "button",
6793
- onClick: onReset,
6794
- className: "flex items-center gap-2 px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors",
6795
- children: [
6796
- /* @__PURE__ */ jsx(ArrowClockwise, { size: 16, weight: "bold" }),
6797
- resetLabel
6798
- ]
6799
- }
6800
- )
8033
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-0 overflow-hidden pointer-events-none opacity-[0.03] dark:opacity-[0.04]", "aria-hidden": "true", children: /* @__PURE__ */ jsxs("svg", { className: "absolute -top-8 -right-8 w-64 h-64 text-red-500", viewBox: "0 0 200 200", fill: "currentColor", children: [
8034
+ /* @__PURE__ */ jsx("circle", { cx: "100", cy: "100", r: "80", opacity: "0.5" }),
8035
+ /* @__PURE__ */ jsx("circle", { cx: "100", cy: "100", r: "50", opacity: "0.3" }),
8036
+ /* @__PURE__ */ jsx("circle", { cx: "100", cy: "100", r: "25", opacity: "0.2" })
8037
+ ] }) }),
8038
+ /* @__PURE__ */ jsxs("div", { className: "relative flex flex-col items-center", children: [
8039
+ /* @__PURE__ */ jsxs("div", { className: "relative mb-5", children: [
8040
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-0 rounded-full bg-red-100 dark:bg-red-900/30 scale-150 blur-md" }),
8041
+ /* @__PURE__ */ jsx("div", { className: "relative flex items-center justify-center w-14 h-14 rounded-full bg-red-50 dark:bg-red-900/20 border border-red-200/60 dark:border-red-800/40", children: /* @__PURE__ */ jsx(WarningCircle, { size: 28, className: "text-red-500 dark:text-red-400", weight: "fill" }) })
8042
+ ] }),
8043
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold text-gray-900 dark:text-white mb-1.5", children: resolvedErrorTitle }),
8044
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400 mb-5 max-w-md leading-relaxed", children: error?.message || t("BiChat.Error.UnexpectedError") }),
8045
+ onReset && /* @__PURE__ */ jsxs(
8046
+ "button",
8047
+ {
8048
+ type: "button",
8049
+ onClick: onReset,
8050
+ 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",
8051
+ children: [
8052
+ /* @__PURE__ */ jsx(ArrowClockwise, { size: 16, weight: "bold" }),
8053
+ resolvedResetLabel
8054
+ ]
8055
+ }
8056
+ )
8057
+ ] })
6801
8058
  ] });
6802
8059
  }
6803
8060
  var ErrorBoundary = class extends Component {
@@ -6934,10 +8191,13 @@ var TouchContextMenu = ({
6934
8191
  const [focusedIndex, setFocusedIndex] = useState(-1);
6935
8192
  const menuRef = useRef(null);
6936
8193
  const itemRefs = useRef([]);
6937
- const enabledIndices = items.reduce((acc, item, i) => {
6938
- if (!item.disabled) acc.push(i);
6939
- return acc;
6940
- }, []);
8194
+ const enabledIndices = useMemo(
8195
+ () => items.reduce((acc, item, i) => {
8196
+ if (!item.disabled) acc.push(i);
8197
+ return acc;
8198
+ }, []),
8199
+ [items]
8200
+ );
6941
8201
  const focusItem = useCallback((index) => {
6942
8202
  setFocusedIndex(index);
6943
8203
  itemRefs.current[index]?.focus();
@@ -6953,7 +8213,7 @@ var TouchContextMenu = ({
6953
8213
  }
6954
8214
  });
6955
8215
  return () => cancelAnimationFrame(timer);
6956
- }, [isOpen, enabledIndices.length, focusItem]);
8216
+ }, [isOpen, enabledIndices, focusItem]);
6957
8217
  useEffect(() => {
6958
8218
  if (!isOpen) return;
6959
8219
  const handleKeyDown = (e) => {
@@ -7251,7 +8511,7 @@ var SessionItem = memo(
7251
8511
  MenuItems,
7252
8512
  {
7253
8513
  anchor: "bottom start",
7254
- className: "w-52 bg-white/95 dark:bg-gray-900/95 backdrop-blur rounded-xl shadow-xl border border-gray-200 dark:border-gray-700 z-30 [--anchor-gap:8px] mt-1 p-2 space-y-1",
8514
+ className: "w-52 bg-white dark:bg-gray-900 rounded-xl shadow-xl border border-gray-200 dark:border-gray-700 z-30 [--anchor-gap:8px] mt-1 p-2 space-y-1",
7255
8515
  children: [
7256
8516
  mode !== "archived" && onPin && /* @__PURE__ */ jsx(MenuItem, { children: ({ focus }) => /* @__PURE__ */ jsxs(
7257
8517
  "button",
@@ -7377,108 +8637,18 @@ var SessionItem = memo(
7377
8637
  onClose: () => setMenuOpen(false),
7378
8638
  anchorRect: menuAnchor
7379
8639
  }
7380
- )
7381
- ] });
7382
- }
7383
- );
7384
- SessionItem.displayName = "SessionItem";
7385
- var SessionItem_default = SessionItem;
7386
- function DateGroupHeader({ groupName, count }) {
7387
- return /* @__PURE__ */ jsx("div", { className: "sticky top-0 bg-surface-300 dark:bg-gray-900 px-4 py-2 text-sm font-medium z-10 border-b border-gray-100 dark:border-gray-800", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
7388
- /* @__PURE__ */ jsx("span", { className: "text-gray-700 dark:text-gray-300 font-semibold", children: groupName }),
7389
- /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded-full", children: count })
7390
- ] }) });
7391
- }
7392
- function TabBar({ tabs, activeTab, onTabChange }) {
7393
- const tablistRef = useRef(null);
7394
- const handleKeyDown = useCallback(
7395
- (e) => {
7396
- const currentIndex = tabs.findIndex((tab) => tab.id === activeTab);
7397
- if (currentIndex < 0) return;
7398
- let nextIndex = null;
7399
- switch (e.key) {
7400
- case "ArrowRight":
7401
- e.preventDefault();
7402
- nextIndex = (currentIndex + 1) % tabs.length;
7403
- break;
7404
- case "ArrowLeft":
7405
- e.preventDefault();
7406
- nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
7407
- break;
7408
- case "Home":
7409
- e.preventDefault();
7410
- nextIndex = 0;
7411
- break;
7412
- case "End":
7413
- e.preventDefault();
7414
- nextIndex = tabs.length - 1;
7415
- break;
7416
- }
7417
- if (nextIndex !== null) {
7418
- onTabChange(tabs[nextIndex].id);
7419
- const tablist = tablistRef.current;
7420
- if (tablist) {
7421
- const buttons = tablist.querySelectorAll('[role="tab"]');
7422
- buttons[nextIndex]?.focus();
7423
- }
7424
- }
7425
- },
7426
- [tabs, activeTab, onTabChange]
7427
- );
7428
- if (tabs.length === 0) {
7429
- return null;
8640
+ )
8641
+ ] });
7430
8642
  }
7431
- return /* @__PURE__ */ jsx(
7432
- "div",
7433
- {
7434
- ref: tablistRef,
7435
- className: "flex justify-center gap-1 px-4 pt-4 pb-2 border-b border-gray-200 dark:border-gray-700",
7436
- role: "tablist",
7437
- onKeyDown: handleKeyDown,
7438
- children: tabs.map((tab) => /* @__PURE__ */ jsx(
7439
- TabButton,
7440
- {
7441
- id: tab.id,
7442
- label: tab.label,
7443
- isActive: activeTab === tab.id,
7444
- onClick: () => onTabChange(tab.id)
7445
- },
7446
- tab.id
7447
- ))
7448
- }
7449
- );
7450
- }
7451
- function TabButton({ id, label, isActive, onClick }) {
7452
- return /* @__PURE__ */ jsxs(
7453
- "button",
7454
- {
7455
- id,
7456
- role: "tab",
7457
- "aria-selected": isActive,
7458
- "aria-controls": `${id}-panel`,
7459
- tabIndex: isActive ? 0 : -1,
7460
- onClick,
7461
- className: `
7462
- cursor-pointer relative px-4 py-2 rounded-t-lg text-sm font-medium transition-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50
7463
- ${isActive ? "text-primary-700 dark:text-primary-400" : "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"}
7464
- `,
7465
- children: [
7466
- label,
7467
- isActive && /* @__PURE__ */ jsx(
7468
- motion.div,
7469
- {
7470
- layoutId: "activeTab",
7471
- className: "absolute bottom-0 left-0 right-0 h-0.5 bg-primary-600 dark:bg-primary-500",
7472
- transition: { duration: 0.2, ease: [0.4, 0, 0.2, 1] }
7473
- }
7474
- )
7475
- ]
7476
- }
7477
- );
8643
+ );
8644
+ SessionItem.displayName = "SessionItem";
8645
+ var SessionItem_default = SessionItem;
8646
+ function DateGroupHeader({ groupName, count }) {
8647
+ return /* @__PURE__ */ jsx("div", { className: "sticky top-0 bg-surface-300 dark:bg-gray-900 px-4 py-2 text-sm font-medium z-10 border-b border-gray-100 dark:border-gray-800", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
8648
+ /* @__PURE__ */ jsx("span", { className: "text-gray-700 dark:text-gray-300 font-semibold", children: groupName }),
8649
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded-full", children: count })
8650
+ ] }) });
7478
8651
  }
7479
- var MemoizedTabBar = memo(TabBar);
7480
- MemoizedTabBar.displayName = "TabBar";
7481
- var TabBar_default = MemoizedTabBar;
7482
8652
  init_useTranslation();
7483
8653
  function UserFilter({ users, selectedUser, onUserChange, loading }) {
7484
8654
  const { t } = useTranslation();
@@ -7814,12 +8984,32 @@ init_useTranslation();
7814
8984
  function generateId() {
7815
8985
  return Math.random().toString(36).substring(7);
7816
8986
  }
8987
+ var DEDUPE_WINDOW_MS = 2500;
8988
+ var MAX_ACTIVE_TOASTS = 5;
7817
8989
  function useToast() {
7818
8990
  const [toasts, setToasts] = useState([]);
8991
+ const recentToastMapRef = useRef(/* @__PURE__ */ new Map());
7819
8992
  const showToast = useCallback(
7820
8993
  (type, message, duration) => {
8994
+ const normalizedMessage = message.trim().toLowerCase();
8995
+ const key2 = `${type}:${normalizedMessage}`;
8996
+ const now = Date.now();
8997
+ const lastShownAt = recentToastMapRef.current.get(key2);
8998
+ if (lastShownAt && now - lastShownAt < DEDUPE_WINDOW_MS) {
8999
+ return;
9000
+ }
9001
+ for (const [mapKey, ts] of recentToastMapRef.current.entries()) {
9002
+ if (now - ts > DEDUPE_WINDOW_MS * 4) {
9003
+ recentToastMapRef.current.delete(mapKey);
9004
+ }
9005
+ }
9006
+ recentToastMapRef.current.set(key2, now);
7821
9007
  const id = generateId();
7822
- setToasts((prev) => [...prev, { id, type, message, duration }]);
9008
+ setToasts((prev) => {
9009
+ const next = [...prev, { id, type, message, duration }];
9010
+ if (next.length <= MAX_ACTIVE_TOASTS) return next;
9011
+ return next.slice(next.length - MAX_ACTIVE_TOASTS);
9012
+ });
7823
9013
  },
7824
9014
  []
7825
9015
  );
@@ -7828,6 +9018,7 @@ function useToast() {
7828
9018
  }, []);
7829
9019
  const dismissAll = useCallback(() => {
7830
9020
  setToasts([]);
9021
+ recentToastMapRef.current.clear();
7831
9022
  }, []);
7832
9023
  return {
7833
9024
  toasts,
@@ -7886,49 +9077,6 @@ function groupSessionsByDate(sessions, t) {
7886
9077
  });
7887
9078
  return groups;
7888
9079
  }
7889
-
7890
- // ui/src/bichat/utils/errorDisplay.ts
7891
- function isPermissionDeniedError(error) {
7892
- if (!error) return false;
7893
- if (error instanceof Error) {
7894
- const msg = error.message.toLowerCase();
7895
- if (msg.includes("forbidden") || msg.includes("permission denied")) return true;
7896
- }
7897
- if (typeof error === "object" && error !== null) {
7898
- const obj = error;
7899
- if (obj.code === "forbidden" || obj.code === 403) return true;
7900
- if (obj.status === 403) return true;
7901
- if (obj.statusCode === 403) return true;
7902
- if (typeof obj.response === "object" && obj.response !== null) {
7903
- const resp = obj.response;
7904
- if (resp.status === 403) return true;
7905
- }
7906
- }
7907
- if (typeof error === "string") {
7908
- const lower = error.toLowerCase();
7909
- if (lower.includes("forbidden") || lower.includes("permission denied")) return true;
7910
- }
7911
- return false;
7912
- }
7913
- function toErrorDisplay(error, fallbackTitle) {
7914
- const permDenied = isPermissionDeniedError(error);
7915
- let title = fallbackTitle;
7916
- let description = "";
7917
- if (error instanceof Error) {
7918
- description = error.message;
7919
- } else if (typeof error === "object" && error !== null) {
7920
- const obj = error;
7921
- if (typeof obj.message === "string" && obj.message) description = obj.message;
7922
- if (typeof obj.title === "string" && obj.title) title = obj.title;
7923
- if (typeof obj.detail === "string" && obj.detail) description = obj.detail;
7924
- } else if (typeof error === "string") {
7925
- description = error;
7926
- }
7927
- if (permDenied && !description) {
7928
- description = "Your account does not have permission for this action.";
7929
- }
7930
- return { title, description, isPermissionDenied: permDenied };
7931
- }
7932
9080
  function ErrorAlert({ error }) {
7933
9081
  const amber = error.isPermissionDenied;
7934
9082
  return /* @__PURE__ */ jsxs(
@@ -8012,7 +9160,7 @@ function Sidebar2({
8012
9160
  const shouldReduceMotion = useReducedMotion();
8013
9161
  const sessionListRef = useRef(null);
8014
9162
  const searchContainerRef = useRef(null);
8015
- const { isCollapsed, isCollapsedRef, toggle, expand, collapse } = useSidebarCollapse();
9163
+ const { isCollapsed, toggle, collapse } = useSidebarCollapse();
8016
9164
  const collapsible = !onClose;
8017
9165
  const handleSidebarClick = useCallback(
8018
9166
  (e) => {
@@ -8023,17 +9171,6 @@ function Sidebar2({
8023
9171
  },
8024
9172
  [collapsible, toggle]
8025
9173
  );
8026
- const focusSearch = useCallback(() => {
8027
- if (!collapsible) return;
8028
- if (isCollapsedRef.current) {
8029
- expand();
8030
- setTimeout(() => {
8031
- searchContainerRef.current?.querySelector("input")?.focus();
8032
- }, 250);
8033
- } else {
8034
- searchContainerRef.current?.querySelector("input")?.focus();
8035
- }
8036
- }, [collapsible, expand, isCollapsedRef]);
8037
9174
  useEffect(() => {
8038
9175
  if (!collapsible) return;
8039
9176
  const handleKeyDown = (e) => {
@@ -8042,14 +9179,10 @@ function Sidebar2({
8042
9179
  e.preventDefault();
8043
9180
  toggle();
8044
9181
  }
8045
- if (isMod && e.key === "k") {
8046
- e.preventDefault();
8047
- focusSearch();
8048
- }
8049
9182
  };
8050
9183
  document.addEventListener("keydown", handleKeyDown);
8051
9184
  return () => document.removeEventListener("keydown", handleKeyDown);
8052
- }, [collapsible, toggle, focusSearch]);
9185
+ }, [collapsible, toggle]);
8053
9186
  useEffect(() => {
8054
9187
  if (!collapsible) return;
8055
9188
  const handler = (e) => {
@@ -8072,13 +9205,6 @@ function Sidebar2({
8072
9205
  const [refreshKey, setRefreshKey] = useState(0);
8073
9206
  const [showConfirm, setShowConfirm] = useState(false);
8074
9207
  const [sessionToArchive, setSessionToArchive] = useState(null);
8075
- const tabs = useMemo(() => {
8076
- const items = [{ id: "my-chats", label: t("BiChat.Sidebar.MyChats") }];
8077
- if (showAllChatsTab) {
8078
- items.push({ id: "all-chats", label: t("BiChat.Sidebar.AllChats") });
8079
- }
8080
- return items;
8081
- }, [showAllChatsTab, t]);
8082
9208
  const fetchSessions = useCallback(async () => {
8083
9209
  try {
8084
9210
  setLoading(true);
@@ -8291,14 +9417,6 @@ function Sidebar2({
8291
9417
  }
8292
9418
  )
8293
9419
  ] }),
8294
- showAllChatsTab && /* @__PURE__ */ jsx(
8295
- TabBar_default,
8296
- {
8297
- tabs,
8298
- activeTab,
8299
- onTabChange: (id) => setActiveTab(id)
8300
- }
8301
- ),
8302
9420
  activeTab === "all-chats" && showAllChatsTab ? /* @__PURE__ */ jsx(
8303
9421
  AllChatsList,
8304
9422
  {
@@ -8919,232 +10037,73 @@ function BiChatLayout({
8919
10037
  return () => document.removeEventListener("keydown", onKeyDown);
8920
10038
  }, [closeMobile, isMobile, isMobileOpen]);
8921
10039
  const handleDrawerDragEnd = (_, info) => {
8922
- if (info.offset.x < -80) {
8923
- closeMobile();
8924
- }
8925
- };
8926
- const content = routeKey ? /* @__PURE__ */ jsx(AnimatePresence, { mode: "wait", initial: false, children: /* @__PURE__ */ jsx(
8927
- motion.div,
8928
- {
8929
- className: "flex flex-1 min-h-0",
8930
- initial: { opacity: 0, y: 4 },
8931
- animate: { opacity: 1, y: 0 },
8932
- exit: { opacity: 0, y: -4 },
8933
- transition: { duration: 0.15, ease: "easeOut" },
8934
- children
8935
- },
8936
- routeKey
8937
- ) }) : /* @__PURE__ */ jsx("div", { className: "flex flex-1 min-h-0", children });
8938
- return /* @__PURE__ */ jsxs("div", { className: `relative flex flex-1 w-full h-full min-h-0 overflow-hidden ${className}`, children: [
8939
- /* @__PURE__ */ jsx(SkipLink, {}),
8940
- /* @__PURE__ */ jsx("div", { className: "hidden md:block", children: renderSidebar({}) }),
8941
- /* @__PURE__ */ jsx(AnimatePresence, { children: isMobile && isMobileOpen && /* @__PURE__ */ jsxs(Fragment, { children: [
8942
- /* @__PURE__ */ jsx(
8943
- motion.div,
8944
- {
8945
- className: "fixed inset-0 z-40 bg-black/40",
8946
- initial: { opacity: 0 },
8947
- animate: { opacity: 1 },
8948
- exit: { opacity: 0 },
8949
- onClick: closeMobile,
8950
- "aria-hidden": "true"
8951
- },
8952
- "sidebar-backdrop"
8953
- ),
8954
- /* @__PURE__ */ jsx(
8955
- motion.div,
8956
- {
8957
- className: "fixed inset-y-0 left-0 z-50 w-[18rem] max-w-[85vw] shadow-2xl",
8958
- initial: { x: "-100%" },
8959
- animate: { x: 0 },
8960
- exit: { x: "-100%" },
8961
- transition: { type: "spring", stiffness: 320, damping: 32 },
8962
- drag: "x",
8963
- dragDirectionLock: true,
8964
- dragConstraints: { left: -120, right: 0 },
8965
- dragElastic: { left: 0.2, right: 0 },
8966
- onDragEnd: handleDrawerDragEnd,
8967
- onClick: (e) => e.stopPropagation(),
8968
- children: /* @__PURE__ */ jsx("div", { ref: drawerRef, className: "h-full bg-white dark:bg-gray-900", children: renderSidebar({ onClose: closeMobile }) })
8969
- },
8970
- "sidebar-drawer"
8971
- )
8972
- ] }) }),
8973
- /* @__PURE__ */ jsxs("main", { id: "main-content", className: "relative flex-1 flex flex-col min-h-0 overflow-hidden", children: [
8974
- isMobile && !isMobileOpen && /* @__PURE__ */ jsx(
8975
- "button",
8976
- {
8977
- ref: menuButtonRef,
8978
- onClick: openMobile,
8979
- className: "md:hidden absolute top-3 left-3 z-30 w-10 h-10 rounded-xl bg-white/90 dark:bg-gray-900/90 text-gray-700 dark:text-gray-200 border border-gray-200/60 dark:border-gray-800/80 shadow-sm flex items-center justify-center hover:bg-white dark:hover:bg-gray-900 transition-colors cursor-pointer focus-visible:ring-2 focus-visible:ring-primary-400/50",
8980
- "aria-label": t("BiChat.Layout.OpenSidebar"),
8981
- title: t("BiChat.Layout.OpenSidebar"),
8982
- children: /* @__PURE__ */ jsx(List, { size: 20, weight: "bold" })
8983
- }
8984
- ),
8985
- content
8986
- ] })
8987
- ] });
8988
- }
8989
- init_useTranslation();
8990
- var variantStyles = {
8991
- error: {
8992
- container: "border-red-200 bg-red-50 dark:bg-red-900/20",
8993
- title: "text-red-800 dark:text-red-300",
8994
- message: "text-red-700 dark:text-red-400",
8995
- icon: "text-red-600 dark:text-red-400",
8996
- button: "text-red-400 hover:text-red-600 dark:hover:text-red-300",
8997
- retryButton: "bg-red-600 dark:bg-red-700 hover:bg-red-700 dark:hover:bg-red-800 text-white",
8998
- Icon: XCircle
8999
- },
9000
- success: {
9001
- container: "border-green-200 bg-green-50 dark:bg-green-900/20",
9002
- title: "text-green-800 dark:text-green-300",
9003
- message: "text-green-700 dark:text-green-400",
9004
- icon: "text-green-600 dark:text-green-400",
9005
- button: "text-green-400 hover:text-green-600 dark:hover:text-green-300",
9006
- retryButton: "bg-green-600 dark:bg-green-700 hover:bg-green-700 dark:hover:bg-green-800 text-white",
9007
- Icon: CheckCircle
9008
- },
9009
- warning: {
9010
- container: "border-yellow-200 bg-yellow-50 dark:bg-yellow-900/20",
9011
- title: "text-yellow-800 dark:text-yellow-300",
9012
- message: "text-yellow-700 dark:text-yellow-400",
9013
- icon: "text-yellow-600 dark:text-yellow-400",
9014
- button: "text-yellow-400 hover:text-yellow-600 dark:hover:text-yellow-300",
9015
- retryButton: "bg-yellow-600 dark:bg-yellow-700 hover:bg-yellow-700 dark:hover:bg-yellow-800 text-white",
9016
- Icon: Warning
9017
- },
9018
- info: {
9019
- container: "border-blue-200 bg-blue-50 dark:bg-blue-900/20",
9020
- title: "text-blue-800 dark:text-blue-300",
9021
- message: "text-blue-700 dark:text-blue-400",
9022
- icon: "text-blue-600 dark:text-blue-400",
9023
- button: "text-blue-400 hover:text-blue-600 dark:hover:text-blue-300",
9024
- retryButton: "bg-blue-600 dark:bg-blue-700 hover:bg-blue-700 dark:hover:bg-blue-800 text-white",
9025
- Icon: Info
9026
- }
9027
- };
9028
- function Alert({
9029
- variant = "info",
9030
- message,
9031
- title,
9032
- onDismiss,
9033
- onRetry,
9034
- show = true,
9035
- dismissible = true
9036
- }) {
9037
- const { t } = useTranslation();
9038
- const styles = variantStyles[variant];
9039
- const IconComponent = styles.Icon;
9040
- return /* @__PURE__ */ jsx(AnimatePresence, { children: show && /* @__PURE__ */ jsx(
9041
- motion.div,
9042
- {
9043
- variants: errorMessageVariants,
9044
- initial: "initial",
9045
- animate: "animate",
9046
- exit: "exit",
9047
- className: `border-t border ${styles.container} px-4 py-3`,
9048
- role: "alert",
9049
- "aria-live": "assertive",
9050
- children: /* @__PURE__ */ jsxs("div", { className: "w-full flex items-start justify-between px-4", children: [
9051
- /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 flex-1", children: [
9052
- /* @__PURE__ */ jsx(IconComponent, { size: 20, className: `w-5 h-5 ${styles.icon} flex-shrink-0 mt-0.5` }),
9053
- /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
9054
- title && /* @__PURE__ */ jsx("p", { className: `text-sm ${styles.title} font-medium`, children: title }),
9055
- /* @__PURE__ */ jsx("p", { className: `text-sm ${styles.message} ${title ? "mt-1" : ""}`, children: message }),
9056
- onRetry && /* @__PURE__ */ jsx(
9057
- "button",
9058
- {
9059
- onClick: onRetry,
9060
- className: `mt-2 text-xs px-3 py-1.5 rounded ${styles.retryButton} transition-colors font-medium`,
9061
- children: t("BiChat.Chat.Retry")
9062
- }
9063
- )
9064
- ] })
9065
- ] }),
9066
- dismissible && onDismiss && /* @__PURE__ */ jsx(
9067
- "button",
9068
- {
9069
- onClick: onDismiss,
9070
- className: `${styles.button} transition-colors flex-shrink-0`,
9071
- "aria-label": t("BiChat.Chat.DismissNotification"),
9072
- children: /* @__PURE__ */ jsx(X, { size: 20, className: "w-5 h-5" })
9073
- }
9074
- )
9075
- ] })
9076
- }
9077
- ) });
9078
- }
9079
- var Alert_default = memo(Alert);
9080
- init_useTranslation();
9081
- function ArchiveBanner({
9082
- show = true,
9083
- onRestore,
9084
- restoring = false,
9085
- onRestoreComplete
9086
- }) {
9087
- const { t } = useTranslation();
9088
- const [error, setError] = useState(null);
9089
- const handleRestore = async () => {
9090
- try {
9091
- setError(null);
9092
- if (onRestore) {
9093
- await onRestore();
9094
- }
9095
- if (onRestoreComplete) {
9096
- onRestoreComplete();
9097
- }
9098
- } catch (err) {
9099
- const message = err instanceof Error ? err.message : t("BiChat.Archive.RestoreFailed");
9100
- setError(message);
10040
+ if (info.offset.x < -80) {
10041
+ closeMobile();
9101
10042
  }
9102
10043
  };
9103
- return /* @__PURE__ */ jsxs(Fragment, { children: [
9104
- /* @__PURE__ */ jsx(AnimatePresence, { children: show && /* @__PURE__ */ jsx(
9105
- motion.div,
9106
- {
9107
- variants: errorMessageVariants,
9108
- initial: "initial",
9109
- animate: "animate",
9110
- exit: "exit",
9111
- className: "border-t border border-blue-200 bg-blue-50 dark:bg-blue-900/20 px-4 py-3",
9112
- role: "region",
9113
- "aria-label": t("BiChat.Archive.Banner"),
9114
- children: /* @__PURE__ */ jsxs("div", { className: "w-full flex items-start justify-between px-4", children: [
9115
- /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 flex-1", children: [
9116
- /* @__PURE__ */ jsx(Archive, { size: 20, className: "w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" }),
9117
- /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-blue-700 dark:text-blue-400", children: t("BiChat.Archive.Archived") }) })
9118
- ] }),
9119
- /* @__PURE__ */ jsx(
9120
- "button",
9121
- {
9122
- onClick: handleRestore,
9123
- disabled: restoring,
9124
- className: "ml-2 flex-shrink-0 px-3 py-1.5 text-xs font-medium bg-blue-600 dark:bg-blue-700 hover:bg-blue-700 dark:hover:bg-blue-800 text-white rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5",
9125
- "aria-label": t("BiChat.Archive.Restore"),
9126
- children: restoring ? /* @__PURE__ */ jsxs(Fragment, { children: [
9127
- /* @__PURE__ */ jsx(Spinner, { size: 16, className: "w-4 h-4 animate-spin" }),
9128
- t("BiChat.Archive.Restoring")
9129
- ] }) : t("BiChat.Archive.Restore")
9130
- }
9131
- )
9132
- ] })
9133
- }
9134
- ) }),
9135
- error && /* @__PURE__ */ jsx(
9136
- Alert_default,
9137
- {
9138
- variant: "error",
9139
- message: error,
9140
- title: t("BiChat.Archive.RestoreFailed"),
9141
- onDismiss: () => setError(null),
9142
- dismissible: true
9143
- }
9144
- )
10044
+ const content = routeKey ? /* @__PURE__ */ jsx(AnimatePresence, { mode: "wait", initial: false, children: /* @__PURE__ */ jsx(
10045
+ motion.div,
10046
+ {
10047
+ className: "flex flex-1 min-h-0",
10048
+ initial: { opacity: 0, y: 4 },
10049
+ animate: { opacity: 1, y: 0 },
10050
+ exit: { opacity: 0, y: -4 },
10051
+ transition: { duration: 0.15, ease: "easeOut" },
10052
+ children
10053
+ },
10054
+ routeKey
10055
+ ) }) : /* @__PURE__ */ jsx("div", { className: "flex flex-1 min-h-0", children });
10056
+ return /* @__PURE__ */ jsxs("div", { className: `relative flex flex-1 w-full h-full min-h-0 overflow-hidden ${className}`, children: [
10057
+ /* @__PURE__ */ jsx(SkipLink, {}),
10058
+ /* @__PURE__ */ jsx("div", { className: "hidden md:block", children: renderSidebar({}) }),
10059
+ /* @__PURE__ */ jsx(AnimatePresence, { children: isMobile && isMobileOpen && /* @__PURE__ */ jsxs(Fragment, { children: [
10060
+ /* @__PURE__ */ jsx(
10061
+ motion.div,
10062
+ {
10063
+ className: "fixed inset-0 z-40 bg-black/40",
10064
+ initial: { opacity: 0 },
10065
+ animate: { opacity: 1 },
10066
+ exit: { opacity: 0 },
10067
+ onClick: closeMobile,
10068
+ "aria-hidden": "true"
10069
+ },
10070
+ "sidebar-backdrop"
10071
+ ),
10072
+ /* @__PURE__ */ jsx(
10073
+ motion.div,
10074
+ {
10075
+ className: "fixed inset-y-0 left-0 z-50 w-[18rem] max-w-[85vw] shadow-2xl",
10076
+ initial: { x: "-100%" },
10077
+ animate: { x: 0 },
10078
+ exit: { x: "-100%" },
10079
+ transition: { type: "spring", stiffness: 320, damping: 32 },
10080
+ drag: "x",
10081
+ dragDirectionLock: true,
10082
+ dragConstraints: { left: -120, right: 0 },
10083
+ dragElastic: { left: 0.2, right: 0 },
10084
+ onDragEnd: handleDrawerDragEnd,
10085
+ onClick: (e) => e.stopPropagation(),
10086
+ children: /* @__PURE__ */ jsx("div", { ref: drawerRef, className: "h-full bg-white dark:bg-gray-900", children: renderSidebar({ onClose: closeMobile }) })
10087
+ },
10088
+ "sidebar-drawer"
10089
+ )
10090
+ ] }) }),
10091
+ /* @__PURE__ */ jsxs("main", { id: "main-content", className: "relative flex-1 flex flex-col min-h-0 overflow-hidden", children: [
10092
+ isMobile && !isMobileOpen && /* @__PURE__ */ jsx(
10093
+ "button",
10094
+ {
10095
+ ref: menuButtonRef,
10096
+ onClick: openMobile,
10097
+ className: "md:hidden absolute top-3 left-3 z-30 w-10 h-10 rounded-xl bg-white/90 dark:bg-gray-900/90 text-gray-700 dark:text-gray-200 border border-gray-200/60 dark:border-gray-800/80 shadow-sm flex items-center justify-center hover:bg-white dark:hover:bg-gray-900 transition-colors cursor-pointer focus-visible:ring-2 focus-visible:ring-primary-400/50",
10098
+ "aria-label": t("BiChat.Layout.OpenSidebar"),
10099
+ title: t("BiChat.Layout.OpenSidebar"),
10100
+ children: /* @__PURE__ */ jsx(List, { size: 20, weight: "bold" })
10101
+ }
10102
+ ),
10103
+ content
10104
+ ] })
9145
10105
  ] });
9146
10106
  }
9147
- var ArchiveBanner_default = memo(ArchiveBanner);
9148
10107
 
9149
10108
  // ui/src/bichat/components/RetryActionArea.tsx
9150
10109
  init_useTranslation();
@@ -9198,66 +10157,6 @@ var RetryActionArea = memo(function RetryActionArea2({
9198
10157
  )
9199
10158
  );
9200
10159
  });
9201
-
9202
- // ui/src/bichat/components/StreamError.tsx
9203
- init_useTranslation();
9204
- function StreamError({
9205
- error,
9206
- onRetry,
9207
- onRegenerate,
9208
- compact = false
9209
- }) {
9210
- const { t } = useTranslation();
9211
- return /* @__PURE__ */ jsxs(
9212
- motion.div,
9213
- {
9214
- initial: { opacity: 0, y: 10 },
9215
- animate: { opacity: 1, y: 0 },
9216
- exit: { opacity: 0, y: -10 },
9217
- className: `flex items-center gap-3 ${compact ? "p-3" : "p-4"} bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg`,
9218
- role: "alert",
9219
- children: [
9220
- /* @__PURE__ */ jsx(
9221
- Warning,
9222
- {
9223
- className: "w-5 h-5 text-red-500 dark:text-red-400 flex-shrink-0",
9224
- weight: "fill"
9225
- }
9226
- ),
9227
- /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
9228
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-red-800 dark:text-red-200", children: t("BiChat.Error.Generic") }),
9229
- /* @__PURE__ */ jsx("p", { className: "text-sm text-red-600 dark:text-red-300 break-words", children: error })
9230
- ] }),
9231
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [
9232
- onRetry && /* @__PURE__ */ jsxs(
9233
- "button",
9234
- {
9235
- onClick: onRetry,
9236
- className: "cursor-pointer inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-red-700 dark:text-red-200 bg-red-100 dark:bg-red-800/50 hover:bg-red-200 dark:hover:bg-red-800 rounded-md transition-colors",
9237
- type: "button",
9238
- children: [
9239
- /* @__PURE__ */ jsx(ArrowClockwise, { className: "w-4 h-4" }),
9240
- t("BiChat.StreamError.Retry")
9241
- ]
9242
- }
9243
- ),
9244
- onRegenerate && /* @__PURE__ */ jsxs(
9245
- "button",
9246
- {
9247
- onClick: onRegenerate,
9248
- className: "cursor-pointer inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-200 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md transition-colors",
9249
- type: "button",
9250
- children: [
9251
- /* @__PURE__ */ jsx(ArrowsCounterClockwise, { className: "w-4 h-4" }),
9252
- t("BiChat.StreamError.Regenerate")
9253
- ]
9254
- }
9255
- )
9256
- ] })
9257
- ]
9258
- }
9259
- );
9260
- }
9261
10160
  init_useTranslation();
9262
10161
  function MessageActions({
9263
10162
  message,
@@ -10656,9 +11555,10 @@ function useMarkdownCopy(options = {}) {
10656
11555
  const [copiedStates, setCopiedStates] = useState(/* @__PURE__ */ new Map());
10657
11556
  const timeoutsRef = useRef(/* @__PURE__ */ new Map());
10658
11557
  useEffect(() => {
11558
+ const timeouts = timeoutsRef.current;
10659
11559
  return () => {
10660
- timeoutsRef.current.forEach((timeout) => clearTimeout(timeout));
10661
- timeoutsRef.current.clear();
11560
+ timeouts.forEach((timeout) => clearTimeout(timeout));
11561
+ timeouts.clear();
10662
11562
  };
10663
11563
  }, []);
10664
11564
  const isCopied = useCallback(
@@ -10781,8 +11681,9 @@ function ConfigProvider({ config, useGlobalConfig = false, children }) {
10781
11681
  if (config) {
10782
11682
  resolvedConfig = config;
10783
11683
  } else if (useGlobalConfig && typeof window !== "undefined") {
10784
- const globalContext = window.__BICHAT_CONTEXT__;
10785
- const globalCSRF = window.__CSRF_TOKEN__;
11684
+ const w = window;
11685
+ const globalContext = w.__APPLET_CONTEXT__;
11686
+ const globalCSRF = w.__CSRF_TOKEN__;
10786
11687
  if (globalContext) {
10787
11688
  resolvedConfig = {
10788
11689
  user: {
@@ -10979,98 +11880,6 @@ function createHeadersWithCSRF(init) {
10979
11880
  return addCSRFHeader(headers);
10980
11881
  }
10981
11882
 
10982
- // ui/src/applet-devtools/enabled.ts
10983
- function shouldEnableAppletDevtools() {
10984
- if (typeof window === "undefined") return false;
10985
- const url = new URL(window.location.href);
10986
- if (url.searchParams.get("appletDebug") === "1") return true;
10987
- try {
10988
- return window.localStorage.getItem("iotaAppletDevtools") === "1";
10989
- } catch {
10990
- return false;
10991
- }
10992
- }
10993
-
10994
- // ui/src/applet-host/rpc.ts
10995
- var AppletRPCException = class extends Error {
10996
- constructor(args) {
10997
- super(args.message);
10998
- this.name = "AppletRPCException";
10999
- this.code = args.code;
11000
- this.details = args.details;
11001
- this.cause = args.cause;
11002
- }
11003
- };
11004
- function createAppletRPCClient(options) {
11005
- const fetcher = options.fetcher ?? fetch;
11006
- async function call(method, params) {
11007
- const req = { id: crypto.randomUUID(), method, params };
11008
- const startedAt = typeof performance !== "undefined" ? performance.now() : Date.now();
11009
- maybeDispatchRPCEvent({
11010
- id: req.id,
11011
- method: req.method,
11012
- status: "start"
11013
- });
11014
- try {
11015
- const resp = await fetcher(options.endpoint, {
11016
- method: "POST",
11017
- headers: { "Content-Type": "application/json" },
11018
- body: JSON.stringify(req)
11019
- });
11020
- if (!resp.ok) {
11021
- throw new AppletRPCException({
11022
- code: "http_error",
11023
- message: `HTTP ${resp.status}`,
11024
- details: { status: resp.status }
11025
- });
11026
- }
11027
- const json = await resp.json();
11028
- if (json.error) {
11029
- throw new AppletRPCException({
11030
- code: json.error.code,
11031
- message: json.error.message,
11032
- details: json.error.details
11033
- });
11034
- }
11035
- if (json.result === void 0) {
11036
- throw new AppletRPCException({
11037
- code: "invalid_response",
11038
- message: "Missing result in successful response"
11039
- });
11040
- }
11041
- maybeDispatchRPCEvent({
11042
- id: req.id,
11043
- method: req.method,
11044
- status: "success",
11045
- durationMs: elapsedMs(startedAt)
11046
- });
11047
- return json.result;
11048
- } catch (err) {
11049
- maybeDispatchRPCEvent({
11050
- id: req.id,
11051
- method: req.method,
11052
- status: "error",
11053
- durationMs: elapsedMs(startedAt),
11054
- error: err
11055
- });
11056
- throw err;
11057
- }
11058
- }
11059
- async function callTyped(method, params) {
11060
- return call(method, params);
11061
- }
11062
- return { call, callTyped };
11063
- }
11064
- function maybeDispatchRPCEvent(detail) {
11065
- if (typeof window === "undefined") return;
11066
- if (!shouldEnableAppletDevtools()) return;
11067
- window.dispatchEvent(new CustomEvent("iota:applet-rpc", { detail }));
11068
- }
11069
- function elapsedMs(startedAt) {
11070
- const now = typeof performance !== "undefined" ? performance.now() : Date.now();
11071
- return Math.max(0, Math.round(now - startedAt));
11072
- }
11073
-
11074
11883
  // ui/src/bichat/data/HttpDataSource.ts
11075
11884
  init_chartSpec();
11076
11885
 
@@ -11116,7 +11925,9 @@ async function* parseSSEStream(reader) {
11116
11925
  reader.releaseLock();
11117
11926
  }
11118
11927
  }
11928
+ var TERMINAL_TYPES = /* @__PURE__ */ new Set(["done", "error"]);
11119
11929
  async function* parseBichatStream(reader) {
11930
+ let yieldedTerminal = false;
11120
11931
  for await (const event of parseSSEStream(reader)) {
11121
11932
  const parsed = event;
11122
11933
  const inferredType = parsed.type || (parsed.content ? "content" : "error");
@@ -11124,11 +11935,50 @@ async function* parseBichatStream(reader) {
11124
11935
  ...parsed,
11125
11936
  type: inferredType
11126
11937
  };
11938
+ if (TERMINAL_TYPES.has(inferredType)) {
11939
+ yieldedTerminal = true;
11940
+ }
11127
11941
  yield normalized;
11128
11942
  }
11943
+ if (!yieldedTerminal) {
11944
+ yield { type: "done" };
11945
+ }
11946
+ }
11947
+ async function* parseBichatStreamEvents(reader) {
11948
+ for await (const chunk of parseBichatStream(reader)) {
11949
+ const event = toStreamEvent(chunk);
11950
+ if (event) yield event;
11951
+ }
11952
+ }
11953
+ function toStreamEvent(chunk) {
11954
+ switch (chunk.type) {
11955
+ case "chunk":
11956
+ case "content":
11957
+ return { type: "content", content: chunk.content ?? "" };
11958
+ case "tool_start":
11959
+ return chunk.tool ? { type: "tool_start", tool: chunk.tool } : null;
11960
+ case "tool_end":
11961
+ return chunk.tool ? { type: "tool_end", tool: chunk.tool } : null;
11962
+ case "usage":
11963
+ return chunk.usage ? { type: "usage", usage: chunk.usage } : null;
11964
+ case "user_message":
11965
+ return chunk.sessionId ? { type: "user_message", sessionId: chunk.sessionId } : null;
11966
+ case "interrupt":
11967
+ return chunk.interrupt ? { type: "interrupt", interrupt: chunk.interrupt, sessionId: chunk.sessionId } : null;
11968
+ case "done":
11969
+ return { type: "done", sessionId: chunk.sessionId, generationMs: chunk.generationMs };
11970
+ case "error":
11971
+ return { type: "error", error: chunk.error ?? "Unknown error" };
11972
+ default:
11973
+ return null;
11974
+ }
11129
11975
  }
11130
11976
 
11131
11977
  // ui/src/bichat/data/HttpDataSource.ts
11978
+ function isSessionNotFoundError(err) {
11979
+ if (!(err instanceof AppletRPCException)) return false;
11980
+ return err.code === "not_found" || err.code === "session_not_found";
11981
+ }
11132
11982
  function toSession(session) {
11133
11983
  return {
11134
11984
  ...session,
@@ -11316,34 +12166,18 @@ function attachArtifactsToTurns(turns, artifacts) {
11316
12166
  }
11317
12167
  };
11318
12168
  });
11319
- const assistantPositions = [];
11320
12169
  const turnIndexByMessageID = /* @__PURE__ */ new Map();
11321
12170
  nextTurns.forEach((turn, index) => {
11322
12171
  turnIndexByMessageID.set(turn.userTurn.id, index);
11323
12172
  const assistantTurn = turn.assistantTurn;
11324
12173
  if (!assistantTurn) return;
11325
12174
  turnIndexByMessageID.set(assistantTurn.id, index);
11326
- assistantPositions.push({
11327
- index,
11328
- createdAtMs: toMillis(assistantTurn.createdAt || turn.createdAt)
11329
- });
11330
12175
  });
11331
- if (assistantPositions.length === 0) return turns;
11332
- const findFallbackAssistantIndex = (artifactCreatedAt) => {
11333
- const artifactMs = toMillis(artifactCreatedAt);
11334
- if (!Number.isFinite(artifactMs)) {
11335
- return assistantPositions[assistantPositions.length - 1].index;
11336
- }
11337
- for (const pos of assistantPositions) {
11338
- if (Number.isFinite(pos.createdAtMs) && pos.createdAtMs >= artifactMs) {
11339
- return pos.index;
11340
- }
11341
- }
11342
- return assistantPositions[assistantPositions.length - 1].index;
11343
- };
11344
12176
  for (const entry of downloadArtifacts) {
11345
12177
  const messageID = entry.raw.messageId;
11346
- const targetIndex = (messageID ? turnIndexByMessageID.get(messageID) : void 0) ?? findFallbackAssistantIndex(entry.raw.createdAt);
12178
+ if (!messageID) continue;
12179
+ const targetIndex = turnIndexByMessageID.get(messageID);
12180
+ if (targetIndex === void 0) continue;
11347
12181
  const assistantTurn = nextTurns[targetIndex]?.assistantTurn;
11348
12182
  if (!assistantTurn) continue;
11349
12183
  const exists = assistantTurn.artifacts.some(
@@ -11355,7 +12189,9 @@ function attachArtifactsToTurns(turns, artifacts) {
11355
12189
  }
11356
12190
  for (const raw of chartArtifacts) {
11357
12191
  const messageID = raw.messageId;
11358
- const targetIndex = (messageID ? turnIndexByMessageID.get(messageID) : void 0) ?? findFallbackAssistantIndex(raw.createdAt);
12192
+ if (!messageID) continue;
12193
+ const targetIndex = turnIndexByMessageID.get(messageID);
12194
+ if (targetIndex === void 0) continue;
11359
12195
  const assistantTurn = nextTurns[targetIndex]?.assistantTurn;
11360
12196
  if (!assistantTurn) continue;
11361
12197
  if (assistantTurn.chartData) continue;
@@ -11381,7 +12217,8 @@ var HttpDataSource = class {
11381
12217
  this.navigateToSession = config.navigateToSession;
11382
12218
  }
11383
12219
  this.rpc = createAppletRPCClient({
11384
- endpoint: `${this.config.baseUrl}${this.config.rpcEndpoint}`
12220
+ endpoint: `${this.config.baseUrl}${this.config.rpcEndpoint}`,
12221
+ timeoutMs: this.config.timeout
11385
12222
  });
11386
12223
  }
11387
12224
  /**
@@ -11437,8 +12274,11 @@ var HttpDataSource = class {
11437
12274
  pendingQuestion: toPendingQuestion(data.pendingQuestion)
11438
12275
  };
11439
12276
  } catch (err) {
12277
+ if (isSessionNotFoundError(err)) {
12278
+ return null;
12279
+ }
11440
12280
  console.error("Failed to fetch session:", err);
11441
- return null;
12281
+ throw err instanceof Error ? err : new Error("Failed to fetch session");
11442
12282
  }
11443
12283
  }
11444
12284
  async fetchSessionArtifacts(sessionId, options) {
@@ -11527,15 +12367,28 @@ var HttpDataSource = class {
11527
12367
  url: a.url
11528
12368
  }))
11529
12369
  };
12370
+ let connectionTimeoutID;
12371
+ let connectionTimedOut = false;
11530
12372
  try {
12373
+ const timeoutMs = this.config.timeout ?? 0;
12374
+ if (timeoutMs > 0) {
12375
+ connectionTimeoutID = setTimeout(() => {
12376
+ connectionTimedOut = true;
12377
+ this.abortController?.abort();
12378
+ }, timeoutMs);
12379
+ }
11531
12380
  const response = await fetch(url, {
11532
12381
  method: "POST",
11533
12382
  headers: this.createHeaders(),
11534
12383
  body: JSON.stringify(payload),
11535
12384
  signal: this.abortController.signal
11536
12385
  });
12386
+ if (connectionTimeoutID !== void 0) {
12387
+ clearTimeout(connectionTimeoutID);
12388
+ connectionTimeoutID = void 0;
12389
+ }
11537
12390
  if (!response.ok) {
11538
- throw new Error(`Stream request failed: ${response.statusText}`);
12391
+ throw new Error(`Stream request failed: HTTP ${response.status}`);
11539
12392
  }
11540
12393
  if (!response.body) {
11541
12394
  throw new Error("Response body is null");
@@ -11552,7 +12405,7 @@ var HttpDataSource = class {
11552
12405
  if (err.name === "AbortError") {
11553
12406
  yield {
11554
12407
  type: "error",
11555
- error: "Stream cancelled"
12408
+ error: connectionTimedOut ? `Stream request timed out after ${this.config.timeout}ms` : "Stream cancelled"
11556
12409
  };
11557
12410
  } else {
11558
12411
  yield {
@@ -11567,6 +12420,9 @@ var HttpDataSource = class {
11567
12420
  };
11568
12421
  }
11569
12422
  } finally {
12423
+ if (connectionTimeoutID !== void 0) {
12424
+ clearTimeout(connectionTimeoutID);
12425
+ }
11570
12426
  if (signal && onExternalAbort) {
11571
12427
  signal.removeEventListener("abort", onExternalAbort);
11572
12428
  }
@@ -11681,6 +12537,6 @@ function createHttpDataSource(config) {
11681
12537
  return new HttpDataSource(config);
11682
12538
  }
11683
12539
 
11684
- export { ATTACHMENT_ACCEPT_ATTRIBUTE, ActionButton, Alert_default as Alert, AllChatsList, ArchiveBanner_default as ArchiveBanner, ArchivedChatList, AssistantMessage, AssistantTurnView, MemoizedAttachmentGrid as AttachmentGrid, AttachmentPreview_default as AttachmentPreview, AttachmentUpload_default as AttachmentUpload, Avatar, BiChatLayout, Bubble, CHART_VISUAL, ChartCard, ChatHeader, ChatSession, ChatSessionProvider, MemoizedCodeBlock as CodeBlock, CodeOutputsPanel, CompactionDoodle, ConfigProvider, ConfirmModal, ConfirmationStep, DateGroupHeader, DebugPanel, DefaultErrorContent, DownloadCard, MemoizedEditableText as EditableText, MemoizedEmptyState as EmptyState, ErrorBoundary, HttpDataSource, ImageModal, InlineQuestionForm, IotaContextProvider, ListItemSkeleton, MemoizedLoadingSpinner as LoadingSpinner, MemoizedMarkdownRenderer as MarkdownRenderer, MessageActions, MessageInput, MessageList, MessageRole, PermissionGuard, QuestionForm, QuestionStep, RateLimiter, RetryActionArea, ScreenReaderAnnouncer, ScrollToBottomButton, MemoizedSearchInput as SearchInput, SessionArtifactList, SessionArtifactPreview, SessionArtifactsPanel, SessionItem_default as SessionItem, SessionSkeleton, Sidebar2 as Sidebar, MemoizedSkeleton as Skeleton, SkeletonAvatar, SkeletonCard, SkeletonGroup, SkeletonText, SkipLink, Slot, SourcesPanel, StreamError, StreamingCursor, SystemMessage, MemoizedTabBar as TabBar, TableExportButton, TableWithExport, ThemeProvider, Toast, ToastContainer, TouchContextMenu, Turn, TurnBubble, MemoizedTypingIndicator as TypingIndicator, MemoizedUserAvatar as UserAvatar, MemoizedUserFilter as UserFilter, UserMessage, UserTurnView, WelcomeContent, addCSRFHeader, backdropVariants, buttonVariants, convertToBase64, createDataUrl, createHeadersWithCSRF, createHttpDataSource, darkTheme, dropdownVariants, errorMessageVariants, fadeInUpVariants, fadeInVariants, floatingButtonVariants, formatFileSize, getCSRFToken, getFileVisual, getValidChildren, groupSessionsByDate, hasPermission, isImageMimeType, isPermissionDeniedError, lightTheme, listItemVariants, messageContainerVariants, messageVariants, scaleFadeVariants, sessionItemVariants, staggerContainerVariants, toErrorDisplay, toastVariants, typingDotVariants, useActionButtonContext, useAttachments, useAutoScroll, useAvatarContext, useBubbleContext, useChatInput, useChatMessaging, useChatSession, useConfig, useFocusTrap, useImageGallery, useIotaContext, useKeyboardShortcuts, useLongPress, useMarkdownCopy, useMessageActions, useModalLock, useOptionalChatMessaging, useRequiredConfig, useScrollToBottom, useSidebarState, useStreaming, useTheme, useToast, useTranslation, useTurnContext, validateAttachmentFile, validateFileCount, validateImageFile, verbTransitionVariants };
12540
+ export { ATTACHMENT_ACCEPT_ATTRIBUTE, ActionButton, Alert_default as Alert, AllChatsList, ArchiveBanner_default as ArchiveBanner, ArchivedChatList, AssistantMessage, AssistantTurnView, MemoizedAttachmentGrid as AttachmentGrid, AttachmentPreview_default as AttachmentPreview, AttachmentUpload_default as AttachmentUpload, Avatar, BiChatLayout, Bubble, CHART_VISUAL, ChartCard, ChatHeader, ChatMachine, ChatSession, ChatSessionProvider, MemoizedCodeBlock as CodeBlock, CodeOutputsPanel, CompactionDoodle, ConfigProvider, ConfirmModal, ConfirmationStep, DateGroupHeader, DebugPanel, DefaultErrorContent, DownloadCard, MemoizedEditableText as EditableText, MemoizedEmptyState as EmptyState, ErrorBoundary, HttpDataSource, ImageModal, InlineQuestionForm, IotaContextProvider, ListItemSkeleton, MemoizedLoadingSpinner as LoadingSpinner, MemoizedMarkdownRenderer as MarkdownRenderer, MessageActions, MessageInput, MessageList, MessageRole, PermissionGuard, QuestionForm, QuestionStep, RateLimiter, RetryActionArea, ScreenReaderAnnouncer, ScrollToBottomButton, MemoizedSearchInput as SearchInput, SessionArtifactList, SessionArtifactPreview, SessionArtifactsPanel, SessionItem_default as SessionItem, SessionSkeleton, Sidebar2 as Sidebar, MemoizedSkeleton as Skeleton, SkeletonAvatar, SkeletonCard, SkeletonGroup, SkeletonText, SkipLink, Slot, SourcesPanel, StreamError, StreamingCursor, SystemMessage, TableExportButton, TableWithExport, ThemeProvider, Toast, ToastContainer, TouchContextMenu, Turn, TurnBubble, MemoizedTypingIndicator as TypingIndicator, MemoizedUserAvatar as UserAvatar, MemoizedUserFilter as UserFilter, UserMessage, UserTurnView, WelcomeContent, addCSRFHeader, backdropVariants, buttonVariants, convertToBase64, createDataUrl, createHeadersWithCSRF, createHttpDataSource, darkTheme, dropdownVariants, errorMessageVariants, fadeInUpVariants, fadeInVariants, floatingButtonVariants, formatFileSize, getCSRFToken, getFileVisual, getValidChildren, groupSessionsByDate, hasPermission, isImageMimeType, isPermissionDeniedError, lightTheme, listItemVariants, messageContainerVariants, messageVariants, parseBichatStream, parseBichatStreamEvents, parseSSEStream, scaleFadeVariants, sessionItemVariants, staggerContainerVariants, toErrorDisplay, typingDotVariants, useActionButtonContext, useAttachments, useAutoScroll, useAvatarContext, useBubbleContext, useChatInput, useChatMessaging, useChatSession, useConfig, useFocusTrap, useImageGallery, useIotaContext, useKeyboardShortcuts, useLongPress, useMarkdownCopy, useMessageActions, useModalLock, useOptionalChatMessaging, useRequiredConfig, useScrollToBottom, useSidebarState, useStreaming, useTheme, useToast, useTranslation, useTurnContext, validateAttachmentFile, validateFileCount, validateImageFile, verbTransitionVariants };
11685
12541
  //# sourceMappingURL=index.mjs.map
11686
12542
  //# sourceMappingURL=index.mjs.map