@burtson-labs/bandit-engine 2.0.159 → 2.0.160

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.
Files changed (49) hide show
  1. package/README.md +17 -0
  2. package/dist/{aiProviderStore-ZQ5VVVKQ.mjs → aiProviderStore-BNZGM6QU.mjs} +2 -2
  3. package/dist/chat-4BOIV4VP.mjs +16 -0
  4. package/dist/chat-provider.js +2 -2
  5. package/dist/chat-provider.js.map +1 -1
  6. package/dist/chat-provider.mjs +6 -6
  7. package/dist/{chunk-GWQRTUKF.mjs → chunk-B3HAQC6F.mjs} +2 -2
  8. package/dist/chunk-B3HAQC6F.mjs.map +1 -0
  9. package/dist/{chunk-7HC6ONWM.mjs → chunk-IABD4L6K.mjs} +43 -44
  10. package/dist/chunk-IABD4L6K.mjs.map +1 -0
  11. package/dist/{chunk-R45DM35B.mjs → chunk-LFQH6M6B.mjs} +3 -3
  12. package/dist/chunk-LFQH6M6B.mjs.map +1 -0
  13. package/dist/{chunk-IS2LWSBZ.mjs → chunk-LNGF2XAZ.mjs} +3 -3
  14. package/dist/chunk-LNGF2XAZ.mjs.map +1 -0
  15. package/dist/{chunk-GZDJNVAS.mjs → chunk-LTWMMHFT.mjs} +11 -9
  16. package/dist/chunk-LTWMMHFT.mjs.map +1 -0
  17. package/dist/{chunk-F5JS4W2K.mjs → chunk-MHF76C2F.mjs} +246 -127
  18. package/dist/chunk-MHF76C2F.mjs.map +1 -0
  19. package/dist/{chunk-B2TJZM4Z.mjs → chunk-P6SLB5RC.mjs} +3 -3
  20. package/dist/{chunk-ZW3YNAD7.mjs → chunk-PDQT564P.mjs} +5 -5
  21. package/dist/{chunk-ZW3YNAD7.mjs.map → chunk-PDQT564P.mjs.map} +1 -1
  22. package/dist/{chunk-TCW753JI.mjs → chunk-PXCIDXRP.mjs} +6 -6
  23. package/dist/{chunk-2X3ZTDUW.mjs → chunk-TKSOJGHX.mjs} +5 -5
  24. package/dist/{chunk-ZOCXFQID.mjs → chunk-XTB6ZLUI.mjs} +2 -2
  25. package/dist/{chunk-ZOCXFQID.mjs.map → chunk-XTB6ZLUI.mjs.map} +1 -1
  26. package/dist/index.js +298 -163
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.mjs +11 -11
  29. package/dist/management/management.js +292 -157
  30. package/dist/management/management.js.map +1 -1
  31. package/dist/management/management.mjs +9 -9
  32. package/dist/modals/chat-modal/chat-modal.js +9 -7
  33. package/dist/modals/chat-modal/chat-modal.js.map +1 -1
  34. package/dist/modals/chat-modal/chat-modal.mjs +5 -5
  35. package/dist/{modelStore-KBUW3XAV.mjs → modelStore-NMCRL3FR.mjs} +2 -2
  36. package/package.json +6 -4
  37. package/dist/chat-HM563VOV.mjs +0 -16
  38. package/dist/chunk-7HC6ONWM.mjs.map +0 -1
  39. package/dist/chunk-F5JS4W2K.mjs.map +0 -1
  40. package/dist/chunk-GWQRTUKF.mjs.map +0 -1
  41. package/dist/chunk-GZDJNVAS.mjs.map +0 -1
  42. package/dist/chunk-IS2LWSBZ.mjs.map +0 -1
  43. package/dist/chunk-R45DM35B.mjs.map +0 -1
  44. /package/dist/{aiProviderStore-ZQ5VVVKQ.mjs.map → aiProviderStore-BNZGM6QU.mjs.map} +0 -0
  45. /package/dist/{chat-HM563VOV.mjs.map → chat-4BOIV4VP.mjs.map} +0 -0
  46. /package/dist/{chunk-B2TJZM4Z.mjs.map → chunk-P6SLB5RC.mjs.map} +0 -0
  47. /package/dist/{chunk-TCW753JI.mjs.map → chunk-PXCIDXRP.mjs.map} +0 -0
  48. /package/dist/{chunk-2X3ZTDUW.mjs.map → chunk-TKSOJGHX.mjs.map} +0 -0
  49. /package/dist/{modelStore-KBUW3XAV.mjs.map → modelStore-NMCRL3FR.mjs.map} +0 -0
@@ -5,10 +5,10 @@ import {
5
5
  StreamingMarkdown_default,
6
6
  isNativeShell,
7
7
  isStandalonePwa
8
- } from "./chunk-R45DM35B.mjs";
8
+ } from "./chunk-LFQH6M6B.mjs";
9
9
  import {
10
10
  useMCPToolsStore
11
- } from "./chunk-IS2LWSBZ.mjs";
11
+ } from "./chunk-LNGF2XAZ.mjs";
12
12
  import {
13
13
  AddIcon,
14
14
  ArrowDownwardIcon,
@@ -38,7 +38,7 @@ import {
38
38
  useNotificationService,
39
39
  useTTS,
40
40
  useVoiceStore
41
- } from "./chunk-GZDJNVAS.mjs";
41
+ } from "./chunk-LTWMMHFT.mjs";
42
42
  import {
43
43
  brandingService_default,
44
44
  detectMessageMood,
@@ -61,19 +61,19 @@ import {
61
61
  useMemoryStore,
62
62
  useProjectStore,
63
63
  useVectorStore
64
- } from "./chunk-ZW3YNAD7.mjs";
64
+ } from "./chunk-PDQT564P.mjs";
65
65
  import {
66
66
  indexedDBService_default,
67
67
  useModelStore,
68
68
  usePackageSettingsStore,
69
69
  usePreferencesStore
70
- } from "./chunk-ZOCXFQID.mjs";
70
+ } from "./chunk-XTB6ZLUI.mjs";
71
71
  import {
72
- authenticationService as authenticationService2,
72
+ authenticationService,
73
73
  readPersistedToken,
74
74
  useAIProviderStore,
75
75
  useAuthenticationStore
76
- } from "./chunk-GWQRTUKF.mjs";
76
+ } from "./chunk-B3HAQC6F.mjs";
77
77
  import {
78
78
  debugLogger
79
79
  } from "./chunk-KCI46M23.mjs";
@@ -928,7 +928,7 @@ var useEngineStore = create((set, get) => ({
928
928
  }
929
929
  try {
930
930
  const headers = { "Content-Type": "application/json" };
931
- const token = authenticationService2.getToken();
931
+ const token = authenticationService.getToken();
932
932
  if (token) headers["Authorization"] = `Bearer ${token}`;
933
933
  const res = await fetch(`${base}/models`, { headers });
934
934
  const data = await res.json();
@@ -1217,7 +1217,7 @@ var STTClient = class {
1217
1217
  filename,
1218
1218
  url: `${gatewayUrl}/stt/transcribe`
1219
1219
  });
1220
- const token = authenticationService2.getToken();
1220
+ const token = authenticationService.getToken();
1221
1221
  const response = await fetch(`${gatewayUrl}/stt/transcribe`, {
1222
1222
  method: "POST",
1223
1223
  headers: {
@@ -1324,6 +1324,97 @@ var Transcriber = ({ onTranscriptionCompleted }) => {
1324
1324
  };
1325
1325
  var transcriber_default = Transcriber;
1326
1326
 
1327
+ // src/chat/composer-submission.ts
1328
+ var isPromiseLike = (value) => !!value && typeof value.then === "function";
1329
+ var awaitAcceptance = async (ack) => {
1330
+ if (ack === false) return false;
1331
+ if (!isPromiseLike(ack)) return true;
1332
+ try {
1333
+ return await ack !== false;
1334
+ } catch {
1335
+ return false;
1336
+ }
1337
+ };
1338
+ var buildSubmission = (text, images, documents) => {
1339
+ const typed = text.trim();
1340
+ const base = typed || (images.length === 1 ? "[\u{1F4F7} You attached an image]" : images.length > 1 ? `[\u{1F4F7} You attached ${images.length} images]` : "");
1341
+ const fileSummary = documents.length > 0 ? `[\u{1F4CE} Attached ${documents.length} file${documents.length > 1 ? "s" : ""}: ${documents.map((d) => d.fileName).join(", ")}]` : "";
1342
+ const displayQuestion = [base || (fileSummary ? "[\u{1F4E4} You submitted content without text]" : ""), fileSummary].filter(Boolean).join("\n\n");
1343
+ const prompt = [text, ...documents.map((d) => d.block)].filter(Boolean).join("\n\n");
1344
+ return { prompt, images: [...images], displayQuestion };
1345
+ };
1346
+ var isImeComposing = (event) => event.nativeEvent?.isComposing === true || event.keyCode === 229;
1347
+
1348
+ // src/chat/document-import.ts
1349
+ var CODE_EXTENSIONS = [".js", ".ts", ".tsx", ".json", ".py", ".java", ".c", ".cpp", ".cs"];
1350
+ var TEXT_EXTENSIONS = [".md", ".txt", ...CODE_EXTENSIONS];
1351
+ var isCodeFileName = (name) => {
1352
+ const lower = name.toLowerCase();
1353
+ return CODE_EXTENSIONS.some((ext) => lower.endsWith(ext));
1354
+ };
1355
+ var isPlainTextFile = (name, mimeType = "") => mimeType.startsWith("text/") || TEXT_EXTENSIONS.some((ext) => name.toLowerCase().endsWith(ext));
1356
+ var CONTROL_CHARS = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g;
1357
+ var INVISIBLE_FORMATTING = /[\u202A-\u202E\u2066-\u2069\u200B\u2060\uFEFF]/g;
1358
+ var stripInvisible = (text) => text.normalize("NFC").replace(CONTROL_CHARS, "").replace(INVISIBLE_FORMATTING, "");
1359
+ var neutralizeRoleMarkers = (text) => text.replace(/<\s*\/?\s*script\b[^>]*>/gi, "").replace(/(^|\n)([ \t]*)\/?(system|assistant|user)\s*:/gi, "$1$2[removed-role]");
1360
+ var prepareDocumentText = (text, fileName) => {
1361
+ const visible = stripInvisible(text);
1362
+ return (isCodeFileName(fileName) ? visible : neutralizeRoleMarkers(visible)).trim();
1363
+ };
1364
+ var formatDocumentBlock = (fileName, text) => `\u{1F4C4} ${fileName}
1365
+ ${prepareDocumentText(text, fileName)}`;
1366
+ var readText = (file) => typeof file.text === "function" ? file.text() : new Promise((resolve, reject) => {
1367
+ const reader = new FileReader();
1368
+ reader.onload = () => resolve(String(reader.result ?? ""));
1369
+ reader.onerror = () => reject(reader.error ?? new Error("It couldn't be read."));
1370
+ reader.readAsText(file);
1371
+ });
1372
+ var readPdfText = async (file, maxPages = 10) => {
1373
+ const pdfjsLib = await import("pdfjs-dist/legacy/build/pdf.mjs");
1374
+ pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdn.burtson.ai/scripts/pdf.worker.js";
1375
+ const pdf = await pdfjsLib.getDocument({ data: await file.arrayBuffer() }).promise;
1376
+ const pages = await Promise.all(
1377
+ Array.from({ length: Math.min(pdf.numPages, maxPages) }, async (_, i) => {
1378
+ try {
1379
+ const page = await pdf.getPage(i + 1);
1380
+ const content = await page.getTextContent();
1381
+ return content.items.map((item) => typeof item?.str === "string" ? item.str : "").join(" ");
1382
+ } catch {
1383
+ return "";
1384
+ }
1385
+ })
1386
+ );
1387
+ return pages.join("\n\n");
1388
+ };
1389
+ var readDocument = async (file) => {
1390
+ const name = file.name.toLowerCase();
1391
+ try {
1392
+ let text;
1393
+ if (isPlainTextFile(name, file.type)) {
1394
+ text = await readText(file);
1395
+ } else if (name.endsWith(".docx")) {
1396
+ const mammoth = await import("mammoth");
1397
+ text = (await mammoth.extractRawText({ arrayBuffer: await file.arrayBuffer() })).value;
1398
+ } else if (name.endsWith(".pdf")) {
1399
+ text = await readPdfText(file);
1400
+ } else {
1401
+ return { ok: false, fileName: file.name, reason: "This file type can't be read yet." };
1402
+ }
1403
+ const block = formatDocumentBlock(file.name, text);
1404
+ if (block === `\u{1F4C4} ${file.name}
1405
+ `) {
1406
+ return { ok: false, fileName: file.name, reason: "No readable text was found in it." };
1407
+ }
1408
+ return { ok: true, fileName: file.name, block };
1409
+ } catch (error) {
1410
+ return {
1411
+ ok: false,
1412
+ fileName: file.name,
1413
+ reason: error instanceof Error && error.message ? error.message : "It couldn't be read."
1414
+ };
1415
+ }
1416
+ };
1417
+
1327
1418
  // src/chat/chat-input.tsx
1328
1419
  import { useTheme as useTheme6, alpha as alpha3 } from "@mui/material/styles";
1329
1420
 
@@ -1524,102 +1615,60 @@ var ChatInput = (props) => {
1524
1615
  const removeImage = (index) => {
1525
1616
  setPastedImages((prev) => prev.filter((_, i) => i !== index));
1526
1617
  };
1618
+ const latestDraftRef = useRef2({ text: inputValue, images: pastedImages, files: fileInputs });
1619
+ latestDraftRef.current = { text: inputValue, images: pastedImages, files: fileInputs };
1620
+ const submittingRef = useRef2(false);
1621
+ const [sendError, setSendError] = useState6(null);
1527
1622
  const handleSubmit = async () => {
1528
- if (inputValue.trim() === "" && pastedImages.length === 0 && fileInputs.length === 0)
1529
- return;
1530
- const tempImages = [...pastedImages];
1531
- const escapePromptInjection = (text) => {
1532
- return text.replace(/<\s*\/?script.*?>/gi, "").replace(/```.*?```/gs, "").replace(/(?<=\n|^)\/?(system|assistant|user):/gi, "[removed-role]");
1533
- };
1534
- const getFileText = async (file) => {
1535
- const isCodeFile = (name2) => name2.endsWith(".js") || name2.endsWith(".ts") || name2.endsWith(".tsx") || name2.endsWith(".json") || name2.endsWith(".py") || name2.endsWith(".java") || name2.endsWith(".c") || name2.endsWith(".cpp") || name2.endsWith(".cs");
1536
- const sanitize = (text, name2) => isCodeFile(name2) ? text.trim() : text.replace(/[^\x20-\x7E\r\n\t]/g, "").trim();
1537
- const name = file.name.toLowerCase();
1538
- try {
1539
- if (file.type.startsWith("text/") || name.endsWith(".md") || name.endsWith(".txt") || name.endsWith(".js") || name.endsWith(".ts") || name.endsWith(".tsx") || name.endsWith(".json") || name.endsWith(".py") || name.endsWith(".java") || name.endsWith(".c") || name.endsWith(".cpp") || name.endsWith(".cs")) {
1540
- const text = await file.text();
1541
- if (name.endsWith(".txt") || name.endsWith(".md") || name.endsWith(".docx") || name.endsWith(".pdf")) {
1542
- return `\u{1F4C4} ${file.name}
1543
- ${sanitize(escapePromptInjection(text), file.name)}`;
1544
- } else {
1545
- return `\u{1F4C4} ${file.name}
1546
- ${sanitize(text, file.name)}`;
1547
- }
1548
- }
1549
- if (name.endsWith(".docx")) {
1550
- const mammoth = await import("mammoth");
1551
- const arrayBuffer = await file.arrayBuffer();
1552
- const { value } = await mammoth.extractRawText({ arrayBuffer });
1553
- return `\u{1F4C4} ${file.name}
1554
- ${sanitize(escapePromptInjection(value), file.name)}`;
1555
- }
1556
- if (name.endsWith(".pdf")) {
1557
- const pdfjsLib = await import("pdfjs-dist/legacy/build/pdf");
1558
- pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdn.burtson.ai/scripts/pdf.worker.js";
1559
- const buffer = await file.arrayBuffer();
1560
- const loadingTask = pdfjsLib.getDocument({ data: buffer });
1561
- try {
1562
- const pdf = await loadingTask.promise;
1563
- debugLogger.info("PDF loaded", { numPages: pdf.numPages });
1564
- const maxPages = Math.min(pdf.numPages, 10);
1565
- const pages = await Promise.all(
1566
- Array.from({ length: maxPages }, async (_, i) => {
1567
- try {
1568
- const page = await pdf.getPage(i + 1);
1569
- const content = await page.getTextContent();
1570
- return content.items.map((item) => {
1571
- if (item && typeof item === "object" && "str" in item && typeof item.str === "string") {
1572
- return item.str;
1573
- }
1574
- return "";
1575
- }).join(" ");
1576
- } catch (err) {
1577
- debugLogger.error(`Failed to read page ${i + 1}`, { error: err });
1578
- return "";
1579
- }
1580
- })
1581
- );
1582
- const joined = pages.join("\n\n");
1583
- debugLogger.info("Extracted PDF content", { contentPreview: joined.slice(0, 500) });
1584
- return `\u{1F4C4} ${file.name}
1585
- ${sanitize(
1586
- escapePromptInjection(joined),
1587
- file.name
1588
- )}`;
1589
- } catch (error) {
1590
- debugLogger.error("PDF load failed", {
1591
- error: error instanceof Error ? error.message : String(error),
1592
- fileName: file.name
1593
- });
1594
- return "";
1595
- }
1623
+ if (submittingRef.current) return;
1624
+ if (inputValue.trim() === "" && pastedImages.length === 0 && fileInputs.length === 0) return;
1625
+ submittingRef.current = true;
1626
+ const draft = { text: inputValue, images: [...pastedImages], files: [...fileInputs] };
1627
+ try {
1628
+ const reads = await Promise.all(draft.files.map(readDocument));
1629
+ const failed = reads.filter((r) => !r.ok);
1630
+ if (failed.length > 0) {
1631
+ setSendError(
1632
+ failed.map((f) => `Couldn't attach ${f.fileName}: ${f.reason}`).join(" ")
1633
+ );
1634
+ return;
1635
+ }
1636
+ const documents = reads.filter((r) => r.ok);
1637
+ const submission = buildSubmission(draft.text, draft.images, documents);
1638
+ debugLogger.debug("Composer submission", {
1639
+ documents: documents.map((d) => d.fileName),
1640
+ promptLength: submission.prompt.length
1641
+ });
1642
+ setSendError(null);
1643
+ setInputValue("");
1644
+ setPastedImages([]);
1645
+ setFileInputs([]);
1646
+ if (fileInputRef.current) fileInputRef.current.value = "";
1647
+ inputRef.current?.blur();
1648
+ const accepted = await awaitAcceptance(
1649
+ onSend(submission.prompt, submission.images, submission.displayQuestion)
1650
+ );
1651
+ if (!accepted) {
1652
+ const now = latestDraftRef.current;
1653
+ const untouched = now.text.trim() === "" && now.images.length === 0 && now.files.length === 0;
1654
+ if (untouched) {
1655
+ setInputValue(draft.text);
1656
+ setPastedImages(draft.images);
1657
+ setFileInputs(draft.files);
1658
+ setSendError("Your message wasn't sent. It's back in the box so you can try again.");
1659
+ } else {
1660
+ setSendError("Your last message wasn't sent.");
1596
1661
  }
1597
- return "";
1598
- } catch {
1599
- return "";
1600
1662
  }
1601
- };
1602
- const fileTexts = await Promise.all(fileInputs.map(getFileText));
1603
- debugLogger.debug("All extracted file texts", { fileTexts });
1604
- const fileContent = fileTexts.filter(Boolean).join("\n\n");
1605
- const tempQuestionBase = inputValue.trim() || (tempImages.length === 1 ? "[\u{1F4F7} You attached an image]" : tempImages.length > 1 ? `[\u{1F4F7} You attached ${tempImages.length} images]` : "");
1606
- const fileDisplaySummary = fileInputs.length > 0 ? `[\u{1F4CE} Attached ${fileInputs.length} file${fileInputs.length > 1 ? "s" : ""}: ${fileInputs.map((f) => f.name).join(", ")}]` : "";
1607
- const displayQuestion = [
1608
- tempQuestionBase || (fileDisplaySummary ? "[\u{1F4E4} You submitted content without text]" : ""),
1609
- fileDisplaySummary
1610
- ].filter(Boolean).join("\n\n");
1611
- const fullPrompt = [inputValue, fileContent].filter(Boolean).join("\n\n");
1612
- setInputValue("");
1613
- setPastedImages([]);
1614
- setFileInputs([]);
1615
- onSend(fullPrompt, tempImages, displayQuestion);
1616
- inputRef.current?.blur();
1617
- fileInputRef.current.value = "";
1663
+ } finally {
1664
+ submittingRef.current = false;
1665
+ }
1618
1666
  };
1619
1667
  const handleKeyPress = (e) => {
1668
+ if (isImeComposing(e)) return;
1620
1669
  if (e.key === "Enter" && !e.shiftKey) {
1621
1670
  e.preventDefault();
1622
- handleSubmit();
1671
+ void handleSubmit();
1623
1672
  }
1624
1673
  };
1625
1674
  const handleTranscriptionCompleted = (transcribed) => {
@@ -1737,6 +1786,15 @@ ${sanitize(
1737
1786
  gap: isMobile ? compactMobile ? 0.75 : 1 : 0.5
1738
1787
  },
1739
1788
  children: [
1789
+ sendError && /* @__PURE__ */ jsx9(
1790
+ Typography3,
1791
+ {
1792
+ role: "alert",
1793
+ variant: "caption",
1794
+ sx: { color: theme.palette.error.main, px: 0.5, pt: isMobile ? 0 : 0.5 },
1795
+ children: sendError
1796
+ }
1797
+ ),
1740
1798
  /* @__PURE__ */ jsxs6(
1741
1799
  Box6,
1742
1800
  {
@@ -1782,7 +1840,10 @@ ${sanitize(
1782
1840
  IconButton5,
1783
1841
  {
1784
1842
  size: "small",
1785
- onClick: () => setFileInputs((prev) => prev.filter((_, i) => i !== idx)),
1843
+ onClick: () => {
1844
+ setSendError(null);
1845
+ setFileInputs((prev) => prev.filter((_, i) => i !== idx));
1846
+ },
1786
1847
  sx: { ml: 0.5, color: theme.palette.mode === "dark" ? "#aaa" : "#444" },
1787
1848
  children: /* @__PURE__ */ jsx9(CloseIcon, { sx: { fontSize: 14 } })
1788
1849
  }
@@ -2760,7 +2821,7 @@ function syncTelemetry() {
2760
2821
  const settings = usePackageSettingsStore.getState().settings;
2761
2822
  const cfg = resolveTelemetryConfig({
2762
2823
  telemetry: settings?.telemetry,
2763
- banditApiKey: authenticationService2.getToken() ?? void 0
2824
+ banditApiKey: authenticationService.getToken() ?? void 0
2764
2825
  });
2765
2826
  active = cfg ? new TelemetryExporter(cfg) : null;
2766
2827
  } catch {
@@ -3369,7 +3430,7 @@ var executeMCPTool = async (toolCall) => {
3369
3430
  }
3370
3431
  if (tool.mcpServerId) {
3371
3432
  const base2 = settings.gatewayApiUrl.replace(/\/$/, "");
3372
- const token2 = authenticationService2.getToken();
3433
+ const token2 = authenticationService.getToken();
3373
3434
  const controller2 = new AbortController();
3374
3435
  const timeoutId2 = setTimeout(() => controller2.abort(), 3e4);
3375
3436
  try {
@@ -3416,7 +3477,7 @@ var executeMCPTool = async (toolCall) => {
3416
3477
  "Content-Type": "application/json"
3417
3478
  }
3418
3479
  };
3419
- const token = authenticationService2.getToken();
3480
+ const token = authenticationService.getToken();
3420
3481
  if (token) {
3421
3482
  requestOptions.headers = {
3422
3483
  ...requestOptions.headers,
@@ -3785,8 +3846,11 @@ var useAIProvider = ({
3785
3846
  history,
3786
3847
  addHistory,
3787
3848
  inputRef,
3788
- onError
3849
+ onError,
3850
+ onTurnOutcome
3789
3851
  }) => {
3852
+ const onTurnOutcomeRef = useRef4(onTurnOutcome);
3853
+ onTurnOutcomeRef.current = onTurnOutcome;
3790
3854
  const currentSubRef = useRef4(null);
3791
3855
  const lastPartialRef = useRef4({
3792
3856
  text: "",
@@ -3832,10 +3896,14 @@ var useAIProvider = ({
3832
3896
  };
3833
3897
  const runAIProvider = useCallback(
3834
3898
  async (systemPrompt, question, images) => {
3899
+ let turnHadOutput = false;
3900
+ const reportOutcome = (outcome) => onTurnOutcomeRef.current?.(outcome);
3835
3901
  if (!provider) {
3836
3902
  debugLogger.error("No AI provider configured");
3837
3903
  overrideComponentStatus("Error");
3838
- if (onError) onError(new Error("No AI provider configured"));
3904
+ const error = new Error("No AI provider configured");
3905
+ if (onError) onError(error);
3906
+ reportOutcome({ kind: "failed", hadOutput: false, error });
3839
3907
  return;
3840
3908
  }
3841
3909
  const userPreferences = preferences;
@@ -3989,7 +4057,7 @@ Use this information to personalize your responses whenever relevant.` : "";
3989
4057
  s3Url: resolvedId,
3990
4058
  isUserContent: result.isUserContent,
3991
4059
  isTeamContent: result.isTeamContent,
3992
- contentSource: result.contentSource,
4060
+ contentSource: result.contentSource === "user" || result.contentSource === "team" ? result.contentSource : void 0,
3993
4061
  originalFileName: result.originalFileName || result.filename || result.name,
3994
4062
  // Preserve original data for downstream checks (e.g. vector modal detection)
3995
4063
  _vectorResult: result
@@ -4344,6 +4412,10 @@ ${protocol}`;
4344
4412
  const sub = stream.subscribe({
4345
4413
  next: (data) => {
4346
4414
  if (!data?.message?.content && !data?.message?.tool_calls) return;
4415
+ if (!turnHadOutput) {
4416
+ turnHadOutput = true;
4417
+ reportOutcome({ kind: "output" });
4418
+ }
4347
4419
  if (Array.isArray(data.message.tool_calls) && data.message.tool_calls.length > 0) {
4348
4420
  nativeToolCalls.push(...data.message.tool_calls);
4349
4421
  sawToolBlock = true;
@@ -4378,13 +4450,14 @@ ${protocol}`;
4378
4450
  },
4379
4451
  error: (err) => {
4380
4452
  debugLogger.error("Stream error:", err);
4453
+ reportOutcome({ kind: "failed", hadOutput: turnHadOutput, error: err });
4381
4454
  stopWorking();
4382
4455
  overrideComponentStatus("Idle");
4383
4456
  setIsSubmitting(false);
4384
4457
  setIsStreaming(false);
4385
4458
  if (/lifetime validation|token.*(expired|invalid)|unauthorized|\b401\b/i.test(err.message || "")) {
4386
- const { replaceLastAnswer: replaceLastAnswer2 } = useConversationStore.getState();
4387
- replaceLastAnswer2(
4459
+ const { replaceLastAnswer: replaceLastAnswer3 } = useConversationStore.getState();
4460
+ replaceLastAnswer3(
4388
4461
  "_Your session ended \u2014 sign back in to continue._",
4389
4462
  lastPartialRef.current.images,
4390
4463
  false,
@@ -4411,8 +4484,8 @@ ${protocol}`;
4411
4484
  scope: rateLimit.scope,
4412
4485
  frontier: rateLimit.frontier
4413
4486
  });
4414
- const { replaceLastAnswer: replaceLastAnswer2 } = useConversationStore.getState();
4415
- replaceLastAnswer2(
4487
+ const { replaceLastAnswer: replaceLastAnswer3 } = useConversationStore.getState();
4488
+ replaceLastAnswer3(
4416
4489
  rateLimit.scope === "weekly" ? "_You've reached your weekly limit. Upgrade to keep chatting._" : "_You've hit your session limit. It resets soon \u2014 or upgrade for higher limits._",
4417
4490
  lastPartialRef.current.images,
4418
4491
  false,
@@ -4433,10 +4506,16 @@ ${protocol}`;
4433
4506
  if (partial && !partial.trim().endsWith("\u2026") && !partial.trim().endsWith("...")) {
4434
4507
  partial = partial.trimEnd() + " \u2026";
4435
4508
  }
4509
+ const { replaceLastAnswer: replaceLastAnswer2 } = useConversationStore.getState();
4436
4510
  if (partial) {
4437
- const { replaceLastAnswer: replaceLastAnswer2 } = useConversationStore.getState();
4438
4511
  replaceLastAnswer2(partial, lastPartialRef.current.images, false, lastPartialRef.current.usedDocs, true);
4439
4512
  setResponse(partial);
4513
+ } else {
4514
+ const { conversations: convs, currentId: convId } = useConversationStore.getState();
4515
+ const last = convs.find((c) => c.id === convId)?.history.at(-1);
4516
+ if (last?.answer === "...") {
4517
+ replaceLastAnswer2("_No response came back. Try sending again._", lastPartialRef.current.images, false, lastPartialRef.current.usedDocs, true);
4518
+ }
4440
4519
  }
4441
4520
  setStreamBuffer("");
4442
4521
  setIsThinking?.(false);
@@ -4448,6 +4527,7 @@ ${protocol}`;
4448
4527
  }
4449
4528
  },
4450
4529
  complete: async () => {
4530
+ reportOutcome({ kind: "completed" });
4451
4531
  try {
4452
4532
  setIsThinking?.(false);
4453
4533
  telemetryEvent("tool_loop:llm_response", { responseLength: fullMessage.length });
@@ -4714,7 +4794,7 @@ _This link is temporary and expires in about ${mins} minutes._`;
4714
4794
  }
4715
4795
  } else {
4716
4796
  const data = result.data ?? {};
4717
- const informative = [data.message, data.details, data.error, result.error].map((value) => typeof value === "string" ? value.trim() : void 0).filter((value) => Boolean(value) && value.toLowerCase() !== "n/a");
4797
+ const informative = [data.message, data.details, data.error, result.error].map((value) => typeof value === "string" ? value.trim() : "").filter((value) => value.length > 0 && value.toLowerCase() !== "n/a");
4718
4798
  resultText = informative.length ? informative.join(" \u2014 ") : `I couldn't complete that request: ${result.error || "Unknown error"}.`;
4719
4799
  }
4720
4800
  enhancedMessage = enhancedMessage.replace(placeholderToken, resultText);
@@ -5061,6 +5141,7 @@ ${inlineImageBlocks.join("\n\n")}` : "");
5061
5141
  } catch {
5062
5142
  }
5063
5143
  currentSubRef.current = null;
5144
+ onTurnOutcomeRef.current?.({ kind: "cancelled" });
5064
5145
  let partial = lastPartialRef.current.text;
5065
5146
  if (partial && !partial.trim().endsWith("\u2026") && !partial.trim().endsWith("...")) {
5066
5147
  partial = partial.trimEnd() + " \u2026";
@@ -5267,7 +5348,7 @@ var useNetworkStatus = () => {
5267
5348
  if (connection) {
5268
5349
  const updateConnectionInfo = () => {
5269
5350
  const slowConnections = ["slow-2g", "2g"];
5270
- const isSlowConnection = slowConnections.includes(connection.effectiveType);
5351
+ const isSlowConnection = slowConnections.includes(connection.effectiveType ?? "");
5271
5352
  setNetworkStatus((prev) => ({
5272
5353
  ...prev,
5273
5354
  isSlowConnection,
@@ -7746,7 +7827,7 @@ var ConversationDrawer = ({ open, onClose }) => {
7746
7827
  title: "Sign out",
7747
7828
  onClick: (event) => {
7748
7829
  event.stopPropagation();
7749
- authenticationService2.clearToken();
7830
+ authenticationService.clearToken();
7750
7831
  window.location.href = "/login";
7751
7832
  },
7752
7833
  sx: {
@@ -8873,7 +8954,7 @@ var EnhancedMobileConversationsModal = ({
8873
8954
  title: "Sign out",
8874
8955
  onClick: (event) => {
8875
8956
  event.stopPropagation();
8876
- authenticationService2.clearToken();
8957
+ authenticationService.clearToken();
8877
8958
  window.location.href = "/login";
8878
8959
  },
8879
8960
  sx: {
@@ -9649,7 +9730,8 @@ var QuerySuggestionPicker = ({
9649
9730
  }
9650
9731
  },
9651
9732
  onClick: () => {
9652
- onSend(prompt, []);
9733
+ void Promise.resolve(onSend(prompt, [])).catch(() => {
9734
+ });
9653
9735
  setHasSentPrompt(true);
9654
9736
  },
9655
9737
  children: /* @__PURE__ */ jsx19(
@@ -9703,7 +9785,7 @@ var UnderReview = () => {
9703
9785
  const theme = useTheme17();
9704
9786
  const navigate = useNavigate4();
9705
9787
  const handleResetSession = () => {
9706
- authenticationService2.clearToken();
9788
+ authenticationService.clearToken();
9707
9789
  navigate("/login");
9708
9790
  };
9709
9791
  return /* @__PURE__ */ jsxs16(
@@ -10304,8 +10386,8 @@ var ChatContent = () => {
10304
10386
  const notificationService = useNotificationService();
10305
10387
  const [selectedTheme, setSelectedTheme] = useState19(null);
10306
10388
  const [themeLoading, setThemeLoading] = useState19(true);
10307
- const token = authenticationService2.getToken();
10308
- const claims = token ? authenticationService2.parseJwtClaims(token) : null;
10389
+ const token = authenticationService.getToken();
10390
+ const claims = token ? authenticationService.parseJwtClaims(token) : null;
10309
10391
  const baseTheme = themeMap_default[selectedTheme ?? "bandit-dark"] || banditDarkTheme;
10310
10392
  const banditTheme = useMemo4(() => {
10311
10393
  return createTheme(baseTheme, {
@@ -10378,7 +10460,7 @@ var ChatContent = () => {
10378
10460
  } = useTTS();
10379
10461
  useEffect17(() => {
10380
10462
  const timer = setTimeout(() => {
10381
- const isAuthenticated = authenticationService2.isAuthenticated();
10463
+ const isAuthenticated = authenticationService.isAuthenticated();
10382
10464
  if (!initialized || availableVoices.length === 0) {
10383
10465
  if (token && isAuthenticated) {
10384
10466
  debugLogger.debug("Chat: Voices not initialized or unavailable, attempting to load from API");
@@ -10391,7 +10473,7 @@ var ChatContent = () => {
10391
10473
  return () => clearTimeout(timer);
10392
10474
  }, [initialized, availableVoices.length, loadVoicesFromAPI, token]);
10393
10475
  useEffect17(() => {
10394
- const isAuthenticated = authenticationService2.isAuthenticated();
10476
+ const isAuthenticated = authenticationService.isAuthenticated();
10395
10477
  if (packageSettings?.gatewayApiUrl && availableVoices.length === 0 && !initialized) {
10396
10478
  if (token && isAuthenticated) {
10397
10479
  debugLogger.debug("Chat: Gateway URL available and no voices loaded, attempting to load from API");
@@ -11006,7 +11088,16 @@ var ChatContent = () => {
11006
11088
  if (rafId) cancelAnimationFrame(rafId);
11007
11089
  };
11008
11090
  }, [_hasHydrated, chatContainerEl, getScrollState, history.length]);
11091
+ const sendAckRef = useRef16(null);
11092
+ const settleSendAck = useCallback6((outcome) => {
11093
+ const ack = sendAckRef.current;
11094
+ if (!ack) return;
11095
+ sendAckRef.current = null;
11096
+ if (outcome.kind === "failed" && !outcome.hadOutput) ack.reject(outcome.error);
11097
+ else ack.resolve();
11098
+ }, []);
11009
11099
  const aiProvider = useAIProvider({
11100
+ onTurnOutcome: settleSendAck,
11010
11101
  overrideComponentStatus: setComponentStatus,
11011
11102
  setIsSubmitting,
11012
11103
  setIsStreaming,
@@ -11057,6 +11148,26 @@ var ChatContent = () => {
11057
11148
  }, [ttsStop, isStreaming, handleStop]);
11058
11149
  const handleSend = useCallback6(
11059
11150
  (question, images, displayQuestion) => {
11151
+ sendAckRef.current?.resolve();
11152
+ const acknowledged = new Promise((resolve, reject) => {
11153
+ sendAckRef.current = { resolve, reject };
11154
+ });
11155
+ const runProvider = (systemPrompt2, providerImages2) => {
11156
+ aiProvider(systemPrompt2, question, providerImages2).catch((error) => {
11157
+ debugLogger.error("AI provider failed before streaming", {
11158
+ error: error instanceof Error ? error.message : String(error)
11159
+ });
11160
+ setIsStreaming(false);
11161
+ setResponseStarted(false);
11162
+ setIsSubmitting(false);
11163
+ setPendingMessage(null);
11164
+ const { conversations: convs, currentId: convId, replaceLastAnswer } = useConversationStore.getState();
11165
+ if (convs.find((c) => c.id === convId)?.history.at(-1)?.answer === "...") {
11166
+ replaceLastAnswer("_No response came back. Try sending again._", void 0, false, void 0, true);
11167
+ }
11168
+ settleSendAck({ kind: "failed", hadOutput: false, error });
11169
+ });
11170
+ };
11060
11171
  const requestStartTime = trackRequestStart();
11061
11172
  const questionForDisplay = displayQuestion || question;
11062
11173
  const pendingImages = images.length > 0 ? [...images] : void 0;
@@ -11095,10 +11206,15 @@ var ChatContent = () => {
11095
11206
  rawQuestion: question
11096
11207
  });
11097
11208
  const providerImages2 = pendingImages ? [...pendingImages] : [];
11098
- aiProvider(systemPrompt, question, providerImages2);
11209
+ runProvider(systemPrompt, providerImages2);
11099
11210
  }, 0);
11211
+ }).catch((error) => {
11212
+ debugLogger.error("Starting a new conversation failed", {
11213
+ error: error instanceof Error ? error.message : String(error)
11214
+ });
11215
+ settleSendAck({ kind: "failed", hadOutput: false, error });
11100
11216
  });
11101
- return;
11217
+ return acknowledged;
11102
11218
  }
11103
11219
  if (existing.name === "New Conversation") {
11104
11220
  generateName(question).then((newName) => {
@@ -11146,10 +11262,12 @@ var ChatContent = () => {
11146
11262
  }
11147
11263
  setResponse("");
11148
11264
  const providerImages = pendingImages ? [...pendingImages] : [];
11149
- aiProvider(systemPrompt, question, providerImages);
11265
+ runProvider(systemPrompt, providerImages);
11266
+ return acknowledged;
11150
11267
  },
11151
11268
  [
11152
11269
  aiProvider,
11270
+ settleSendAck,
11153
11271
  chatContainerRef,
11154
11272
  generateName,
11155
11273
  getScrollState,
@@ -11173,7 +11291,8 @@ var ChatContent = () => {
11173
11291
  }
11174
11292
  setInputValue("");
11175
11293
  setPastedImages([]);
11176
- handleSend(cleaned, [], cleaned);
11294
+ handleSend(cleaned, [], cleaned).catch(() => {
11295
+ });
11177
11296
  },
11178
11297
  [handleSend, setInputValue, setPastedImages]
11179
11298
  );
@@ -11751,12 +11870,12 @@ var Chat = () => {
11751
11870
  ossConfigured,
11752
11871
  isOSSMode: isOSSMode(),
11753
11872
  tier: featureFlag.getEvaluation()?.tier,
11754
- hasToken: authenticationService2.isAuthenticated(),
11873
+ hasToken: authenticationService.isAuthenticated(),
11755
11874
  playgroundBypassAuth,
11756
11875
  isPlaygroundRoute,
11757
11876
  bypassAuth
11758
11877
  });
11759
- if (!allowUnauthenticated && !bypassAuth && !authenticationService2.isAuthenticated()) {
11878
+ if (!allowUnauthenticated && !bypassAuth && !authenticationService.isAuthenticated()) {
11760
11879
  debugLogger.debug("User is not authenticated, redirecting to login");
11761
11880
  return /* @__PURE__ */ jsx23(Navigate, { to: "/login", replace: true });
11762
11881
  }
@@ -11767,4 +11886,4 @@ var chat_default = Chat;
11767
11886
  export {
11768
11887
  chat_default
11769
11888
  };
11770
- //# sourceMappingURL=chunk-F5JS4W2K.mjs.map
11889
+ //# sourceMappingURL=chunk-MHF76C2F.mjs.map