@crestapps/ai-chat-ui 2.0.0-preview.191 → 2.0.0-preview.197

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.
package/dist/ai-chat.js CHANGED
@@ -6,8 +6,6 @@
6
6
  var _excluded = ["placeholder"];
7
7
  function _regenerator() { /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */ var e, t, r = "function" == typeof Symbol ? Symbol : {}, n = r.iterator || "@@iterator", o = r.toStringTag || "@@toStringTag"; function i(r, n, o, i) { var c = n && n.prototype instanceof Generator ? n : Generator, u = Object.create(c.prototype); return _regeneratorDefine2(u, "_invoke", function (r, n, o) { var i, c, u, f = 0, p = o || [], y = !1, G = { p: 0, n: 0, v: e, a: d, f: d.bind(e, 4), d: function d(t, r) { return i = t, c = 0, u = e, G.n = r, a; } }; function d(r, n) { for (c = r, u = n, t = 0; !y && f && !o && t < p.length; t++) { var o, i = p[t], d = G.p, l = i[2]; r > 3 ? (o = l === n) && (u = i[(c = i[4]) ? 5 : (c = 3, 3)], i[4] = i[5] = e) : i[0] <= d && ((o = r < 2 && d < i[1]) ? (c = 0, G.v = n, G.n = i[1]) : d < l && (o = r < 3 || i[0] > n || n > l) && (i[4] = r, i[5] = n, G.n = l, c = 0)); } if (o || r > 1) return a; throw y = !0, n; } return function (o, p, l) { if (f > 1) throw TypeError("Generator is already running"); for (y && 1 === p && d(p, l), c = p, u = l; (t = c < 2 ? e : u) || !y;) { i || (c ? c < 3 ? (c > 1 && (G.n = -1), d(c, u)) : G.n = u : G.v = u); try { if (f = 2, i) { if (c || (o = "next"), t = i[o]) { if (!(t = t.call(i, u))) throw TypeError("iterator result is not an object"); if (!t.done) return t; u = t.value, c < 2 && (c = 0); } else 1 === c && (t = i["return"]) && t.call(i), c < 2 && (u = TypeError("The iterator does not provide a '" + o + "' method"), c = 1); i = e; } else if ((t = (y = G.n < 0) ? u : r.call(n, G)) !== a) break; } catch (t) { i = e, c = 1, u = t; } finally { f = 1; } } return { value: t, done: y }; }; }(r, o, i), !0), u; } var a = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} t = Object.getPrototypeOf; var c = [][n] ? t(t([][n]())) : (_regeneratorDefine2(t = {}, n, function () { return this; }), t), u = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(c); function f(e) { return Object.setPrototypeOf ? Object.setPrototypeOf(e, GeneratorFunctionPrototype) : (e.__proto__ = GeneratorFunctionPrototype, _regeneratorDefine2(e, o, "GeneratorFunction")), e.prototype = Object.create(u), e; } return GeneratorFunction.prototype = GeneratorFunctionPrototype, _regeneratorDefine2(u, "constructor", GeneratorFunctionPrototype), _regeneratorDefine2(GeneratorFunctionPrototype, "constructor", GeneratorFunction), GeneratorFunction.displayName = "GeneratorFunction", _regeneratorDefine2(GeneratorFunctionPrototype, o, "GeneratorFunction"), _regeneratorDefine2(u), _regeneratorDefine2(u, o, "Generator"), _regeneratorDefine2(u, n, function () { return this; }), _regeneratorDefine2(u, "toString", function () { return "[object Generator]"; }), (_regenerator = function _regenerator() { return { w: i, m: f }; })(); }
8
8
  function _regeneratorDefine2(e, r, n, t) { var i = Object.defineProperty; try { i({}, "", {}); } catch (e) { i = 0; } _regeneratorDefine2 = function _regeneratorDefine(e, r, n, t) { function o(r, n) { _regeneratorDefine2(e, r, function (e) { return this._invoke(r, n, e); }); } r ? i ? i(e, r, { value: n, enumerable: !t, configurable: !t, writable: !t }) : e[r] = n : (o("next", 0), o("throw", 1), o("return", 2)); }, _regeneratorDefine2(e, r, n, t); }
9
- function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }
10
- function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
11
9
  function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
12
10
  function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
13
11
  function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
@@ -15,6 +13,8 @@ function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(
15
13
  function _objectWithoutProperties(e, t) { if (null == e) return {}; var o, r, i = _objectWithoutPropertiesLoose(e, t); if (Object.getOwnPropertySymbols) { var n = Object.getOwnPropertySymbols(e); for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]); } return i; }
16
14
  function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
17
15
  function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; }
16
+ function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }
17
+ function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
18
18
  function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
19
19
  function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
20
20
  function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
@@ -22,7 +22,25 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
22
22
  function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
23
23
  function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
24
24
  function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
25
- window.coreAIChatManager = function () {
25
+ /*
26
+ * Figure markers: the short labels the model writes, turned into pictures by the host.
27
+ *
28
+ * Retrieval hands the model a label -- [fig:1] -- and keeps the picture's real link in the message's reference
29
+ * map, the same way a text citation keeps its source behind "[doc:1]". A model asked to reproduce a long opaque
30
+ * identifier does not reproduce it: it copies the shape and substitutes ordinals, so links to figures that were
31
+ * never in the results came back as 404s. A label short enough to be written exactly, with the link put back
32
+ * here where the model cannot reach it, is what stops that.
33
+ *
34
+ * It sits on window rather than inside the manager below so the host can reach it, and so its rules can be
35
+ * exercised in a bare sandbox with no browser and no DOM -- see tests/realtime-client/figure-markers.test.js --
36
+ * exactly as window.CoreAIRealtime exposes the microphone gate's rules.
37
+ *
38
+ * The same block sits at the top of chat-interaction.js: the two chat clients are separate bundles that share no
39
+ * module of their own, and both render the same markdown. Change one, change the other.
40
+ */
41
+ // CoreAIChatMarkers lives in chat-markers.js, which every chat surface loads before this file.
42
+
43
+ window.coreAIChatManager = function (_window$CoreAIChatMar, _window$CoreAIChatMar2, _window$CoreAIChatMar3, _window$CoreAIChatMar4, _window$CoreAIChatMar5, _window$CoreAIChatMar6, _window$CoreAIChatMar7, _window$CoreAIChatMar8, _window$CoreAIChatMar9, _window$CoreAIChatMar0) {
26
44
  // Defaults (can be overridden by instanceConfig)
27
45
  var defaultConfig = {
28
46
  // UI defaults for generated media
@@ -127,7 +145,7 @@ window.coreAIChatManager = function () {
127
145
  return Math.min(Math.max(value, min), max);
128
146
  }
129
147
  function normalizeReference(reference) {
130
- var _ref, _normalized$index, _ref2, _normalized$text, _ref3, _normalized$title, _ref4, _normalized$link, _ref5, _normalized$reference, _normalized$isGenerat;
148
+ var _ref, _normalized$index, _ref2, _normalized$text, _ref3, _normalized$title, _ref4, _normalized$link, _ref5, _normalized$reference, _normalized$isGenerat, _normalized$isImage;
131
149
  if (!reference || _typeof(reference) !== 'object') {
132
150
  return null;
133
151
  }
@@ -138,6 +156,7 @@ window.coreAIChatManager = function () {
138
156
  normalized.link = sanitizeUrl((_ref4 = (_normalized$link = normalized.link) !== null && _normalized$link !== void 0 ? _normalized$link : normalized.Link) !== null && _ref4 !== void 0 ? _ref4 : null);
139
157
  normalized.referenceType = (_ref5 = (_normalized$reference = normalized.referenceType) !== null && _normalized$reference !== void 0 ? _normalized$reference : normalized.ReferenceType) !== null && _ref5 !== void 0 ? _ref5 : null;
140
158
  normalized.isGenerated = ((_normalized$isGenerat = normalized.isGenerated) !== null && _normalized$isGenerat !== void 0 ? _normalized$isGenerat : normalized.IsGenerated) === true;
159
+ normalized.isImage = ((_normalized$isImage = normalized.isImage) !== null && _normalized$isImage !== void 0 ? _normalized$isImage : normalized.IsImage) === true;
141
160
  return normalized;
142
161
  }
143
162
  function isDownloadCitationReference(reference) {
@@ -166,11 +185,95 @@ window.coreAIChatManager = function () {
166
185
  }
167
186
  return normalized;
168
187
  }
188
+
189
+ // Installed at the top of this file so it can be unit tested without a browser; see the comment there.
190
+
191
+ /*
192
+ * Reads a JSON response body, or explains why it is not JSON.
193
+ *
194
+ * fetch follows a redirect silently and reports the page it landed on as a success, so an endpoint that
195
+ * answers "forbidden" with a redirect to a sign-in page arrives here as response.ok with HTML in it.
196
+ * Calling response.json() on that throws "unexpected character at line 1 column 1", which is what the
197
+ * reader was shown instead of being told they are not allowed to upload.
198
+ *
199
+ * Returns { ok, data, message }. A caller shows message when ok is false; it is never a parser error.
200
+ */
201
+ function readJsonResponse(_x, _x2) {
202
+ return _readJsonResponse.apply(this, arguments);
203
+ }
204
+ function _readJsonResponse() {
205
+ _readJsonResponse = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee0(response, fallbackMessage) {
206
+ var contentType, message, _t6, _t7;
207
+ return _regenerator().w(function (_context0) {
208
+ while (1) switch (_context0.p = _context0.n) {
209
+ case 0:
210
+ contentType = response.headers && response.headers.get('content-type') || '';
211
+ if (!(contentType.toLowerCase().indexOf('json') < 0)) {
212
+ _context0.n = 1;
213
+ break;
214
+ }
215
+ // A redirect that landed somewhere else is the common case and worth naming, because the endpoint
216
+ // did not refuse -- something in front of it did.
217
+ message = response.redirected ? 'You may not be signed in, or you do not have permission for this. Please reload and try again.' : fallbackMessage;
218
+ return _context0.a(2, {
219
+ ok: false,
220
+ data: null,
221
+ message: message
222
+ });
223
+ case 1:
224
+ _context0.p = 1;
225
+ _context0.n = 2;
226
+ return response.json();
227
+ case 2:
228
+ _t6 = _context0.v;
229
+ return _context0.a(2, {
230
+ ok: true,
231
+ data: _t6,
232
+ message: null
233
+ });
234
+ case 3:
235
+ _context0.p = 3;
236
+ _t7 = _context0.v;
237
+ return _context0.a(2, {
238
+ ok: false,
239
+ data: null,
240
+ message: fallbackMessage
241
+ });
242
+ }
243
+ }, _callee0, null, [[1, 3]]);
244
+ }));
245
+ return _readJsonResponse.apply(this, arguments);
246
+ }
247
+ var expandImageMarkers = window.CoreAIChatMarkers.expandImageMarkers;
169
248
  function getCitationLabel(reference, key) {
170
249
  return reference.title || reference.text || key;
171
250
  }
251
+
252
+ // Which references are one citation, and how a repeated marker reads, are statements about strings and
253
+ // live in chat-markers.js so every chat surface numbers them the same way. Without the module each
254
+ // reference keeps an identity of its own and nothing merges, which is the numbering this script had
255
+ // before it.
256
+ var unmergedCitationCount = 0;
257
+ var citationIdentity = (_window$CoreAIChatMar = (_window$CoreAIChatMar2 = window.CoreAIChatMarkers) === null || _window$CoreAIChatMar2 === void 0 ? void 0 : _window$CoreAIChatMar2.citationIdentity) !== null && _window$CoreAIChatMar !== void 0 ? _window$CoreAIChatMar : function () {
258
+ return "unmerged-".concat(++unmergedCitationCount);
259
+ };
260
+ var splitCombinedCitations = (_window$CoreAIChatMar3 = (_window$CoreAIChatMar4 = window.CoreAIChatMarkers) === null || _window$CoreAIChatMar4 === void 0 ? void 0 : _window$CoreAIChatMar4.splitCombinedCitations) !== null && _window$CoreAIChatMar3 !== void 0 ? _window$CoreAIChatMar3 : function (content) {
261
+ return content;
262
+ };
263
+ var collapseRepeatedCitations = (_window$CoreAIChatMar5 = (_window$CoreAIChatMar6 = window.CoreAIChatMarkers) === null || _window$CoreAIChatMar6 === void 0 ? void 0 : _window$CoreAIChatMar6.collapseRepeatedCitations) !== null && _window$CoreAIChatMar5 !== void 0 ? _window$CoreAIChatMar5 : function (html) {
264
+ return html;
265
+ };
266
+ var separateAdjacentCitations = (_window$CoreAIChatMar7 = (_window$CoreAIChatMar8 = window.CoreAIChatMarkers) === null || _window$CoreAIChatMar8 === void 0 ? void 0 : _window$CoreAIChatMar8.separateAdjacentCitations) !== null && _window$CoreAIChatMar7 !== void 0 ? _window$CoreAIChatMar7 : function (html) {
267
+ return html;
268
+ };
269
+ var citationMarkerHtml = (_window$CoreAIChatMar9 = (_window$CoreAIChatMar0 = window.CoreAIChatMarkers) === null || _window$CoreAIChatMar0 === void 0 ? void 0 : _window$CoreAIChatMar0.citationMarkerHtml) !== null && _window$CoreAIChatMar9 !== void 0 ? _window$CoreAIChatMar9 : function (displayIndex) {
270
+ return "<sup>".concat(displayIndex, "</sup>");
271
+ };
172
272
  function buildCitationDisplay(content, references) {
173
- var processedContent = (content || '').trim();
273
+ // Before anything looks for a reference key, the combined form the model tends to write is split into
274
+ // the keys it means. A key that is not found is not replaced, and an unreplaced key reaches the reader
275
+ // as a raw marker mid-sentence.
276
+ var processedContent = splitCombinedCitations((content || '').trim());
174
277
  var messageReferences = normalizeReferences(references);
175
278
  var referenceEntries = Object.entries(messageReferences);
176
279
  if (!referenceEntries.length) {
@@ -211,6 +314,8 @@ window.coreAIChatManager = function () {
211
314
  return a.index - b.index;
212
315
  });
213
316
  var citations = [];
317
+ var citationsByIdentity = new Map();
318
+ var placeholders = [];
214
319
  var displayIndex = 1;
215
320
  var _iterator = _createForOfIteratorHelper(citedRefs),
216
321
  _step;
@@ -219,28 +324,48 @@ window.coreAIChatManager = function () {
219
324
  var _step$value = _slicedToArray(_step.value, 2),
220
325
  key = _step$value[0],
221
326
  value = _step$value[1];
222
- var placeholder = "__CITE_".concat(displayIndex, "_").concat(value.index || displayIndex, "__");
327
+ var label = getCitationLabel(value, key);
328
+ var link = value.link || null;
329
+ var identity = citationIdentity(label, link);
330
+ var citation = citationsByIdentity.get(identity);
331
+
332
+ // Every reference still has its key replaced, so the text never keeps a raw key; what the merge
333
+ // changes is only how many numbers the reader is given for them.
334
+ if (!citation) {
335
+ citation = {
336
+ referenceKey: key,
337
+ referenceKeys: [],
338
+ displayIndex: displayIndex++,
339
+ label: label,
340
+ link: link,
341
+ isDownload: isDownloadCitationReference(value)
342
+ };
343
+ citationsByIdentity.set(identity, citation);
344
+ citations.push(citation);
345
+ }
346
+ citation.referenceKeys.push(key);
347
+ var placeholder = "__CITE_".concat(citation.displayIndex, "_").concat(value.index || citation.displayIndex, "__");
223
348
  processedContent = processedContent.replaceAll(key, placeholder);
224
- citations.push({
225
- referenceKey: key,
226
- displayIndex: displayIndex,
227
- label: getCitationLabel(value, key),
228
- link: value.link || null,
229
- isDownload: isDownloadCitationReference(value),
230
- placeholder: placeholder
349
+ placeholders.push({
350
+ placeholder: placeholder,
351
+ displayIndex: citation.displayIndex,
352
+ label: citation.label
231
353
  });
232
- displayIndex++;
233
354
  }
234
355
  } catch (err) {
235
356
  _iterator.e(err);
236
357
  } finally {
237
358
  _iterator.f();
238
359
  }
239
- for (var _i2 = 0, _citations = citations; _i2 < _citations.length; _i2++) {
240
- var citation = _citations[_i2];
241
- processedContent = processedContent.replaceAll(citation.placeholder, "<sup>".concat(citation.displayIndex, "</sup>"));
360
+ for (var _i2 = 0, _placeholders = placeholders; _i2 < _placeholders.length; _i2++) {
361
+ var entry = _placeholders[_i2];
362
+ processedContent = processedContent.replaceAll(entry.placeholder, citationMarkerHtml(entry.displayIndex, entry.label));
242
363
  }
243
- processedContent = processedContent.replaceAll('</sup><sup>', '</sup><sup>,</sup><sup>');
364
+
365
+ // Merged citations can leave the same number twice over a sentence; that is collapsed before the
366
+ // commas go in, or the reader is given "1,1".
367
+ processedContent = collapseRepeatedCitations(processedContent);
368
+ processedContent = separateAdjacentCitations(processedContent);
244
369
 
245
370
  // Generated files (such as exported tabular data) are always offered as a download even when
246
371
  // the model does not cite them inline, so the user never loses access to the produced file.
@@ -253,6 +378,7 @@ window.coreAIChatManager = function () {
253
378
  _value = _step2$value[1];
254
379
  citations.push({
255
380
  referenceKey: _key,
381
+ referenceKeys: [_key],
256
382
  displayIndex: displayIndex,
257
383
  label: getCitationLabel(_value, _key),
258
384
  link: _value.link || null,
@@ -280,12 +406,27 @@ window.coreAIChatManager = function () {
280
406
  if (!copyContent || !Array.isArray(citations) || citations.length === 0) {
281
407
  return copyContent;
282
408
  }
409
+
410
+ // Every key the citation absorbed is replaced, not just the first: a merged key left behind would
411
+ // reach the clipboard as the raw reference token the reader never saw on the page.
283
412
  var _iterator3 = _createForOfIteratorHelper(citations),
284
413
  _step3;
285
414
  try {
286
415
  for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
416
+ var _citation$referenceKe;
287
417
  var citation = _step3.value;
288
- copyContent = copyContent.replaceAll(citation.referenceKey, "[".concat(citation.displayIndex, "]"));
418
+ var _iterator5 = _createForOfIteratorHelper((_citation$referenceKe = citation.referenceKeys) !== null && _citation$referenceKe !== void 0 && _citation$referenceKe.length ? citation.referenceKeys : [citation.referenceKey]),
419
+ _step5;
420
+ try {
421
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
422
+ var key = _step5.value;
423
+ copyContent = copyContent.replaceAll(key, "[".concat(citation.displayIndex, "]"));
424
+ }
425
+ } catch (err) {
426
+ _iterator5.e(err);
427
+ } finally {
428
+ _iterator5.f();
429
+ }
289
430
  }
290
431
  } catch (err) {
291
432
  _iterator3.e(err);
@@ -314,13 +455,20 @@ window.coreAIChatManager = function () {
314
455
  function updateMessagePresentation(message, references) {
315
456
  var messageReferences = normalizeReferences(references !== null && references !== void 0 ? references : message.references);
316
457
  var rawContent = typeof message.rawContent === 'string' ? message.rawContent : typeof message.content === 'string' ? message.content : '';
317
- var citationDisplay = buildCitationDisplay(rawContent, messageReferences);
458
+ // Figure markers become images before anything else reads the content: the citation pass looks for its
459
+ // own keys in what is left, and the markdown parser then sees an ordinary image.
460
+ var displayContent = expandImageMarkers(rawContent, messageReferences);
461
+ var citationDisplay = buildCitationDisplay(displayContent, messageReferences);
318
462
  message.rawContent = rawContent;
319
463
  message.content = rawContent;
320
464
  message.displayContent = citationDisplay.content;
321
465
  message.references = messageReferences;
322
466
  message.citationReferences = citationDisplay.citations;
323
- message.copyContent = buildCopyContent(rawContent, citationDisplay.citations);
467
+ // Copied from the expanded content, so a copied answer carries the picture's link rather than a label
468
+ // that means nothing outside this page.
469
+ // Split here too: the copy is built from the expanded content rather than the display path's output,
470
+ // so a combined marker would otherwise survive into the clipboard as a raw key.
471
+ message.copyContent = buildCopyContent(splitCombinedCitations(displayContent), citationDisplay.citations);
324
472
  message.htmlContent = parseMarkdownContent(citationDisplay.content, message);
325
473
  return message;
326
474
  }
@@ -364,7 +512,12 @@ window.coreAIChatManager = function () {
364
512
  if (!src) return '';
365
513
  var alt = data.text || defaultConfig.generatedImageAltText;
366
514
  var maxWidth = defaultConfig.generatedImageMaxWidth;
367
- return "<div class=\"generated-image-container\">\n <img src=\"".concat(src, "\" alt=\"").concat(alt, "\" class=\"img-thumbnail\" style=\"max-width: ").concat(maxWidth, "px; height: auto;\" />\n <div class=\"mt-2\">\n <a href=\"").concat(src, "\" target=\"_blank\" download=\"").concat(alt, "\" title=\"").concat(defaultConfig.downloadImageTitle, "\" class=\"btn btn-sm btn-outline-secondary ai-download-image\">\n <i class=\"fa-solid fa-download\"></i>\n </a>\n </div>\n </div>");
515
+ // Spans, not divs. A figure marker is often written mid-sentence -- "Figures [fig:1], [fig:2] and
516
+ // [fig:3] show..." -- and a block element cannot sit inside a paragraph: the browser closes the <p>
517
+ // at the first one, so the sentence is torn into fragments and the commas between the markers are
518
+ // left stranded on their own lines. Inline-block keeps the figure looking exactly the same while
519
+ // remaining something a paragraph can legally contain.
520
+ return "<span class=\"generated-image-container\">\n <img src=\"".concat(src, "\" alt=\"").concat(alt, "\" class=\"img-thumbnail\" style=\"max-width: ").concat(maxWidth, "px; height: auto;\" />\n <span class=\"mt-2 d-block\">\n <a href=\"").concat(src, "\" target=\"_blank\" download=\"").concat(alt, "\" title=\"").concat(defaultConfig.downloadImageTitle, "\" class=\"btn btn-sm btn-outline-secondary ai-download-image\">\n <i class=\"fa-solid fa-download\"></i>\n </a>\n </span>\n </span>");
368
521
  };
369
522
 
370
523
  // Chart counter for unique IDs
@@ -381,6 +534,11 @@ window.coreAIChatManager = function () {
381
534
  return "<div class=\"chart-container\" style=\"position: relative; width: 100%; max-width: 560px; min-height: 420px;\">" + "<canvas id=\"".concat(chartId, "\"></canvas>") + "</div>" + "<div class=\"mt-2\">" + "<button type=\"button\" class=\"btn btn-sm btn-outline-secondary download-chart-btn\" data-chart-id=\"".concat(chartId, "\" title=\"").concat(defaultConfig.downloadChartTitle, "\">") + "<i class=\"fa-solid fa-download\"></i> ".concat(defaultConfig.downloadChartButtonText) + "</button>" + "</div>";
382
535
  }
383
536
 
537
+ // The marker itself is read by chat-markers.js, shared with every other chat surface, so the three of them
538
+ // cannot disagree about what a marker is. Everything below -- the container, the id scheme, the Chart.js
539
+ // call -- is this surface's own and legitimately differs from the others.
540
+ var findChartMarker = window.CoreAIChatMarkers.findChartMarker;
541
+
384
542
  // Register [chart:{...json...}] as a native marked block extension so the
385
543
  // markdown parser handles chart markers inline with surrounding text.
386
544
  marked.use({
@@ -392,7 +550,7 @@ window.coreAIChatManager = function () {
392
550
  return idx >= 0 ? idx : undefined;
393
551
  },
394
552
  tokenizer: function tokenizer(src) {
395
- var extracted = tryExtractChartMarker(src);
553
+ var extracted = findChartMarker(src);
396
554
  if (!extracted || extracted.startIndex !== 0) {
397
555
  return undefined;
398
556
  }
@@ -415,69 +573,60 @@ window.coreAIChatManager = function () {
415
573
  }]
416
574
  });
417
575
 
418
- // Extract a [chart:{...json...}] marker. This avoids regex issues with nested brackets.
419
- function tryExtractChartMarker(text) {
420
- var token = '[chart:';
421
- var start = text.indexOf(token);
422
- if (start < 0) {
423
- return null;
576
+ // An image the assistant named but the server will not serve. The address is written by a language
577
+ // model from what retrieval handed it, so a mistyped or invented one is possible, and the browser's
578
+ // default for that is a broken-image icon captioned with the alt text -- which reads as a real picture
579
+ // that failed to arrive. Saying plainly that it could not be loaded is both truer and quieter.
580
+ //
581
+ // Wired here rather than as an inline onerror attribute because message bodies go through DOMPurify,
582
+ // which strips event-handler attributes.
583
+ function markBrokenImagesInMessage() {
584
+ // Scanning the document rather than one message, because the value threaded through the render
585
+ // path is the message DATA object -- the thing carrying content, references and pending charts --
586
+ // and not the element it was rendered into. Wiring is idempotent via the data-broken-wired mark,
587
+ // so re-scanning after each render costs a query over the handful of images already handled.
588
+ if (typeof document === 'undefined' || typeof requestAnimationFrame !== 'function') {
589
+ return;
424
590
  }
425
591
 
426
- // Find JSON object boundary by balancing braces
427
- var jsonStart = start + token.length;
428
- var i = jsonStart;
429
- while (i < text.length && (text[i] === ' ' || text[i] === '\n' || text[i] === '\r' || text[i] === '\t')) {
430
- i++;
431
- }
432
- if (i >= text.length || text[i] !== '{') {
433
- return null;
434
- }
435
- var depth = 0;
436
- var inString = false;
437
- var escape = false;
438
- for (; i < text.length; i++) {
439
- var ch = text[i];
440
- if (inString) {
441
- if (escape) {
442
- escape = false;
443
- continue;
444
- }
445
- if (ch === '\\') {
446
- escape = true;
447
- continue;
448
- }
449
- if (ch === '"') {
450
- inString = false;
451
- }
452
- continue;
453
- }
454
- if (ch === '"') {
455
- inString = true;
456
- continue;
457
- }
458
- if (ch === '{') {
459
- depth++;
460
- } else if (ch === '}') {
461
- depth--;
462
- if (depth === 0) {
463
- var jsonEnd = i;
464
- // Expect closing bracket after JSON
465
- var closeBracketIndex = text.indexOf(']', jsonEnd + 1);
466
- if (closeBracketIndex < 0) {
467
- return null;
592
+ // Deferred for the same reason the charts below are: the render that produced these elements has
593
+ // not necessarily been flushed to the DOM when this runs.
594
+ requestAnimationFrame(wireBrokenImageHandlers);
595
+ }
596
+ function wireBrokenImageHandlers() {
597
+ var images = document.querySelectorAll('.generated-image-container img:not([data-broken-wired])');
598
+ var _iterator6 = _createForOfIteratorHelper(images),
599
+ _step6;
600
+ try {
601
+ var _loop = function _loop() {
602
+ var image = _step6.value;
603
+ image.setAttribute('data-broken-wired', 'true');
604
+ image.addEventListener('error', function () {
605
+ var container = image.closest('.generated-image-container');
606
+ if (!container || container.dataset.brokenHandled) {
607
+ return;
468
608
  }
469
- var json = text.substring(jsonStart, jsonEnd + 1).trim();
470
- return {
471
- startIndex: start,
472
- endIndex: closeBracketIndex + 1,
473
- json: json
474
- };
475
- }
609
+ container.dataset.brokenHandled = 'true';
610
+ container.hidden = true;
611
+ var note = document.createElement('p');
612
+ note.className = 'text-muted small fst-italic mb-2';
613
+ note.textContent = image.alt ? "(the picture \"".concat(image.alt, "\" could not be loaded)") : '(that picture could not be loaded)';
614
+ container.insertAdjacentElement('afterend', note);
615
+ }, {
616
+ once: true
617
+ });
618
+ };
619
+ for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
620
+ _loop();
476
621
  }
622
+ } catch (err) {
623
+ _iterator6.e(err);
624
+ } finally {
625
+ _iterator6.f();
477
626
  }
478
- return null;
479
627
  }
480
- function renderChartsInMessage(message) {
628
+ function renderMessageMedia(message) {
629
+ markBrokenImagesInMessage();
481
630
  if (!message || !message._pendingCharts || !message._pendingCharts.length) {
482
631
  return;
483
632
  }
@@ -489,17 +638,17 @@ window.coreAIChatManager = function () {
489
638
  // Defer to requestAnimationFrame so the browser has fully laid out the
490
639
  // canvas elements before Chart.js reads their dimensions.
491
640
  requestAnimationFrame(function () {
492
- var _iterator5 = _createForOfIteratorHelper(charts),
493
- _step5;
641
+ var _iterator7 = _createForOfIteratorHelper(charts),
642
+ _step7;
494
643
  try {
495
- for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
496
- var c = _step5.value;
644
+ for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
645
+ var c = _step7.value;
497
646
  renderChartOnCanvas(c.chartId, c.config);
498
647
  }
499
648
  } catch (err) {
500
- _iterator5.e(err);
649
+ _iterator7.e(err);
501
650
  } finally {
502
- _iterator5.f();
651
+ _iterator7.f();
503
652
  }
504
653
  });
505
654
  }
@@ -662,8 +811,10 @@ window.coreAIChatManager = function () {
662
811
  mediaRecorder: null,
663
812
  preRecordingPrompt: '',
664
813
  micButton: null,
665
- speechToTextEnabled: config.chatMode === 'AudioInput' || config.chatMode === 'Conversation',
666
- textToSpeechEnabled: config.chatMode === 'Conversation' || !!config.textToSpeechEnabled,
814
+ // Conversation mode is carried either by a realtime session or by the speech-to-text plus
815
+ // text-to-speech cascade, never both. Dictation and spoken playback belong to the cascade.
816
+ speechToTextEnabled: !config.realtimeEnabled && (config.chatMode === 'AudioInput' || config.chatMode === 'Conversation'),
817
+ textToSpeechEnabled: !config.realtimeEnabled && config.chatMode === 'Conversation' || !!config.textToSpeechEnabled,
667
818
  ttsVoiceName: config.ttsVoiceName || null,
668
819
  audioChunks: [],
669
820
  audioPlayQueue: [],
@@ -686,6 +837,7 @@ window.coreAIChatManager = function () {
686
837
  realtimeWebRtcIceServers: Array.isArray(config.realtimeWebRtcIceServers) && config.realtimeWebRtcIceServers.length ? config.realtimeWebRtcIceServers : null,
687
838
  realtimeVoiceName: config.realtimeVoiceName || null,
688
839
  conversationButton: null,
840
+ realtimeHintElement: config.realtimeHintElementSelector ? document.querySelector(config.realtimeHintElementSelector) : null,
689
841
  isConversationMode: false,
690
842
  notificationDismissTimers: {},
691
843
  pendingSessionPromise: null,
@@ -800,7 +952,7 @@ window.coreAIChatManager = function () {
800
952
  return _context3.a(2);
801
953
  case 2:
802
954
  return _context3.a(2, _this.queueDocumentOperation(/*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
803
- var sessionId, profileId, formData, i, response, errorText, uploadError, result, _t, _t2;
955
+ var sessionId, profileId, formData, i, response, errorText, uploadError, parsed, result, _t, _t2;
804
956
  return _regenerator().w(function (_context2) {
805
957
  while (1) switch (_context2.p = _context2.n) {
806
958
  case 0:
@@ -873,9 +1025,24 @@ window.coreAIChatManager = function () {
873
1025
  return _context2.a(2);
874
1026
  case 9:
875
1027
  _context2.n = 10;
876
- return response.json();
1028
+ return readJsonResponse(response, 'Upload failed. Please try again.');
877
1029
  case 10:
878
- result = _context2.v;
1030
+ parsed = _context2.v;
1031
+ if (parsed.ok) {
1032
+ _context2.n = 11;
1033
+ break;
1034
+ }
1035
+ console.error('Upload failed: response was not JSON.', {
1036
+ status: response.status,
1037
+ redirected: response.redirected
1038
+ });
1039
+ _this.uploadErrors = [{
1040
+ fileName: '',
1041
+ error: parsed.message
1042
+ }];
1043
+ return _context2.a(2);
1044
+ case 11:
1045
+ result = parsed.data;
879
1046
  if (result.sessionId && result.sessionId !== _this.getSessionId()) {
880
1047
  _this.initializeSession(result.sessionId);
881
1048
  }
@@ -887,10 +1054,10 @@ window.coreAIChatManager = function () {
887
1054
  if (result.failed && result.failed.length > 0) {
888
1055
  _this.uploadErrors = result.failed;
889
1056
  }
890
- _context2.n = 12;
1057
+ _context2.n = 13;
891
1058
  break;
892
- case 11:
893
- _context2.p = 11;
1059
+ case 12:
1060
+ _context2.p = 12;
894
1061
  _t2 = _context2.v;
895
1062
  console.error('Upload error:', _t2);
896
1063
  _this.uploadErrors = [{
@@ -900,15 +1067,15 @@ window.coreAIChatManager = function () {
900
1067
  if (_this.getSessionId()) {
901
1068
  _this.reloadCurrentSession();
902
1069
  }
903
- case 12:
904
- _context2.p = 12;
1070
+ case 13:
1071
+ _context2.p = 13;
905
1072
  _this.isUploading = false;
906
1073
  _this.renderDocumentBar();
907
- return _context2.f(12);
908
- case 13:
1074
+ return _context2.f(13);
1075
+ case 14:
909
1076
  return _context2.a(2);
910
1077
  }
911
- }, _callee2, null, [[6, 11, 12, 13], [1, 3]]);
1078
+ }, _callee2, null, [[6, 12, 13, 14], [1, 3]]);
912
1079
  }))));
913
1080
  }
914
1081
  }, _callee3);
@@ -927,7 +1094,7 @@ window.coreAIChatManager = function () {
927
1094
  return _context5.a(2);
928
1095
  case 1:
929
1096
  return _context5.a(2, _this2.queueDocumentOperation(/*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4() {
930
- var sessionId, response, result, idx, errorText, removeError, _t3;
1097
+ var sessionId, response, secondParsed, result, idx, errorText, removeError, _t3;
931
1098
  return _regenerator().w(function (_context4) {
932
1099
  while (1) switch (_context4.p = _context4.n) {
933
1100
  case 0:
@@ -947,13 +1114,24 @@ window.coreAIChatManager = function () {
947
1114
  case 1:
948
1115
  response = _context4.v;
949
1116
  if (!response.ok) {
950
- _context4.n = 3;
1117
+ _context4.n = 4;
951
1118
  break;
952
1119
  }
953
1120
  _context4.n = 2;
954
- return response.json();
1121
+ return readJsonResponse(response, 'The request could not be completed.');
955
1122
  case 2:
956
- result = _context4.v;
1123
+ secondParsed = _context4.v;
1124
+ if (secondParsed.ok) {
1125
+ _context4.n = 3;
1126
+ break;
1127
+ }
1128
+ console.error('Response was not JSON.', {
1129
+ status: response.status,
1130
+ redirected: response.redirected
1131
+ });
1132
+ return _context4.a(2);
1133
+ case 3:
1134
+ result = secondParsed.data;
957
1135
  if (Array.isArray(result.documents)) {
958
1136
  _this2.documents = result.documents;
959
1137
  } else {
@@ -962,12 +1140,12 @@ window.coreAIChatManager = function () {
962
1140
  _this2.documents.splice(idx, 1);
963
1141
  }
964
1142
  }
965
- _context4.n = 5;
1143
+ _context4.n = 6;
966
1144
  break;
967
- case 3:
968
- _context4.n = 4;
969
- return response.text();
970
1145
  case 4:
1146
+ _context4.n = 5;
1147
+ return response.text();
1148
+ case 5:
971
1149
  errorText = _context4.v;
972
1150
  removeError = _this2.extractReadableErrorMessage(errorText, 'Failed to remove document. Please try again.');
973
1151
  console.error('Failed to remove document:', response.status, errorText);
@@ -979,11 +1157,11 @@ window.coreAIChatManager = function () {
979
1157
  _this2.reloadCurrentSession();
980
1158
  }
981
1159
  _this2.renderDocumentBar();
982
- case 5:
983
- _context4.n = 7;
984
- break;
985
1160
  case 6:
986
- _context4.p = 6;
1161
+ _context4.n = 8;
1162
+ break;
1163
+ case 7:
1164
+ _context4.p = 7;
987
1165
  _t3 = _context4.v;
988
1166
  console.error('Remove document error:', _t3);
989
1167
  _this2.uploadErrors = [{
@@ -994,10 +1172,10 @@ window.coreAIChatManager = function () {
994
1172
  _this2.reloadCurrentSession();
995
1173
  }
996
1174
  _this2.renderDocumentBar();
997
- case 7:
1175
+ case 8:
998
1176
  return _context4.a(2);
999
1177
  }
1000
- }, _callee4, null, [[0, 6]]);
1178
+ }, _callee4, null, [[0, 7]]);
1001
1179
  }))));
1002
1180
  }
1003
1181
  }, _callee5);
@@ -1031,11 +1209,19 @@ window.coreAIChatManager = function () {
1031
1209
  var failedItem = this.uploadErrors[m];
1032
1210
  var failedName = failedItem.fileName || 'File';
1033
1211
  var errorMsg = failedItem.error || 'Upload failed';
1034
- if (failedName.length > 15) failedName = failedName.substring(0, 12) + '...';
1035
- html += '<span class="badge bg-danger bg-opacity-25 text-danger d-inline-flex align-items-center gap-1 px-2 py-1" style="font-size: 0.8rem;" title="' + this.escapeHtml((failedItem.fileName || '') + ': ' + errorMsg) + '">';
1036
- html += '<i class="fa-solid fa-circle-exclamation" style="font-size: 0.7rem;"></i> ';
1037
- html += this.escapeHtml(failedName);
1038
- html += ' <button type="button" class="btn-close btn-close-sm ms-1" style="font-size: 0.5rem;" data-error-index="' + m + '" aria-label="Dismiss"></button>';
1212
+ // The reason is shown, not hidden behind a tooltip. A pill reading "quarterly-report-2..."
1213
+ // tells a reader their upload failed and nothing about why, and a title attribute is
1214
+ // unreachable on a touch device -- so the one sentence that tells them what to do next
1215
+ // was the one thing the widget would not display.
1216
+ html += '<span class="alert alert-danger d-flex align-items-start gap-2 w-100 mb-1 py-1 px-2" role="alert" style="font-size: 0.8rem;">';
1217
+ html += '<i class="fa-solid fa-circle-exclamation mt-1" style="font-size: 0.7rem;"></i>';
1218
+ html += '<span class="flex-grow-1">';
1219
+ if (failedItem.fileName) {
1220
+ html += '<strong>' + this.escapeHtml(failedItem.fileName) + '</strong>: ';
1221
+ }
1222
+ html += this.escapeHtml(errorMsg);
1223
+ html += '</span>';
1224
+ html += '<button type="button" class="btn-close btn-close-sm" style="font-size: 0.5rem;" data-error-index="' + m + '" aria-label="Dismiss"></button>';
1039
1225
  html += '</span>';
1040
1226
  }
1041
1227
  if (this.isUploading) {
@@ -1195,7 +1381,7 @@ window.coreAIChatManager = function () {
1195
1381
  ((_data$messages = data.messages) !== null && _data$messages !== void 0 ? _data$messages : []).forEach(function (msg) {
1196
1382
  _this3.addMessage(msg);
1197
1383
  _this3.$nextTick(function () {
1198
- renderChartsInMessage(msg);
1384
+ renderMessageMedia(msg);
1199
1385
  });
1200
1386
  });
1201
1387
 
@@ -1388,7 +1574,7 @@ window.coreAIChatManager = function () {
1388
1574
  msg.references = normalizeReferences(Object.assign({}, msg.references || {}, references || {}));
1389
1575
  updateMessagePresentation(msg, msg.references);
1390
1576
  _this3.$nextTick(function () {
1391
- renderChartsInMessage(msg);
1577
+ renderMessageMedia(msg);
1392
1578
  _this3.scrollToBottom();
1393
1579
  });
1394
1580
  }
@@ -1577,7 +1763,7 @@ window.coreAIChatManager = function () {
1577
1763
  this.hidePlaceholder();
1578
1764
  this.$nextTick(function () {
1579
1765
  // Render any pending charts once the DOM is updated
1580
- renderChartsInMessage(message);
1766
+ renderMessageMedia(message);
1581
1767
  _this6.scrollToBottom();
1582
1768
  });
1583
1769
  },
@@ -1644,6 +1830,11 @@ window.coreAIChatManager = function () {
1644
1830
  return;
1645
1831
  }
1646
1832
 
1833
+ // A live voice session and a typed turn are two writers into one chat session, and they
1834
+ // would interleave. End the session first; the turns it already produced are persisted, so
1835
+ // the typed message simply continues the same thread.
1836
+ this.forceStopActiveVoice();
1837
+
1647
1838
  // Stop any active recording before sending.
1648
1839
  if (this.isRecording) {
1649
1840
  this.stopRecording();
@@ -1903,7 +2094,7 @@ window.coreAIChatManager = function () {
1903
2094
  updateMessagePresentation(message, references);
1904
2095
  _this9.messages[messageIndex] = message;
1905
2096
  _this9.$nextTick(function () {
1906
- renderChartsInMessage(message);
2097
+ renderMessageMedia(message);
1907
2098
  _this9.scrollToBottom();
1908
2099
  });
1909
2100
  },
@@ -1971,7 +2162,7 @@ window.coreAIChatManager = function () {
1971
2162
  updateMessagePresentation(message, references);
1972
2163
  this.messages[messageIndex] = message;
1973
2164
  this.$nextTick(function () {
1974
- renderChartsInMessage(message);
2165
+ renderMessageMedia(message);
1975
2166
  _this0.scrollToBottom();
1976
2167
  });
1977
2168
  }
@@ -2122,18 +2313,18 @@ window.coreAIChatManager = function () {
2122
2313
  }, 0);
2123
2314
  var combined = new Uint8Array(totalLength);
2124
2315
  var offset = 0;
2125
- var _iterator6 = _createForOfIteratorHelper(this.audioChunks),
2126
- _step6;
2316
+ var _iterator8 = _createForOfIteratorHelper(this.audioChunks),
2317
+ _step8;
2127
2318
  try {
2128
- for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
2129
- var chunk = _step6.value;
2319
+ for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) {
2320
+ var chunk = _step8.value;
2130
2321
  combined.set(chunk, offset);
2131
2322
  offset += chunk.length;
2132
2323
  }
2133
2324
  } catch (err) {
2134
- _iterator6.e(err);
2325
+ _iterator8.e(err);
2135
2326
  } finally {
2136
- _iterator6.f();
2327
+ _iterator8.f();
2137
2328
  }
2138
2329
  this.audioChunks = [];
2139
2330
  var blob = new Blob([combined], {
@@ -2216,6 +2407,45 @@ window.coreAIChatManager = function () {
2216
2407
  return _this16.updateTtsPlaybackButtons();
2217
2408
  });
2218
2409
  },
2410
+ // A live voice session takes the message box away and puts the audio settings in its place; ending
2411
+ // the session gives it back. Sending a typed message already force-stopped the session, so the box
2412
+ // was never a way to say something *during* one -- hiding it only makes that honest, and frees the
2413
+ // row for the gear. Both transports land here: realtime through the module's onActivate, the
2414
+ // speech-to-text cascade through startConversationMode.
2415
+ // The realtime module inserts its settings popover directly after the button it was handed, and
2416
+ // hard-codes the element id. A page can host two chat clients at once -- this one and the admin
2417
+ // widget -- so that id is not unique and getElementById would hand the widget the page's gear.
2418
+ // Each client finds its own next to its own button.
2419
+ voiceSettingsElement: function voiceSettingsElement() {
2420
+ var next = this.conversationButton && this.conversationButton.nextElementSibling;
2421
+ return next && next.id === 'realtime-audio-settings' ? next : null;
2422
+ },
2423
+ setVoiceSessionLive: function setVoiceSessionLive(live) {
2424
+ if (this.inputElement) {
2425
+ this.inputElement.hidden = live;
2426
+ }
2427
+ if (this.buttonElement) {
2428
+ this.buttonElement.hidden = live;
2429
+ }
2430
+
2431
+ // With the message box gone the button would otherwise sit at its natural width in an empty
2432
+ // row, so it takes the space the box left behind and reads as the thing now in charge. The
2433
+ // settings popover does not grow, so it stays at the right-hand end.
2434
+ if (this.conversationButton) {
2435
+ this.conversationButton.classList.toggle('flex-grow-1', live);
2436
+ }
2437
+
2438
+ // The gear is built by the realtime module next to its button, and only in realtime mode, so
2439
+ // it is absent on the cascade. It belongs to a live session rather than to the surface.
2440
+ var audioSettings = this.voiceSettingsElement();
2441
+ if (audioSettings) {
2442
+ audioSettings.hidden = !live;
2443
+ }
2444
+ var hint = this.realtimeHintElement;
2445
+ if (hint) {
2446
+ hint.classList.toggle('d-none', !live);
2447
+ }
2448
+ },
2219
2449
  toggleConversationMode: function toggleConversationMode() {
2220
2450
  if (this.realtimeEnabled) {
2221
2451
  // Realtime is driven by the shared module, which owns the button; this only exists for
@@ -2291,19 +2521,25 @@ window.coreAIChatManager = function () {
2291
2521
  },
2292
2522
  realtimeEnabled: true,
2293
2523
  // The conversation button doubles as the realtime button on this host; the module owns its
2294
- // click, label and state from here on. The text controls are handed over too so the module
2295
- // hides them: realtime is audio-only, and a message box that plays no part in a spoken
2296
- // conversation only invites the user to type into it.
2524
+ // click, label and state from here on.
2525
+ //
2526
+ // input and sendButton are deliberately not handed over. The module hides every control it
2527
+ // is given when realtime takes over, and typing has to stay available during a voice
2528
+ // conversation -- that is the whole feature.
2297
2529
  //
2298
- // conversationButton is deliberately not passed. It is the same element as the realtime
2530
+ // conversationButton is not handed over either. It is the same element as the realtime
2299
2531
  // button here, and the module hides the conversation button before showing the realtime
2300
2532
  // one -- naming it twice would ask it to hide the control it then has to show.
2301
2533
  selectors: {
2302
2534
  realtimeButton: config.conversationButtonElementSelector,
2303
- input: config.inputElementSelector,
2304
- sendButton: config.sendButtonElementSelector,
2305
2535
  micButton: config.micButtonElementSelector
2306
2536
  },
2537
+ // The module owns whether a session is live, so the message box follows its flag rather
2538
+ // than this app's copy of it. Losing one of these events used to leave the box hidden
2539
+ // under a button that had already gone back to "Start speaking".
2540
+ onSessionStateChanged: function onSessionStateChanged(active) {
2541
+ self.setVoiceSessionLive(active);
2542
+ },
2307
2543
  onActivate: function onActivate() {
2308
2544
  self.isConversationMode = true;
2309
2545
  self._conversationAssistantMessage = null;
@@ -2946,6 +3182,11 @@ window.coreAIChatManager = function () {
2946
3182
  // The shared realtime module owns the button (click, label, state) and adds the
2947
3183
  // voice settings popover next to it.
2948
3184
  this.setupRealtimeController(config);
3185
+
3186
+ // The module reveals the gear as soon as the surface enters realtime mode, but it
3187
+ // belongs to a live session. The watcher that would hide it only runs on a change,
3188
+ // so the idle state has to be stated once here.
3189
+ this.setVoiceSessionLive(false);
2949
3190
  } else {
2950
3191
  this.conversationButton.addEventListener('click', function () {
2951
3192
  _this23.toggleConversationMode();
@@ -3139,22 +3380,20 @@ window.coreAIChatManager = function () {
3139
3380
  });
3140
3381
  },
3141
3382
  isConversationMode: function isConversationMode(active) {
3142
- // Hide/show mic button.
3383
+ // The dictation microphone is hidden while a voice conversation runs: the conversation
3384
+ // already owns the microphone, and a second one would mean something different.
3143
3385
  if (this.micButton) {
3144
3386
  this.micButton.style.display = active ? 'none' : this.speechToTextEnabled ? '' : 'none';
3145
3387
  }
3146
3388
 
3147
- // Hide/show send button.
3148
- if (this.buttonElement) {
3149
- this.buttonElement.style.display = active ? 'none' : '';
3150
- }
3151
-
3152
- // Disable/enable textarea.
3153
- if (this.inputElement) {
3154
- this.inputElement.disabled = active;
3155
- if (active) {
3156
- this.inputElement.placeholder = '';
3157
- }
3389
+ // The message box and the send button give way to the voice settings while the conversation
3390
+ // runs, and come back when it ends. Both kinds of turn still land in the same thread; what
3391
+ // they cannot do is overlap.
3392
+ //
3393
+ // Only the cascade is driven from here. Realtime follows the module's own session flag
3394
+ // (onSessionStateChanged), so the two never disagree about whether a session is live.
3395
+ if (!this.realtimeEnabled) {
3396
+ this.setVoiceSessionLive(active);
3158
3397
  }
3159
3398
  }
3160
3399
  },
@@ -3267,6 +3506,7 @@ window.coreAIChatManager = function () {
3267
3506
  chatMode: getAttributeValue(element, 'data-coreai-chat-mode'),
3268
3507
  micButtonElementSelector: getAttributeValue(element, 'data-coreai-chat-mic-button-element-selector'),
3269
3508
  conversationButtonElementSelector: getAttributeValue(element, 'data-coreai-chat-conversation-button-element-selector'),
3509
+ realtimeHintElementSelector: getAttributeValue(element, 'data-coreai-chat-realtime-hint-element-selector'),
3270
3510
  ttsVoiceName: getAttributeValue(element, 'data-coreai-chat-tts-voice-name'),
3271
3511
  realtimeVoiceName: getAttributeValue(element, 'data-coreai-chat-realtime-voice-name'),
3272
3512
  documentBarSelector: getAttributeValue(element, 'data-coreai-chat-document-bar-selector'),