@iota-uz/sdk 0.4.12 → 0.4.13

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.
@@ -10,10 +10,10 @@ var prism = require('react-syntax-highlighter/dist/esm/styles/prism');
10
10
  var ReactMarkdown = require('react-markdown');
11
11
  var remarkGfm = require('remark-gfm');
12
12
  var framerMotion = require('framer-motion');
13
+ require('react-dom/client');
13
14
  var dateFns = require('date-fns');
14
15
  var react$1 = require('@headlessui/react');
15
16
  var reactDom = require('react-dom');
16
- require('react-dom/client');
17
17
 
18
18
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
19
19
 
@@ -32,12 +32,12 @@ var __export = (target, all) => {
32
32
  for (var name in all)
33
33
  __defProp(target, name, { get: all[name], enumerable: true });
34
34
  };
35
- function IotaContextProvider({ children }) {
36
- const initialContext = window.__BICHAT_CONTEXT__;
37
- if (!initialContext) {
38
- throw new Error("BICHAT_CONTEXT not found. Ensure server injected context into window object.");
35
+ function IotaContextProvider({ context, children }) {
36
+ const resolved = context ?? (typeof window !== "undefined" ? window.__APPLET_CONTEXT__ : void 0);
37
+ if (!resolved) {
38
+ throw new Error("APPLET_CONTEXT not found. Pass a `context` prop or ensure the server injected context into window.__APPLET_CONTEXT__.");
39
39
  }
40
- return /* @__PURE__ */ jsxRuntime.jsx(IotaContext.Provider, { value: initialContext, children });
40
+ return /* @__PURE__ */ jsxRuntime.jsx(IotaContext.Provider, { value: resolved, children });
41
41
  }
42
42
  function useIotaContext() {
43
43
  const context = React.useContext(IotaContext);
@@ -47,7 +47,7 @@ function useIotaContext() {
47
47
  return context;
48
48
  }
49
49
  function hasPermission(permission) {
50
- const context = window.__BICHAT_CONTEXT__;
50
+ const context = typeof window !== "undefined" ? window.__APPLET_CONTEXT__ : void 0;
51
51
  if (!context) {
52
52
  return false;
53
53
  }
@@ -304,12 +304,16 @@ var init_chartSpec = __esm({
304
304
  exports.TableExportButton = void 0;
305
305
  var init_TableExportButton = __esm({
306
306
  "ui/src/bichat/components/TableExportButton.tsx"() {
307
+ init_useTranslation();
307
308
  exports.TableExportButton = React.memo(function TableExportButton2({
308
309
  onClick,
309
310
  disabled = false,
310
- label = "Export",
311
- disabledTooltip = "Please wait..."
311
+ label,
312
+ disabledTooltip
312
313
  }) {
314
+ const { t } = useTranslation();
315
+ const resolvedLabel = label ?? t("BiChat.Export");
316
+ const resolvedDisabledTooltip = disabledTooltip ?? t("BiChat.Common.PleaseWait");
313
317
  return /* @__PURE__ */ jsxRuntime.jsxs(
314
318
  "button",
315
319
  {
@@ -317,35 +321,38 @@ var init_TableExportButton = __esm({
317
321
  onClick,
318
322
  disabled,
319
323
  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",
320
- "aria-label": label,
321
- title: disabled ? disabledTooltip : label,
324
+ "aria-label": resolvedLabel,
325
+ title: disabled ? resolvedDisabledTooltip : resolvedLabel,
322
326
  children: [
323
327
  /* @__PURE__ */ jsxRuntime.jsx(react.FileXls, { size: 16, weight: "fill" }),
324
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: label })
328
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: resolvedLabel })
325
329
  ]
326
330
  }
327
331
  );
328
332
  });
329
333
  }
330
334
  });
331
- var DEFAULT_EXPORT_MESSAGE; exports.TableWithExport = void 0;
335
+ exports.TableWithExport = void 0;
332
336
  var init_TableWithExport = __esm({
333
337
  "ui/src/bichat/components/TableWithExport.tsx"() {
334
338
  init_TableExportButton();
335
- DEFAULT_EXPORT_MESSAGE = "Export the table above to Excel";
339
+ init_useTranslation();
336
340
  exports.TableWithExport = React.memo(function TableWithExport2({
337
341
  children,
338
342
  sendMessage,
339
343
  disabled = false,
340
- exportMessage = DEFAULT_EXPORT_MESSAGE,
341
- exportLabel = "Export"
344
+ exportMessage,
345
+ exportLabel
342
346
  }) {
347
+ const { t } = useTranslation();
348
+ const resolvedExportMessage = exportMessage ?? t("BiChat.ExportTableToExcel");
349
+ const resolvedExportLabel = exportLabel ?? t("BiChat.Export");
343
350
  const handleExport = React.useCallback(() => {
344
- sendMessage?.(exportMessage);
345
- }, [sendMessage, exportMessage]);
351
+ sendMessage?.(resolvedExportMessage);
352
+ }, [sendMessage, resolvedExportMessage]);
346
353
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
347
354
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "markdown-table-wrapper overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsx("table", { className: "markdown-table w-full border-collapse", children }) }),
348
- sendMessage && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end mt-1", children: /* @__PURE__ */ jsxRuntime.jsx(exports.TableExportButton, { onClick: handleExport, disabled, label: exportLabel }) })
355
+ sendMessage && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end mt-1", children: /* @__PURE__ */ jsxRuntime.jsx(exports.TableExportButton, { onClick: handleExport, disabled, label: resolvedExportLabel }) })
349
356
  ] });
350
357
  });
351
358
  }
@@ -365,9 +372,12 @@ function CodeBlock({
365
372
  language,
366
373
  value,
367
374
  inline,
368
- copyLabel = "Copy",
369
- copiedLabel = "Copied!"
375
+ copyLabel,
376
+ copiedLabel
370
377
  }) {
378
+ const { t } = useTranslation();
379
+ const resolvedCopyLabel = copyLabel ?? t("BiChat.Message.Copy");
380
+ const resolvedCopiedLabel = copiedLabel ?? t("BiChat.Message.Copied");
371
381
  const [copied, setCopied] = React.useState(false);
372
382
  const [copyFailed, setCopyFailed] = React.useState(false);
373
383
  const [isDarkMode, setIsDarkMode] = React.useState(getInitialDarkMode);
@@ -443,14 +453,14 @@ function CodeBlock({
443
453
  {
444
454
  onClick: handleCopy,
445
455
  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"}`,
446
- title: copyLabel,
456
+ title: resolvedCopyLabel,
447
457
  "aria-live": "polite",
448
458
  children: copied ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
449
459
  /* @__PURE__ */ jsxRuntime.jsx(react.Check, { size: 16, className: "w-4 h-4" }),
450
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: copiedLabel })
460
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: resolvedCopiedLabel })
451
461
  ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
452
462
  /* @__PURE__ */ jsxRuntime.jsx(react.Copy, { size: 16, className: "w-4 h-4" }),
453
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: copyFailed ? "Failed" : copyLabel })
463
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: copyFailed ? t("BiChat.Message.CopyFailed") : resolvedCopyLabel })
454
464
  ] })
455
465
  }
456
466
  )
@@ -482,6 +492,7 @@ function CodeBlock({
482
492
  var getInitialDarkMode, languageMap; exports.CodeBlock = void 0; var CodeBlock_default;
483
493
  var init_CodeBlock = __esm({
484
494
  "ui/src/bichat/components/CodeBlock.tsx"() {
495
+ init_useTranslation();
485
496
  getInitialDarkMode = () => {
486
497
  if (typeof document === "undefined") return false;
487
498
  return document.documentElement.classList.contains("dark");
@@ -540,10 +551,14 @@ function MarkdownRenderer({
540
551
  citations,
541
552
  sendMessage,
542
553
  sendDisabled = false,
543
- copyLabel = "Copy",
544
- copiedLabel = "Copied!",
545
- exportLabel = "Export"
554
+ copyLabel,
555
+ copiedLabel,
556
+ exportLabel
546
557
  }) {
558
+ const { t } = useTranslation();
559
+ const resolvedCopyLabel = copyLabel ?? t("BiChat.Message.Copy");
560
+ const resolvedCopiedLabel = copiedLabel ?? t("BiChat.Message.Copied");
561
+ const resolvedExportLabel = exportLabel ?? t("BiChat.Export");
547
562
  const processed = React.useMemo(() => {
548
563
  return processCitations(content, citations);
549
564
  }, [content, citations]);
@@ -578,8 +593,8 @@ function MarkdownRenderer({
578
593
  language,
579
594
  value,
580
595
  inline: false,
581
- copyLabel,
582
- copiedLabel
596
+ copyLabel: resolvedCopyLabel,
597
+ copiedLabel: resolvedCopiedLabel
583
598
  }
584
599
  )
585
600
  }
@@ -616,7 +631,7 @@ function MarkdownRenderer({
616
631
  {
617
632
  sendMessage,
618
633
  disabled: sendDisabled,
619
- exportLabel,
634
+ exportLabel: resolvedExportLabel,
620
635
  children
621
636
  }
622
637
  ),
@@ -638,6 +653,7 @@ var init_MarkdownRenderer = __esm({
638
653
  init_chartSpec();
639
654
  init_TableWithExport();
640
655
  init_ChartCard();
656
+ init_useTranslation();
641
657
  CodeBlock2 = React.lazy(() => Promise.resolve().then(() => (init_CodeBlock(), CodeBlock_exports)).then((module) => ({ default: module.CodeBlock })));
642
658
  INLINE_TAGS = /* @__PURE__ */ new Set([
643
659
  "a",
@@ -706,43 +722,300 @@ var RateLimiter = class {
706
722
  }
707
723
  };
708
724
 
709
- // ui/src/bichat/utils/debugTrace.ts
710
- function hasMeaningfulUsage(trace) {
711
- if (!trace) return false;
712
- return trace.promptTokens > 0 || trace.completionTokens > 0 || trace.totalTokens > 0 || (trace.cachedTokens ?? 0) > 0 || (trace.cost ?? 0) > 0;
725
+ // ui/src/applet-devtools/enabled.ts
726
+ function shouldEnableAppletDevtools() {
727
+ if (typeof window === "undefined") return false;
728
+ const url = new URL(window.location.href);
729
+ if (url.searchParams.get("appletDebug") === "1") return true;
730
+ try {
731
+ return window.localStorage.getItem("iotaAppletDevtools") === "1";
732
+ } catch {
733
+ return false;
734
+ }
713
735
  }
714
- function hasDebugTrace(trace) {
715
- return trace.tools.length > 0 || hasMeaningfulUsage(trace.usage) || !!trace.generationMs;
736
+
737
+ // ui/src/applet-host/rpc.ts
738
+ var AppletRPCException = class extends Error {
739
+ constructor(args) {
740
+ super(args.message);
741
+ this.name = "AppletRPCException";
742
+ this.code = args.code;
743
+ this.details = args.details;
744
+ this.cause = args.cause;
745
+ }
746
+ };
747
+ function createAppletRPCClient(options) {
748
+ const fetcher = options.fetcher ?? fetch;
749
+ const timeoutMs = typeof options.timeoutMs === "number" && options.timeoutMs > 0 ? options.timeoutMs : 0;
750
+ async function call(method, params) {
751
+ const req = { id: crypto.randomUUID(), method, params };
752
+ const startedAt = typeof performance !== "undefined" ? performance.now() : Date.now();
753
+ const abortController = timeoutMs > 0 ? new AbortController() : void 0;
754
+ let timeoutHandle;
755
+ let timedOut = false;
756
+ maybeDispatchRPCEvent({
757
+ id: req.id,
758
+ method: req.method,
759
+ status: "start"
760
+ });
761
+ try {
762
+ if (abortController) {
763
+ timeoutHandle = setTimeout(() => {
764
+ timedOut = true;
765
+ abortController.abort();
766
+ }, timeoutMs);
767
+ }
768
+ const resp = await fetcher(options.endpoint, {
769
+ method: "POST",
770
+ headers: { "Content-Type": "application/json" },
771
+ body: JSON.stringify(req),
772
+ signal: abortController?.signal
773
+ });
774
+ if (!resp.ok) {
775
+ throw new AppletRPCException({
776
+ code: "http_error",
777
+ message: `HTTP ${resp.status}`,
778
+ details: { status: resp.status }
779
+ });
780
+ }
781
+ const json = await resp.json();
782
+ if (json.error) {
783
+ throw new AppletRPCException({
784
+ code: json.error.code,
785
+ message: json.error.message,
786
+ details: json.error.details
787
+ });
788
+ }
789
+ if (json.result === void 0) {
790
+ throw new AppletRPCException({
791
+ code: "invalid_response",
792
+ message: "Missing result in successful response"
793
+ });
794
+ }
795
+ maybeDispatchRPCEvent({
796
+ id: req.id,
797
+ method: req.method,
798
+ status: "success",
799
+ durationMs: elapsedMs(startedAt)
800
+ });
801
+ return json.result;
802
+ } catch (err) {
803
+ let rpcErr = err;
804
+ if (err instanceof Error && err.name === "AbortError") {
805
+ rpcErr = new AppletRPCException({
806
+ code: timedOut ? "timeout" : "aborted",
807
+ message: timedOut ? `RPC request timed out after ${timeoutMs}ms` : "RPC request was aborted",
808
+ cause: err
809
+ });
810
+ }
811
+ maybeDispatchRPCEvent({
812
+ id: req.id,
813
+ method: req.method,
814
+ status: "error",
815
+ durationMs: elapsedMs(startedAt),
816
+ error: rpcErr
817
+ });
818
+ throw rpcErr;
819
+ } finally {
820
+ if (timeoutHandle !== void 0) {
821
+ clearTimeout(timeoutHandle);
822
+ }
823
+ }
824
+ }
825
+ async function callTyped(method, params) {
826
+ return call(method, params);
827
+ }
828
+ return { call, callTyped };
716
829
  }
717
- function getSessionDebugUsage(turns) {
718
- let promptTokens = 0;
719
- let completionTokens = 0;
720
- let totalTokens = 0;
721
- let turnsWithUsage = 0;
722
- let latestPromptTokens = 0;
723
- let latestCompletionTokens = 0;
724
- let latestTotalTokens = 0;
725
- for (const turn of turns) {
726
- const usage = turn.assistantTurn?.debug?.usage;
727
- if (!hasMeaningfulUsage(usage) || !usage) {
728
- continue;
830
+ function maybeDispatchRPCEvent(detail) {
831
+ if (typeof window === "undefined") return;
832
+ if (!shouldEnableAppletDevtools()) return;
833
+ window.dispatchEvent(new CustomEvent("iota:applet-rpc", { detail }));
834
+ }
835
+ function elapsedMs(startedAt) {
836
+ const now = typeof performance !== "undefined" ? performance.now() : Date.now();
837
+ return Math.max(0, Math.round(now - startedAt));
838
+ }
839
+
840
+ // ui/src/bichat/utils/errorDisplay.ts
841
+ function isPermissionDeniedError(error) {
842
+ if (!error) return false;
843
+ if (error instanceof Error) {
844
+ const msg = error.message.toLowerCase();
845
+ if (msg.includes("forbidden") || msg.includes("permission denied")) return true;
846
+ }
847
+ if (typeof error === "object" && error !== null) {
848
+ const obj = error;
849
+ if (obj.code === "forbidden" || obj.code === 403) return true;
850
+ if (obj.status === 403) return true;
851
+ if (obj.statusCode === 403) return true;
852
+ if (typeof obj.response === "object" && obj.response !== null) {
853
+ const resp = obj.response;
854
+ if (resp.status === 403) return true;
729
855
  }
730
- turnsWithUsage++;
731
- promptTokens += usage.promptTokens;
732
- completionTokens += usage.completionTokens;
733
- totalTokens += usage.totalTokens;
734
- latestPromptTokens = usage.promptTokens;
735
- latestCompletionTokens = usage.completionTokens;
736
- latestTotalTokens = usage.totalTokens;
856
+ }
857
+ if (typeof error === "string") {
858
+ const lower = error.toLowerCase();
859
+ if (lower.includes("forbidden") || lower.includes("permission denied")) return true;
860
+ }
861
+ return false;
862
+ }
863
+ function extractStatus(error) {
864
+ if (!error || typeof error !== "object") return void 0;
865
+ const obj = error;
866
+ if (typeof obj.status === "number") return obj.status;
867
+ if (typeof obj.statusCode === "number") return obj.statusCode;
868
+ if (typeof obj.details === "object" && obj.details !== null) {
869
+ const details = obj.details;
870
+ if (typeof details.status === "number") return details.status;
871
+ if (typeof details.statusCode === "number") return details.statusCode;
872
+ }
873
+ if (typeof obj.response === "object" && obj.response !== null) {
874
+ const response = obj.response;
875
+ if (typeof response.status === "number") return response.status;
876
+ }
877
+ return void 0;
878
+ }
879
+ function isOfflineNow() {
880
+ return typeof navigator !== "undefined" && navigator.onLine === false;
881
+ }
882
+ function inferErrorCode(error) {
883
+ if (isOfflineNow()) return "offline";
884
+ if (error instanceof AppletRPCException) {
885
+ const code = String(error.code || "").toLowerCase().trim();
886
+ if (code) return code;
887
+ }
888
+ if (typeof error === "object" && error !== null) {
889
+ const obj = error;
890
+ if (typeof obj.code === "string" && obj.code.trim() !== "") {
891
+ return obj.code.toLowerCase();
892
+ }
893
+ if (typeof obj.error === "string" && obj.error.trim() !== "") {
894
+ return obj.error.toLowerCase();
895
+ }
896
+ }
897
+ const status = extractStatus(error);
898
+ if (status === 401 || status === 403) return "forbidden";
899
+ if (status === 404) return "not_found";
900
+ if (status === 408) return "timeout";
901
+ if (status === 413) return "payload_too_large";
902
+ if (status === 429) return "rate_limited";
903
+ if (status && status >= 500) return "server_error";
904
+ if (status && status >= 400) return "bad_request";
905
+ let message = "";
906
+ if (error instanceof Error) message = error.message;
907
+ if (!message && typeof error === "string") message = error;
908
+ const lower = message.toLowerCase();
909
+ if (lower.includes("timeout") || lower.includes("timed out")) return "timeout";
910
+ if (lower.includes("abort") || lower.includes("cancel")) return "aborted";
911
+ if (lower.includes("forbidden") || lower.includes("permission denied")) return "forbidden";
912
+ if (lower.includes("not found") || lower.includes("session not found")) return "not_found";
913
+ if (lower.includes("payload too large") || lower.includes("request too large") || lower.includes("413")) return "payload_too_large";
914
+ if (lower.includes("network") || lower.includes("failed to fetch")) return "network_error";
915
+ return "unknown";
916
+ }
917
+ function describeCode(code, fallbackTitle) {
918
+ switch (code) {
919
+ case "offline":
920
+ return {
921
+ title: "You are offline",
922
+ description: "Check your internet connection and try again.",
923
+ retryable: true
924
+ };
925
+ case "timeout":
926
+ return {
927
+ title: "Request timed out",
928
+ description: "The request took too long. Please try again.",
929
+ retryable: true
930
+ };
931
+ case "aborted":
932
+ return {
933
+ title: "Request canceled",
934
+ description: "The request was canceled before completion.",
935
+ retryable: true
936
+ };
937
+ case "forbidden":
938
+ return {
939
+ title: "Access denied",
940
+ description: "Your account does not have permission for this action.",
941
+ retryable: false
942
+ };
943
+ case "not_found":
944
+ return {
945
+ title: "Not found",
946
+ description: "The requested resource could not be found.",
947
+ retryable: false
948
+ };
949
+ case "payload_too_large":
950
+ return {
951
+ title: "Attachment too large",
952
+ description: "The uploaded payload exceeds allowed limits. Reduce file size and retry.",
953
+ retryable: false
954
+ };
955
+ case "invalid_request":
956
+ case "validation":
957
+ case "bad_request":
958
+ return {
959
+ title: "Invalid request",
960
+ description: "The request could not be processed. Review the input and try again.",
961
+ retryable: false
962
+ };
963
+ case "rate_limited":
964
+ return {
965
+ title: "Too many requests",
966
+ description: "Please wait a moment before trying again.",
967
+ retryable: true
968
+ };
969
+ case "http_error":
970
+ case "server_error":
971
+ return {
972
+ title: "Server error",
973
+ description: "The server failed to process this request. Please retry shortly.",
974
+ retryable: true
975
+ };
976
+ case "network_error":
977
+ return {
978
+ title: "Network error",
979
+ description: "A network issue interrupted the request. Please try again.",
980
+ retryable: true
981
+ };
982
+ default:
983
+ return {
984
+ title: fallbackTitle,
985
+ description: "Something went wrong. Please try again.",
986
+ retryable: true
987
+ };
988
+ }
989
+ }
990
+ function normalizeRPCError(error, fallbackTitle) {
991
+ const code = inferErrorCode(error);
992
+ const base = describeCode(code, fallbackTitle);
993
+ const permissionDenied = code === "forbidden" || isPermissionDeniedError(error);
994
+ let description = base.description;
995
+ if (error instanceof AppletRPCException && typeof error.message === "string" && error.message.trim() !== "" && base.title === fallbackTitle) {
996
+ description = error.message;
997
+ } else if (error instanceof Error && error.message && code === "unknown") {
998
+ description = error.message;
737
999
  }
738
1000
  return {
739
- promptTokens,
740
- completionTokens,
741
- totalTokens,
742
- turnsWithUsage,
743
- latestPromptTokens,
744
- latestCompletionTokens,
745
- latestTotalTokens
1001
+ code,
1002
+ title: base.title,
1003
+ description,
1004
+ userMessage: description || base.title,
1005
+ retryable: base.retryable,
1006
+ isPermissionDenied: permissionDenied,
1007
+ isTimeout: code === "timeout",
1008
+ isOffline: code === "offline",
1009
+ isCanceled: code === "aborted",
1010
+ isNotFound: code === "not_found"
1011
+ };
1012
+ }
1013
+ function toErrorDisplay(error, fallbackTitle) {
1014
+ const normalized = normalizeRPCError(error, fallbackTitle);
1015
+ return {
1016
+ title: normalized.title,
1017
+ description: normalized.description,
1018
+ isPermissionDenied: normalized.isPermissionDenied
746
1019
  };
747
1020
  }
748
1021
 
@@ -795,8 +1068,45 @@ function loadQueue(sessionId) {
795
1068
  }
796
1069
  }
797
1070
 
798
- // ui/src/bichat/context/ChatContext.tsx
799
- init_IotaContext();
1071
+ // ui/src/bichat/utils/debugTrace.ts
1072
+ function hasMeaningfulUsage(trace) {
1073
+ if (!trace) return false;
1074
+ return trace.promptTokens > 0 || trace.completionTokens > 0 || trace.totalTokens > 0 || (trace.cachedTokens ?? 0) > 0 || (trace.cost ?? 0) > 0;
1075
+ }
1076
+ function hasDebugTrace(trace) {
1077
+ return trace.tools.length > 0 || hasMeaningfulUsage(trace.usage) || !!trace.generationMs;
1078
+ }
1079
+ function getSessionDebugUsage(turns) {
1080
+ let promptTokens = 0;
1081
+ let completionTokens = 0;
1082
+ let totalTokens = 0;
1083
+ let turnsWithUsage = 0;
1084
+ let latestPromptTokens = 0;
1085
+ let latestCompletionTokens = 0;
1086
+ let latestTotalTokens = 0;
1087
+ for (const turn of turns) {
1088
+ const usage = turn.assistantTurn?.debug?.usage;
1089
+ if (!hasMeaningfulUsage(usage) || !usage) {
1090
+ continue;
1091
+ }
1092
+ turnsWithUsage++;
1093
+ promptTokens += usage.promptTokens;
1094
+ completionTokens += usage.completionTokens;
1095
+ totalTokens += usage.totalTokens;
1096
+ latestPromptTokens = usage.promptTokens;
1097
+ latestCompletionTokens = usage.completionTokens;
1098
+ latestTotalTokens = usage.totalTokens;
1099
+ }
1100
+ return {
1101
+ promptTokens,
1102
+ completionTokens,
1103
+ totalTokens,
1104
+ turnsWithUsage,
1105
+ latestPromptTokens,
1106
+ latestCompletionTokens,
1107
+ latestTotalTokens
1108
+ };
1109
+ }
800
1110
 
801
1111
  // ui/src/bichat/types/index.ts
802
1112
  var MessageRole = /* @__PURE__ */ ((MessageRole2) => {
@@ -873,7 +1183,7 @@ function readDebugLimitsFromGlobalContext() {
873
1183
  if (typeof window === "undefined") {
874
1184
  return null;
875
1185
  }
876
- const limits = window.__BICHAT_CONTEXT__?.extensions?.debug?.limits;
1186
+ const limits = window.__APPLET_CONTEXT__?.extensions?.debug?.limits;
877
1187
  if (!limits) {
878
1188
  return null;
879
1189
  }
@@ -893,580 +1203,924 @@ function readDebugLimitsFromGlobalContext() {
893
1203
  completionReserveTokens
894
1204
  };
895
1205
  }
896
- var SessionCtx = React.createContext(null);
897
- var MessagingCtx = React.createContext(null);
898
- var InputCtx = React.createContext(null);
899
- var DEFAULT_RATE_LIMIT_CONFIG = {
900
- maxRequests: 20,
901
- windowMs: 6e4
902
- };
903
- function ChatSessionProvider({
904
- dataSource,
905
- sessionId,
906
- rateLimiter: externalRateLimiter,
907
- rateLimitConfig,
908
- children
909
- }) {
910
- const [currentSessionId, setCurrentSessionId] = React.useState(sessionId);
911
- const [session, setSession] = React.useState(null);
912
- const [fetching, setFetching] = React.useState(false);
913
- const [error, setError] = React.useState(null);
914
- const [debugModeBySession, setDebugModeBySession] = React.useState({});
915
- const debugSessionKey = currentSessionId || "new";
916
- const debugMode = debugModeBySession[debugSessionKey] ?? false;
917
- const debugLimits = React.useMemo(() => readDebugLimitsFromGlobalContext(), []);
918
- const [turns, setTurns] = React.useState([]);
919
- const [loading, setLoading] = React.useState(false);
920
- const [streamingContent, setStreamingContent] = React.useState("");
921
- const [isStreaming, setIsStreaming] = React.useState(false);
922
- const [pendingQuestion, setPendingQuestion] = React.useState(null);
923
- const [codeOutputs, setCodeOutputs] = React.useState([]);
924
- const [isCompacting, setIsCompacting] = React.useState(false);
925
- const [compactionSummary, setCompactionSummary] = React.useState(null);
926
- const [artifactsInvalidationTrigger, setArtifactsInvalidationTrigger] = React.useState(0);
927
- const abortControllerRef = React.useRef(null);
928
- const [message, setMessage] = React.useState("");
929
- const [inputError, setInputError] = React.useState(null);
930
- const [messageQueue, setMessageQueue] = React.useState([]);
931
- const rateLimiterRef = React.useRef(
932
- externalRateLimiter || new RateLimiter(rateLimitConfig || DEFAULT_RATE_LIMIT_CONFIG)
933
- );
934
- const sessionRef = React.useRef({ currentSessionId, debugMode, debugSessionKey });
935
- sessionRef.current = { currentSessionId, debugMode, debugSessionKey };
936
- const messagingRef = React.useRef({ turns, pendingQuestion, loading });
937
- messagingRef.current = { turns, pendingQuestion, loading };
938
- const sessionDebugUsage = React.useMemo(() => getSessionDebugUsage(turns), [turns]);
939
- React.useEffect(() => {
940
- setCurrentSessionId(sessionId);
941
- }, [sessionId]);
942
- React.useEffect(() => {
943
- const sid = currentSessionId;
944
- if (!sid || sid === "new") {
945
- setMessageQueue([]);
946
- return;
947
- }
948
- setMessageQueue(loadQueue(sid));
949
- }, [currentSessionId]);
950
- React.useEffect(() => {
951
- const sid = currentSessionId;
952
- if (!sid || sid === "new") return;
953
- saveQueue(sid, messageQueue);
954
- }, [currentSessionId, messageQueue]);
955
- React.useEffect(() => {
956
- if (!currentSessionId || currentSessionId === "new") {
957
- setSession(null);
958
- setTurns([]);
959
- setPendingQuestion(null);
960
- setFetching(false);
961
- setInputError(null);
1206
+
1207
+ // ui/src/bichat/machine/selectors.ts
1208
+ function deriveDebugMode(state) {
1209
+ const key2 = state.session.currentSessionId || "new";
1210
+ return state.session.debugModeBySession[key2] ?? false;
1211
+ }
1212
+ function deriveSessionSnapshot(state, methods) {
1213
+ return {
1214
+ session: state.session.session,
1215
+ currentSessionId: state.session.currentSessionId,
1216
+ fetching: state.session.fetching,
1217
+ error: state.session.error,
1218
+ errorRetryable: state.session.errorRetryable,
1219
+ debugMode: deriveDebugMode(state),
1220
+ sessionDebugUsage: getSessionDebugUsage(state.messaging.turns),
1221
+ debugLimits: state.session.debugLimits,
1222
+ setError: methods.setError,
1223
+ retryFetchSession: methods.retryFetchSession
1224
+ };
1225
+ }
1226
+ function deriveMessagingSnapshot(state, methods) {
1227
+ return {
1228
+ turns: state.messaging.turns,
1229
+ streamingContent: state.messaging.streamingContent,
1230
+ isStreaming: state.messaging.isStreaming,
1231
+ streamError: state.messaging.streamError,
1232
+ streamErrorRetryable: state.messaging.streamErrorRetryable,
1233
+ loading: state.messaging.loading,
1234
+ pendingQuestion: state.messaging.pendingQuestion,
1235
+ codeOutputs: state.messaging.codeOutputs,
1236
+ isCompacting: state.messaging.isCompacting,
1237
+ compactionSummary: null,
1238
+ artifactsInvalidationTrigger: state.messaging.artifactsInvalidationTrigger,
1239
+ ...methods
1240
+ };
1241
+ }
1242
+ function deriveInputSnapshot(state, methods) {
1243
+ return {
1244
+ message: state.input.message,
1245
+ inputError: state.input.inputError,
1246
+ messageQueue: state.input.messageQueue,
1247
+ ...methods
1248
+ };
1249
+ }
1250
+
1251
+ // ui/src/bichat/machine/ChatMachine.ts
1252
+ var MAX_QUEUE_SIZE = 5;
1253
+ var ChatMachine = class {
1254
+ constructor(config) {
1255
+ // ── Refs (mutable, no subscription) ─────────────────────────────────────
1256
+ this.abortController = null;
1257
+ this.lastSendAttempt = null;
1258
+ /** Prevents fetchSession effect from clobbering state while stream is active. */
1259
+ this.sendingSessionId = null;
1260
+ this.fetchCancelled = false;
1261
+ this.disposed = false;
1262
+ /** Memoized sessionDebugUsage avoids unnecessary session re-renders during streaming. */
1263
+ this.lastSessionDebugUsage = null;
1264
+ // ── Listeners ───────────────────────────────────────────────────────────
1265
+ this.sessionListeners = /* @__PURE__ */ new Set();
1266
+ this.messagingListeners = /* @__PURE__ */ new Set();
1267
+ this.inputListeners = /* @__PURE__ */ new Set();
1268
+ // ── Snapshot caches (for useSyncExternalStore identity stability) ───────
1269
+ this.cachedSessionSnapshot = null;
1270
+ this.cachedMessagingSnapshot = null;
1271
+ this.cachedInputSnapshot = null;
1272
+ this.sessionSnapshotVersion = 0;
1273
+ this.messagingSnapshotVersion = 0;
1274
+ this.inputSnapshotVersion = 0;
1275
+ this.lastSessionSnapshotVersion = -1;
1276
+ this.lastMessagingSnapshotVersion = -1;
1277
+ this.lastInputSnapshotVersion = -1;
1278
+ // =====================================================================
1279
+ // Subscribe / getSnapshot (for useSyncExternalStore)
1280
+ // =====================================================================
1281
+ this.subscribeSession = (listener) => {
1282
+ this.sessionListeners.add(listener);
1283
+ return () => {
1284
+ this.sessionListeners.delete(listener);
1285
+ };
1286
+ };
1287
+ this.getSessionSnapshot = () => {
1288
+ if (this.lastSessionSnapshotVersion !== this.sessionSnapshotVersion) {
1289
+ this.cachedSessionSnapshot = deriveSessionSnapshot(this.state, {
1290
+ setError: this.setError,
1291
+ retryFetchSession: this.retryFetchSession
1292
+ });
1293
+ this.lastSessionSnapshotVersion = this.sessionSnapshotVersion;
1294
+ }
1295
+ return this.cachedSessionSnapshot;
1296
+ };
1297
+ this.subscribeMessaging = (listener) => {
1298
+ this.messagingListeners.add(listener);
1299
+ return () => {
1300
+ this.messagingListeners.delete(listener);
1301
+ };
1302
+ };
1303
+ this.getMessagingSnapshot = () => {
1304
+ if (this.lastMessagingSnapshotVersion !== this.messagingSnapshotVersion) {
1305
+ this.cachedMessagingSnapshot = deriveMessagingSnapshot(this.state, {
1306
+ sendMessage: this.sendMessage,
1307
+ handleRegenerate: this.handleRegenerate,
1308
+ handleEdit: this.handleEdit,
1309
+ handleCopy: this.handleCopy,
1310
+ handleSubmitQuestionAnswers: this.handleSubmitQuestionAnswers,
1311
+ handleRejectPendingQuestion: this.handleRejectPendingQuestion,
1312
+ retryLastMessage: this.retryLastMessage,
1313
+ clearStreamError: this.clearStreamError,
1314
+ cancel: this.cancel,
1315
+ setCodeOutputs: this.setCodeOutputs
1316
+ });
1317
+ this.lastMessagingSnapshotVersion = this.messagingSnapshotVersion;
1318
+ }
1319
+ return this.cachedMessagingSnapshot;
1320
+ };
1321
+ this.subscribeInput = (listener) => {
1322
+ this.inputListeners.add(listener);
1323
+ return () => {
1324
+ this.inputListeners.delete(listener);
1325
+ };
1326
+ };
1327
+ this.getInputSnapshot = () => {
1328
+ if (this.lastInputSnapshotVersion !== this.inputSnapshotVersion) {
1329
+ this.cachedInputSnapshot = deriveInputSnapshot(this.state, {
1330
+ setMessage: this.setMessage,
1331
+ setInputError: this.setInputError,
1332
+ handleSubmit: this.handleSubmit,
1333
+ handleUnqueue: this.handleUnqueue,
1334
+ enqueueMessage: this.enqueueMessage,
1335
+ removeQueueItem: this.removeQueueItem,
1336
+ updateQueueItem: this.updateQueueItem
1337
+ });
1338
+ this.lastInputSnapshotVersion = this.inputSnapshotVersion;
1339
+ }
1340
+ return this.cachedInputSnapshot;
1341
+ };
1342
+ this.dataSource = config.dataSource;
1343
+ this.rateLimiter = config.rateLimiter;
1344
+ this.onSessionCreated = config.onSessionCreated;
1345
+ this.state = {
1346
+ session: {
1347
+ currentSessionId: void 0,
1348
+ session: null,
1349
+ fetching: false,
1350
+ error: null,
1351
+ errorRetryable: false,
1352
+ debugModeBySession: {},
1353
+ debugLimits: readDebugLimitsFromGlobalContext()
1354
+ },
1355
+ messaging: {
1356
+ turns: [],
1357
+ streamingContent: "",
1358
+ isStreaming: false,
1359
+ streamError: null,
1360
+ streamErrorRetryable: false,
1361
+ loading: false,
1362
+ pendingQuestion: null,
1363
+ codeOutputs: [],
1364
+ isCompacting: false,
1365
+ artifactsInvalidationTrigger: 0
1366
+ },
1367
+ input: {
1368
+ message: "",
1369
+ inputError: null,
1370
+ messageQueue: []
1371
+ }
1372
+ };
1373
+ this.setError = this._setError.bind(this);
1374
+ this.retryFetchSession = this._retryFetchSession.bind(this);
1375
+ this.sendMessage = this._sendMessage.bind(this);
1376
+ this.handleRegenerate = this._handleRegenerate.bind(this);
1377
+ this.handleEdit = this._handleEdit.bind(this);
1378
+ this.handleCopy = this._handleCopy.bind(this);
1379
+ this.handleSubmitQuestionAnswers = this._handleSubmitQuestionAnswers.bind(this);
1380
+ this.handleRejectPendingQuestion = this._handleRejectPendingQuestion.bind(this);
1381
+ this.retryLastMessage = this._retryLastMessage.bind(this);
1382
+ this.clearStreamError = this._clearStreamError.bind(this);
1383
+ this.cancel = this._cancel.bind(this);
1384
+ this.setCodeOutputs = this._setCodeOutputs.bind(this);
1385
+ this.setMessage = this._setMessage.bind(this);
1386
+ this.setInputError = this._setInputError.bind(this);
1387
+ this.handleSubmit = this._handleSubmit.bind(this);
1388
+ this.handleUnqueue = this._handleUnqueue.bind(this);
1389
+ this.enqueueMessage = this._enqueueMessage.bind(this);
1390
+ this.removeQueueItem = this._removeQueueItem.bind(this);
1391
+ this.updateQueueItem = this._updateQueueItem.bind(this);
1392
+ }
1393
+ // =====================================================================
1394
+ // Lifecycle
1395
+ // =====================================================================
1396
+ /**
1397
+ * Set the active session ID. Triggers fetch when transitioning to a real
1398
+ * session, or resets state for 'new'/undefined.
1399
+ */
1400
+ setSessionId(id) {
1401
+ if (this.disposed) return;
1402
+ const prev = this.state.session.currentSessionId;
1403
+ if (id === prev) return;
1404
+ this.state.session.currentSessionId = id;
1405
+ this._notifySession();
1406
+ if (!id || id === "new") {
1407
+ this._updateInput({ messageQueue: [] });
1408
+ } else {
1409
+ this._updateInput({ messageQueue: loadQueue(id) });
1410
+ }
1411
+ this._fetchSessionIfNeeded();
1412
+ }
1413
+ /**
1414
+ * Update mutable config that may change across parent re-renders.
1415
+ * Called from the React provider's useEffect to keep the machine in sync.
1416
+ */
1417
+ updateConfig(config) {
1418
+ this.dataSource = config.dataSource;
1419
+ this.onSessionCreated = config.onSessionCreated;
1420
+ }
1421
+ dispose() {
1422
+ this.disposed = true;
1423
+ this.fetchCancelled = true;
1424
+ this.abortController?.abort();
1425
+ this.sessionListeners.clear();
1426
+ this.messagingListeners.clear();
1427
+ this.inputListeners.clear();
1428
+ }
1429
+ // =====================================================================
1430
+ // Private — state updates + notification
1431
+ // =====================================================================
1432
+ _updateSession(patch) {
1433
+ Object.assign(this.state.session, patch);
1434
+ this._notifySession();
1435
+ }
1436
+ _notifySession() {
1437
+ this.sessionSnapshotVersion++;
1438
+ for (const fn of this.sessionListeners) fn();
1439
+ }
1440
+ _updateMessaging(patch) {
1441
+ Object.assign(this.state.messaging, patch);
1442
+ this._notifyMessaging();
1443
+ if ("turns" in patch) {
1444
+ const newUsage = getSessionDebugUsage(this.state.messaging.turns);
1445
+ if (!sessionDebugUsageEqual(this.lastSessionDebugUsage, newUsage)) {
1446
+ this.lastSessionDebugUsage = newUsage;
1447
+ this._notifySession();
1448
+ }
1449
+ }
1450
+ }
1451
+ _notifyMessaging() {
1452
+ this.messagingSnapshotVersion++;
1453
+ for (const fn of this.messagingListeners) fn();
1454
+ }
1455
+ _updateInput(patch) {
1456
+ Object.assign(this.state.input, patch);
1457
+ if ("messageQueue" in patch) {
1458
+ this._persistQueue();
1459
+ }
1460
+ this._notifyInput();
1461
+ }
1462
+ _notifyInput() {
1463
+ this.inputSnapshotVersion++;
1464
+ for (const fn of this.inputListeners) fn();
1465
+ }
1466
+ _persistQueue() {
1467
+ const sid = this.state.session.currentSessionId;
1468
+ if (!sid || sid === "new") return;
1469
+ saveQueue(sid, this.state.input.messageQueue);
1470
+ }
1471
+ // =====================================================================
1472
+ // Private — session fetch
1473
+ // =====================================================================
1474
+ _fetchSessionIfNeeded() {
1475
+ const id = this.state.session.currentSessionId;
1476
+ if (!id || id === "new") {
1477
+ this._updateSession({
1478
+ session: null,
1479
+ fetching: false,
1480
+ error: null,
1481
+ errorRetryable: false
1482
+ });
1483
+ this._updateMessaging({
1484
+ turns: [],
1485
+ pendingQuestion: null
1486
+ });
1487
+ this._updateInput({ inputError: null });
962
1488
  return;
963
1489
  }
964
- let cancelled = false;
965
- setFetching(true);
966
- setError(null);
967
- setInputError(null);
968
- dataSource.fetchSession(currentSessionId).then((state) => {
969
- if (cancelled) return;
970
- if (state) {
971
- setSession(state.session);
972
- setTurns((prev) => {
973
- const hasPendingUserOnly = prev.length > 0 && !prev[prev.length - 1].assistantTurn;
974
- if (hasPendingUserOnly && (!state.turns || state.turns.length === 0)) {
975
- return prev;
976
- }
977
- return state.turns ?? prev;
978
- });
979
- setPendingQuestion(state.pendingQuestion || null);
1490
+ if (this.sendingSessionId === id) return;
1491
+ this.fetchCancelled = false;
1492
+ this._updateSession({ fetching: true, error: null, errorRetryable: false });
1493
+ this._updateInput({ inputError: null });
1494
+ const fetchId = id;
1495
+ this.dataSource.fetchSession(fetchId).then((result) => {
1496
+ if (this.fetchCancelled || this.disposed) return;
1497
+ if (this.state.session.currentSessionId !== fetchId) return;
1498
+ if (this.sendingSessionId === fetchId) return;
1499
+ if (result) {
1500
+ this._updateSession({ session: result.session, fetching: false });
1501
+ this._setTurnsFromFetch(result.turns);
1502
+ this._updateMessaging({ pendingQuestion: result.pendingQuestion || null });
980
1503
  } else {
981
- setError("Session not found");
1504
+ this._updateSession({ error: "Session not found", fetching: false });
982
1505
  }
983
- setFetching(false);
984
1506
  }).catch((err) => {
985
- if (cancelled) return;
986
- setError(err.message || "Failed to load session");
987
- setFetching(false);
1507
+ if (this.fetchCancelled || this.disposed) return;
1508
+ if (this.state.session.currentSessionId !== fetchId) return;
1509
+ if (this.sendingSessionId === fetchId) return;
1510
+ const normalized = normalizeRPCError(err, "Failed to load session");
1511
+ this._updateSession({
1512
+ error: normalized.userMessage,
1513
+ errorRetryable: normalized.retryable,
1514
+ fetching: false
1515
+ });
988
1516
  });
989
- return () => {
990
- cancelled = true;
991
- };
992
- }, [dataSource, currentSessionId]);
993
- const handleCopy = React.useCallback(async (text) => {
994
- await navigator.clipboard.writeText(text);
995
- }, []);
996
- const executeSlashCommand = React.useCallback(
997
- async (command) => {
998
- if (command.hasArgs) {
999
- setInputError("slash.error.noArguments");
1000
- return true;
1001
- }
1002
- setError(null);
1003
- setInputError(null);
1004
- if (command.name === "/debug") {
1005
- if (!hasPermission("bichat.export")) {
1006
- setInputError("slash.error.debugUnauthorized");
1007
- return true;
1008
- }
1009
- const curDebugMode = sessionRef.current.debugMode;
1010
- const curDebugSessionKey = sessionRef.current.debugSessionKey;
1011
- const curSessionId2 = sessionRef.current.currentSessionId;
1012
- const nextDebugMode = !curDebugMode;
1013
- setDebugModeBySession((prev) => ({
1014
- ...prev,
1015
- [curDebugSessionKey]: nextDebugMode
1016
- }));
1017
- if (nextDebugMode && curSessionId2 && curSessionId2 !== "new") {
1018
- try {
1019
- const state = await dataSource.fetchSession(curSessionId2);
1020
- if (state) {
1021
- setSession(state.session);
1022
- setTurns(state.turns);
1023
- setPendingQuestion(state.pendingQuestion || null);
1024
- }
1025
- } catch (err) {
1026
- console.error("Failed to refresh session for debug mode:", err);
1027
- }
1517
+ }
1518
+ /** Sets turns from fetch, preserving pending user-only turns if server hasn't caught up. */
1519
+ _setTurnsFromFetch(fetchedTurns) {
1520
+ const prev = this.state.messaging.turns;
1521
+ const hasPendingUserOnly = prev.length > 0 && !prev[prev.length - 1].assistantTurn;
1522
+ if (hasPendingUserOnly && (!fetchedTurns || fetchedTurns.length === 0)) {
1523
+ return;
1524
+ }
1525
+ this._updateMessaging({ turns: fetchedTurns ?? prev });
1526
+ }
1527
+ // =====================================================================
1528
+ // Private — actions
1529
+ // =====================================================================
1530
+ _setError(error) {
1531
+ this._updateSession({
1532
+ error,
1533
+ errorRetryable: false
1534
+ });
1535
+ }
1536
+ _retryFetchSession() {
1537
+ this._fetchSessionIfNeeded();
1538
+ }
1539
+ _clearStreamError() {
1540
+ this._updateMessaging({
1541
+ streamError: null,
1542
+ streamErrorRetryable: false
1543
+ });
1544
+ }
1545
+ _cancel() {
1546
+ if (this.abortController) {
1547
+ this.abortController.abort();
1548
+ this.abortController = null;
1549
+ this._updateMessaging({ isStreaming: false, loading: false });
1550
+ }
1551
+ }
1552
+ _setCodeOutputs(outputs) {
1553
+ this._updateMessaging({ codeOutputs: outputs });
1554
+ }
1555
+ _setMessage(message) {
1556
+ this._updateInput({ message });
1557
+ }
1558
+ _setInputError(error) {
1559
+ this._updateInput({ inputError: error });
1560
+ }
1561
+ // ── Slash commands ──────────────────────────────────────────────────────
1562
+ async _executeSlashCommand(command) {
1563
+ if (command.hasArgs) {
1564
+ this._updateInput({ inputError: "BiChat.Slash.ErrorNoArguments" });
1565
+ return true;
1566
+ }
1567
+ this._updateSession({ error: null, errorRetryable: false });
1568
+ this._updateInput({ inputError: null });
1569
+ this._clearStreamError();
1570
+ if (command.name === "/debug") {
1571
+ const debugMode = deriveDebugMode(this.state);
1572
+ const key2 = this.state.session.currentSessionId || "new";
1573
+ const nextDebugMode = !debugMode;
1574
+ this._updateSession({
1575
+ debugModeBySession: {
1576
+ ...this.state.session.debugModeBySession,
1577
+ [key2]: nextDebugMode
1028
1578
  }
1029
- setMessage("");
1030
- return true;
1031
- }
1032
- const curSessionId = sessionRef.current.currentSessionId;
1033
- if (!curSessionId || curSessionId === "new") {
1034
- setInputError("slash.error.sessionRequired");
1035
- return true;
1036
- }
1037
- if (command.name === "/clear") {
1038
- setLoading(true);
1039
- setStreamingContent("");
1579
+ });
1580
+ if (nextDebugMode && this.state.session.currentSessionId && this.state.session.currentSessionId !== "new") {
1040
1581
  try {
1041
- await dataSource.clearSessionHistory(curSessionId);
1042
- const state = await dataSource.fetchSession(curSessionId);
1043
- if (state) {
1044
- setSession(state.session);
1045
- setTurns(state.turns);
1046
- setPendingQuestion(state.pendingQuestion || null);
1047
- } else {
1048
- setTurns([]);
1582
+ const result = await this.dataSource.fetchSession(this.state.session.currentSessionId);
1583
+ if (result) {
1584
+ this._updateSession({ session: result.session });
1585
+ this._updateMessaging({
1586
+ turns: result.turns,
1587
+ pendingQuestion: result.pendingQuestion || null
1588
+ });
1049
1589
  }
1050
- setCompactionSummary(null);
1051
- setCodeOutputs([]);
1052
- setMessage("");
1053
1590
  } catch (err) {
1054
- setInputError(err instanceof Error ? err.message : "slash.error.clearFailed");
1055
- } finally {
1056
- setLoading(false);
1057
- setIsStreaming(false);
1591
+ console.error("Failed to refresh session for debug mode:", err);
1058
1592
  }
1059
- return true;
1060
1593
  }
1061
- if (command.name === "/compact") {
1062
- setLoading(true);
1063
- setIsCompacting(true);
1064
- setCompactionSummary(null);
1065
- setStreamingContent("");
1066
- try {
1067
- const result = await dataSource.compactSessionHistory(curSessionId);
1068
- const summary = result.summary || "";
1069
- setTurns([createCompactedSystemTurn(curSessionId, summary)]);
1070
- setCompactionSummary(null);
1071
- const state = await dataSource.fetchSession(curSessionId);
1072
- if (state) {
1073
- setSession(state.session);
1074
- setTurns(state.turns);
1075
- setPendingQuestion(state.pendingQuestion || null);
1076
- } else {
1077
- setTurns([]);
1078
- }
1079
- setCodeOutputs([]);
1080
- setMessage("");
1081
- } catch (err) {
1082
- setInputError(err instanceof Error ? err.message : "slash.error.compactFailed");
1083
- } finally {
1084
- setIsCompacting(false);
1085
- setLoading(false);
1086
- setIsStreaming(false);
1594
+ this._updateInput({ message: "" });
1595
+ return true;
1596
+ }
1597
+ const curSessionId = this.state.session.currentSessionId;
1598
+ if (!curSessionId || curSessionId === "new") {
1599
+ this._updateInput({ inputError: "BiChat.Slash.ErrorSessionRequired" });
1600
+ return true;
1601
+ }
1602
+ if (command.name === "/clear") {
1603
+ this._updateInput({ message: "" });
1604
+ this._updateMessaging({ loading: true, streamingContent: "" });
1605
+ try {
1606
+ await this.dataSource.clearSessionHistory(curSessionId);
1607
+ const result = await this.dataSource.fetchSession(curSessionId);
1608
+ if (result) {
1609
+ this._updateSession({ session: result.session });
1610
+ this._updateMessaging({
1611
+ turns: result.turns,
1612
+ pendingQuestion: result.pendingQuestion || null
1613
+ });
1614
+ } else {
1615
+ this._updateMessaging({ turns: [] });
1087
1616
  }
1088
- return true;
1617
+ this._updateMessaging({ codeOutputs: [] });
1618
+ } catch (err) {
1619
+ const normalized = normalizeRPCError(err, "Failed to clear session history");
1620
+ this._updateInput({ inputError: normalized.userMessage });
1621
+ } finally {
1622
+ this._updateMessaging({ loading: false, isStreaming: false });
1089
1623
  }
1090
- setInputError("slash.error.unknownCommand");
1091
1624
  return true;
1092
- },
1093
- [dataSource]
1094
- );
1095
- const sendMessageDirect = React.useCallback(
1096
- async (content, attachments = [], options) => {
1097
- if (!content.trim() || messagingRef.current.loading) return;
1098
- const trimmedContent = content.trim();
1099
- if (trimmedContent.startsWith("/")) {
1100
- const maybeCommand = parseSlashCommand(content);
1101
- if (!maybeCommand) {
1102
- setInputError("slash.error.unknownCommand");
1103
- return;
1104
- }
1105
- if (attachments.length > 0) {
1106
- setInputError("slash.error.noAttachments");
1107
- return;
1625
+ }
1626
+ if (command.name === "/compact") {
1627
+ this._updateInput({ message: "" });
1628
+ this._updateMessaging({
1629
+ loading: true,
1630
+ isCompacting: true,
1631
+ streamingContent: ""
1632
+ });
1633
+ try {
1634
+ const compactResult = await this.dataSource.compactSessionHistory(curSessionId);
1635
+ const summary = compactResult.summary || "";
1636
+ this._updateMessaging({
1637
+ turns: [createCompactedSystemTurn(curSessionId, summary)]
1638
+ });
1639
+ const result = await this.dataSource.fetchSession(curSessionId);
1640
+ if (result) {
1641
+ this._updateSession({ session: result.session });
1642
+ this._updateMessaging({
1643
+ turns: result.turns,
1644
+ pendingQuestion: result.pendingQuestion || null
1645
+ });
1646
+ } else {
1647
+ this._updateMessaging({ turns: [] });
1108
1648
  }
1109
- await executeSlashCommand(maybeCommand);
1649
+ this._updateMessaging({ codeOutputs: [] });
1650
+ } catch (err) {
1651
+ const normalized = normalizeRPCError(err, "Failed to compact session history");
1652
+ this._updateInput({ inputError: normalized.userMessage });
1653
+ } finally {
1654
+ this._updateMessaging({ isCompacting: false, loading: false, isStreaming: false });
1655
+ }
1656
+ return true;
1657
+ }
1658
+ this._updateInput({ inputError: "BiChat.Slash.ErrorUnknownCommand" });
1659
+ return true;
1660
+ }
1661
+ // ── Send message ────────────────────────────────────────────────────────
1662
+ /**
1663
+ * Public entry point (no options). Calls _sendMessageCore internally.
1664
+ */
1665
+ async _sendMessage(content, attachments = []) {
1666
+ return this._sendMessageCore(content, attachments);
1667
+ }
1668
+ /**
1669
+ * Internal entry point with options (for regenerate/edit).
1670
+ */
1671
+ async _sendMessageDirect(content, attachments, options) {
1672
+ return this._sendMessageCore(content, attachments, options);
1673
+ }
1674
+ /**
1675
+ * Core send-message logic. Handles slash commands, rate limiting, streaming,
1676
+ * session creation, optimistic turns, and auto-queue-drain.
1677
+ */
1678
+ async _sendMessageCore(content, attachments = [], options) {
1679
+ if (this.disposed) return;
1680
+ if (!content.trim() || this.state.messaging.loading) return;
1681
+ const trimmedContent = content.trim();
1682
+ if (trimmedContent.startsWith("/")) {
1683
+ const maybeCommand = parseSlashCommand(content);
1684
+ if (!maybeCommand) {
1685
+ this._updateInput({ inputError: "BiChat.Slash.ErrorUnknownCommand" });
1110
1686
  return;
1111
1687
  }
1112
- if (!rateLimiterRef.current.canMakeRequest()) {
1113
- const timeUntilNext = rateLimiterRef.current.getTimeUntilNextRequest();
1114
- const seconds = Math.ceil(timeUntilNext / 1e3);
1115
- setError(`Rate limit exceeded. Please wait ${seconds} seconds before sending another message.`);
1688
+ if (attachments.length > 0) {
1689
+ this._updateInput({ inputError: "BiChat.Slash.ErrorNoAttachments" });
1116
1690
  return;
1117
1691
  }
1118
- setMessage("");
1119
- setLoading(true);
1120
- setError(null);
1121
- setInputError(null);
1122
- setStreamingContent("");
1123
- setCompactionSummary(null);
1124
- abortControllerRef.current = new AbortController();
1125
- const curSessionId = sessionRef.current.currentSessionId;
1126
- const curDebugMode = sessionRef.current.debugMode;
1127
- const tempTurn = createPendingTurn(curSessionId || "new", content, attachments);
1128
- const replaceFromMessageID = options?.replaceFromMessageID;
1129
- setTurns((prev) => {
1130
- if (!replaceFromMessageID) {
1131
- return [...prev, tempTurn];
1132
- }
1133
- const replaceIndex = prev.findIndex((turn) => turn.userTurn.id === replaceFromMessageID);
1134
- if (replaceIndex === -1) {
1135
- console.warn(
1136
- `[ChatContext] replaceFromMessageID "${replaceFromMessageID}" not found in turns; appending as new turn`
1137
- );
1138
- return [...prev, tempTurn];
1139
- }
1140
- return [...prev.slice(0, replaceIndex), tempTurn];
1692
+ await this._executeSlashCommand(maybeCommand);
1693
+ return;
1694
+ }
1695
+ if (!this.rateLimiter.canMakeRequest()) {
1696
+ const timeUntilNext = this.rateLimiter.getTimeUntilNextRequest();
1697
+ const seconds = Math.ceil(timeUntilNext / 1e3);
1698
+ this._updateInput({
1699
+ inputError: `Rate limit exceeded. Please wait ${seconds} seconds before sending another message.`
1141
1700
  });
1142
- try {
1143
- let activeSessionId = curSessionId;
1144
- let shouldNavigateAfter = false;
1145
- if (!activeSessionId || activeSessionId === "new") {
1146
- const result = await dataSource.createSession();
1147
- if (result) {
1148
- const createdSessionID = result.id;
1149
- activeSessionId = createdSessionID;
1150
- setCurrentSessionId(createdSessionID);
1151
- setDebugModeBySession((prev) => {
1152
- if (!curDebugMode) return prev;
1153
- return { ...prev, [createdSessionID]: true };
1701
+ return;
1702
+ }
1703
+ this._updateInput({ message: "", inputError: null });
1704
+ this._updateSession({ error: null, errorRetryable: false });
1705
+ this._clearStreamError();
1706
+ this._updateMessaging({
1707
+ loading: true,
1708
+ streamingContent: ""
1709
+ });
1710
+ this.abortController = new AbortController();
1711
+ const curSessionId = this.state.session.currentSessionId;
1712
+ const curDebugMode = deriveDebugMode(this.state);
1713
+ const replaceFromMessageID = options?.replaceFromMessageID;
1714
+ const tempTurn = createPendingTurn(curSessionId || "new", content, attachments);
1715
+ this.lastSendAttempt = { content, attachments, options };
1716
+ const prevTurns = this.state.messaging.turns;
1717
+ if (!replaceFromMessageID) {
1718
+ this._updateMessaging({ turns: [...prevTurns, tempTurn] });
1719
+ } else {
1720
+ const idx = prevTurns.findIndex((t) => t.userTurn.id === replaceFromMessageID);
1721
+ if (idx === -1) {
1722
+ console.warn(`[ChatMachine] replaceFromMessageID "${replaceFromMessageID}" not found; appending as new turn`);
1723
+ this._updateMessaging({ turns: [...prevTurns, tempTurn] });
1724
+ } else {
1725
+ this._updateMessaging({ turns: [...prevTurns.slice(0, idx), tempTurn] });
1726
+ }
1727
+ }
1728
+ let shouldDrainQueue = true;
1729
+ try {
1730
+ let activeSessionId = curSessionId;
1731
+ let shouldNavigateAfter = false;
1732
+ if (!activeSessionId || activeSessionId === "new") {
1733
+ const result = await this.dataSource.createSession();
1734
+ if (result) {
1735
+ const createdSessionID = result.id;
1736
+ activeSessionId = createdSessionID;
1737
+ this._updateSession({ currentSessionId: createdSessionID });
1738
+ if (curDebugMode) {
1739
+ this._updateSession({
1740
+ debugModeBySession: {
1741
+ ...this.state.session.debugModeBySession,
1742
+ [createdSessionID]: true
1743
+ }
1154
1744
  });
1155
- shouldNavigateAfter = true;
1156
1745
  }
1746
+ shouldNavigateAfter = true;
1157
1747
  }
1158
- let accumulatedContent = "";
1159
- let createdSessionId;
1160
- let sessionFetched = false;
1161
- setIsStreaming(true);
1162
- for await (const chunk of dataSource.sendMessage(
1163
- activeSessionId || "new",
1164
- content,
1165
- attachments,
1166
- abortControllerRef.current?.signal,
1167
- {
1168
- debugMode: curDebugMode,
1169
- replaceFromMessageID
1170
- }
1171
- )) {
1172
- if (abortControllerRef.current?.signal.aborted) {
1173
- break;
1748
+ }
1749
+ this.sendingSessionId = activeSessionId || null;
1750
+ let accumulatedContent = "";
1751
+ let createdSessionId;
1752
+ let sessionFetched = false;
1753
+ this._updateMessaging({ isStreaming: true });
1754
+ for await (const chunk of this.dataSource.sendMessage(
1755
+ activeSessionId || "new",
1756
+ content,
1757
+ attachments,
1758
+ this.abortController?.signal,
1759
+ {
1760
+ debugMode: curDebugMode,
1761
+ replaceFromMessageID
1762
+ }
1763
+ )) {
1764
+ if (this.abortController?.signal.aborted) break;
1765
+ if ((chunk.type === "chunk" || chunk.type === "content") && chunk.content) {
1766
+ accumulatedContent += chunk.content;
1767
+ this._updateMessaging({ streamingContent: accumulatedContent });
1768
+ } else if (chunk.type === "error") {
1769
+ throw new Error(chunk.error || "Stream error");
1770
+ } else if (chunk.type === "interrupt" || chunk.type === "done") {
1771
+ if (chunk.sessionId) {
1772
+ createdSessionId = chunk.sessionId;
1174
1773
  }
1175
- if ((chunk.type === "chunk" || chunk.type === "content") && chunk.content) {
1176
- accumulatedContent += chunk.content;
1177
- setStreamingContent(accumulatedContent);
1178
- } else if (chunk.type === "error") {
1179
- throw new Error(chunk.error || "Stream error");
1180
- } else if (chunk.type === "interrupt" || chunk.type === "done") {
1181
- if (chunk.sessionId) {
1182
- createdSessionId = chunk.sessionId;
1183
- }
1184
- if (!sessionFetched) {
1185
- sessionFetched = true;
1186
- const finalSessionId = createdSessionId || activeSessionId;
1187
- if (finalSessionId && finalSessionId !== "new") {
1188
- const state = await dataSource.fetchSession(finalSessionId);
1189
- if (state) {
1190
- setSession(state.session);
1191
- setTurns((prev) => {
1192
- const hasPendingUserOnly = prev.length > 0 && !prev[prev.length - 1].assistantTurn;
1193
- if (hasPendingUserOnly && (!state.turns || state.turns.length === 0)) {
1194
- return prev;
1195
- }
1196
- return state.turns ?? prev;
1197
- });
1198
- setPendingQuestion(state.pendingQuestion || null);
1199
- }
1774
+ if (!sessionFetched) {
1775
+ sessionFetched = true;
1776
+ const finalSessionId = createdSessionId || activeSessionId;
1777
+ if (finalSessionId && finalSessionId !== "new") {
1778
+ const fetchResult = await this.dataSource.fetchSession(finalSessionId);
1779
+ if (fetchResult) {
1780
+ this._updateSession({ session: fetchResult.session });
1781
+ this._setTurnsFromFetch(fetchResult.turns);
1782
+ this._updateMessaging({ pendingQuestion: fetchResult.pendingQuestion || null });
1200
1783
  }
1201
1784
  }
1202
- } else if (chunk.type === "user_message" && chunk.sessionId) {
1203
- createdSessionId = chunk.sessionId;
1204
- } else if (chunk.type === "tool_end" && chunk.tool?.name && ARTIFACT_TOOL_NAMES.has(chunk.tool.name)) {
1205
- setArtifactsInvalidationTrigger((n) => n + 1);
1206
1785
  }
1786
+ } else if (chunk.type === "user_message" && chunk.sessionId) {
1787
+ createdSessionId = chunk.sessionId;
1788
+ } else if (chunk.type === "tool_end" && chunk.tool?.name && ARTIFACT_TOOL_NAMES.has(chunk.tool.name)) {
1789
+ this._updateMessaging({
1790
+ artifactsInvalidationTrigger: this.state.messaging.artifactsInvalidationTrigger + 1
1791
+ });
1207
1792
  }
1208
- const targetSessionId = createdSessionId || activeSessionId;
1209
- if (shouldNavigateAfter && targetSessionId && targetSessionId !== "new") {
1210
- dataSource.navigateToSession?.(targetSessionId);
1211
- }
1212
- } catch (err) {
1213
- if (err instanceof Error && err.name === "AbortError") {
1214
- setMessage(content);
1215
- return;
1216
- }
1217
- setTurns((prev) => prev.filter((t) => t.id !== tempTurn.id));
1218
- const errorMessage = err instanceof Error ? err.message : "Error.NetworkError";
1219
- setInputError(errorMessage);
1220
- console.error("Send message error:", err);
1221
- } finally {
1222
- setLoading(false);
1223
- setStreamingContent("");
1224
- setIsStreaming(false);
1225
- abortControllerRef.current = null;
1226
1793
  }
1227
- },
1228
- [dataSource, executeSlashCommand]
1229
- );
1230
- const cancelStream = React.useCallback(() => {
1231
- if (abortControllerRef.current) {
1232
- abortControllerRef.current.abort();
1233
- abortControllerRef.current = null;
1234
- setIsStreaming(false);
1235
- setLoading(false);
1236
- }
1237
- }, []);
1238
- const handleSubmit = React.useCallback(
1239
- (e, attachments = []) => {
1240
- e.preventDefault();
1241
- if (!message.trim() && attachments.length === 0) return;
1242
- setInputError(null);
1243
- const convertedAttachments = attachments.map((att) => ({
1244
- clientKey: att.clientKey || crypto.randomUUID(),
1245
- filename: att.filename,
1246
- mimeType: att.mimeType,
1247
- sizeBytes: att.sizeBytes,
1248
- base64Data: att.base64Data,
1249
- url: att.url,
1250
- preview: att.preview
1251
- }));
1252
- sendMessageDirect(message, convertedAttachments);
1253
- },
1254
- [message, sendMessageDirect]
1255
- );
1256
- const handleUnqueue = React.useCallback(() => {
1257
- if (messageQueue.length === 0) {
1258
- return null;
1259
- }
1260
- const lastQueued = messageQueue[messageQueue.length - 1];
1261
- setMessageQueue((prev) => prev.slice(0, -1));
1262
- return {
1263
- content: lastQueued.content,
1264
- attachments: lastQueued.attachments
1265
- };
1266
- }, [messageQueue]);
1267
- const handleRegenerate = React.useCallback(
1268
- async (turnId) => {
1269
- const curSessionId = sessionRef.current.currentSessionId;
1270
- if (!curSessionId || curSessionId === "new") return;
1271
- const turn = messagingRef.current.turns.find((t) => t.id === turnId);
1272
- if (!turn) return;
1273
- setError(null);
1274
- try {
1275
- await sendMessageDirect(turn.userTurn.content, turn.userTurn.attachments, {
1276
- replaceFromMessageID: turn.userTurn.id
1277
- });
1278
- } catch (err) {
1279
- const errorMessage = err instanceof Error ? err.message : "Failed to regenerate response";
1280
- setError(errorMessage);
1281
- console.error("Regenerate error:", err);
1794
+ if (!sessionFetched) {
1795
+ const finalSessionId = createdSessionId || activeSessionId;
1796
+ if (finalSessionId && finalSessionId !== "new") {
1797
+ try {
1798
+ const fetchResult = await this.dataSource.fetchSession(finalSessionId);
1799
+ if (fetchResult) {
1800
+ this._updateSession({ session: fetchResult.session });
1801
+ this._updateMessaging({
1802
+ turns: fetchResult.turns ?? [],
1803
+ pendingQuestion: fetchResult.pendingQuestion || null
1804
+ });
1805
+ }
1806
+ } catch (fetchErr) {
1807
+ console.error("Failed to fetch session after stream:", fetchErr);
1808
+ }
1809
+ }
1282
1810
  }
1283
- },
1284
- [sendMessageDirect]
1285
- );
1286
- const handleEdit = React.useCallback(
1287
- async (turnId, newContent) => {
1288
- const curSessionId = sessionRef.current.currentSessionId;
1289
- if (!curSessionId || curSessionId === "new") {
1290
- setMessage(newContent);
1291
- setTurns((prev) => prev.filter((t) => t.id !== turnId));
1292
- return;
1811
+ const targetSessionId = createdSessionId || activeSessionId;
1812
+ if (shouldNavigateAfter && targetSessionId && targetSessionId !== "new") {
1813
+ if (this.onSessionCreated) {
1814
+ this.onSessionCreated(targetSessionId);
1815
+ } else {
1816
+ this.dataSource.navigateToSession?.(targetSessionId);
1817
+ }
1293
1818
  }
1294
- const turn = messagingRef.current.turns.find((t) => t.id === turnId);
1295
- if (!turn) {
1296
- setError("Failed to edit message");
1819
+ this._clearStreamError();
1820
+ this.lastSendAttempt = null;
1821
+ } catch (err) {
1822
+ if (err instanceof Error && err.name === "AbortError") {
1823
+ this._updateInput({ message: content });
1824
+ this._clearStreamError();
1825
+ shouldDrainQueue = false;
1297
1826
  return;
1298
1827
  }
1299
- setError(null);
1300
- try {
1301
- await sendMessageDirect(newContent, turn.userTurn.attachments, {
1302
- replaceFromMessageID: turn.userTurn.id
1303
- });
1304
- } catch (err) {
1305
- const errorMessage = err instanceof Error ? err.message : "Failed to edit message";
1306
- setError(errorMessage);
1307
- console.error("Edit error:", err);
1828
+ this._updateMessaging({
1829
+ turns: this.state.messaging.turns.filter((t) => t.id !== tempTurn.id)
1830
+ });
1831
+ const normalized = normalizeRPCError(err, "Failed to send message");
1832
+ this._updateInput({ inputError: normalized.userMessage });
1833
+ this._updateMessaging({
1834
+ streamError: normalized.userMessage,
1835
+ streamErrorRetryable: normalized.retryable
1836
+ });
1837
+ console.error("Send message error:", err);
1838
+ shouldDrainQueue = false;
1839
+ } finally {
1840
+ this._updateMessaging({
1841
+ loading: false,
1842
+ streamingContent: "",
1843
+ isStreaming: false
1844
+ });
1845
+ this.abortController = null;
1846
+ this.sendingSessionId = null;
1847
+ if (shouldDrainQueue) {
1848
+ const queue = this.state.input.messageQueue;
1849
+ if (queue.length > 0) {
1850
+ const next = queue[0];
1851
+ this._updateInput({ messageQueue: queue.slice(1) });
1852
+ setTimeout(() => {
1853
+ this._sendMessageCore(next.content, next.attachments);
1854
+ }, 0);
1855
+ }
1308
1856
  }
1309
- },
1310
- [sendMessageDirect]
1311
- );
1312
- const handleSubmitQuestionAnswers = React.useCallback(
1313
- (answers) => {
1314
- const curSessionId = sessionRef.current.currentSessionId;
1315
- const curPendingQuestion = messagingRef.current.pendingQuestion;
1316
- if (!curSessionId || !curPendingQuestion) return;
1317
- setLoading(true);
1318
- setError(null);
1319
- const previousPendingQuestion = curPendingQuestion;
1320
- setPendingQuestion(null);
1321
- (async () => {
1322
- try {
1323
- const result = await dataSource.submitQuestionAnswers(
1324
- curSessionId,
1325
- previousPendingQuestion.id,
1326
- answers
1327
- );
1328
- if (result.success) {
1329
- if (curSessionId !== "new") {
1330
- try {
1331
- const state = await dataSource.fetchSession(curSessionId);
1332
- if (state) {
1333
- setTurns(state.turns);
1334
- setPendingQuestion(state.pendingQuestion || null);
1335
- } else {
1336
- setPendingQuestion(previousPendingQuestion);
1337
- setError("Failed to load updated session");
1338
- }
1339
- } catch (fetchErr) {
1340
- setPendingQuestion(previousPendingQuestion);
1341
- const errorMessage = fetchErr instanceof Error ? fetchErr.message : "Failed to load updated session";
1342
- setError(errorMessage);
1343
- }
1857
+ }
1858
+ }
1859
+ // ── Retry ───────────────────────────────────────────────────────────────
1860
+ async _retryLastMessage() {
1861
+ const lastAttempt = this.lastSendAttempt;
1862
+ if (!lastAttempt || this.state.messaging.loading) return;
1863
+ this._clearStreamError();
1864
+ this._updateInput({ inputError: null });
1865
+ await this._sendMessageDirect(lastAttempt.content, lastAttempt.attachments, lastAttempt.options);
1866
+ }
1867
+ // ── Regenerate / Edit ───────────────────────────────────────────────────
1868
+ async _handleRegenerate(turnId) {
1869
+ const curSessionId = this.state.session.currentSessionId;
1870
+ if (!curSessionId || curSessionId === "new") return;
1871
+ const turn = this.state.messaging.turns.find((t) => t.id === turnId);
1872
+ if (!turn) return;
1873
+ this._updateSession({ error: null, errorRetryable: false });
1874
+ await this._sendMessageDirect(turn.userTurn.content, turn.userTurn.attachments, {
1875
+ replaceFromMessageID: turn.userTurn.id
1876
+ });
1877
+ }
1878
+ async _handleEdit(turnId, newContent) {
1879
+ const curSessionId = this.state.session.currentSessionId;
1880
+ if (!curSessionId || curSessionId === "new") {
1881
+ this._updateInput({ message: newContent });
1882
+ this._updateMessaging({
1883
+ turns: this.state.messaging.turns.filter((t) => t.id !== turnId)
1884
+ });
1885
+ return;
1886
+ }
1887
+ const turn = this.state.messaging.turns.find((t) => t.id === turnId);
1888
+ if (!turn) {
1889
+ this._updateSession({ error: "Failed to edit message", errorRetryable: false });
1890
+ return;
1891
+ }
1892
+ this._updateSession({ error: null, errorRetryable: false });
1893
+ await this._sendMessageDirect(newContent, turn.userTurn.attachments, {
1894
+ replaceFromMessageID: turn.userTurn.id
1895
+ });
1896
+ }
1897
+ async _handleCopy(text) {
1898
+ if (typeof navigator === "undefined" || !navigator.clipboard) return;
1899
+ try {
1900
+ await navigator.clipboard.writeText(text);
1901
+ } catch {
1902
+ }
1903
+ }
1904
+ // ── HITL ────────────────────────────────────────────────────────────────
1905
+ async _handleSubmitQuestionAnswers(answers) {
1906
+ const curSessionId = this.state.session.currentSessionId;
1907
+ const curPendingQuestion = this.state.messaging.pendingQuestion;
1908
+ if (!curSessionId || !curPendingQuestion) return;
1909
+ this._updateMessaging({ loading: true });
1910
+ this._updateSession({ error: null, errorRetryable: false });
1911
+ const previousPendingQuestion = curPendingQuestion;
1912
+ this._updateMessaging({ pendingQuestion: null });
1913
+ try {
1914
+ const result = await this.dataSource.submitQuestionAnswers(
1915
+ curSessionId,
1916
+ previousPendingQuestion.id,
1917
+ answers
1918
+ );
1919
+ if (this.disposed) return;
1920
+ if (result.success) {
1921
+ if (curSessionId !== "new") {
1922
+ try {
1923
+ const fetchResult = await this.dataSource.fetchSession(curSessionId);
1924
+ if (this.disposed) return;
1925
+ if (fetchResult) {
1926
+ this._updateMessaging({
1927
+ turns: fetchResult.turns,
1928
+ pendingQuestion: fetchResult.pendingQuestion || null
1929
+ });
1930
+ } else {
1931
+ this._updateMessaging({ pendingQuestion: previousPendingQuestion });
1932
+ this._updateSession({ error: "Failed to load updated session", errorRetryable: false });
1344
1933
  }
1345
- } else {
1346
- setPendingQuestion(previousPendingQuestion);
1347
- setError(result.error || "Failed to submit answers");
1934
+ } catch (fetchErr) {
1935
+ if (this.disposed) return;
1936
+ this._updateMessaging({ pendingQuestion: previousPendingQuestion });
1937
+ const normalized = normalizeRPCError(fetchErr, "Failed to load updated session");
1938
+ this._updateSession({ error: normalized.userMessage, errorRetryable: normalized.retryable });
1348
1939
  }
1349
- } catch (err) {
1350
- setPendingQuestion(previousPendingQuestion);
1351
- const errorMessage = err instanceof Error ? err.message : "Failed to submit answers";
1352
- setError(errorMessage);
1353
- } finally {
1354
- setLoading(false);
1355
1940
  }
1356
- })();
1357
- },
1358
- [dataSource]
1359
- );
1360
- const handleRejectPendingQuestion = React.useCallback(async () => {
1361
- const curSessionId = sessionRef.current.currentSessionId;
1362
- const curPendingQuestion = messagingRef.current.pendingQuestion;
1941
+ } else {
1942
+ this._updateMessaging({ pendingQuestion: previousPendingQuestion });
1943
+ this._updateSession({ error: result.error || "Failed to submit answers", errorRetryable: false });
1944
+ }
1945
+ } catch (err) {
1946
+ if (this.disposed) return;
1947
+ this._updateMessaging({ pendingQuestion: previousPendingQuestion });
1948
+ const normalized = normalizeRPCError(err, "Failed to submit answers");
1949
+ this._updateSession({ error: normalized.userMessage, errorRetryable: normalized.retryable });
1950
+ } finally {
1951
+ if (!this.disposed) {
1952
+ this._updateMessaging({ loading: false });
1953
+ }
1954
+ }
1955
+ }
1956
+ async _handleRejectPendingQuestion() {
1957
+ const curSessionId = this.state.session.currentSessionId;
1958
+ const curPendingQuestion = this.state.messaging.pendingQuestion;
1363
1959
  if (!curSessionId || !curPendingQuestion) return;
1364
1960
  try {
1365
- const result = await dataSource.rejectPendingQuestion(curSessionId);
1961
+ const result = await this.dataSource.rejectPendingQuestion(curSessionId);
1962
+ if (this.disposed) return;
1366
1963
  if (result.success) {
1367
- setPendingQuestion(null);
1964
+ this._updateMessaging({ pendingQuestion: null });
1368
1965
  if (curSessionId !== "new") {
1369
- const state = await dataSource.fetchSession(curSessionId);
1370
- if (state) {
1371
- setSession(state.session);
1372
- setTurns((prev) => {
1373
- const hasPendingUserOnly = prev.length > 0 && !prev[prev.length - 1].assistantTurn;
1374
- if (hasPendingUserOnly && (!state.turns || state.turns.length === 0)) {
1375
- return prev;
1376
- }
1377
- return state.turns ?? prev;
1378
- });
1379
- setPendingQuestion(state.pendingQuestion || null);
1966
+ const fetchResult = await this.dataSource.fetchSession(curSessionId);
1967
+ if (this.disposed) return;
1968
+ if (fetchResult) {
1969
+ this._updateSession({ session: fetchResult.session });
1970
+ this._setTurnsFromFetch(fetchResult.turns);
1971
+ this._updateMessaging({ pendingQuestion: fetchResult.pendingQuestion || null });
1380
1972
  }
1381
1973
  }
1382
1974
  } else {
1383
- setError(result.error || "Failed to reject question");
1975
+ this._updateSession({ error: result.error || "Failed to reject question", errorRetryable: false });
1384
1976
  }
1385
1977
  } catch (err) {
1386
- const errorMessage = err instanceof Error ? err.message : "Failed to reject question";
1387
- setError(errorMessage);
1978
+ if (this.disposed) return;
1979
+ const normalized = normalizeRPCError(err, "Failed to reject question");
1980
+ this._updateSession({ error: normalized.userMessage, errorRetryable: normalized.retryable });
1388
1981
  }
1389
- }, [dataSource]);
1390
- const sessionValue = React.useMemo(() => ({
1391
- session,
1392
- currentSessionId,
1393
- fetching,
1394
- error,
1395
- debugMode,
1396
- sessionDebugUsage,
1397
- debugLimits,
1398
- setError
1399
- }), [session, currentSessionId, fetching, error, debugMode, sessionDebugUsage, debugLimits]);
1400
- const messagingValue = React.useMemo(() => ({
1401
- turns,
1402
- streamingContent,
1403
- isStreaming,
1404
- loading,
1405
- pendingQuestion,
1406
- codeOutputs,
1407
- isCompacting,
1408
- compactionSummary,
1409
- artifactsInvalidationTrigger,
1410
- sendMessage: sendMessageDirect,
1411
- handleRegenerate,
1412
- handleEdit,
1413
- handleCopy,
1414
- handleSubmitQuestionAnswers,
1415
- handleRejectPendingQuestion,
1416
- cancel: cancelStream,
1417
- setCodeOutputs
1418
- }), [
1419
- turns,
1420
- streamingContent,
1421
- isStreaming,
1422
- loading,
1423
- pendingQuestion,
1424
- codeOutputs,
1425
- isCompacting,
1426
- compactionSummary,
1427
- artifactsInvalidationTrigger,
1428
- sendMessageDirect,
1429
- handleRegenerate,
1430
- handleEdit,
1431
- handleCopy,
1432
- handleSubmitQuestionAnswers,
1433
- handleRejectPendingQuestion,
1434
- cancelStream
1435
- ]);
1436
- const inputValue = React.useMemo(() => ({
1437
- message,
1438
- inputError,
1439
- messageQueue,
1440
- setMessage,
1441
- setInputError,
1442
- handleSubmit,
1443
- handleUnqueue
1444
- }), [message, inputError, messageQueue, handleSubmit, handleUnqueue]);
1445
- return /* @__PURE__ */ jsxRuntime.jsx(SessionCtx.Provider, { value: sessionValue, children: /* @__PURE__ */ jsxRuntime.jsx(MessagingCtx.Provider, { value: messagingValue, children: /* @__PURE__ */ jsxRuntime.jsx(InputCtx.Provider, { value: inputValue, children }) }) });
1982
+ }
1983
+ // ── Input / queue ───────────────────────────────────────────────────────
1984
+ _handleSubmit(e, attachments = []) {
1985
+ e.preventDefault();
1986
+ const msg = this.state.input.message;
1987
+ if (!msg.trim() && attachments.length === 0) return;
1988
+ this._updateInput({ inputError: null });
1989
+ this._clearStreamError();
1990
+ const convertedAttachments = attachments.map((att) => ({
1991
+ clientKey: att.clientKey || crypto.randomUUID(),
1992
+ filename: att.filename,
1993
+ mimeType: att.mimeType,
1994
+ sizeBytes: att.sizeBytes,
1995
+ base64Data: att.base64Data,
1996
+ url: att.url,
1997
+ preview: att.preview
1998
+ }));
1999
+ if (this.state.messaging.loading) {
2000
+ const ok = this._enqueueMessage(msg.trim(), convertedAttachments);
2001
+ if (ok) {
2002
+ this._updateInput({ message: "" });
2003
+ }
2004
+ return;
2005
+ }
2006
+ this._sendMessage(msg.trim(), convertedAttachments);
2007
+ }
2008
+ _handleUnqueue() {
2009
+ const queue = this.state.input.messageQueue;
2010
+ if (queue.length === 0) return null;
2011
+ const last = queue[queue.length - 1];
2012
+ this._updateInput({ messageQueue: queue.slice(0, -1) });
2013
+ return { content: last.content, attachments: last.attachments };
2014
+ }
2015
+ _enqueueMessage(content, attachments) {
2016
+ if (this.state.input.messageQueue.length >= MAX_QUEUE_SIZE) {
2017
+ this._updateInput({ inputError: "BiChat.Input.QueueFull" });
2018
+ return false;
2019
+ }
2020
+ this._updateInput({
2021
+ messageQueue: [...this.state.input.messageQueue, { content, attachments }]
2022
+ });
2023
+ return true;
2024
+ }
2025
+ _removeQueueItem(index) {
2026
+ this._updateInput({
2027
+ messageQueue: this.state.input.messageQueue.filter((_, i) => i !== index)
2028
+ });
2029
+ }
2030
+ _updateQueueItem(index, content) {
2031
+ this._updateInput({
2032
+ messageQueue: this.state.input.messageQueue.map(
2033
+ (item, i) => i === index ? { ...item, content } : item
2034
+ )
2035
+ });
2036
+ }
2037
+ };
2038
+ function sessionDebugUsageEqual(a, b) {
2039
+ if (!a) return false;
2040
+ 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;
1446
2041
  }
1447
- function useChatSession() {
1448
- const context = React.useContext(SessionCtx);
1449
- if (!context) {
1450
- throw new Error("useChatSession must be used within ChatSessionProvider");
2042
+ var MachineCtx = React.createContext(null);
2043
+ var DEFAULT_RATE_LIMIT_CONFIG = {
2044
+ maxRequests: 20,
2045
+ windowMs: 6e4
2046
+ };
2047
+ function ChatSessionProvider({
2048
+ dataSource,
2049
+ sessionId,
2050
+ rateLimiter: externalRateLimiter,
2051
+ rateLimitConfig,
2052
+ onSessionCreated,
2053
+ children
2054
+ }) {
2055
+ const machineRef = React.useRef(null);
2056
+ if (!machineRef.current) {
2057
+ machineRef.current = new ChatMachine({
2058
+ dataSource,
2059
+ rateLimiter: externalRateLimiter || new RateLimiter(rateLimitConfig || DEFAULT_RATE_LIMIT_CONFIG),
2060
+ onSessionCreated
2061
+ });
1451
2062
  }
1452
- return context;
2063
+ const machine = machineRef.current;
2064
+ React.useEffect(() => {
2065
+ machine.updateConfig({ dataSource, onSessionCreated });
2066
+ }, [machine, dataSource, onSessionCreated]);
2067
+ React.useEffect(() => {
2068
+ machine.setSessionId(sessionId);
2069
+ }, [machine, sessionId]);
2070
+ React.useEffect(() => {
2071
+ return () => {
2072
+ machine.dispose();
2073
+ };
2074
+ }, [machine]);
2075
+ return /* @__PURE__ */ jsxRuntime.jsx(MachineCtx.Provider, { value: machine, children });
1453
2076
  }
1454
- function useChatMessaging() {
1455
- const context = React.useContext(MessagingCtx);
1456
- if (!context) {
1457
- throw new Error("useChatMessaging must be used within ChatSessionProvider");
2077
+ function useMachine() {
2078
+ const machine = React.useContext(MachineCtx);
2079
+ if (!machine) {
2080
+ throw new Error("Chat hooks must be used within ChatSessionProvider");
1458
2081
  }
1459
- return context;
2082
+ return machine;
2083
+ }
2084
+ function useChatSession() {
2085
+ const machine = useMachine();
2086
+ return React.useSyncExternalStore(
2087
+ machine.subscribeSession,
2088
+ machine.getSessionSnapshot,
2089
+ machine.getSessionSnapshot
2090
+ // SSR fallback
2091
+ );
2092
+ }
2093
+ function useChatMessaging() {
2094
+ const machine = useMachine();
2095
+ return React.useSyncExternalStore(
2096
+ machine.subscribeMessaging,
2097
+ machine.getMessagingSnapshot,
2098
+ machine.getMessagingSnapshot
2099
+ );
1460
2100
  }
1461
2101
  function useOptionalChatMessaging() {
1462
- return React.useContext(MessagingCtx);
2102
+ const machine = React.useContext(MachineCtx);
2103
+ const snapshot = React.useSyncExternalStore(
2104
+ machine ? machine.subscribeMessaging : noopSubscribe,
2105
+ machine ? machine.getMessagingSnapshot : nullSnapshot,
2106
+ machine ? machine.getMessagingSnapshot : nullSnapshot
2107
+ );
2108
+ return machine ? snapshot : null;
2109
+ }
2110
+ function useChatInput() {
2111
+ const machine = useMachine();
2112
+ return React.useSyncExternalStore(
2113
+ machine.subscribeInput,
2114
+ machine.getInputSnapshot,
2115
+ machine.getInputSnapshot
2116
+ );
2117
+ }
2118
+ function noopSubscribe() {
2119
+ return () => {
2120
+ };
1463
2121
  }
1464
- function useChatInput() {
1465
- const context = React.useContext(InputCtx);
1466
- if (!context) {
1467
- throw new Error("useChatInput must be used within ChatSessionProvider");
1468
- }
1469
- return context;
2122
+ function nullSnapshot() {
2123
+ return null;
1470
2124
  }
1471
2125
 
1472
2126
  // ui/src/bichat/components/ChatHeader.tsx
@@ -1563,6 +2217,26 @@ function ChatHeader({ session, onBack, readOnly, logoSlot, actionsSlot }) {
1563
2217
  ] })
1564
2218
  ] }) });
1565
2219
  }
2220
+ function formatRelativeTime(date, t) {
2221
+ const messageDate = new Date(date);
2222
+ const now = /* @__PURE__ */ new Date();
2223
+ const diffMins = dateFns.differenceInMinutes(now, messageDate);
2224
+ const diffHours = dateFns.differenceInHours(now, messageDate);
2225
+ const diffDays = dateFns.differenceInDays(now, messageDate);
2226
+ if (diffMins < 1) {
2227
+ return t ? t("BiChat.RelativeTime.JustNow") : "Just now";
2228
+ }
2229
+ if (diffMins < 60) {
2230
+ return t ? t("BiChat.RelativeTime.MinutesAgo", { count: diffMins }) : `${diffMins}m ago`;
2231
+ }
2232
+ if (diffHours < 24) {
2233
+ return t ? t("BiChat.RelativeTime.HoursAgo", { count: diffHours }) : `${diffHours}h ago`;
2234
+ }
2235
+ if (diffDays <= 7) {
2236
+ return t ? t("BiChat.RelativeTime.DaysAgo", { count: diffDays }) : `${diffDays}d ago`;
2237
+ }
2238
+ return dateFns.format(messageDate, "HH:mm");
2239
+ }
1566
2240
  var MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024;
1567
2241
  var ALLOWED_MIME_TYPES = /* @__PURE__ */ new Set([
1568
2242
  "image/jpeg",
@@ -2284,7 +2958,7 @@ function UserMessage({
2284
2958
  [selectedImageIndex, imageAttachments.length]
2285
2959
  );
2286
2960
  const currentAttachment = selectedImageIndex !== null ? imageAttachments[selectedImageIndex] : null;
2287
- const timestamp = dateFns.formatDistanceToNow(new Date(turn.createdAt), { addSuffix: true });
2961
+ const timestamp = formatRelativeTime(turn.createdAt, t);
2288
2962
  const avatarSlotProps = { initials };
2289
2963
  const contentSlotProps = { content: turn.content };
2290
2964
  const attachmentsSlotProps = {
@@ -2329,7 +3003,7 @@ function UserMessage({
2329
3003
  value: draftContent,
2330
3004
  onChange: (e) => setDraftContent(e.target.value),
2331
3005
  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",
2332
- "aria-label": "Edit message"
3006
+ "aria-label": t("BiChat.Message.EditMessage")
2333
3007
  }
2334
3008
  ),
2335
3009
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end gap-2", children: [
@@ -2339,7 +3013,7 @@ function UserMessage({
2339
3013
  type: "button",
2340
3014
  onClick: handleEditCancel,
2341
3015
  className: "cursor-pointer px-3 py-1.5 rounded-lg bg-white/10 hover:bg-white/15 transition-colors text-sm font-medium",
2342
- children: "Cancel"
3016
+ children: t("BiChat.Message.Cancel")
2343
3017
  }
2344
3018
  ),
2345
3019
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -2349,7 +3023,7 @@ function UserMessage({
2349
3023
  onClick: handleEditSave,
2350
3024
  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",
2351
3025
  disabled: !draftContent.trim() || draftContent === turn.content,
2352
- children: "Save"
3026
+ children: t("BiChat.Message.Save")
2353
3027
  }
2354
3028
  )
2355
3029
  ] })
@@ -2364,7 +3038,7 @@ function UserMessage({
2364
3038
  {
2365
3039
  onClick: handleCopyClick,
2366
3040
  className: `cursor-pointer ${classes.actionButton} ${isCopied ? "text-green-600 dark:text-green-400" : ""}`,
2367
- "aria-label": "Copy message",
3041
+ "aria-label": t("BiChat.Message.CopyMessage"),
2368
3042
  title: isCopied ? t("BiChat.Message.Copied") : t("BiChat.Message.Copy"),
2369
3043
  children: isCopied ? /* @__PURE__ */ jsxRuntime.jsx(react.Check, { size: 14, weight: "bold" }) : /* @__PURE__ */ jsxRuntime.jsx(react.Copy, { size: 14, weight: "regular" })
2370
3044
  }
@@ -2374,8 +3048,8 @@ function UserMessage({
2374
3048
  {
2375
3049
  onClick: handleEditClick,
2376
3050
  className: `cursor-pointer ${classes.actionButton}`,
2377
- "aria-label": "Edit message",
2378
- title: "Edit",
3051
+ "aria-label": t("BiChat.Message.EditMessage"),
3052
+ title: t("BiChat.Message.EditMessage"),
2379
3053
  disabled: isEditing,
2380
3054
  children: /* @__PURE__ */ jsxRuntime.jsx(react.PencilSimple, { size: 14, weight: "regular" })
2381
3055
  }
@@ -2425,6 +3099,7 @@ function UserTurnView({
2425
3099
  }
2426
3100
  );
2427
3101
  }
3102
+ init_useTranslation();
2428
3103
  function toBase64(str) {
2429
3104
  const bytes = new TextEncoder().encode(str);
2430
3105
  let binary = "";
@@ -2434,16 +3109,17 @@ function toBase64(str) {
2434
3109
  return btoa(binary);
2435
3110
  }
2436
3111
  function CodeOutputsPanel({ outputs }) {
3112
+ const { t } = useTranslation();
2437
3113
  if (!outputs || outputs.length === 0) return null;
2438
3114
  return /* @__PURE__ */ jsxRuntime.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: [
2439
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs font-semibold text-gray-600 dark:text-gray-400 mb-2", children: "Code Output" }),
3115
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs font-semibold text-gray-600 dark:text-gray-400 mb-2", children: t("BiChat.CodeOutput.Title") }),
2440
3116
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", children: outputs.map((output, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2441
3117
  output.type === "image" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative group", children: [
2442
3118
  /* @__PURE__ */ jsxRuntime.jsx(
2443
3119
  "img",
2444
3120
  {
2445
3121
  src: output.content.startsWith("data:") ? output.content : `data:${output.mimeType || "image/png"};base64,${output.content}`,
2446
- alt: output.filename || "Code output",
3122
+ alt: output.filename || t("BiChat.CodeOutput.CodeOutput"),
2447
3123
  className: "max-w-full rounded border border-gray-300 dark:border-gray-600"
2448
3124
  }
2449
3125
  ),
@@ -2475,19 +3151,23 @@ function CodeOutputsPanel({ outputs }) {
2475
3151
  ] })
2476
3152
  ] }),
2477
3153
  output.type === "error" && /* @__PURE__ */ jsxRuntime.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: [
2478
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Error" }),
3154
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: t("BiChat.Error.Label") }),
2479
3155
  /* @__PURE__ */ jsxRuntime.jsx("pre", { className: "whitespace-pre-wrap", children: output.content })
2480
3156
  ] })
2481
3157
  ] }, index)) })
2482
3158
  ] });
2483
3159
  }
2484
3160
  var CodeOutputsPanel_default = CodeOutputsPanel;
3161
+
3162
+ // ui/src/bichat/components/StreamingCursor.tsx
3163
+ init_useTranslation();
2485
3164
  function StreamingCursor() {
3165
+ const { t } = useTranslation();
2486
3166
  return /* @__PURE__ */ jsxRuntime.jsx(
2487
3167
  "span",
2488
3168
  {
2489
3169
  className: "inline-block w-1.5 h-4 ml-0.5 bg-primary-600 dark:bg-primary-500 animate-pulse",
2490
- "aria-label": "AI is typing"
3170
+ "aria-label": t("BiChat.Common.AITyping")
2491
3171
  }
2492
3172
  );
2493
3173
  }
@@ -3266,7 +3946,7 @@ function AssistantMessage({
3266
3946
  await onRegenerate(turnId);
3267
3947
  }
3268
3948
  }, [onRegenerate, turnId]);
3269
- const timestamp = dateFns.formatDistanceToNow(new Date(turn.createdAt), { addSuffix: true });
3949
+ const timestamp = formatRelativeTime(turn.createdAt, t);
3270
3950
  const avatarSlotProps = { text: isSystemMessage ? "SYS" : "AI" };
3271
3951
  const contentSlotProps = {
3272
3952
  content: turn.content,
@@ -3320,7 +4000,7 @@ function AssistantMessage({
3320
4000
  {
3321
4001
  fallback: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm text-gray-400 dark:text-gray-500", children: [
3322
4002
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-4 h-4 border-2 border-gray-300 dark:border-gray-600 border-t-transparent rounded-full animate-spin" }),
3323
- "Loading..."
4003
+ t("BiChat.Common.Loading")
3324
4004
  ] }),
3325
4005
  children: /* @__PURE__ */ jsxRuntime.jsx(
3326
4006
  MarkdownRenderer2,
@@ -3374,7 +4054,7 @@ function AssistantMessage({
3374
4054
  ]
3375
4055
  }
3376
4056
  ),
3377
- explanationExpanded && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pt-3 text-sm text-gray-600 dark:text-gray-400", children: /* @__PURE__ */ jsxRuntime.jsx(React.Suspense, { fallback: /* @__PURE__ */ jsxRuntime.jsx("div", { children: "Loading..." }), children: /* @__PURE__ */ jsxRuntime.jsx(MarkdownRenderer2, { content: turn.explanation }) }) })
4057
+ explanationExpanded && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pt-3 text-sm text-gray-600 dark:text-gray-400", children: /* @__PURE__ */ jsxRuntime.jsx(React.Suspense, { fallback: /* @__PURE__ */ jsxRuntime.jsx("div", { children: t("BiChat.Common.Loading") }), children: /* @__PURE__ */ jsxRuntime.jsx(MarkdownRenderer2, { content: turn.explanation }) }) })
3378
4058
  ] })
3379
4059
  ) }),
3380
4060
  showDebug && /* @__PURE__ */ jsxRuntime.jsx(DebugPanel, { trace: turn.debug })
@@ -3395,7 +4075,7 @@ function AssistantMessage({
3395
4075
  {
3396
4076
  onClick: handleCopyClick,
3397
4077
  className: `cursor-pointer ${classes.actionButton} ${isCopied ? "text-green-600 dark:text-green-400" : ""}`,
3398
- "aria-label": "Copy message",
4078
+ "aria-label": t("BiChat.Message.CopyMessage"),
3399
4079
  title: isCopied ? t("BiChat.Message.Copied") : t("BiChat.Message.Copy"),
3400
4080
  children: isCopied ? /* @__PURE__ */ jsxRuntime.jsx(react.Check, { size: 14, weight: "bold" }) : /* @__PURE__ */ jsxRuntime.jsx(react.Copy, { size: 14, weight: "regular" })
3401
4081
  }
@@ -3405,8 +4085,8 @@ function AssistantMessage({
3405
4085
  {
3406
4086
  onClick: handleRegenerateClick,
3407
4087
  className: `cursor-pointer ${classes.actionButton}`,
3408
- "aria-label": "Regenerate response",
3409
- title: "Regenerate",
4088
+ "aria-label": t("BiChat.Message.Regenerate"),
4089
+ title: t("BiChat.Message.Regenerate"),
3410
4090
  children: /* @__PURE__ */ jsxRuntime.jsx(react.ArrowsClockwise, { size: 14, weight: "regular" })
3411
4091
  }
3412
4092
  )
@@ -3415,8 +4095,6 @@ function AssistantMessage({
3415
4095
  ] })
3416
4096
  ] });
3417
4097
  }
3418
-
3419
- // ui/src/bichat/components/SystemMessage.tsx
3420
4098
  init_useTranslation();
3421
4099
  var MarkdownRenderer3 = React.lazy(
3422
4100
  () => Promise.resolve().then(() => (init_MarkdownRenderer(), MarkdownRenderer_exports)).then((module) => ({ default: module.MarkdownRenderer }))
@@ -3483,7 +4161,7 @@ function SystemMessage({
3483
4161
  setIsCopied(false);
3484
4162
  }
3485
4163
  }, [content, onCopy]);
3486
- const timestamp = dateFns.formatDistanceToNow(new Date(createdAt), { addSuffix: true });
4164
+ const timestamp = formatRelativeTime(createdAt, t);
3487
4165
  const resolvedHeight = isExpanded ? contentHeight : COLLAPSED_HEIGHT;
3488
4166
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full max-w-3xl px-2 sm:px-4", children: /* @__PURE__ */ jsxRuntime.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: [
3489
4167
  /* @__PURE__ */ jsxRuntime.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" }),
@@ -3503,7 +4181,7 @@ function SystemMessage({
3503
4181
  focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50
3504
4182
  ${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"}
3505
4183
  `,
3506
- "aria-label": "Copy message",
4184
+ "aria-label": t("BiChat.Message.CopyMessage"),
3507
4185
  title: isCopied ? t("BiChat.Message.Copied") : t("BiChat.Message.Copy"),
3508
4186
  children: isCopied ? /* @__PURE__ */ jsxRuntime.jsx(react.Check, { size: 13, weight: "bold" }) : /* @__PURE__ */ jsxRuntime.jsx(react.Copy, { size: 13, weight: "regular" })
3509
4187
  }
@@ -3843,23 +4521,6 @@ var dropdownVariants = {
3843
4521
  transition: { duration: 0.1 }
3844
4522
  }
3845
4523
  };
3846
- var toastVariants = {
3847
- initial: { opacity: 0, y: -8 },
3848
- animate: {
3849
- opacity: 1,
3850
- y: 0,
3851
- transition: {
3852
- duration: prefersReducedMotion() ? 0 : 0.2
3853
- }
3854
- },
3855
- exit: {
3856
- opacity: 0,
3857
- y: -8,
3858
- transition: {
3859
- duration: prefersReducedMotion() ? 0 : 0.15
3860
- }
3861
- }
3862
- };
3863
4524
  var sessionItemVariants = {
3864
4525
  initial: { opacity: 0, x: -20 },
3865
4526
  animate: {
@@ -3921,7 +4582,7 @@ var prefersReducedMotion2 = () => {
3921
4582
  var getRandomVerb = (verbs, current) => {
3922
4583
  const available = verbs.filter((v) => v !== current);
3923
4584
  if (available.length === 0) {
3924
- return current || verbs[0] || "Thinking";
4585
+ return current || verbs[0] || "";
3925
4586
  }
3926
4587
  return available[Math.floor(Math.random() * available.length)];
3927
4588
  };
@@ -3963,7 +4624,7 @@ function TypingIndicator({
3963
4624
  animate: "animate",
3964
4625
  exit: "exit",
3965
4626
  className: "text-sm bichat-thinking-shimmer block",
3966
- "aria-label": `AI is ${verb}`,
4627
+ "aria-label": t("BiChat.Thinking.AriaLabel", { verb }),
3967
4628
  children: [
3968
4629
  verb,
3969
4630
  "..."
@@ -3977,6 +4638,9 @@ function TypingIndicator({
3977
4638
  }
3978
4639
  var MemoizedTypingIndicator = React.memo(TypingIndicator);
3979
4640
  MemoizedTypingIndicator.displayName = "TypingIndicator";
4641
+
4642
+ // ui/src/bichat/components/ScrollToBottomButton.tsx
4643
+ init_useTranslation();
3980
4644
  function ScrollToBottomButton({
3981
4645
  show,
3982
4646
  onClick,
@@ -3984,6 +4648,7 @@ function ScrollToBottomButton({
3984
4648
  disabled = false,
3985
4649
  label
3986
4650
  }) {
4651
+ const { t } = useTranslation();
3987
4652
  return /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: show && /* @__PURE__ */ jsxRuntime.jsx(
3988
4653
  "div",
3989
4654
  {
@@ -3999,7 +4664,7 @@ function ScrollToBottomButton({
3999
4664
  onClick: disabled ? void 0 : onClick,
4000
4665
  disabled,
4001
4666
  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"}`,
4002
- "aria-label": label || "Scroll to bottom",
4667
+ "aria-label": label || t("BiChat.Common.ScrollToBottom"),
4003
4668
  children: label ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4004
4669
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium text-white", children: label }),
4005
4670
  /* @__PURE__ */ jsxRuntime.jsx(react.ArrowDown, { size: 16, weight: "bold", className: "text-white" })
@@ -4013,23 +4678,6 @@ function ScrollToBottomButton({
4013
4678
  ) });
4014
4679
  }
4015
4680
  var ScrollToBottomButton_default = ScrollToBottomButton;
4016
- function CompactionDoodle({ title, subtitle }) {
4017
- return /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
4018
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-10 h-10", children: [
4019
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 rounded-full bg-primary-500/20 animate-pulse motion-reduce:animate-none" }),
4020
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-1 rounded-full bg-primary-500/40 animate-pulse motion-reduce:animate-none" }),
4021
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-3 rounded-full bg-primary-600" })
4022
- ] }),
4023
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4024
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-gray-900 dark:text-gray-100", children: title }),
4025
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: subtitle })
4026
- ] })
4027
- ] }) });
4028
- }
4029
- var CompactionDoodle_default = CompactionDoodle;
4030
-
4031
- // ui/src/bichat/components/MessageList.tsx
4032
- init_useTranslation();
4033
4681
 
4034
4682
  // ui/src/bichat/utils/markdownStream.ts
4035
4683
  function normalizeStreamingMarkdown(text) {
@@ -4058,7 +4706,6 @@ var MarkdownRenderer4 = React.lazy(
4058
4706
  () => Promise.resolve().then(() => (init_MarkdownRenderer(), MarkdownRenderer_exports)).then((m) => ({ default: m.MarkdownRenderer }))
4059
4707
  );
4060
4708
  function MessageList({ renderUserTurn, renderAssistantTurn, thinkingVerbs, readOnly }) {
4061
- const { t } = useTranslation();
4062
4709
  const { currentSessionId, fetching } = useChatSession();
4063
4710
  const { turns, streamingContent, isStreaming, loading, isCompacting } = useChatMessaging();
4064
4711
  const messagesEndRef = React.useRef(null);
@@ -4116,13 +4763,6 @@ function MessageList({ renderUserTurn, renderAssistantTurn, thinkingVerbs, readO
4116
4763
  );
4117
4764
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex-1 min-h-0", children: [
4118
4765
  /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className: "h-full overflow-y-auto px-4 py-6", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto space-y-6", children: [
4119
- isCompacting && /* @__PURE__ */ jsxRuntime.jsx(
4120
- CompactionDoodle_default,
4121
- {
4122
- title: t("BiChat.Slash.CompactingTitle"),
4123
- subtitle: t("BiChat.Slash.CompactingSubtitle")
4124
- }
4125
- ),
4126
4766
  fetching && turns.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-6", "aria-hidden": "true", children: [
4127
4767
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-3/5 max-w-md rounded-2xl bg-gray-100 dark:bg-gray-800 p-4 space-y-2", children: [
4128
4768
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-3 w-full rounded bg-gray-200 dark:bg-gray-700 animate-pulse" }),
@@ -4185,6 +4825,128 @@ function MessageList({ renderUserTurn, renderAssistantTurn, thinkingVerbs, readO
4185
4825
  )
4186
4826
  ] });
4187
4827
  }
4828
+
4829
+ // ui/src/bichat/components/MessageQueueList.tsx
4830
+ init_useTranslation();
4831
+ function MessageQueueList({ queue, onRemove, onUpdate }) {
4832
+ const { t } = useTranslation();
4833
+ const [editingIndex, setEditingIndex] = React.useState(null);
4834
+ const [editValue, setEditValue] = React.useState("");
4835
+ const startEdit = React.useCallback((index) => {
4836
+ setEditingIndex(index);
4837
+ setEditValue(queue[index].content);
4838
+ }, [queue]);
4839
+ const saveEdit = React.useCallback(() => {
4840
+ if (editingIndex === null) return;
4841
+ const trimmed = editValue.trim();
4842
+ if (trimmed) {
4843
+ onUpdate(editingIndex, trimmed);
4844
+ }
4845
+ setEditingIndex(null);
4846
+ setEditValue("");
4847
+ }, [editingIndex, editValue, onUpdate]);
4848
+ const cancelEdit = React.useCallback(() => {
4849
+ setEditingIndex(null);
4850
+ setEditValue("");
4851
+ }, []);
4852
+ if (queue.length === 0) return null;
4853
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-3 space-y-1.5", children: [
4854
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: t("BiChat.Input.QueuedMessages", { count: queue.length }) }) }),
4855
+ /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { initial: false, children: queue.map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(
4856
+ framerMotion.motion.div,
4857
+ {
4858
+ initial: { opacity: 0, height: 0 },
4859
+ animate: { opacity: 1, height: "auto" },
4860
+ exit: { opacity: 0, height: 0 },
4861
+ transition: { duration: 0.15 },
4862
+ className: "overflow-hidden",
4863
+ children: /* @__PURE__ */ jsxRuntime.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: [
4864
+ /* @__PURE__ */ jsxRuntime.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 }),
4865
+ editingIndex === index ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0 flex flex-col gap-1.5", children: [
4866
+ /* @__PURE__ */ jsxRuntime.jsx(
4867
+ "textarea",
4868
+ {
4869
+ value: editValue,
4870
+ onChange: (e) => setEditValue(e.target.value),
4871
+ onKeyDown: (e) => {
4872
+ if (e.key === "Enter" && !e.shiftKey) {
4873
+ e.preventDefault();
4874
+ saveEdit();
4875
+ }
4876
+ if (e.key === "Escape") cancelEdit();
4877
+ },
4878
+ 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",
4879
+ rows: 1,
4880
+ autoFocus: true
4881
+ }
4882
+ ),
4883
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-1", children: [
4884
+ /* @__PURE__ */ jsxRuntime.jsxs(
4885
+ "button",
4886
+ {
4887
+ type: "button",
4888
+ onClick: saveEdit,
4889
+ 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",
4890
+ children: [
4891
+ /* @__PURE__ */ jsxRuntime.jsx(react.Check, { size: 12, weight: "bold" }),
4892
+ t("BiChat.Message.Save")
4893
+ ]
4894
+ }
4895
+ ),
4896
+ /* @__PURE__ */ jsxRuntime.jsxs(
4897
+ "button",
4898
+ {
4899
+ type: "button",
4900
+ onClick: cancelEdit,
4901
+ 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",
4902
+ children: [
4903
+ /* @__PURE__ */ jsxRuntime.jsx(react.ArrowCounterClockwise, { size: 12 }),
4904
+ t("BiChat.Message.Cancel")
4905
+ ]
4906
+ }
4907
+ )
4908
+ ] })
4909
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "flex-1 min-w-0 text-gray-700 dark:text-gray-300 truncate", children: [
4910
+ item.content,
4911
+ item.attachments.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-1.5 text-gray-400 dark:text-gray-500", children: [
4912
+ "+",
4913
+ item.attachments.length,
4914
+ " ",
4915
+ t("BiChat.Input.AttachFiles").toLowerCase()
4916
+ ] })
4917
+ ] }),
4918
+ editingIndex !== index && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5 flex-shrink-0", children: [
4919
+ /* @__PURE__ */ jsxRuntime.jsx(
4920
+ "button",
4921
+ {
4922
+ type: "button",
4923
+ onClick: () => startEdit(index),
4924
+ 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",
4925
+ "aria-label": t("BiChat.Input.EditQueueItem"),
4926
+ title: t("BiChat.Input.EditQueueItem"),
4927
+ children: /* @__PURE__ */ jsxRuntime.jsx(react.PencilSimple, { size: 14 })
4928
+ }
4929
+ ),
4930
+ /* @__PURE__ */ jsxRuntime.jsx(
4931
+ "button",
4932
+ {
4933
+ type: "button",
4934
+ onClick: () => onRemove(index),
4935
+ 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",
4936
+ "aria-label": t("BiChat.Input.RemoveQueueItem"),
4937
+ title: t("BiChat.Input.RemoveQueueItem"),
4938
+ children: /* @__PURE__ */ jsxRuntime.jsx(react.X, { size: 14 })
4939
+ }
4940
+ )
4941
+ ] })
4942
+ ] })
4943
+ },
4944
+ `queue-${index}-${item.content.slice(0, 20)}`
4945
+ )) })
4946
+ ] });
4947
+ }
4948
+
4949
+ // ui/src/bichat/components/MessageInput.tsx
4188
4950
  init_useTranslation();
4189
4951
  var MAX_FILES_DEFAULT = 10;
4190
4952
  var MAX_FILE_SIZE_DEFAULT = 20 * 1024 * 1024;
@@ -4204,6 +4966,8 @@ var MessageInput = React.forwardRef(
4204
4966
  onMessageChange,
4205
4967
  onSubmit,
4206
4968
  onUnqueue,
4969
+ onRemoveQueueItem,
4970
+ onUpdateQueueItem,
4207
4971
  placeholder: placeholderOverride,
4208
4972
  maxFiles = MAX_FILES_DEFAULT,
4209
4973
  maxFileSize = MAX_FILE_SIZE_DEFAULT,
@@ -4427,14 +5191,22 @@ var MessageInput = React.forwardRef(
4427
5191
  return;
4428
5192
  }
4429
5193
  if (isCommandListVisible) {
4430
- if (e.key === "ArrowDown" || e.key === "Tab" && !e.shiftKey) {
5194
+ if (e.key === "Tab") {
5195
+ e.preventDefault();
5196
+ if (filteredCommands.length > 0) {
5197
+ onMessageChange(filteredCommands[activeCommandIndex].name);
5198
+ setCommandListDismissed(true);
5199
+ }
5200
+ return;
5201
+ }
5202
+ if (e.key === "ArrowDown") {
4431
5203
  e.preventDefault();
4432
5204
  if (filteredCommands.length > 0) {
4433
5205
  setActiveCommandIndex((prev) => (prev + 1) % filteredCommands.length);
4434
5206
  }
4435
5207
  return;
4436
5208
  }
4437
- if (e.key === "ArrowUp" || e.key === "Tab" && e.shiftKey) {
5209
+ if (e.key === "ArrowUp") {
4438
5210
  e.preventDefault();
4439
5211
  if (filteredCommands.length > 0) {
4440
5212
  setActiveCommandIndex(
@@ -4460,7 +5232,7 @@ var MessageInput = React.forwardRef(
4460
5232
  }
4461
5233
  if (e.key === "Enter" && !e.shiftKey) {
4462
5234
  e.preventDefault();
4463
- if (!loading && (message.trim() || attachments.length > 0)) {
5235
+ if (message.trim() || attachments.length > 0) {
4464
5236
  handleFormSubmit(e);
4465
5237
  }
4466
5238
  }
@@ -4484,7 +5256,7 @@ var MessageInput = React.forwardRef(
4484
5256
  const handleFormSubmit = (e) => {
4485
5257
  e.preventDefault();
4486
5258
  if (isComposing) return;
4487
- if (loading || disabled || !message.trim() && attachments.length === 0) {
5259
+ if (disabled || !message.trim() && attachments.length === 0) {
4488
5260
  return;
4489
5261
  }
4490
5262
  setCommandListDismissed(true);
@@ -4492,7 +5264,7 @@ var MessageInput = React.forwardRef(
4492
5264
  setAttachments([]);
4493
5265
  setError(null);
4494
5266
  };
4495
- const canSubmit = !loading && !disabled && (message.trim() || attachments.length > 0);
5267
+ const canSubmit = !disabled && (message.trim() || attachments.length > 0);
4496
5268
  const visibleError = error || commandError;
4497
5269
  const visibleErrorText = visibleError ? t(visibleError) : "";
4498
5270
  const defaultContainerClassName = "shrink-0 px-4 pt-4 pb-6";
@@ -4513,8 +5285,9 @@ var MessageInput = React.forwardRef(
4513
5285
  ref: containerRef,
4514
5286
  className: containerClassName ?? defaultContainerClassName,
4515
5287
  children: /* @__PURE__ */ jsxRuntime.jsxs("form", { ref: formRef, onSubmit: handleFormSubmit, className: formClassName ?? "mx-auto", children: [
4516
- visibleError && /* @__PURE__ */ jsxRuntime.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: [
4517
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: visibleErrorText }),
5288
+ visibleError && /* @__PURE__ */ jsxRuntime.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: [
5289
+ /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(react.X, { size: 10, className: "text-red-600 dark:text-red-400", weight: "bold" }) }),
5290
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 text-red-700 dark:text-red-300 text-xs leading-relaxed", children: visibleErrorText }),
4518
5291
  /* @__PURE__ */ jsxRuntime.jsx(
4519
5292
  "button",
4520
5293
  {
@@ -4523,13 +5296,20 @@ var MessageInput = React.forwardRef(
4523
5296
  setError(null);
4524
5297
  onClearCommandError?.();
4525
5298
  },
4526
- className: "cursor-pointer ml-2 p-1 hover:bg-red-100 dark:hover:bg-red-800 rounded transition-colors",
5299
+ 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",
4527
5300
  "aria-label": t("BiChat.Input.DismissError"),
4528
5301
  children: /* @__PURE__ */ jsxRuntime.jsx(react.X, { size: 14 })
4529
5302
  }
4530
5303
  )
4531
5304
  ] }),
4532
- messageQueue.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-3 text-xs text-gray-500 dark:text-gray-400", children: /* @__PURE__ */ jsxRuntime.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 }) }) }),
5305
+ messageQueue.length > 0 && onRemoveQueueItem && onUpdateQueueItem && /* @__PURE__ */ jsxRuntime.jsx(
5306
+ MessageQueueList,
5307
+ {
5308
+ queue: messageQueue,
5309
+ onRemove: onRemoveQueueItem,
5310
+ onUpdate: onUpdateQueueItem
5311
+ }
5312
+ ),
4533
5313
  debugMode && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-3 space-y-2 text-xs", children: [
4534
5314
  /* @__PURE__ */ jsxRuntime.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: [
4535
5315
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "relative flex h-1.5 w-1.5", "aria-hidden": "true", children: [
@@ -4686,7 +5466,7 @@ var MessageInput = React.forwardRef(
4686
5466
  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",
4687
5467
  style: { maxHeight: `${MAX_HEIGHT}px` },
4688
5468
  rows: 1,
4689
- disabled: loading || disabled,
5469
+ disabled,
4690
5470
  "aria-busy": loading,
4691
5471
  "aria-label": t("BiChat.Input.MessageInput")
4692
5472
  }
@@ -4752,13 +5532,36 @@ var MessageInput = React.forwardRef(
4752
5532
  }
4753
5533
  );
4754
5534
  MessageInput.displayName = "MessageInput";
5535
+ function CompactionDoodle({ title, subtitle }) {
5536
+ return /* @__PURE__ */ jsxRuntime.jsxs(
5537
+ framerMotion.motion.div,
5538
+ {
5539
+ initial: { opacity: 0, y: 8, scale: 0.96 },
5540
+ animate: { opacity: 1, y: 0, scale: 1 },
5541
+ exit: { opacity: 0, y: 4, scale: 0.98 },
5542
+ transition: { type: "spring", stiffness: 400, damping: 28 },
5543
+ 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",
5544
+ children: [
5545
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex h-5 w-5 items-center justify-center", children: [
5546
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-primary-400/30" }),
5547
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relative inline-flex h-2 w-2 rounded-full bg-primary-500" })
5548
+ ] }),
5549
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-baseline gap-1.5", children: [
5550
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-gray-700 dark:text-gray-200", children: title }),
5551
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[11px] text-gray-400 dark:text-gray-500", children: subtitle })
5552
+ ] })
5553
+ ]
5554
+ }
5555
+ );
5556
+ }
5557
+ var CompactionDoodle_default = CompactionDoodle;
4755
5558
 
4756
5559
  // ui/src/bichat/components/WelcomeContent.tsx
4757
5560
  init_useTranslation();
4758
5561
  var PROMPT_DEFS = [
4759
- { categoryKey: "Welcome.Prompt1Category", textKey: "Welcome.Prompt1Text", icon: react.ChartBar, defaultCategory: "OSAGO Portfolio", defaultText: "What is the total amount of accrued OSAGO premiums for the reporting period?" },
4760
- { categoryKey: "Welcome.Prompt2Category", textKey: "Welcome.Prompt2Text", icon: react.FileText, defaultCategory: "Regional Analysis", defaultText: "Show me the top 5 regions by collected insurance premiums" },
4761
- { categoryKey: "Welcome.Prompt3Category", textKey: "Welcome.Prompt3Text", icon: react.Lightbulb, defaultCategory: "Loss Analysis", defaultText: "Calculate the loss ratio across the entire OSAGO portfolio" }
5562
+ { categoryKey: "BiChat.Welcome.Prompt1Category", textKey: "BiChat.Welcome.Prompt1Text", icon: react.ChartBar, defaultCategory: "Data Analysis", defaultText: "Show me a summary of key metrics" },
5563
+ { categoryKey: "BiChat.Welcome.Prompt2Category", textKey: "BiChat.Welcome.Prompt2Text", icon: react.FileText, defaultCategory: "Reports", defaultText: "Generate a report for the current period" },
5564
+ { categoryKey: "BiChat.Welcome.Prompt3Category", textKey: "BiChat.Welcome.Prompt3Text", icon: react.Lightbulb, defaultCategory: "Insights", defaultText: "What trends can you identify in the data?" }
4762
5565
  ];
4763
5566
  var PROMPT_STYLES = [
4764
5567
  {
@@ -4908,11 +5711,172 @@ function WelcomeContent({
4908
5711
  );
4909
5712
  }) })
4910
5713
  ] })
4911
- ]
4912
- }
4913
- );
5714
+ ]
5715
+ }
5716
+ );
5717
+ }
5718
+ var WelcomeContent_default = WelcomeContent;
5719
+ init_useTranslation();
5720
+ var variantStyles = {
5721
+ error: {
5722
+ container: "border-red-200 bg-red-50 dark:bg-red-900/20",
5723
+ title: "text-red-800 dark:text-red-300",
5724
+ message: "text-red-700 dark:text-red-400",
5725
+ icon: "text-red-600 dark:text-red-400",
5726
+ button: "text-red-400 hover:text-red-600 dark:hover:text-red-300",
5727
+ retryButton: "bg-red-600 dark:bg-red-700 hover:bg-red-700 dark:hover:bg-red-800 text-white",
5728
+ Icon: react.XCircle
5729
+ },
5730
+ success: {
5731
+ container: "border-green-200 bg-green-50 dark:bg-green-900/20",
5732
+ title: "text-green-800 dark:text-green-300",
5733
+ message: "text-green-700 dark:text-green-400",
5734
+ icon: "text-green-600 dark:text-green-400",
5735
+ button: "text-green-400 hover:text-green-600 dark:hover:text-green-300",
5736
+ retryButton: "bg-green-600 dark:bg-green-700 hover:bg-green-700 dark:hover:bg-green-800 text-white",
5737
+ Icon: react.CheckCircle
5738
+ },
5739
+ warning: {
5740
+ container: "border-yellow-200 bg-yellow-50 dark:bg-yellow-900/20",
5741
+ title: "text-yellow-800 dark:text-yellow-300",
5742
+ message: "text-yellow-700 dark:text-yellow-400",
5743
+ icon: "text-yellow-600 dark:text-yellow-400",
5744
+ button: "text-yellow-400 hover:text-yellow-600 dark:hover:text-yellow-300",
5745
+ retryButton: "bg-yellow-600 dark:bg-yellow-700 hover:bg-yellow-700 dark:hover:bg-yellow-800 text-white",
5746
+ Icon: react.Warning
5747
+ },
5748
+ info: {
5749
+ container: "border-blue-200 bg-blue-50 dark:bg-blue-900/20",
5750
+ title: "text-blue-800 dark:text-blue-300",
5751
+ message: "text-blue-700 dark:text-blue-400",
5752
+ icon: "text-blue-600 dark:text-blue-400",
5753
+ button: "text-blue-400 hover:text-blue-600 dark:hover:text-blue-300",
5754
+ retryButton: "bg-blue-600 dark:bg-blue-700 hover:bg-blue-700 dark:hover:bg-blue-800 text-white",
5755
+ Icon: react.Info
5756
+ }
5757
+ };
5758
+ function Alert({
5759
+ variant = "info",
5760
+ message,
5761
+ title,
5762
+ onDismiss,
5763
+ onRetry,
5764
+ show = true,
5765
+ dismissible = true
5766
+ }) {
5767
+ const { t } = useTranslation();
5768
+ const styles = variantStyles[variant];
5769
+ const IconComponent = styles.Icon;
5770
+ return /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: show && /* @__PURE__ */ jsxRuntime.jsx(
5771
+ framerMotion.motion.div,
5772
+ {
5773
+ variants: errorMessageVariants,
5774
+ initial: "initial",
5775
+ animate: "animate",
5776
+ exit: "exit",
5777
+ className: `border-t border ${styles.container} px-4 py-3`,
5778
+ role: "alert",
5779
+ "aria-live": "assertive",
5780
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full flex items-start justify-between px-4", children: [
5781
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-3 flex-1", children: [
5782
+ /* @__PURE__ */ jsxRuntime.jsx(IconComponent, { size: 20, className: `w-5 h-5 ${styles.icon} flex-shrink-0 mt-0.5` }),
5783
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
5784
+ title && /* @__PURE__ */ jsxRuntime.jsx("p", { className: `text-sm ${styles.title} font-medium`, children: title }),
5785
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: `text-sm ${styles.message} ${title ? "mt-1" : ""}`, children: message }),
5786
+ onRetry && /* @__PURE__ */ jsxRuntime.jsx(
5787
+ "button",
5788
+ {
5789
+ onClick: onRetry,
5790
+ className: `mt-2 text-xs px-3 py-1.5 rounded ${styles.retryButton} transition-colors font-medium`,
5791
+ children: t("BiChat.Chat.Retry")
5792
+ }
5793
+ )
5794
+ ] })
5795
+ ] }),
5796
+ dismissible && onDismiss && /* @__PURE__ */ jsxRuntime.jsx(
5797
+ "button",
5798
+ {
5799
+ onClick: onDismiss,
5800
+ className: `${styles.button} transition-colors flex-shrink-0`,
5801
+ "aria-label": t("BiChat.Chat.DismissNotification"),
5802
+ children: /* @__PURE__ */ jsxRuntime.jsx(react.X, { size: 20, className: "w-5 h-5" })
5803
+ }
5804
+ )
5805
+ ] })
5806
+ }
5807
+ ) });
5808
+ }
5809
+ var Alert_default = React.memo(Alert);
5810
+
5811
+ // ui/src/bichat/components/ArchiveBanner.tsx
5812
+ init_useTranslation();
5813
+ function ArchiveBanner({
5814
+ show = true,
5815
+ onRestore,
5816
+ restoring = false,
5817
+ onRestoreComplete
5818
+ }) {
5819
+ const { t } = useTranslation();
5820
+ const [error, setError] = React.useState(null);
5821
+ const handleRestore = async () => {
5822
+ try {
5823
+ setError(null);
5824
+ if (onRestore) {
5825
+ await onRestore();
5826
+ }
5827
+ if (onRestoreComplete) {
5828
+ onRestoreComplete();
5829
+ }
5830
+ } catch (err) {
5831
+ const message = err instanceof Error ? err.message : t("BiChat.Archive.RestoreFailed");
5832
+ setError(message);
5833
+ }
5834
+ };
5835
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5836
+ /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: show && /* @__PURE__ */ jsxRuntime.jsx(
5837
+ framerMotion.motion.div,
5838
+ {
5839
+ variants: errorMessageVariants,
5840
+ initial: "initial",
5841
+ animate: "animate",
5842
+ exit: "exit",
5843
+ className: "border-t border border-blue-200 bg-blue-50 dark:bg-blue-900/20 px-4 py-3",
5844
+ role: "region",
5845
+ "aria-label": t("BiChat.Archive.Banner"),
5846
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full flex items-start justify-between px-4", children: [
5847
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-3 flex-1", children: [
5848
+ /* @__PURE__ */ jsxRuntime.jsx(react.Archive, { size: 20, className: "w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" }),
5849
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-blue-700 dark:text-blue-400", children: t("BiChat.Archive.Archived") }) })
5850
+ ] }),
5851
+ /* @__PURE__ */ jsxRuntime.jsx(
5852
+ "button",
5853
+ {
5854
+ onClick: handleRestore,
5855
+ disabled: restoring,
5856
+ 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",
5857
+ "aria-label": t("BiChat.Archive.Restore"),
5858
+ children: restoring ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5859
+ /* @__PURE__ */ jsxRuntime.jsx(react.Spinner, { size: 16, className: "w-4 h-4 animate-spin" }),
5860
+ t("BiChat.Archive.Restoring")
5861
+ ] }) : t("BiChat.Archive.Restore")
5862
+ }
5863
+ )
5864
+ ] })
5865
+ }
5866
+ ) }),
5867
+ error && /* @__PURE__ */ jsxRuntime.jsx(
5868
+ Alert_default,
5869
+ {
5870
+ variant: "error",
5871
+ message: error,
5872
+ title: t("BiChat.Archive.RestoreFailed"),
5873
+ onDismiss: () => setError(null),
5874
+ dismissible: true
5875
+ }
5876
+ )
5877
+ ] });
4914
5878
  }
4915
- var WelcomeContent_default = WelcomeContent;
5879
+ var ArchiveBanner_default = React.memo(ArchiveBanner);
4916
5880
 
4917
5881
  // ui/src/bichat/components/ChatSession.tsx
4918
5882
  init_useTranslation();
@@ -4923,11 +5887,11 @@ init_useTranslation();
4923
5887
  // ui/src/bichat/components/SessionArtifactList.tsx
4924
5888
  init_useTranslation();
4925
5889
  var TYPE_LABEL_KEYS = {
4926
- chart: "Artifacts.GroupCharts",
4927
- code_output: "Artifacts.GroupCodeOutputs",
4928
- export: "Artifacts.GroupExports",
4929
- attachment: "Artifacts.GroupAttachments",
4930
- other: "Artifacts.GroupOther"
5890
+ chart: "BiChat.Artifacts.GroupCharts",
5891
+ code_output: "BiChat.Artifacts.GroupCodeOutputs",
5892
+ export: "BiChat.Artifacts.GroupExports",
5893
+ attachment: "BiChat.Artifacts.GroupAttachments",
5894
+ other: "BiChat.Artifacts.GroupOther"
4931
5895
  };
4932
5896
  function getGroupIcon(type) {
4933
5897
  const cls = "h-3.5 w-3.5";
@@ -5003,7 +5967,8 @@ function SessionArtifactList({
5003
5967
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-12 w-12 items-center justify-center rounded-xl bg-gray-100 dark:bg-gray-800", children: /* @__PURE__ */ jsxRuntime.jsx(react.Package, { className: "h-6 w-6 text-gray-400 dark:text-gray-500", weight: "duotone" }) }),
5004
5968
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
5005
5969
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-gray-500 dark:text-gray-400", children: t("BiChat.Artifacts.Empty") }),
5006
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 text-xs text-gray-400 dark:text-gray-500", children: t("BiChat.Artifacts.EmptySubtitle") })
5970
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 text-xs text-gray-400 dark:text-gray-500", children: t("BiChat.Artifacts.EmptySubtitle") }),
5971
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-xs text-gray-400 dark:text-gray-500", children: t("BiChat.Artifacts.EmptyHint") })
5007
5972
  ] })
5008
5973
  ] });
5009
5974
  }
@@ -5695,6 +6660,29 @@ function SessionArtifactsPanel({
5695
6660
  },
5696
6661
  [canDeleteArtifact, dataSource]
5697
6662
  );
6663
+ const fileInputRef = React.useRef(null);
6664
+ const handleAttachClick = React.useCallback(() => {
6665
+ fileInputRef.current?.click();
6666
+ }, []);
6667
+ const handleFileInputChange = React.useCallback(
6668
+ async (e) => {
6669
+ if (!dataSource.uploadSessionArtifacts || !e.target.files?.length) return;
6670
+ const files = Array.from(e.target.files);
6671
+ try {
6672
+ const result = await dataSource.uploadSessionArtifacts(sessionId, files);
6673
+ if ((result.artifacts || []).length > 0) {
6674
+ setDropSuccessState();
6675
+ void fetchArtifacts({ reset: true, manual: false });
6676
+ }
6677
+ setError(null);
6678
+ } catch (err) {
6679
+ setError(err instanceof Error ? err.message : tRef.current("BiChat.Artifacts.FailedToLoad"));
6680
+ } finally {
6681
+ e.target.value = "";
6682
+ }
6683
+ },
6684
+ [dataSource, fetchArtifacts, sessionId, setDropSuccessState]
6685
+ );
5698
6686
  return /* @__PURE__ */ jsxRuntime.jsxs(
5699
6687
  "aside",
5700
6688
  {
@@ -5722,12 +6710,38 @@ function SessionArtifactsPanel({
5722
6710
  ]
5723
6711
  }
5724
6712
  ) }),
5725
- /* @__PURE__ */ jsxRuntime.jsx("header", { className: "flex items-center justify-between border-b border-gray-200 px-3 py-2 dark:border-gray-700/80", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsxRuntime.jsxs("h2", { className: "truncate text-sm font-semibold text-gray-900 dark:text-gray-100", children: [
5726
- t("BiChat.Artifacts.Title"),
5727
- " (",
5728
- artifacts.length,
5729
- ")"
5730
- ] }) }) }),
6713
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center justify-between border-b border-gray-200 px-3 py-2 dark:border-gray-700/80", children: [
6714
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsxRuntime.jsxs("h2", { className: "truncate text-sm font-semibold text-gray-900 dark:text-gray-100", children: [
6715
+ t("BiChat.Artifacts.Title"),
6716
+ " (",
6717
+ artifacts.length,
6718
+ ")"
6719
+ ] }) }),
6720
+ canDropFiles && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
6721
+ /* @__PURE__ */ jsxRuntime.jsx(
6722
+ "button",
6723
+ {
6724
+ type: "button",
6725
+ onClick: handleAttachClick,
6726
+ 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",
6727
+ "aria-label": t("BiChat.Artifacts.AttachFiles"),
6728
+ title: t("BiChat.Artifacts.AttachFiles"),
6729
+ children: /* @__PURE__ */ jsxRuntime.jsx(react.Plus, { className: "h-4 w-4", weight: "bold" })
6730
+ }
6731
+ ),
6732
+ /* @__PURE__ */ jsxRuntime.jsx(
6733
+ "input",
6734
+ {
6735
+ ref: fileInputRef,
6736
+ type: "file",
6737
+ multiple: true,
6738
+ className: "hidden",
6739
+ onChange: handleFileInputChange,
6740
+ "aria-label": t("BiChat.Artifacts.AttachFiles")
6741
+ }
6742
+ )
6743
+ ] })
6744
+ ] }),
5731
6745
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-3 py-3", children: fetching ? /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.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: [
5732
6746
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-red-800 dark:text-red-300", children: t("BiChat.Artifacts.FailedToLoad") }),
5733
6747
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-red-700 dark:text-red-400", children: error }),
@@ -5780,6 +6794,77 @@ function SessionArtifactsPanel({
5780
6794
  }
5781
6795
  );
5782
6796
  }
6797
+
6798
+ // ui/src/bichat/components/StreamError.tsx
6799
+ init_useTranslation();
6800
+ function StreamError({
6801
+ error,
6802
+ onRetry,
6803
+ onRegenerate,
6804
+ onDismiss,
6805
+ compact = false
6806
+ }) {
6807
+ const { t } = useTranslation();
6808
+ return /* @__PURE__ */ jsxRuntime.jsxs(
6809
+ framerMotion.motion.div,
6810
+ {
6811
+ initial: { opacity: 0, y: 10 },
6812
+ animate: { opacity: 1, y: 0 },
6813
+ exit: { opacity: 0, y: -10 },
6814
+ 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`,
6815
+ role: "alert",
6816
+ children: [
6817
+ /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(
6818
+ react.Warning,
6819
+ {
6820
+ className: "w-4 h-4 text-red-600 dark:text-red-400",
6821
+ weight: "fill"
6822
+ }
6823
+ ) }),
6824
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
6825
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-red-800 dark:text-red-200 leading-snug", children: t("BiChat.Error.Generic") }),
6826
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 text-xs text-red-600/80 dark:text-red-400/70 break-words leading-relaxed", children: error }),
6827
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mt-2", children: [
6828
+ onRetry && /* @__PURE__ */ jsxRuntime.jsxs(
6829
+ "button",
6830
+ {
6831
+ onClick: onRetry,
6832
+ 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",
6833
+ type: "button",
6834
+ children: [
6835
+ /* @__PURE__ */ jsxRuntime.jsx(react.ArrowClockwise, { className: "w-3.5 h-3.5" }),
6836
+ t("BiChat.StreamError.Retry")
6837
+ ]
6838
+ }
6839
+ ),
6840
+ onRegenerate && /* @__PURE__ */ jsxRuntime.jsxs(
6841
+ "button",
6842
+ {
6843
+ onClick: onRegenerate,
6844
+ 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",
6845
+ type: "button",
6846
+ children: [
6847
+ /* @__PURE__ */ jsxRuntime.jsx(react.ArrowsCounterClockwise, { className: "w-3.5 h-3.5" }),
6848
+ t("BiChat.StreamError.Regenerate")
6849
+ ]
6850
+ }
6851
+ )
6852
+ ] })
6853
+ ] }),
6854
+ onDismiss && /* @__PURE__ */ jsxRuntime.jsx(
6855
+ "button",
6856
+ {
6857
+ onClick: onDismiss,
6858
+ 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",
6859
+ type: "button",
6860
+ "aria-label": t("BiChat.Chat.DismissNotification"),
6861
+ children: /* @__PURE__ */ jsxRuntime.jsx(react.X, { className: "w-3.5 h-3.5" })
6862
+ }
6863
+ )
6864
+ ]
6865
+ }
6866
+ );
6867
+ }
5783
6868
  var ARTIFACTS_PANEL_WIDTH_DEFAULT = 352;
5784
6869
  var ARTIFACTS_PANEL_WIDTH_MIN = 280;
5785
6870
  var ARTIFACTS_PANEL_WIDTH_MAX = 560;
@@ -5796,13 +6881,34 @@ function ChatSessionCore({
5796
6881
  actionsSlot,
5797
6882
  onBack,
5798
6883
  thinkingVerbs,
6884
+ onSessionRestored,
5799
6885
  showArtifactsPanel = false,
5800
6886
  artifactsPanelDefaultExpanded = false,
5801
6887
  artifactsPanelStorageKey = "bichat.artifacts-panel.expanded"
5802
6888
  }) {
5803
6889
  const { t } = useTranslation();
5804
- const { session, fetching, error, debugMode, sessionDebugUsage, debugLimits, currentSessionId } = useChatSession();
5805
- const { turns, loading, isStreaming } = useChatMessaging();
6890
+ const {
6891
+ session,
6892
+ fetching,
6893
+ error,
6894
+ errorRetryable,
6895
+ debugMode,
6896
+ sessionDebugUsage,
6897
+ debugLimits,
6898
+ currentSessionId,
6899
+ setError,
6900
+ retryFetchSession
6901
+ } = useChatSession();
6902
+ const {
6903
+ turns,
6904
+ loading,
6905
+ isStreaming,
6906
+ streamError,
6907
+ streamErrorRetryable,
6908
+ isCompacting,
6909
+ retryLastMessage,
6910
+ clearStreamError
6911
+ } = useChatMessaging();
5806
6912
  const {
5807
6913
  inputError,
5808
6914
  message,
@@ -5810,9 +6916,23 @@ function ChatSessionCore({
5810
6916
  setInputError,
5811
6917
  handleSubmit,
5812
6918
  messageQueue,
5813
- handleUnqueue
6919
+ handleUnqueue,
6920
+ removeQueueItem,
6921
+ updateQueueItem
5814
6922
  } = useChatInput();
5815
- const effectiveReadOnly = Boolean(readOnly ?? isReadOnly);
6923
+ const isArchived = session?.status === "archived";
6924
+ const effectiveReadOnly = Boolean(readOnly ?? isReadOnly) || isArchived;
6925
+ const [restoring, setRestoring] = React.useState(false);
6926
+ const handleRestore = React.useCallback(async () => {
6927
+ if (!session?.id) return;
6928
+ setRestoring(true);
6929
+ try {
6930
+ await dataSource.unarchiveSession(session.id);
6931
+ onSessionRestored?.(session.id);
6932
+ } finally {
6933
+ setRestoring(false);
6934
+ }
6935
+ }, [dataSource, session?.id, onSessionRestored]);
5816
6936
  const [artifactsPanelExpanded, setArtifactsPanelExpanded] = React.useState(
5817
6937
  artifactsPanelDefaultExpanded
5818
6938
  );
@@ -5884,16 +7004,9 @@ function ChatSessionCore({
5884
7004
  document.body.style.userSelect = "";
5885
7005
  };
5886
7006
  }, [isResizingArtifactsPanel, artifactsPanelStorageKey]);
5887
- if (fetching) {
7007
+ if (fetching && turns.length === 0 && !session) {
5888
7008
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-gray-500 dark:text-gray-400", children: t("BiChat.Input.Processing") }) });
5889
7009
  }
5890
- if (error) {
5891
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-red-500 dark:text-red-400", children: [
5892
- t("BiChat.Error.Generic"),
5893
- ": ",
5894
- error
5895
- ] }) });
5896
- }
5897
7010
  const showWelcome = !session && turns.length === 0;
5898
7011
  const activeSessionId = session?.id || (currentSessionId && currentSessionId !== "new" ? currentSessionId : void 0);
5899
7012
  const supportsArtifactsPanel = typeof dataSource.fetchSessionArtifacts === "function";
@@ -5950,6 +7063,16 @@ function ChatSessionCore({
5950
7063
  actionsSlot: headerActions
5951
7064
  }
5952
7065
  ),
7066
+ error && /* @__PURE__ */ jsxRuntime.jsx(
7067
+ Alert_default,
7068
+ {
7069
+ variant: errorRetryable ? "warning" : "error",
7070
+ title: t("BiChat.Error.Generic"),
7071
+ message: error,
7072
+ onDismiss: () => setError(null),
7073
+ onRetry: errorRetryable ? retryFetchSession : void 0
7074
+ }
7075
+ ),
5953
7076
  /* @__PURE__ */ jsxRuntime.jsxs(
5954
7077
  "div",
5955
7078
  {
@@ -5958,6 +7081,15 @@ function ChatSessionCore({
5958
7081
  children: [
5959
7082
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex min-h-0 flex-1 flex-col", children: showWelcome ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-1 flex-col overflow-auto", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-1 items-center justify-center px-4 py-8", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full max-w-5xl", children: [
5960
7083
  welcomeSlot || /* @__PURE__ */ jsxRuntime.jsx(WelcomeContent_default, { onPromptSelect: handlePromptSelect, disabled: loading }),
7084
+ streamError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 pt-4", children: /* @__PURE__ */ jsxRuntime.jsx(
7085
+ StreamError,
7086
+ {
7087
+ error: streamError,
7088
+ compact: true,
7089
+ onRetry: streamErrorRetryable ? () => void retryLastMessage() : void 0,
7090
+ onDismiss: clearStreamError
7091
+ }
7092
+ ) }),
5961
7093
  !effectiveReadOnly && /* @__PURE__ */ jsxRuntime.jsx(
5962
7094
  MessageInput,
5963
7095
  {
@@ -5973,12 +7105,22 @@ function ChatSessionCore({
5973
7105
  onSubmit: handleSubmit,
5974
7106
  messageQueue,
5975
7107
  onUnqueue: handleUnqueue,
7108
+ onRemoveQueueItem: removeQueueItem,
7109
+ onUpdateQueueItem: updateQueueItem,
5976
7110
  containerClassName: "pt-6 px-6",
5977
7111
  formClassName: "mx-auto"
5978
7112
  }
5979
7113
  ),
5980
7114
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-4 pb-1 text-center text-xs text-gray-500 dark:text-gray-400", children: t("BiChat.Welcome.Disclaimer") })
5981
7115
  ] }) }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
7116
+ isArchived && /* @__PURE__ */ jsxRuntime.jsx(
7117
+ ArchiveBanner_default,
7118
+ {
7119
+ show: true,
7120
+ onRestore: handleRestore,
7121
+ restoring
7122
+ }
7123
+ ),
5982
7124
  /* @__PURE__ */ jsxRuntime.jsx(
5983
7125
  MessageList,
5984
7126
  {
@@ -5988,6 +7130,22 @@ function ChatSessionCore({
5988
7130
  readOnly: effectiveReadOnly
5989
7131
  }
5990
7132
  ),
7133
+ /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: isCompacting && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-center px-4 pb-2", children: /* @__PURE__ */ jsxRuntime.jsx(
7134
+ CompactionDoodle_default,
7135
+ {
7136
+ title: t("BiChat.Slash.CompactingTitle"),
7137
+ subtitle: t("BiChat.Slash.CompactingSubtitle")
7138
+ }
7139
+ ) }) }),
7140
+ streamError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 pb-2", children: /* @__PURE__ */ jsxRuntime.jsx(
7141
+ StreamError,
7142
+ {
7143
+ error: streamError,
7144
+ compact: true,
7145
+ onRetry: streamErrorRetryable ? () => void retryLastMessage() : void 0,
7146
+ onDismiss: clearStreamError
7147
+ }
7148
+ ) }),
5991
7149
  !effectiveReadOnly && /* @__PURE__ */ jsxRuntime.jsx(
5992
7150
  MessageInput,
5993
7151
  {
@@ -6002,7 +7160,9 @@ function ChatSessionCore({
6002
7160
  onMessageChange: setMessage,
6003
7161
  onSubmit: handleSubmit,
6004
7162
  messageQueue,
6005
- onUnqueue: handleUnqueue
7163
+ onUnqueue: handleUnqueue,
7164
+ onRemoveQueueItem: removeQueueItem,
7165
+ onUpdateQueueItem: updateQueueItem
6006
7166
  }
6007
7167
  )
6008
7168
  ] }) }),
@@ -6093,8 +7253,17 @@ function ChatSessionCore({
6093
7253
  );
6094
7254
  }
6095
7255
  function ChatSession(props) {
6096
- const { dataSource, sessionId, rateLimiter, ...coreProps } = props;
6097
- return /* @__PURE__ */ jsxRuntime.jsx(ChatSessionProvider, { dataSource, sessionId, rateLimiter, children: /* @__PURE__ */ jsxRuntime.jsx(ChatSessionCore, { dataSource, ...coreProps }) });
7256
+ const { dataSource, sessionId, rateLimiter, onSessionCreated, ...coreProps } = props;
7257
+ return /* @__PURE__ */ jsxRuntime.jsx(
7258
+ ChatSessionProvider,
7259
+ {
7260
+ dataSource,
7261
+ sessionId,
7262
+ rateLimiter,
7263
+ onSessionCreated,
7264
+ children: /* @__PURE__ */ jsxRuntime.jsx(ChatSessionCore, { dataSource, ...coreProps })
7265
+ }
7266
+ );
6098
7267
  }
6099
7268
 
6100
7269
  // ui/src/bichat/index.ts
@@ -6196,20 +7365,19 @@ var EditableText = React.forwardRef(
6196
7365
  onSave,
6197
7366
  maxLength = 100,
6198
7367
  isLoading = false,
6199
- placeholder = "Untitled",
7368
+ placeholder,
6200
7369
  className = "",
6201
7370
  inputClassName = "",
6202
7371
  size = "sm"
6203
7372
  }, ref) => {
6204
7373
  const { t } = useTranslation();
7374
+ const resolvedPlaceholder = placeholder ?? t("BiChat.Common.Untitled");
6205
7375
  const [isEditing, setIsEditing] = React.useState(false);
6206
7376
  const [editValue, setEditValue] = React.useState(value);
6207
7377
  const inputRef = React.useRef(null);
6208
7378
  React.useImperativeHandle(ref, () => ({
6209
7379
  startEditing: () => {
6210
- if (!isLoading) {
6211
- setIsEditing(true);
6212
- }
7380
+ setIsEditing(true);
6213
7381
  },
6214
7382
  cancelEditing: () => {
6215
7383
  setEditValue(value);
@@ -6251,9 +7419,7 @@ var EditableText = React.forwardRef(
6251
7419
  }
6252
7420
  };
6253
7421
  const handleDoubleClick = () => {
6254
- if (!isLoading) {
6255
- setIsEditing(true);
6256
- }
7422
+ setIsEditing(true);
6257
7423
  };
6258
7424
  const handleBlur = () => {
6259
7425
  handleSave();
@@ -6275,7 +7441,7 @@ var EditableText = React.forwardRef(
6275
7441
  onKeyDown: handleKeyDown,
6276
7442
  onBlur: handleBlur,
6277
7443
  maxLength,
6278
- placeholder,
7444
+ placeholder: resolvedPlaceholder,
6279
7445
  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}`,
6280
7446
  "aria-label": t("BiChat.EditableText.AriaLabel")
6281
7447
  }
@@ -6283,7 +7449,7 @@ var EditableText = React.forwardRef(
6283
7449
  }
6284
7450
  );
6285
7451
  }
6286
- const displayValue = value || placeholder;
7452
+ const displayValue = value || resolvedPlaceholder;
6287
7453
  return /* @__PURE__ */ jsxRuntime.jsx(
6288
7454
  "span",
6289
7455
  {
@@ -6331,16 +7497,18 @@ var sizeClasses3 = {
6331
7497
  function SearchInput({
6332
7498
  value,
6333
7499
  onChange,
6334
- placeholder = "Search...",
7500
+ placeholder,
6335
7501
  autoFocus = false,
6336
7502
  onSubmit,
6337
7503
  onEscape,
6338
7504
  className = "",
6339
7505
  size = "md",
6340
7506
  disabled = false,
6341
- ariaLabel = "Search"
7507
+ ariaLabel
6342
7508
  }) {
6343
7509
  const { t } = useTranslation();
7510
+ const resolvedPlaceholder = placeholder ?? t("BiChat.Common.Search");
7511
+ const resolvedAriaLabel = ariaLabel ?? t("BiChat.Common.Search");
6344
7512
  const inputRef = React.useRef(null);
6345
7513
  const sizes = sizeClasses3[size];
6346
7514
  React.useEffect(() => {
@@ -6383,10 +7551,10 @@ function SearchInput({
6383
7551
  value,
6384
7552
  onChange: (e) => onChange(e.target.value),
6385
7553
  onKeyDown: handleKeyDown,
6386
- placeholder,
7554
+ placeholder: resolvedPlaceholder,
6387
7555
  disabled,
6388
7556
  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`,
6389
- "aria-label": ariaLabel
7557
+ "aria-label": resolvedAriaLabel
6390
7558
  }
6391
7559
  ),
6392
7560
  value && !disabled && /* @__PURE__ */ jsxRuntime.jsx(
@@ -6586,26 +7754,37 @@ var LoadingSpinner_default = MemoizedLoadingSpinner;
6586
7754
  // ui/src/bichat/index.ts
6587
7755
  init_TableExportButton();
6588
7756
  init_TableWithExport();
7757
+
7758
+ // ui/src/bichat/components/Toast.tsx
7759
+ init_useTranslation();
6589
7760
  var typeConfig = {
6590
7761
  success: {
6591
- bgColor: "bg-green-700",
6592
- darkBgColor: "dark:bg-green-800",
6593
- icon: /* @__PURE__ */ jsxRuntime.jsx(react.CheckCircle, { size: 20, className: "w-5 h-5 flex-shrink-0", weight: "fill" })
7762
+ accent: "text-emerald-600 dark:text-emerald-400",
7763
+ bg: "bg-emerald-50 dark:bg-emerald-950/40 border-emerald-200/80 dark:border-emerald-800/50",
7764
+ icon: "bg-emerald-100 dark:bg-emerald-900/50",
7765
+ progress: "bg-emerald-500 dark:bg-emerald-400",
7766
+ iconEl: react.CheckCircle
6594
7767
  },
6595
7768
  error: {
6596
- bgColor: "bg-red-700",
6597
- darkBgColor: "dark:bg-red-800",
6598
- icon: /* @__PURE__ */ jsxRuntime.jsx(react.XCircle, { size: 20, className: "w-5 h-5 flex-shrink-0", weight: "fill" })
7769
+ accent: "text-red-600 dark:text-red-400",
7770
+ bg: "bg-red-50 dark:bg-red-950/40 border-red-200/80 dark:border-red-800/50",
7771
+ icon: "bg-red-100 dark:bg-red-900/50",
7772
+ progress: "bg-red-500 dark:bg-red-400",
7773
+ iconEl: react.XCircle
6599
7774
  },
6600
7775
  info: {
6601
- bgColor: "bg-blue-700",
6602
- darkBgColor: "dark:bg-blue-800",
6603
- icon: /* @__PURE__ */ jsxRuntime.jsx(react.Info, { size: 20, className: "w-5 h-5 flex-shrink-0", weight: "fill" })
7776
+ accent: "text-blue-600 dark:text-blue-400",
7777
+ bg: "bg-blue-50 dark:bg-blue-950/40 border-blue-200/80 dark:border-blue-800/50",
7778
+ icon: "bg-blue-100 dark:bg-blue-900/50",
7779
+ progress: "bg-blue-500 dark:bg-blue-400",
7780
+ iconEl: react.Info
6604
7781
  },
6605
7782
  warning: {
6606
- bgColor: "bg-amber-700",
6607
- darkBgColor: "dark:bg-amber-800",
6608
- icon: /* @__PURE__ */ jsxRuntime.jsx(react.Warning, { size: 20, className: "w-5 h-5 flex-shrink-0", weight: "fill" })
7783
+ accent: "text-amber-600 dark:text-amber-400",
7784
+ bg: "bg-amber-50 dark:bg-amber-950/40 border-amber-200/80 dark:border-amber-800/50",
7785
+ icon: "bg-amber-100 dark:bg-amber-900/50",
7786
+ progress: "bg-amber-500 dark:bg-amber-400",
7787
+ iconEl: react.Warning
6609
7788
  }
6610
7789
  };
6611
7790
  function Toast({
@@ -6614,55 +7793,117 @@ function Toast({
6614
7793
  message,
6615
7794
  duration = 5e3,
6616
7795
  onDismiss,
6617
- dismissLabel = "Dismiss notification"
7796
+ dismissLabel
6618
7797
  }) {
7798
+ const { t } = useTranslation();
7799
+ const resolvedDismissLabel = dismissLabel ?? t("BiChat.Chat.DismissNotification");
6619
7800
  const config = typeConfig[type];
7801
+ const Icon = config.iconEl;
7802
+ const [show, setShow] = React.useState(false);
7803
+ const [paused, setPaused] = React.useState(false);
7804
+ const remainingRef = React.useRef(duration);
7805
+ const startRef = React.useRef(Date.now());
7806
+ React.useEffect(() => {
7807
+ const frame = requestAnimationFrame(() => setShow(true));
7808
+ return () => cancelAnimationFrame(frame);
7809
+ }, []);
7810
+ React.useEffect(() => {
7811
+ if (paused) return;
7812
+ startRef.current = Date.now();
7813
+ const timer = setTimeout(() => {
7814
+ setShow(false);
7815
+ setTimeout(() => onDismiss(id), 200);
7816
+ }, remainingRef.current);
7817
+ return () => {
7818
+ const elapsed = Date.now() - startRef.current;
7819
+ remainingRef.current = Math.max(0, remainingRef.current - elapsed);
7820
+ clearTimeout(timer);
7821
+ };
7822
+ }, [id, paused, onDismiss]);
7823
+ const handleDismiss = React.useCallback(() => {
7824
+ setShow(false);
7825
+ setTimeout(() => onDismiss(id), 200);
7826
+ }, [id, onDismiss]);
6620
7827
  const ariaLive = type === "error" ? "assertive" : "polite";
6621
7828
  const role = type === "error" || type === "warning" ? "alert" : "status";
6622
- React.useEffect(() => {
6623
- const timer = setTimeout(() => onDismiss(id), duration);
6624
- return () => clearTimeout(timer);
6625
- }, [id, duration, onDismiss]);
6626
- return /* @__PURE__ */ jsxRuntime.jsxs(
6627
- framerMotion.motion.div,
7829
+ return /* @__PURE__ */ jsxRuntime.jsx(
7830
+ react$1.Transition,
6628
7831
  {
6629
- initial: { opacity: 0, y: -50, scale: 0.95 },
6630
- animate: { opacity: 1, y: 0, scale: 1 },
6631
- exit: { opacity: 0, scale: 0.95 },
6632
- transition: { duration: 0.2 },
6633
- 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}`,
6634
- role,
6635
- "aria-live": ariaLive,
6636
- "aria-atomic": "true",
6637
- children: [
6638
- config.icon,
6639
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "flex-1 text-sm font-medium", children: message }),
6640
- /* @__PURE__ */ jsxRuntime.jsx(
6641
- "button",
6642
- {
6643
- onClick: () => onDismiss(id),
6644
- className: "cursor-pointer ml-2 text-white hover:bg-white/20 p-1 rounded transition-colors flex-shrink-0",
6645
- "aria-label": dismissLabel,
6646
- children: /* @__PURE__ */ jsxRuntime.jsx(react.X, { size: 16, className: "w-4 h-4", weight: "bold" })
6647
- }
6648
- )
6649
- ]
7832
+ show,
7833
+ enter: "transition duration-200 ease-out",
7834
+ enterFrom: "-translate-y-2 opacity-0 scale-95",
7835
+ enterTo: "translate-y-0 opacity-100 scale-100",
7836
+ leave: "transition duration-150 ease-in",
7837
+ leaveFrom: "translate-y-0 opacity-100 scale-100",
7838
+ leaveTo: "-translate-y-2 opacity-0 scale-95",
7839
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
7840
+ "div",
7841
+ {
7842
+ 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}`,
7843
+ role,
7844
+ "aria-live": ariaLive,
7845
+ "aria-atomic": "true",
7846
+ onMouseEnter: () => setPaused(true),
7847
+ onMouseLeave: () => setPaused(false),
7848
+ children: [
7849
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: `mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-lg ${config.icon}`, children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { size: 16, className: config.accent, weight: "fill" }) }),
7850
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "flex-1 pt-0.5 text-sm font-medium leading-snug text-gray-800 dark:text-gray-100", children: message }),
7851
+ /* @__PURE__ */ jsxRuntime.jsx(
7852
+ "button",
7853
+ {
7854
+ onClick: handleDismiss,
7855
+ 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",
7856
+ "aria-label": resolvedDismissLabel,
7857
+ children: /* @__PURE__ */ jsxRuntime.jsx(react.X, { size: 14, weight: "bold" })
7858
+ }
7859
+ ),
7860
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-x-0 bottom-0 h-0.5 bg-black/5 dark:bg-white/5", children: /* @__PURE__ */ jsxRuntime.jsx(
7861
+ "div",
7862
+ {
7863
+ className: `h-full ${config.progress} origin-left`,
7864
+ style: {
7865
+ animation: `bichat-toast-progress ${duration}ms linear forwards`,
7866
+ animationPlayState: paused ? "paused" : "running"
7867
+ }
7868
+ }
7869
+ ) })
7870
+ ]
7871
+ }
7872
+ )
6650
7873
  }
6651
7874
  );
6652
7875
  }
7876
+
7877
+ // ui/src/bichat/components/ToastContainer.tsx
7878
+ init_useTranslation();
6653
7879
  function ToastContainer({ toasts, onDismiss, dismissLabel }) {
6654
- return /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: toasts.map((toast) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pointer-events-auto", children: /* @__PURE__ */ jsxRuntime.jsx(Toast, { ...toast, onDismiss, dismissLabel }) }, toast.id)) }) });
7880
+ const { t } = useTranslation();
7881
+ if (toasts.length === 0) return null;
7882
+ return /* @__PURE__ */ jsxRuntime.jsx(
7883
+ "div",
7884
+ {
7885
+ "aria-label": t("BiChat.Common.Notifications"),
7886
+ className: "fixed top-6 right-6 z-[var(--bichat-z-toast,60)] flex flex-col gap-2 pointer-events-none",
7887
+ children: toasts.map((toast) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pointer-events-auto", children: /* @__PURE__ */ jsxRuntime.jsx(Toast, { ...toast, onDismiss, dismissLabel }) }, toast.id))
7888
+ }
7889
+ );
6655
7890
  }
7891
+
7892
+ // ui/src/bichat/components/ConfirmModal.tsx
7893
+ init_useTranslation();
6656
7894
  function ConfirmModalBase({
6657
7895
  isOpen,
6658
7896
  title,
6659
7897
  message,
6660
7898
  onConfirm,
6661
7899
  onCancel,
6662
- confirmText = "Confirm",
6663
- cancelText = "Cancel",
7900
+ confirmText,
7901
+ cancelText,
6664
7902
  isDanger = false
6665
7903
  }) {
7904
+ const { t } = useTranslation();
7905
+ const resolvedConfirmText = confirmText ?? t("BiChat.Common.Confirm");
7906
+ const resolvedCancelText = cancelText ?? t("BiChat.Common.Cancel");
6666
7907
  return /* @__PURE__ */ jsxRuntime.jsxs(react$1.Dialog, { open: isOpen, onClose: onCancel, className: "relative z-40", children: [
6667
7908
  /* @__PURE__ */ jsxRuntime.jsx(react$1.DialogBackdrop, { className: "fixed inset-0 bg-black/40 dark:bg-black/60 backdrop-blur-sm transition-opacity duration-200" }),
6668
7909
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fixed inset-0 flex items-center justify-center z-50 p-4", children: /* @__PURE__ */ jsxRuntime.jsxs(react$1.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: [
@@ -6679,9 +7920,9 @@ function ConfirmModalBase({
6679
7920
  {
6680
7921
  onClick: onCancel,
6681
7922
  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",
6682
- "aria-label": `Cancel ${title.toLowerCase()}`,
7923
+ "aria-label": resolvedCancelText,
6683
7924
  "data-testid": "confirm-modal-cancel",
6684
- children: cancelText
7925
+ children: resolvedCancelText
6685
7926
  }
6686
7927
  ),
6687
7928
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -6694,9 +7935,9 @@ function ConfirmModalBase({
6694
7935
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-gray-800",
6695
7936
  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"
6696
7937
  ].join(" "),
6697
- "aria-label": `Confirm ${title.toLowerCase()}`,
7938
+ "aria-label": resolvedConfirmText,
6698
7939
  "data-testid": "confirm-modal-confirm",
6699
- children: confirmText
7940
+ children: resolvedConfirmText
6700
7941
  }
6701
7942
  )
6702
7943
  ] })
@@ -6786,28 +8027,44 @@ function PermissionGuard({
6786
8027
  const permitted = mode === "all" ? permissions.every((p) => hasPermission2(p)) : permissions.some((p) => hasPermission2(p));
6787
8028
  return permitted ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children }) : /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: fallback });
6788
8029
  }
8030
+
8031
+ // ui/src/bichat/components/ErrorBoundary.tsx
8032
+ init_useTranslation();
6789
8033
  function DefaultErrorContent({
6790
8034
  error,
6791
8035
  onReset,
6792
- resetLabel = "Try Again",
6793
- errorTitle = "Something went wrong"
8036
+ resetLabel,
8037
+ errorTitle
6794
8038
  }) {
8039
+ const { t } = useTranslation();
8040
+ const resolvedResetLabel = resetLabel ?? t("BiChat.Common.TryAgain");
8041
+ const resolvedErrorTitle = errorTitle ?? t("BiChat.Error.SomethingWentWrong");
6795
8042
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center p-8 text-center min-h-[200px]", children: [
6796
- /* @__PURE__ */ jsxRuntime.jsx(react.WarningCircle, { size: 48, className: "text-red-500 mb-4", weight: "fill" }),
6797
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-semibold text-gray-900 dark:text-white mb-2", children: errorTitle }),
6798
- /* @__PURE__ */ jsxRuntime.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." }),
6799
- onReset && /* @__PURE__ */ jsxRuntime.jsxs(
6800
- "button",
6801
- {
6802
- type: "button",
6803
- onClick: onReset,
6804
- className: "flex items-center gap-2 px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors",
6805
- children: [
6806
- /* @__PURE__ */ jsxRuntime.jsx(react.ArrowClockwise, { size: 16, weight: "bold" }),
6807
- resetLabel
6808
- ]
6809
- }
6810
- )
8043
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 overflow-hidden pointer-events-none opacity-[0.03] dark:opacity-[0.04]", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { className: "absolute -top-8 -right-8 w-64 h-64 text-red-500", viewBox: "0 0 200 200", fill: "currentColor", children: [
8044
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "100", cy: "100", r: "80", opacity: "0.5" }),
8045
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "100", cy: "100", r: "50", opacity: "0.3" }),
8046
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "100", cy: "100", r: "25", opacity: "0.2" })
8047
+ ] }) }),
8048
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex flex-col items-center", children: [
8049
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative mb-5", children: [
8050
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 rounded-full bg-red-100 dark:bg-red-900/30 scale-150 blur-md" }),
8051
+ /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(react.WarningCircle, { size: 28, className: "text-red-500 dark:text-red-400", weight: "fill" }) })
8052
+ ] }),
8053
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-semibold text-gray-900 dark:text-white mb-1.5", children: resolvedErrorTitle }),
8054
+ /* @__PURE__ */ jsxRuntime.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") }),
8055
+ onReset && /* @__PURE__ */ jsxRuntime.jsxs(
8056
+ "button",
8057
+ {
8058
+ type: "button",
8059
+ onClick: onReset,
8060
+ 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",
8061
+ children: [
8062
+ /* @__PURE__ */ jsxRuntime.jsx(react.ArrowClockwise, { size: 16, weight: "bold" }),
8063
+ resolvedResetLabel
8064
+ ]
8065
+ }
8066
+ )
8067
+ ] })
6811
8068
  ] });
6812
8069
  }
6813
8070
  var ErrorBoundary = class extends React.Component {
@@ -6944,10 +8201,13 @@ var TouchContextMenu = ({
6944
8201
  const [focusedIndex, setFocusedIndex] = React.useState(-1);
6945
8202
  const menuRef = React.useRef(null);
6946
8203
  const itemRefs = React.useRef([]);
6947
- const enabledIndices = items.reduce((acc, item, i) => {
6948
- if (!item.disabled) acc.push(i);
6949
- return acc;
6950
- }, []);
8204
+ const enabledIndices = React.useMemo(
8205
+ () => items.reduce((acc, item, i) => {
8206
+ if (!item.disabled) acc.push(i);
8207
+ return acc;
8208
+ }, []),
8209
+ [items]
8210
+ );
6951
8211
  const focusItem = React.useCallback((index) => {
6952
8212
  setFocusedIndex(index);
6953
8213
  itemRefs.current[index]?.focus();
@@ -6963,7 +8223,7 @@ var TouchContextMenu = ({
6963
8223
  }
6964
8224
  });
6965
8225
  return () => cancelAnimationFrame(timer);
6966
- }, [isOpen, enabledIndices.length, focusItem]);
8226
+ }, [isOpen, enabledIndices, focusItem]);
6967
8227
  React.useEffect(() => {
6968
8228
  if (!isOpen) return;
6969
8229
  const handleKeyDown = (e) => {
@@ -7261,7 +8521,7 @@ var SessionItem = React.memo(
7261
8521
  react$1.MenuItems,
7262
8522
  {
7263
8523
  anchor: "bottom start",
7264
- 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",
8524
+ 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",
7265
8525
  children: [
7266
8526
  mode !== "archived" && onPin && /* @__PURE__ */ jsxRuntime.jsx(react$1.MenuItem, { children: ({ focus }) => /* @__PURE__ */ jsxRuntime.jsxs(
7267
8527
  "button",
@@ -7387,108 +8647,18 @@ var SessionItem = React.memo(
7387
8647
  onClose: () => setMenuOpen(false),
7388
8648
  anchorRect: menuAnchor
7389
8649
  }
7390
- )
7391
- ] });
7392
- }
7393
- );
7394
- SessionItem.displayName = "SessionItem";
7395
- var SessionItem_default = SessionItem;
7396
- function DateGroupHeader({ groupName, count }) {
7397
- return /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
7398
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-700 dark:text-gray-300 font-semibold", children: groupName }),
7399
- /* @__PURE__ */ jsxRuntime.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 })
7400
- ] }) });
7401
- }
7402
- function TabBar({ tabs, activeTab, onTabChange }) {
7403
- const tablistRef = React.useRef(null);
7404
- const handleKeyDown = React.useCallback(
7405
- (e) => {
7406
- const currentIndex = tabs.findIndex((tab) => tab.id === activeTab);
7407
- if (currentIndex < 0) return;
7408
- let nextIndex = null;
7409
- switch (e.key) {
7410
- case "ArrowRight":
7411
- e.preventDefault();
7412
- nextIndex = (currentIndex + 1) % tabs.length;
7413
- break;
7414
- case "ArrowLeft":
7415
- e.preventDefault();
7416
- nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
7417
- break;
7418
- case "Home":
7419
- e.preventDefault();
7420
- nextIndex = 0;
7421
- break;
7422
- case "End":
7423
- e.preventDefault();
7424
- nextIndex = tabs.length - 1;
7425
- break;
7426
- }
7427
- if (nextIndex !== null) {
7428
- onTabChange(tabs[nextIndex].id);
7429
- const tablist = tablistRef.current;
7430
- if (tablist) {
7431
- const buttons = tablist.querySelectorAll('[role="tab"]');
7432
- buttons[nextIndex]?.focus();
7433
- }
7434
- }
7435
- },
7436
- [tabs, activeTab, onTabChange]
7437
- );
7438
- if (tabs.length === 0) {
7439
- return null;
8650
+ )
8651
+ ] });
7440
8652
  }
7441
- return /* @__PURE__ */ jsxRuntime.jsx(
7442
- "div",
7443
- {
7444
- ref: tablistRef,
7445
- className: "flex justify-center gap-1 px-4 pt-4 pb-2 border-b border-gray-200 dark:border-gray-700",
7446
- role: "tablist",
7447
- onKeyDown: handleKeyDown,
7448
- children: tabs.map((tab) => /* @__PURE__ */ jsxRuntime.jsx(
7449
- TabButton,
7450
- {
7451
- id: tab.id,
7452
- label: tab.label,
7453
- isActive: activeTab === tab.id,
7454
- onClick: () => onTabChange(tab.id)
7455
- },
7456
- tab.id
7457
- ))
7458
- }
7459
- );
7460
- }
7461
- function TabButton({ id, label, isActive, onClick }) {
7462
- return /* @__PURE__ */ jsxRuntime.jsxs(
7463
- "button",
7464
- {
7465
- id,
7466
- role: "tab",
7467
- "aria-selected": isActive,
7468
- "aria-controls": `${id}-panel`,
7469
- tabIndex: isActive ? 0 : -1,
7470
- onClick,
7471
- className: `
7472
- 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
7473
- ${isActive ? "text-primary-700 dark:text-primary-400" : "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"}
7474
- `,
7475
- children: [
7476
- label,
7477
- isActive && /* @__PURE__ */ jsxRuntime.jsx(
7478
- framerMotion.motion.div,
7479
- {
7480
- layoutId: "activeTab",
7481
- className: "absolute bottom-0 left-0 right-0 h-0.5 bg-primary-600 dark:bg-primary-500",
7482
- transition: { duration: 0.2, ease: [0.4, 0, 0.2, 1] }
7483
- }
7484
- )
7485
- ]
7486
- }
7487
- );
8653
+ );
8654
+ SessionItem.displayName = "SessionItem";
8655
+ var SessionItem_default = SessionItem;
8656
+ function DateGroupHeader({ groupName, count }) {
8657
+ return /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
8658
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-700 dark:text-gray-300 font-semibold", children: groupName }),
8659
+ /* @__PURE__ */ jsxRuntime.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 })
8660
+ ] }) });
7488
8661
  }
7489
- var MemoizedTabBar = React.memo(TabBar);
7490
- MemoizedTabBar.displayName = "TabBar";
7491
- var TabBar_default = MemoizedTabBar;
7492
8662
  init_useTranslation();
7493
8663
  function UserFilter({ users, selectedUser, onUserChange, loading }) {
7494
8664
  const { t } = useTranslation();
@@ -7824,12 +8994,32 @@ init_useTranslation();
7824
8994
  function generateId() {
7825
8995
  return Math.random().toString(36).substring(7);
7826
8996
  }
8997
+ var DEDUPE_WINDOW_MS = 2500;
8998
+ var MAX_ACTIVE_TOASTS = 5;
7827
8999
  function useToast() {
7828
9000
  const [toasts, setToasts] = React.useState([]);
9001
+ const recentToastMapRef = React.useRef(/* @__PURE__ */ new Map());
7829
9002
  const showToast = React.useCallback(
7830
9003
  (type, message, duration) => {
9004
+ const normalizedMessage = message.trim().toLowerCase();
9005
+ const key2 = `${type}:${normalizedMessage}`;
9006
+ const now = Date.now();
9007
+ const lastShownAt = recentToastMapRef.current.get(key2);
9008
+ if (lastShownAt && now - lastShownAt < DEDUPE_WINDOW_MS) {
9009
+ return;
9010
+ }
9011
+ for (const [mapKey, ts] of recentToastMapRef.current.entries()) {
9012
+ if (now - ts > DEDUPE_WINDOW_MS * 4) {
9013
+ recentToastMapRef.current.delete(mapKey);
9014
+ }
9015
+ }
9016
+ recentToastMapRef.current.set(key2, now);
7831
9017
  const id = generateId();
7832
- setToasts((prev) => [...prev, { id, type, message, duration }]);
9018
+ setToasts((prev) => {
9019
+ const next = [...prev, { id, type, message, duration }];
9020
+ if (next.length <= MAX_ACTIVE_TOASTS) return next;
9021
+ return next.slice(next.length - MAX_ACTIVE_TOASTS);
9022
+ });
7833
9023
  },
7834
9024
  []
7835
9025
  );
@@ -7838,6 +9028,7 @@ function useToast() {
7838
9028
  }, []);
7839
9029
  const dismissAll = React.useCallback(() => {
7840
9030
  setToasts([]);
9031
+ recentToastMapRef.current.clear();
7841
9032
  }, []);
7842
9033
  return {
7843
9034
  toasts,
@@ -7896,49 +9087,6 @@ function groupSessionsByDate(sessions, t) {
7896
9087
  });
7897
9088
  return groups;
7898
9089
  }
7899
-
7900
- // ui/src/bichat/utils/errorDisplay.ts
7901
- function isPermissionDeniedError(error) {
7902
- if (!error) return false;
7903
- if (error instanceof Error) {
7904
- const msg = error.message.toLowerCase();
7905
- if (msg.includes("forbidden") || msg.includes("permission denied")) return true;
7906
- }
7907
- if (typeof error === "object" && error !== null) {
7908
- const obj = error;
7909
- if (obj.code === "forbidden" || obj.code === 403) return true;
7910
- if (obj.status === 403) return true;
7911
- if (obj.statusCode === 403) return true;
7912
- if (typeof obj.response === "object" && obj.response !== null) {
7913
- const resp = obj.response;
7914
- if (resp.status === 403) return true;
7915
- }
7916
- }
7917
- if (typeof error === "string") {
7918
- const lower = error.toLowerCase();
7919
- if (lower.includes("forbidden") || lower.includes("permission denied")) return true;
7920
- }
7921
- return false;
7922
- }
7923
- function toErrorDisplay(error, fallbackTitle) {
7924
- const permDenied = isPermissionDeniedError(error);
7925
- let title = fallbackTitle;
7926
- let description = "";
7927
- if (error instanceof Error) {
7928
- description = error.message;
7929
- } else if (typeof error === "object" && error !== null) {
7930
- const obj = error;
7931
- if (typeof obj.message === "string" && obj.message) description = obj.message;
7932
- if (typeof obj.title === "string" && obj.title) title = obj.title;
7933
- if (typeof obj.detail === "string" && obj.detail) description = obj.detail;
7934
- } else if (typeof error === "string") {
7935
- description = error;
7936
- }
7937
- if (permDenied && !description) {
7938
- description = "Your account does not have permission for this action.";
7939
- }
7940
- return { title, description, isPermissionDenied: permDenied };
7941
- }
7942
9090
  function ErrorAlert({ error }) {
7943
9091
  const amber = error.isPermissionDenied;
7944
9092
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -8022,7 +9170,7 @@ function Sidebar2({
8022
9170
  const shouldReduceMotion = framerMotion.useReducedMotion();
8023
9171
  const sessionListRef = React.useRef(null);
8024
9172
  const searchContainerRef = React.useRef(null);
8025
- const { isCollapsed, isCollapsedRef, toggle, expand, collapse } = useSidebarCollapse();
9173
+ const { isCollapsed, toggle, collapse } = useSidebarCollapse();
8026
9174
  const collapsible = !onClose;
8027
9175
  const handleSidebarClick = React.useCallback(
8028
9176
  (e) => {
@@ -8033,17 +9181,6 @@ function Sidebar2({
8033
9181
  },
8034
9182
  [collapsible, toggle]
8035
9183
  );
8036
- const focusSearch = React.useCallback(() => {
8037
- if (!collapsible) return;
8038
- if (isCollapsedRef.current) {
8039
- expand();
8040
- setTimeout(() => {
8041
- searchContainerRef.current?.querySelector("input")?.focus();
8042
- }, 250);
8043
- } else {
8044
- searchContainerRef.current?.querySelector("input")?.focus();
8045
- }
8046
- }, [collapsible, expand, isCollapsedRef]);
8047
9184
  React.useEffect(() => {
8048
9185
  if (!collapsible) return;
8049
9186
  const handleKeyDown = (e) => {
@@ -8052,14 +9189,10 @@ function Sidebar2({
8052
9189
  e.preventDefault();
8053
9190
  toggle();
8054
9191
  }
8055
- if (isMod && e.key === "k") {
8056
- e.preventDefault();
8057
- focusSearch();
8058
- }
8059
9192
  };
8060
9193
  document.addEventListener("keydown", handleKeyDown);
8061
9194
  return () => document.removeEventListener("keydown", handleKeyDown);
8062
- }, [collapsible, toggle, focusSearch]);
9195
+ }, [collapsible, toggle]);
8063
9196
  React.useEffect(() => {
8064
9197
  if (!collapsible) return;
8065
9198
  const handler = (e) => {
@@ -8082,13 +9215,6 @@ function Sidebar2({
8082
9215
  const [refreshKey, setRefreshKey] = React.useState(0);
8083
9216
  const [showConfirm, setShowConfirm] = React.useState(false);
8084
9217
  const [sessionToArchive, setSessionToArchive] = React.useState(null);
8085
- const tabs = React.useMemo(() => {
8086
- const items = [{ id: "my-chats", label: t("BiChat.Sidebar.MyChats") }];
8087
- if (showAllChatsTab) {
8088
- items.push({ id: "all-chats", label: t("BiChat.Sidebar.AllChats") });
8089
- }
8090
- return items;
8091
- }, [showAllChatsTab, t]);
8092
9218
  const fetchSessions = React.useCallback(async () => {
8093
9219
  try {
8094
9220
  setLoading(true);
@@ -8301,14 +9427,6 @@ function Sidebar2({
8301
9427
  }
8302
9428
  )
8303
9429
  ] }),
8304
- showAllChatsTab && /* @__PURE__ */ jsxRuntime.jsx(
8305
- TabBar_default,
8306
- {
8307
- tabs,
8308
- activeTab,
8309
- onTabChange: (id) => setActiveTab(id)
8310
- }
8311
- ),
8312
9430
  activeTab === "all-chats" && showAllChatsTab ? /* @__PURE__ */ jsxRuntime.jsx(
8313
9431
  AllChatsList,
8314
9432
  {
@@ -8929,232 +10047,73 @@ function BiChatLayout({
8929
10047
  return () => document.removeEventListener("keydown", onKeyDown);
8930
10048
  }, [closeMobile, isMobile, isMobileOpen]);
8931
10049
  const handleDrawerDragEnd = (_, info) => {
8932
- if (info.offset.x < -80) {
8933
- closeMobile();
8934
- }
8935
- };
8936
- const content = routeKey ? /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { mode: "wait", initial: false, children: /* @__PURE__ */ jsxRuntime.jsx(
8937
- framerMotion.motion.div,
8938
- {
8939
- className: "flex flex-1 min-h-0",
8940
- initial: { opacity: 0, y: 4 },
8941
- animate: { opacity: 1, y: 0 },
8942
- exit: { opacity: 0, y: -4 },
8943
- transition: { duration: 0.15, ease: "easeOut" },
8944
- children
8945
- },
8946
- routeKey
8947
- ) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-1 min-h-0", children });
8948
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `relative flex flex-1 w-full h-full min-h-0 overflow-hidden ${className}`, children: [
8949
- /* @__PURE__ */ jsxRuntime.jsx(SkipLink, {}),
8950
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden md:block", children: renderSidebar({}) }),
8951
- /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: isMobile && isMobileOpen && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
8952
- /* @__PURE__ */ jsxRuntime.jsx(
8953
- framerMotion.motion.div,
8954
- {
8955
- className: "fixed inset-0 z-40 bg-black/40",
8956
- initial: { opacity: 0 },
8957
- animate: { opacity: 1 },
8958
- exit: { opacity: 0 },
8959
- onClick: closeMobile,
8960
- "aria-hidden": "true"
8961
- },
8962
- "sidebar-backdrop"
8963
- ),
8964
- /* @__PURE__ */ jsxRuntime.jsx(
8965
- framerMotion.motion.div,
8966
- {
8967
- className: "fixed inset-y-0 left-0 z-50 w-[18rem] max-w-[85vw] shadow-2xl",
8968
- initial: { x: "-100%" },
8969
- animate: { x: 0 },
8970
- exit: { x: "-100%" },
8971
- transition: { type: "spring", stiffness: 320, damping: 32 },
8972
- drag: "x",
8973
- dragDirectionLock: true,
8974
- dragConstraints: { left: -120, right: 0 },
8975
- dragElastic: { left: 0.2, right: 0 },
8976
- onDragEnd: handleDrawerDragEnd,
8977
- onClick: (e) => e.stopPropagation(),
8978
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { ref: drawerRef, className: "h-full bg-white dark:bg-gray-900", children: renderSidebar({ onClose: closeMobile }) })
8979
- },
8980
- "sidebar-drawer"
8981
- )
8982
- ] }) }),
8983
- /* @__PURE__ */ jsxRuntime.jsxs("main", { id: "main-content", className: "relative flex-1 flex flex-col min-h-0 overflow-hidden", children: [
8984
- isMobile && !isMobileOpen && /* @__PURE__ */ jsxRuntime.jsx(
8985
- "button",
8986
- {
8987
- ref: menuButtonRef,
8988
- onClick: openMobile,
8989
- 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",
8990
- "aria-label": t("BiChat.Layout.OpenSidebar"),
8991
- title: t("BiChat.Layout.OpenSidebar"),
8992
- children: /* @__PURE__ */ jsxRuntime.jsx(react.List, { size: 20, weight: "bold" })
8993
- }
8994
- ),
8995
- content
8996
- ] })
8997
- ] });
8998
- }
8999
- init_useTranslation();
9000
- var variantStyles = {
9001
- error: {
9002
- container: "border-red-200 bg-red-50 dark:bg-red-900/20",
9003
- title: "text-red-800 dark:text-red-300",
9004
- message: "text-red-700 dark:text-red-400",
9005
- icon: "text-red-600 dark:text-red-400",
9006
- button: "text-red-400 hover:text-red-600 dark:hover:text-red-300",
9007
- retryButton: "bg-red-600 dark:bg-red-700 hover:bg-red-700 dark:hover:bg-red-800 text-white",
9008
- Icon: react.XCircle
9009
- },
9010
- success: {
9011
- container: "border-green-200 bg-green-50 dark:bg-green-900/20",
9012
- title: "text-green-800 dark:text-green-300",
9013
- message: "text-green-700 dark:text-green-400",
9014
- icon: "text-green-600 dark:text-green-400",
9015
- button: "text-green-400 hover:text-green-600 dark:hover:text-green-300",
9016
- retryButton: "bg-green-600 dark:bg-green-700 hover:bg-green-700 dark:hover:bg-green-800 text-white",
9017
- Icon: react.CheckCircle
9018
- },
9019
- warning: {
9020
- container: "border-yellow-200 bg-yellow-50 dark:bg-yellow-900/20",
9021
- title: "text-yellow-800 dark:text-yellow-300",
9022
- message: "text-yellow-700 dark:text-yellow-400",
9023
- icon: "text-yellow-600 dark:text-yellow-400",
9024
- button: "text-yellow-400 hover:text-yellow-600 dark:hover:text-yellow-300",
9025
- retryButton: "bg-yellow-600 dark:bg-yellow-700 hover:bg-yellow-700 dark:hover:bg-yellow-800 text-white",
9026
- Icon: react.Warning
9027
- },
9028
- info: {
9029
- container: "border-blue-200 bg-blue-50 dark:bg-blue-900/20",
9030
- title: "text-blue-800 dark:text-blue-300",
9031
- message: "text-blue-700 dark:text-blue-400",
9032
- icon: "text-blue-600 dark:text-blue-400",
9033
- button: "text-blue-400 hover:text-blue-600 dark:hover:text-blue-300",
9034
- retryButton: "bg-blue-600 dark:bg-blue-700 hover:bg-blue-700 dark:hover:bg-blue-800 text-white",
9035
- Icon: react.Info
9036
- }
9037
- };
9038
- function Alert({
9039
- variant = "info",
9040
- message,
9041
- title,
9042
- onDismiss,
9043
- onRetry,
9044
- show = true,
9045
- dismissible = true
9046
- }) {
9047
- const { t } = useTranslation();
9048
- const styles = variantStyles[variant];
9049
- const IconComponent = styles.Icon;
9050
- return /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: show && /* @__PURE__ */ jsxRuntime.jsx(
9051
- framerMotion.motion.div,
9052
- {
9053
- variants: errorMessageVariants,
9054
- initial: "initial",
9055
- animate: "animate",
9056
- exit: "exit",
9057
- className: `border-t border ${styles.container} px-4 py-3`,
9058
- role: "alert",
9059
- "aria-live": "assertive",
9060
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full flex items-start justify-between px-4", children: [
9061
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-3 flex-1", children: [
9062
- /* @__PURE__ */ jsxRuntime.jsx(IconComponent, { size: 20, className: `w-5 h-5 ${styles.icon} flex-shrink-0 mt-0.5` }),
9063
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
9064
- title && /* @__PURE__ */ jsxRuntime.jsx("p", { className: `text-sm ${styles.title} font-medium`, children: title }),
9065
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: `text-sm ${styles.message} ${title ? "mt-1" : ""}`, children: message }),
9066
- onRetry && /* @__PURE__ */ jsxRuntime.jsx(
9067
- "button",
9068
- {
9069
- onClick: onRetry,
9070
- className: `mt-2 text-xs px-3 py-1.5 rounded ${styles.retryButton} transition-colors font-medium`,
9071
- children: t("BiChat.Chat.Retry")
9072
- }
9073
- )
9074
- ] })
9075
- ] }),
9076
- dismissible && onDismiss && /* @__PURE__ */ jsxRuntime.jsx(
9077
- "button",
9078
- {
9079
- onClick: onDismiss,
9080
- className: `${styles.button} transition-colors flex-shrink-0`,
9081
- "aria-label": t("BiChat.Chat.DismissNotification"),
9082
- children: /* @__PURE__ */ jsxRuntime.jsx(react.X, { size: 20, className: "w-5 h-5" })
9083
- }
9084
- )
9085
- ] })
9086
- }
9087
- ) });
9088
- }
9089
- var Alert_default = React.memo(Alert);
9090
- init_useTranslation();
9091
- function ArchiveBanner({
9092
- show = true,
9093
- onRestore,
9094
- restoring = false,
9095
- onRestoreComplete
9096
- }) {
9097
- const { t } = useTranslation();
9098
- const [error, setError] = React.useState(null);
9099
- const handleRestore = async () => {
9100
- try {
9101
- setError(null);
9102
- if (onRestore) {
9103
- await onRestore();
9104
- }
9105
- if (onRestoreComplete) {
9106
- onRestoreComplete();
9107
- }
9108
- } catch (err) {
9109
- const message = err instanceof Error ? err.message : t("BiChat.Archive.RestoreFailed");
9110
- setError(message);
10050
+ if (info.offset.x < -80) {
10051
+ closeMobile();
9111
10052
  }
9112
10053
  };
9113
- return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
9114
- /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: show && /* @__PURE__ */ jsxRuntime.jsx(
9115
- framerMotion.motion.div,
9116
- {
9117
- variants: errorMessageVariants,
9118
- initial: "initial",
9119
- animate: "animate",
9120
- exit: "exit",
9121
- className: "border-t border border-blue-200 bg-blue-50 dark:bg-blue-900/20 px-4 py-3",
9122
- role: "region",
9123
- "aria-label": t("BiChat.Archive.Banner"),
9124
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full flex items-start justify-between px-4", children: [
9125
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-3 flex-1", children: [
9126
- /* @__PURE__ */ jsxRuntime.jsx(react.Archive, { size: 20, className: "w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" }),
9127
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-blue-700 dark:text-blue-400", children: t("BiChat.Archive.Archived") }) })
9128
- ] }),
9129
- /* @__PURE__ */ jsxRuntime.jsx(
9130
- "button",
9131
- {
9132
- onClick: handleRestore,
9133
- disabled: restoring,
9134
- 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",
9135
- "aria-label": t("BiChat.Archive.Restore"),
9136
- children: restoring ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
9137
- /* @__PURE__ */ jsxRuntime.jsx(react.Spinner, { size: 16, className: "w-4 h-4 animate-spin" }),
9138
- t("BiChat.Archive.Restoring")
9139
- ] }) : t("BiChat.Archive.Restore")
9140
- }
9141
- )
9142
- ] })
9143
- }
9144
- ) }),
9145
- error && /* @__PURE__ */ jsxRuntime.jsx(
9146
- Alert_default,
9147
- {
9148
- variant: "error",
9149
- message: error,
9150
- title: t("BiChat.Archive.RestoreFailed"),
9151
- onDismiss: () => setError(null),
9152
- dismissible: true
9153
- }
9154
- )
10054
+ const content = routeKey ? /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { mode: "wait", initial: false, children: /* @__PURE__ */ jsxRuntime.jsx(
10055
+ framerMotion.motion.div,
10056
+ {
10057
+ className: "flex flex-1 min-h-0",
10058
+ initial: { opacity: 0, y: 4 },
10059
+ animate: { opacity: 1, y: 0 },
10060
+ exit: { opacity: 0, y: -4 },
10061
+ transition: { duration: 0.15, ease: "easeOut" },
10062
+ children
10063
+ },
10064
+ routeKey
10065
+ ) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-1 min-h-0", children });
10066
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `relative flex flex-1 w-full h-full min-h-0 overflow-hidden ${className}`, children: [
10067
+ /* @__PURE__ */ jsxRuntime.jsx(SkipLink, {}),
10068
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden md:block", children: renderSidebar({}) }),
10069
+ /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: isMobile && isMobileOpen && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
10070
+ /* @__PURE__ */ jsxRuntime.jsx(
10071
+ framerMotion.motion.div,
10072
+ {
10073
+ className: "fixed inset-0 z-40 bg-black/40",
10074
+ initial: { opacity: 0 },
10075
+ animate: { opacity: 1 },
10076
+ exit: { opacity: 0 },
10077
+ onClick: closeMobile,
10078
+ "aria-hidden": "true"
10079
+ },
10080
+ "sidebar-backdrop"
10081
+ ),
10082
+ /* @__PURE__ */ jsxRuntime.jsx(
10083
+ framerMotion.motion.div,
10084
+ {
10085
+ className: "fixed inset-y-0 left-0 z-50 w-[18rem] max-w-[85vw] shadow-2xl",
10086
+ initial: { x: "-100%" },
10087
+ animate: { x: 0 },
10088
+ exit: { x: "-100%" },
10089
+ transition: { type: "spring", stiffness: 320, damping: 32 },
10090
+ drag: "x",
10091
+ dragDirectionLock: true,
10092
+ dragConstraints: { left: -120, right: 0 },
10093
+ dragElastic: { left: 0.2, right: 0 },
10094
+ onDragEnd: handleDrawerDragEnd,
10095
+ onClick: (e) => e.stopPropagation(),
10096
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { ref: drawerRef, className: "h-full bg-white dark:bg-gray-900", children: renderSidebar({ onClose: closeMobile }) })
10097
+ },
10098
+ "sidebar-drawer"
10099
+ )
10100
+ ] }) }),
10101
+ /* @__PURE__ */ jsxRuntime.jsxs("main", { id: "main-content", className: "relative flex-1 flex flex-col min-h-0 overflow-hidden", children: [
10102
+ isMobile && !isMobileOpen && /* @__PURE__ */ jsxRuntime.jsx(
10103
+ "button",
10104
+ {
10105
+ ref: menuButtonRef,
10106
+ onClick: openMobile,
10107
+ 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",
10108
+ "aria-label": t("BiChat.Layout.OpenSidebar"),
10109
+ title: t("BiChat.Layout.OpenSidebar"),
10110
+ children: /* @__PURE__ */ jsxRuntime.jsx(react.List, { size: 20, weight: "bold" })
10111
+ }
10112
+ ),
10113
+ content
10114
+ ] })
9155
10115
  ] });
9156
10116
  }
9157
- var ArchiveBanner_default = React.memo(ArchiveBanner);
9158
10117
 
9159
10118
  // ui/src/bichat/components/RetryActionArea.tsx
9160
10119
  init_useTranslation();
@@ -9208,66 +10167,6 @@ var RetryActionArea = React.memo(function RetryActionArea2({
9208
10167
  )
9209
10168
  );
9210
10169
  });
9211
-
9212
- // ui/src/bichat/components/StreamError.tsx
9213
- init_useTranslation();
9214
- function StreamError({
9215
- error,
9216
- onRetry,
9217
- onRegenerate,
9218
- compact = false
9219
- }) {
9220
- const { t } = useTranslation();
9221
- return /* @__PURE__ */ jsxRuntime.jsxs(
9222
- framerMotion.motion.div,
9223
- {
9224
- initial: { opacity: 0, y: 10 },
9225
- animate: { opacity: 1, y: 0 },
9226
- exit: { opacity: 0, y: -10 },
9227
- 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`,
9228
- role: "alert",
9229
- children: [
9230
- /* @__PURE__ */ jsxRuntime.jsx(
9231
- react.Warning,
9232
- {
9233
- className: "w-5 h-5 text-red-500 dark:text-red-400 flex-shrink-0",
9234
- weight: "fill"
9235
- }
9236
- ),
9237
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
9238
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-red-800 dark:text-red-200", children: t("BiChat.Error.Generic") }),
9239
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-red-600 dark:text-red-300 break-words", children: error })
9240
- ] }),
9241
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [
9242
- onRetry && /* @__PURE__ */ jsxRuntime.jsxs(
9243
- "button",
9244
- {
9245
- onClick: onRetry,
9246
- 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",
9247
- type: "button",
9248
- children: [
9249
- /* @__PURE__ */ jsxRuntime.jsx(react.ArrowClockwise, { className: "w-4 h-4" }),
9250
- t("BiChat.StreamError.Retry")
9251
- ]
9252
- }
9253
- ),
9254
- onRegenerate && /* @__PURE__ */ jsxRuntime.jsxs(
9255
- "button",
9256
- {
9257
- onClick: onRegenerate,
9258
- 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",
9259
- type: "button",
9260
- children: [
9261
- /* @__PURE__ */ jsxRuntime.jsx(react.ArrowsCounterClockwise, { className: "w-4 h-4" }),
9262
- t("BiChat.StreamError.Regenerate")
9263
- ]
9264
- }
9265
- )
9266
- ] })
9267
- ]
9268
- }
9269
- );
9270
- }
9271
10170
  init_useTranslation();
9272
10171
  function MessageActions({
9273
10172
  message,
@@ -10666,9 +11565,10 @@ function useMarkdownCopy(options = {}) {
10666
11565
  const [copiedStates, setCopiedStates] = React.useState(/* @__PURE__ */ new Map());
10667
11566
  const timeoutsRef = React.useRef(/* @__PURE__ */ new Map());
10668
11567
  React.useEffect(() => {
11568
+ const timeouts = timeoutsRef.current;
10669
11569
  return () => {
10670
- timeoutsRef.current.forEach((timeout) => clearTimeout(timeout));
10671
- timeoutsRef.current.clear();
11570
+ timeouts.forEach((timeout) => clearTimeout(timeout));
11571
+ timeouts.clear();
10672
11572
  };
10673
11573
  }, []);
10674
11574
  const isCopied = React.useCallback(
@@ -10791,8 +11691,9 @@ function ConfigProvider({ config, useGlobalConfig = false, children }) {
10791
11691
  if (config) {
10792
11692
  resolvedConfig = config;
10793
11693
  } else if (useGlobalConfig && typeof window !== "undefined") {
10794
- const globalContext = window.__BICHAT_CONTEXT__;
10795
- const globalCSRF = window.__CSRF_TOKEN__;
11694
+ const w = window;
11695
+ const globalContext = w.__APPLET_CONTEXT__;
11696
+ const globalCSRF = w.__CSRF_TOKEN__;
10796
11697
  if (globalContext) {
10797
11698
  resolvedConfig = {
10798
11699
  user: {
@@ -10989,98 +11890,6 @@ function createHeadersWithCSRF(init) {
10989
11890
  return addCSRFHeader(headers);
10990
11891
  }
10991
11892
 
10992
- // ui/src/applet-devtools/enabled.ts
10993
- function shouldEnableAppletDevtools() {
10994
- if (typeof window === "undefined") return false;
10995
- const url = new URL(window.location.href);
10996
- if (url.searchParams.get("appletDebug") === "1") return true;
10997
- try {
10998
- return window.localStorage.getItem("iotaAppletDevtools") === "1";
10999
- } catch {
11000
- return false;
11001
- }
11002
- }
11003
-
11004
- // ui/src/applet-host/rpc.ts
11005
- var AppletRPCException = class extends Error {
11006
- constructor(args) {
11007
- super(args.message);
11008
- this.name = "AppletRPCException";
11009
- this.code = args.code;
11010
- this.details = args.details;
11011
- this.cause = args.cause;
11012
- }
11013
- };
11014
- function createAppletRPCClient(options) {
11015
- const fetcher = options.fetcher ?? fetch;
11016
- async function call(method, params) {
11017
- const req = { id: crypto.randomUUID(), method, params };
11018
- const startedAt = typeof performance !== "undefined" ? performance.now() : Date.now();
11019
- maybeDispatchRPCEvent({
11020
- id: req.id,
11021
- method: req.method,
11022
- status: "start"
11023
- });
11024
- try {
11025
- const resp = await fetcher(options.endpoint, {
11026
- method: "POST",
11027
- headers: { "Content-Type": "application/json" },
11028
- body: JSON.stringify(req)
11029
- });
11030
- if (!resp.ok) {
11031
- throw new AppletRPCException({
11032
- code: "http_error",
11033
- message: `HTTP ${resp.status}`,
11034
- details: { status: resp.status }
11035
- });
11036
- }
11037
- const json = await resp.json();
11038
- if (json.error) {
11039
- throw new AppletRPCException({
11040
- code: json.error.code,
11041
- message: json.error.message,
11042
- details: json.error.details
11043
- });
11044
- }
11045
- if (json.result === void 0) {
11046
- throw new AppletRPCException({
11047
- code: "invalid_response",
11048
- message: "Missing result in successful response"
11049
- });
11050
- }
11051
- maybeDispatchRPCEvent({
11052
- id: req.id,
11053
- method: req.method,
11054
- status: "success",
11055
- durationMs: elapsedMs(startedAt)
11056
- });
11057
- return json.result;
11058
- } catch (err) {
11059
- maybeDispatchRPCEvent({
11060
- id: req.id,
11061
- method: req.method,
11062
- status: "error",
11063
- durationMs: elapsedMs(startedAt),
11064
- error: err
11065
- });
11066
- throw err;
11067
- }
11068
- }
11069
- async function callTyped(method, params) {
11070
- return call(method, params);
11071
- }
11072
- return { call, callTyped };
11073
- }
11074
- function maybeDispatchRPCEvent(detail) {
11075
- if (typeof window === "undefined") return;
11076
- if (!shouldEnableAppletDevtools()) return;
11077
- window.dispatchEvent(new CustomEvent("iota:applet-rpc", { detail }));
11078
- }
11079
- function elapsedMs(startedAt) {
11080
- const now = typeof performance !== "undefined" ? performance.now() : Date.now();
11081
- return Math.max(0, Math.round(now - startedAt));
11082
- }
11083
-
11084
11893
  // ui/src/bichat/data/HttpDataSource.ts
11085
11894
  init_chartSpec();
11086
11895
 
@@ -11126,7 +11935,9 @@ async function* parseSSEStream(reader) {
11126
11935
  reader.releaseLock();
11127
11936
  }
11128
11937
  }
11938
+ var TERMINAL_TYPES = /* @__PURE__ */ new Set(["done", "error"]);
11129
11939
  async function* parseBichatStream(reader) {
11940
+ let yieldedTerminal = false;
11130
11941
  for await (const event of parseSSEStream(reader)) {
11131
11942
  const parsed = event;
11132
11943
  const inferredType = parsed.type || (parsed.content ? "content" : "error");
@@ -11134,11 +11945,50 @@ async function* parseBichatStream(reader) {
11134
11945
  ...parsed,
11135
11946
  type: inferredType
11136
11947
  };
11948
+ if (TERMINAL_TYPES.has(inferredType)) {
11949
+ yieldedTerminal = true;
11950
+ }
11137
11951
  yield normalized;
11138
11952
  }
11953
+ if (!yieldedTerminal) {
11954
+ yield { type: "done" };
11955
+ }
11956
+ }
11957
+ async function* parseBichatStreamEvents(reader) {
11958
+ for await (const chunk of parseBichatStream(reader)) {
11959
+ const event = toStreamEvent(chunk);
11960
+ if (event) yield event;
11961
+ }
11962
+ }
11963
+ function toStreamEvent(chunk) {
11964
+ switch (chunk.type) {
11965
+ case "chunk":
11966
+ case "content":
11967
+ return { type: "content", content: chunk.content ?? "" };
11968
+ case "tool_start":
11969
+ return chunk.tool ? { type: "tool_start", tool: chunk.tool } : null;
11970
+ case "tool_end":
11971
+ return chunk.tool ? { type: "tool_end", tool: chunk.tool } : null;
11972
+ case "usage":
11973
+ return chunk.usage ? { type: "usage", usage: chunk.usage } : null;
11974
+ case "user_message":
11975
+ return chunk.sessionId ? { type: "user_message", sessionId: chunk.sessionId } : null;
11976
+ case "interrupt":
11977
+ return chunk.interrupt ? { type: "interrupt", interrupt: chunk.interrupt, sessionId: chunk.sessionId } : null;
11978
+ case "done":
11979
+ return { type: "done", sessionId: chunk.sessionId, generationMs: chunk.generationMs };
11980
+ case "error":
11981
+ return { type: "error", error: chunk.error ?? "Unknown error" };
11982
+ default:
11983
+ return null;
11984
+ }
11139
11985
  }
11140
11986
 
11141
11987
  // ui/src/bichat/data/HttpDataSource.ts
11988
+ function isSessionNotFoundError(err) {
11989
+ if (!(err instanceof AppletRPCException)) return false;
11990
+ return err.code === "not_found" || err.code === "session_not_found";
11991
+ }
11142
11992
  function toSession(session) {
11143
11993
  return {
11144
11994
  ...session,
@@ -11326,34 +12176,18 @@ function attachArtifactsToTurns(turns, artifacts) {
11326
12176
  }
11327
12177
  };
11328
12178
  });
11329
- const assistantPositions = [];
11330
12179
  const turnIndexByMessageID = /* @__PURE__ */ new Map();
11331
12180
  nextTurns.forEach((turn, index) => {
11332
12181
  turnIndexByMessageID.set(turn.userTurn.id, index);
11333
12182
  const assistantTurn = turn.assistantTurn;
11334
12183
  if (!assistantTurn) return;
11335
12184
  turnIndexByMessageID.set(assistantTurn.id, index);
11336
- assistantPositions.push({
11337
- index,
11338
- createdAtMs: toMillis(assistantTurn.createdAt || turn.createdAt)
11339
- });
11340
12185
  });
11341
- if (assistantPositions.length === 0) return turns;
11342
- const findFallbackAssistantIndex = (artifactCreatedAt) => {
11343
- const artifactMs = toMillis(artifactCreatedAt);
11344
- if (!Number.isFinite(artifactMs)) {
11345
- return assistantPositions[assistantPositions.length - 1].index;
11346
- }
11347
- for (const pos of assistantPositions) {
11348
- if (Number.isFinite(pos.createdAtMs) && pos.createdAtMs >= artifactMs) {
11349
- return pos.index;
11350
- }
11351
- }
11352
- return assistantPositions[assistantPositions.length - 1].index;
11353
- };
11354
12186
  for (const entry of downloadArtifacts) {
11355
12187
  const messageID = entry.raw.messageId;
11356
- const targetIndex = (messageID ? turnIndexByMessageID.get(messageID) : void 0) ?? findFallbackAssistantIndex(entry.raw.createdAt);
12188
+ if (!messageID) continue;
12189
+ const targetIndex = turnIndexByMessageID.get(messageID);
12190
+ if (targetIndex === void 0) continue;
11357
12191
  const assistantTurn = nextTurns[targetIndex]?.assistantTurn;
11358
12192
  if (!assistantTurn) continue;
11359
12193
  const exists = assistantTurn.artifacts.some(
@@ -11365,7 +12199,9 @@ function attachArtifactsToTurns(turns, artifacts) {
11365
12199
  }
11366
12200
  for (const raw of chartArtifacts) {
11367
12201
  const messageID = raw.messageId;
11368
- const targetIndex = (messageID ? turnIndexByMessageID.get(messageID) : void 0) ?? findFallbackAssistantIndex(raw.createdAt);
12202
+ if (!messageID) continue;
12203
+ const targetIndex = turnIndexByMessageID.get(messageID);
12204
+ if (targetIndex === void 0) continue;
11369
12205
  const assistantTurn = nextTurns[targetIndex]?.assistantTurn;
11370
12206
  if (!assistantTurn) continue;
11371
12207
  if (assistantTurn.chartData) continue;
@@ -11391,7 +12227,8 @@ var HttpDataSource = class {
11391
12227
  this.navigateToSession = config.navigateToSession;
11392
12228
  }
11393
12229
  this.rpc = createAppletRPCClient({
11394
- endpoint: `${this.config.baseUrl}${this.config.rpcEndpoint}`
12230
+ endpoint: `${this.config.baseUrl}${this.config.rpcEndpoint}`,
12231
+ timeoutMs: this.config.timeout
11395
12232
  });
11396
12233
  }
11397
12234
  /**
@@ -11447,8 +12284,11 @@ var HttpDataSource = class {
11447
12284
  pendingQuestion: toPendingQuestion(data.pendingQuestion)
11448
12285
  };
11449
12286
  } catch (err) {
12287
+ if (isSessionNotFoundError(err)) {
12288
+ return null;
12289
+ }
11450
12290
  console.error("Failed to fetch session:", err);
11451
- return null;
12291
+ throw err instanceof Error ? err : new Error("Failed to fetch session");
11452
12292
  }
11453
12293
  }
11454
12294
  async fetchSessionArtifacts(sessionId, options) {
@@ -11537,15 +12377,28 @@ var HttpDataSource = class {
11537
12377
  url: a.url
11538
12378
  }))
11539
12379
  };
12380
+ let connectionTimeoutID;
12381
+ let connectionTimedOut = false;
11540
12382
  try {
12383
+ const timeoutMs = this.config.timeout ?? 0;
12384
+ if (timeoutMs > 0) {
12385
+ connectionTimeoutID = setTimeout(() => {
12386
+ connectionTimedOut = true;
12387
+ this.abortController?.abort();
12388
+ }, timeoutMs);
12389
+ }
11541
12390
  const response = await fetch(url, {
11542
12391
  method: "POST",
11543
12392
  headers: this.createHeaders(),
11544
12393
  body: JSON.stringify(payload),
11545
12394
  signal: this.abortController.signal
11546
12395
  });
12396
+ if (connectionTimeoutID !== void 0) {
12397
+ clearTimeout(connectionTimeoutID);
12398
+ connectionTimeoutID = void 0;
12399
+ }
11547
12400
  if (!response.ok) {
11548
- throw new Error(`Stream request failed: ${response.statusText}`);
12401
+ throw new Error(`Stream request failed: HTTP ${response.status}`);
11549
12402
  }
11550
12403
  if (!response.body) {
11551
12404
  throw new Error("Response body is null");
@@ -11562,7 +12415,7 @@ var HttpDataSource = class {
11562
12415
  if (err.name === "AbortError") {
11563
12416
  yield {
11564
12417
  type: "error",
11565
- error: "Stream cancelled"
12418
+ error: connectionTimedOut ? `Stream request timed out after ${this.config.timeout}ms` : "Stream cancelled"
11566
12419
  };
11567
12420
  } else {
11568
12421
  yield {
@@ -11577,6 +12430,9 @@ var HttpDataSource = class {
11577
12430
  };
11578
12431
  }
11579
12432
  } finally {
12433
+ if (connectionTimeoutID !== void 0) {
12434
+ clearTimeout(connectionTimeoutID);
12435
+ }
11580
12436
  if (signal && onExternalAbort) {
11581
12437
  signal.removeEventListener("abort", onExternalAbort);
11582
12438
  }
@@ -11708,6 +12564,7 @@ exports.Bubble = Bubble;
11708
12564
  exports.CHART_VISUAL = CHART_VISUAL;
11709
12565
  exports.ChartCard = ChartCard;
11710
12566
  exports.ChatHeader = ChatHeader;
12567
+ exports.ChatMachine = ChatMachine;
11711
12568
  exports.ChatSession = ChatSession;
11712
12569
  exports.ChatSessionProvider = ChatSessionProvider;
11713
12570
  exports.CodeOutputsPanel = CodeOutputsPanel;
@@ -11757,7 +12614,6 @@ exports.SourcesPanel = SourcesPanel;
11757
12614
  exports.StreamError = StreamError;
11758
12615
  exports.StreamingCursor = StreamingCursor;
11759
12616
  exports.SystemMessage = SystemMessage;
11760
- exports.TabBar = MemoizedTabBar;
11761
12617
  exports.ThemeProvider = ThemeProvider;
11762
12618
  exports.Toast = Toast;
11763
12619
  exports.ToastContainer = ToastContainer;
@@ -11795,11 +12651,13 @@ exports.lightTheme = lightTheme;
11795
12651
  exports.listItemVariants = listItemVariants;
11796
12652
  exports.messageContainerVariants = messageContainerVariants;
11797
12653
  exports.messageVariants = messageVariants;
12654
+ exports.parseBichatStream = parseBichatStream;
12655
+ exports.parseBichatStreamEvents = parseBichatStreamEvents;
12656
+ exports.parseSSEStream = parseSSEStream;
11798
12657
  exports.scaleFadeVariants = scaleFadeVariants;
11799
12658
  exports.sessionItemVariants = sessionItemVariants;
11800
12659
  exports.staggerContainerVariants = staggerContainerVariants;
11801
12660
  exports.toErrorDisplay = toErrorDisplay;
11802
- exports.toastVariants = toastVariants;
11803
12661
  exports.typingDotVariants = typingDotVariants;
11804
12662
  exports.useActionButtonContext = useActionButtonContext;
11805
12663
  exports.useAttachments = useAttachments;