@coffer-org/plugin-webchat 7.0.1 → 7.2.0

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.
@@ -0,0 +1,2398 @@
1
+ import * as __fedReact from "react";
2
+ import { Button, Dialog, DialogContent, Icon, MarkdownView, fileSizeLabel, uploadFile, useMinWidth } from "@coffer-org/web-ui";
3
+ import { createContext, createElement, forwardRef, memo, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
4
+ import { useLocation } from "react-router-dom";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ //#region \0rolldown/runtime.js
7
+ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
8
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) {
9
+ if (typeof require !== "undefined") return require.apply(this, arguments);
10
+ throw Error("Calling `require` for \"" + x + "\" in an environment that doesn't expose the `require` function. See https://rolldown.rs/in-depth/bundling-cjs#require-external-modules for more details.");
11
+ });
12
+ //#endregion
13
+ //#region ../../node_modules/react-i18next/dist/es/utils.js
14
+ var warn = (i18n, code, msg, rest) => {
15
+ const args = [msg, {
16
+ code,
17
+ ...rest || {}
18
+ }];
19
+ if (i18n?.services?.logger?.forward) return i18n.services.logger.forward(args, "warn", "react-i18next::", true);
20
+ if (isString(args[0])) args[0] = `react-i18next:: ${args[0]}`;
21
+ if (i18n?.services?.logger?.warn) i18n.services.logger.warn(...args);
22
+ else if (console?.warn) console.warn(...args);
23
+ };
24
+ var alreadyWarned = {};
25
+ var warnOnce = (i18n, code, msg, rest) => {
26
+ if (isString(msg) && alreadyWarned[msg]) return;
27
+ if (isString(msg)) alreadyWarned[msg] = /* @__PURE__ */ new Date();
28
+ warn(i18n, code, msg, rest);
29
+ };
30
+ var loadedClb = (i18n, cb) => () => {
31
+ if (i18n.isInitialized) cb();
32
+ else {
33
+ const initialized = () => {
34
+ setTimeout(() => {
35
+ i18n.off("initialized", initialized);
36
+ }, 0);
37
+ cb();
38
+ };
39
+ i18n.on("initialized", initialized);
40
+ }
41
+ };
42
+ var loadNamespaces = (i18n, ns, cb) => {
43
+ i18n.loadNamespaces(ns, loadedClb(i18n, cb));
44
+ };
45
+ var loadLanguages = (i18n, lng, ns, cb) => {
46
+ if (isString(ns)) ns = [ns];
47
+ if (i18n.options.preload && i18n.options.preload.indexOf(lng) > -1) return loadNamespaces(i18n, ns, cb);
48
+ ns.forEach((n) => {
49
+ if (i18n.options.ns.indexOf(n) < 0) i18n.options.ns.push(n);
50
+ });
51
+ i18n.loadLanguages(lng, loadedClb(i18n, cb));
52
+ };
53
+ var hasLoadedNamespace = (ns, i18n, options = {}) => {
54
+ if (!i18n.languages || !i18n.languages.length) {
55
+ warnOnce(i18n, "NO_LANGUAGES", "i18n.languages were undefined or empty", { languages: i18n.languages });
56
+ return true;
57
+ }
58
+ return i18n.hasLoadedNamespace(ns, {
59
+ lng: options.lng,
60
+ precheck: (i18nInstance, loadNotPending) => {
61
+ if (options.bindI18n && options.bindI18n.indexOf("languageChanging") > -1 && i18nInstance.services.backendConnector.backend && i18nInstance.isLanguageChangingTo && !loadNotPending(i18nInstance.isLanguageChangingTo, ns)) return false;
62
+ }
63
+ });
64
+ };
65
+ var isString = (obj) => typeof obj === "string";
66
+ var isObject = (obj) => typeof obj === "object" && obj !== null;
67
+ //#endregion
68
+ //#region ../../node_modules/react-i18next/dist/es/unescape.js
69
+ var matchHtmlEntity = /&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34|nbsp|#160|copy|#169|reg|#174|hellip|#8230|#x2F|#47);/g;
70
+ var htmlEntities = {
71
+ "&amp;": "&",
72
+ "&#38;": "&",
73
+ "&lt;": "<",
74
+ "&#60;": "<",
75
+ "&gt;": ">",
76
+ "&#62;": ">",
77
+ "&apos;": "'",
78
+ "&#39;": "'",
79
+ "&quot;": "\"",
80
+ "&#34;": "\"",
81
+ "&nbsp;": " ",
82
+ "&#160;": " ",
83
+ "&copy;": "©",
84
+ "&#169;": "©",
85
+ "&reg;": "®",
86
+ "&#174;": "®",
87
+ "&hellip;": "…",
88
+ "&#8230;": "…",
89
+ "&#x2F;": "/",
90
+ "&#47;": "/"
91
+ };
92
+ var unescapeHtmlEntity = (m) => htmlEntities[m];
93
+ var unescape = (text) => text.replace(matchHtmlEntity, unescapeHtmlEntity);
94
+ //#endregion
95
+ //#region ../../node_modules/react-i18next/dist/es/defaults.js
96
+ var defaultOptions = {
97
+ bindI18n: "languageChanged",
98
+ bindI18nStore: "",
99
+ transEmptyNodeValue: "",
100
+ transSupportBasicHtmlNodes: true,
101
+ transWrapTextNodes: "",
102
+ transKeepBasicHtmlNodesFor: [
103
+ "br",
104
+ "strong",
105
+ "i",
106
+ "p"
107
+ ],
108
+ useSuspense: true,
109
+ unescape,
110
+ transDefaultProps: void 0
111
+ };
112
+ var getDefaults = () => defaultOptions;
113
+ //#endregion
114
+ //#region ../../node_modules/react-i18next/dist/es/i18nInstance.js
115
+ var i18nInstance;
116
+ var getI18n = () => i18nInstance;
117
+ //#endregion
118
+ //#region ../../node_modules/react-i18next/dist/es/context.js
119
+ var I18nContext = createContext();
120
+ var ReportNamespaces = class {
121
+ constructor() {
122
+ this.usedNamespaces = {};
123
+ }
124
+ addUsedNamespaces(namespaces) {
125
+ namespaces.forEach((ns) => {
126
+ if (!this.usedNamespaces[ns]) this.usedNamespaces[ns] = true;
127
+ });
128
+ }
129
+ getUsedNamespaces() {
130
+ return Object.keys(this.usedNamespaces);
131
+ }
132
+ };
133
+ //#endregion
134
+ //#region ../../node_modules/use-sync-external-store/cjs/use-sync-external-store-shim.production.js
135
+ /**
136
+ * @license React
137
+ * use-sync-external-store-shim.production.js
138
+ *
139
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
140
+ *
141
+ * This source code is licensed under the MIT license found in the
142
+ * LICENSE file in the root directory of this source tree.
143
+ */
144
+ var require_use_sync_external_store_shim_production = /* @__PURE__ */ __commonJSMin(((exports) => {
145
+ var React = __fedReact;
146
+ function is(x, y) {
147
+ return x === y && (0 !== x || 1 / x === 1 / y) || x !== x && y !== y;
148
+ }
149
+ var objectIs = "function" === typeof Object.is ? Object.is : is;
150
+ var useState = React.useState;
151
+ var useEffect = React.useEffect;
152
+ var useLayoutEffect = React.useLayoutEffect;
153
+ var useDebugValue = React.useDebugValue;
154
+ function useSyncExternalStore$2(subscribe, getSnapshot) {
155
+ var value = getSnapshot(), _useState = useState({ inst: {
156
+ value,
157
+ getSnapshot
158
+ } }), inst = _useState[0].inst, forceUpdate = _useState[1];
159
+ useLayoutEffect(function() {
160
+ inst.value = value;
161
+ inst.getSnapshot = getSnapshot;
162
+ checkIfSnapshotChanged(inst) && forceUpdate({ inst });
163
+ }, [
164
+ subscribe,
165
+ value,
166
+ getSnapshot
167
+ ]);
168
+ useEffect(function() {
169
+ checkIfSnapshotChanged(inst) && forceUpdate({ inst });
170
+ return subscribe(function() {
171
+ checkIfSnapshotChanged(inst) && forceUpdate({ inst });
172
+ });
173
+ }, [subscribe]);
174
+ useDebugValue(value);
175
+ return value;
176
+ }
177
+ function checkIfSnapshotChanged(inst) {
178
+ var latestGetSnapshot = inst.getSnapshot;
179
+ inst = inst.value;
180
+ try {
181
+ var nextValue = latestGetSnapshot();
182
+ return !objectIs(inst, nextValue);
183
+ } catch (error) {
184
+ return !0;
185
+ }
186
+ }
187
+ function useSyncExternalStore$1(subscribe, getSnapshot) {
188
+ return getSnapshot();
189
+ }
190
+ var shim = "undefined" === typeof window || "undefined" === typeof window.document || "undefined" === typeof window.document.createElement ? useSyncExternalStore$1 : useSyncExternalStore$2;
191
+ exports.useSyncExternalStore = void 0 !== React.useSyncExternalStore ? React.useSyncExternalStore : shim;
192
+ }));
193
+ //#endregion
194
+ //#region ../../node_modules/react-i18next/dist/es/useTranslation.js
195
+ var import_shim = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
196
+ module.exports = require_use_sync_external_store_shim_production();
197
+ })))();
198
+ var notReadyT = (k, optsOrDefaultValue) => {
199
+ if (isString(optsOrDefaultValue)) return optsOrDefaultValue;
200
+ if (isObject(optsOrDefaultValue) && isString(optsOrDefaultValue.defaultValue)) return optsOrDefaultValue.defaultValue;
201
+ if (typeof k === "function") return "";
202
+ if (Array.isArray(k)) {
203
+ const last = k[k.length - 1];
204
+ return typeof last === "function" ? "" : last;
205
+ }
206
+ return k;
207
+ };
208
+ var notReadySnapshot = {
209
+ t: notReadyT,
210
+ ready: false
211
+ };
212
+ var dummySubscribe = () => () => {};
213
+ var useTranslation = (ns, props = {}) => {
214
+ const { i18n: i18nFromProps } = props;
215
+ const { i18n: i18nFromContext, defaultNS: defaultNSFromContext } = useContext(I18nContext) || {};
216
+ const i18n = i18nFromProps || i18nFromContext || getI18n();
217
+ if (i18n && !i18n.reportNamespaces) i18n.reportNamespaces = new ReportNamespaces();
218
+ if (!i18n) warnOnce(i18n, "NO_I18NEXT_INSTANCE", "useTranslation: You will need to pass in an i18next instance by using initReactI18next or by passing it via props or context. In monorepo setups, make sure there is only one instance of react-i18next.");
219
+ const i18nOptions = useMemo(() => ({
220
+ ...getDefaults(),
221
+ ...i18n?.options?.react,
222
+ ...props
223
+ }), [i18n, props]);
224
+ const { useSuspense, keyPrefix } = i18nOptions;
225
+ const nsOrContext = ns || defaultNSFromContext || i18n?.options?.defaultNS;
226
+ const unstableNamespaces = isString(nsOrContext) ? [nsOrContext] : nsOrContext || ["translation"];
227
+ const namespaces = useMemo(() => unstableNamespaces, unstableNamespaces);
228
+ i18n?.reportNamespaces?.addUsedNamespaces?.(namespaces);
229
+ const revisionRef = useRef(0);
230
+ const subscribe = useCallback((callback) => {
231
+ if (!i18n) return dummySubscribe;
232
+ const { bindI18n, bindI18nStore } = i18nOptions;
233
+ const wrappedCallback = () => {
234
+ revisionRef.current += 1;
235
+ callback();
236
+ };
237
+ if (bindI18n) i18n.on(bindI18n, wrappedCallback);
238
+ if (bindI18nStore) i18n.store.on(bindI18nStore, wrappedCallback);
239
+ return () => {
240
+ if (bindI18n) bindI18n.split(" ").forEach((e) => i18n.off(e, wrappedCallback));
241
+ if (bindI18nStore) bindI18nStore.split(" ").forEach((e) => i18n.store.off(e, wrappedCallback));
242
+ };
243
+ }, [i18n, i18nOptions]);
244
+ const snapshotRef = useRef();
245
+ const getSnapshot = useCallback(() => {
246
+ if (!i18n) return notReadySnapshot;
247
+ const calculatedReady = !!(i18n.isInitialized || i18n.initializedStoreOnce) && namespaces.every((n) => hasLoadedNamespace(n, i18n, i18nOptions));
248
+ const currentLng = props.lng || i18n.language;
249
+ const currentRevision = revisionRef.current;
250
+ const lastSnapshot = snapshotRef.current;
251
+ if (lastSnapshot && lastSnapshot.ready === calculatedReady && lastSnapshot.lng === currentLng && lastSnapshot.keyPrefix === keyPrefix && lastSnapshot.revision === currentRevision) return lastSnapshot;
252
+ const newSnapshot = {
253
+ t: i18n.getFixedT(currentLng, i18nOptions.nsMode === "fallback" ? namespaces : namespaces[0], keyPrefix, { scopeNs: namespaces }),
254
+ ready: calculatedReady,
255
+ lng: currentLng,
256
+ keyPrefix,
257
+ revision: currentRevision
258
+ };
259
+ snapshotRef.current = newSnapshot;
260
+ return newSnapshot;
261
+ }, [
262
+ i18n,
263
+ namespaces,
264
+ keyPrefix,
265
+ i18nOptions,
266
+ props.lng
267
+ ]);
268
+ const [loadCount, setLoadCount] = useState(0);
269
+ const { t, ready } = (0, import_shim.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
270
+ useEffect(() => {
271
+ if (i18n && !ready && !useSuspense) {
272
+ const onLoaded = () => setLoadCount((c) => c + 1);
273
+ if (props.lng) loadLanguages(i18n, props.lng, namespaces, onLoaded);
274
+ else loadNamespaces(i18n, namespaces, onLoaded);
275
+ }
276
+ }, [
277
+ i18n,
278
+ props.lng,
279
+ namespaces,
280
+ ready,
281
+ useSuspense,
282
+ loadCount
283
+ ]);
284
+ const finalI18n = i18n || {};
285
+ const wrapperRef = useRef(null);
286
+ const wrapperLangRef = useRef();
287
+ const createI18nWrapper = (original) => {
288
+ const descriptors = Object.getOwnPropertyDescriptors(original);
289
+ if (descriptors.__original) delete descriptors.__original;
290
+ const wrapper = Object.create(Object.getPrototypeOf(original), descriptors);
291
+ if (!Object.prototype.hasOwnProperty.call(wrapper, "__original")) try {
292
+ Object.defineProperty(wrapper, "__original", {
293
+ value: original,
294
+ writable: false,
295
+ enumerable: false,
296
+ configurable: false
297
+ });
298
+ } catch (_) {}
299
+ return wrapper;
300
+ };
301
+ const ret = useMemo(() => {
302
+ const original = finalI18n;
303
+ const lang = original?.language;
304
+ let i18nWrapper = original;
305
+ if (original) if (wrapperRef.current && wrapperRef.current.__original === original) if (wrapperLangRef.current !== lang) {
306
+ i18nWrapper = createI18nWrapper(original);
307
+ wrapperRef.current = i18nWrapper;
308
+ wrapperLangRef.current = lang;
309
+ } else i18nWrapper = wrapperRef.current;
310
+ else {
311
+ i18nWrapper = createI18nWrapper(original);
312
+ wrapperRef.current = i18nWrapper;
313
+ wrapperLangRef.current = lang;
314
+ }
315
+ const effectiveT = !ready && !useSuspense ? (...args) => {
316
+ warnOnce(i18n, "USE_T_BEFORE_READY", "useTranslation: t was called before ready. When using useSuspense: false, make sure to check the ready flag before using t.");
317
+ return t(...args);
318
+ } : t;
319
+ const arr = [
320
+ effectiveT,
321
+ i18nWrapper,
322
+ ready
323
+ ];
324
+ arr.t = effectiveT;
325
+ arr.i18n = i18nWrapper;
326
+ arr.ready = ready;
327
+ return arr;
328
+ }, [
329
+ t,
330
+ finalI18n,
331
+ ready,
332
+ finalI18n.resolvedLanguage,
333
+ finalI18n.language,
334
+ finalI18n.languages
335
+ ]);
336
+ if (i18n && useSuspense && !ready) {
337
+ let inDevelopment = false;
338
+ try {
339
+ inDevelopment = false;
340
+ } catch (e) {}
341
+ if (inDevelopment) warnOnce(i18n, "SUSPENDED_WHILE_LOADING", "useTranslation: suspended while translations are loading (useSuspense is true by default). Add a <Suspense> boundary above this component, or set react.useSuspense: false in the i18next init options. https://react.i18next.com/latest/usetranslation-hook");
342
+ throw new Promise((resolve) => {
343
+ const onLoaded = () => resolve();
344
+ if (props.lng) loadLanguages(i18n, props.lng, namespaces, onLoaded);
345
+ else loadNamespaces(i18n, namespaces, onLoaded);
346
+ });
347
+ }
348
+ return ret;
349
+ };
350
+ //#endregion
351
+ //#region ../../node_modules/lucide-react/dist/esm/shared/src/utils/mergeClasses.mjs
352
+ /**
353
+ * @license lucide-react v1.24.0 - ISC
354
+ *
355
+ * This source code is licensed under the ISC license.
356
+ * See the LICENSE file in the root directory of this source tree.
357
+ */
358
+ var mergeClasses = (...classes) => classes.filter((className, index, array) => {
359
+ return Boolean(className) && className.trim() !== "" && array.indexOf(className) === index;
360
+ }).join(" ").trim();
361
+ //#endregion
362
+ //#region ../../node_modules/lucide-react/dist/esm/shared/src/utils/toKebabCase.mjs
363
+ /**
364
+ * @license lucide-react v1.24.0 - ISC
365
+ *
366
+ * This source code is licensed under the ISC license.
367
+ * See the LICENSE file in the root directory of this source tree.
368
+ */
369
+ var toKebabCase = (string) => string.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
370
+ //#endregion
371
+ //#region ../../node_modules/lucide-react/dist/esm/shared/src/utils/toCamelCase.mjs
372
+ /**
373
+ * @license lucide-react v1.24.0 - ISC
374
+ *
375
+ * This source code is licensed under the ISC license.
376
+ * See the LICENSE file in the root directory of this source tree.
377
+ */
378
+ var toCamelCase = (string) => string.replace(/^([A-Z])|[\s-_]+(\w)/g, (match, p1, p2) => p2 ? p2.toUpperCase() : p1.toLowerCase());
379
+ //#endregion
380
+ //#region ../../node_modules/lucide-react/dist/esm/shared/src/utils/toPascalCase.mjs
381
+ /**
382
+ * @license lucide-react v1.24.0 - ISC
383
+ *
384
+ * This source code is licensed under the ISC license.
385
+ * See the LICENSE file in the root directory of this source tree.
386
+ */
387
+ var toPascalCase = (string) => {
388
+ const camelCase = toCamelCase(string);
389
+ return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
390
+ };
391
+ //#endregion
392
+ //#region ../../node_modules/lucide-react/dist/esm/defaultAttributes.mjs
393
+ /**
394
+ * @license lucide-react v1.24.0 - ISC
395
+ *
396
+ * This source code is licensed under the ISC license.
397
+ * See the LICENSE file in the root directory of this source tree.
398
+ */
399
+ var defaultAttributes = {
400
+ xmlns: "http://www.w3.org/2000/svg",
401
+ width: 24,
402
+ height: 24,
403
+ viewBox: "0 0 24 24",
404
+ fill: "none",
405
+ stroke: "currentColor",
406
+ strokeWidth: 2,
407
+ strokeLinecap: "round",
408
+ strokeLinejoin: "round"
409
+ };
410
+ //#endregion
411
+ //#region ../../node_modules/lucide-react/dist/esm/shared/src/utils/hasA11yProp.mjs
412
+ /**
413
+ * @license lucide-react v1.24.0 - ISC
414
+ *
415
+ * This source code is licensed under the ISC license.
416
+ * See the LICENSE file in the root directory of this source tree.
417
+ */
418
+ var hasA11yProp = (props) => {
419
+ for (const prop in props) if (prop.startsWith("aria-") || prop === "role" || prop === "title") return true;
420
+ return false;
421
+ };
422
+ //#endregion
423
+ //#region ../../node_modules/lucide-react/dist/esm/context.mjs
424
+ /**
425
+ * @license lucide-react v1.24.0 - ISC
426
+ *
427
+ * This source code is licensed under the ISC license.
428
+ * See the LICENSE file in the root directory of this source tree.
429
+ */
430
+ var LucideContext = createContext({});
431
+ var useLucideContext = () => useContext(LucideContext);
432
+ //#endregion
433
+ //#region ../../node_modules/lucide-react/dist/esm/Icon.mjs
434
+ /**
435
+ * @license lucide-react v1.24.0 - ISC
436
+ *
437
+ * This source code is licensed under the ISC license.
438
+ * See the LICENSE file in the root directory of this source tree.
439
+ */
440
+ var Icon$1 = forwardRef(({ color, size, strokeWidth, absoluteStrokeWidth, className = "", children, iconNode, ...rest }, ref) => {
441
+ const { size: contextSize = 24, strokeWidth: contextStrokeWidth = 2, absoluteStrokeWidth: contextAbsoluteStrokeWidth = false, color: contextColor = "currentColor", className: contextClass = "" } = useLucideContext() ?? {};
442
+ const calculatedStrokeWidth = absoluteStrokeWidth ?? contextAbsoluteStrokeWidth ? Number(strokeWidth ?? contextStrokeWidth) * 24 / Number(size ?? contextSize) : strokeWidth ?? contextStrokeWidth;
443
+ return createElement("svg", {
444
+ ref,
445
+ ...defaultAttributes,
446
+ width: size ?? contextSize ?? defaultAttributes.width,
447
+ height: size ?? contextSize ?? defaultAttributes.height,
448
+ stroke: color ?? contextColor,
449
+ strokeWidth: calculatedStrokeWidth,
450
+ className: mergeClasses("lucide", contextClass, className),
451
+ ...!children && !hasA11yProp(rest) && { "aria-hidden": "true" },
452
+ ...rest
453
+ }, [...iconNode.map(([tag, attrs]) => createElement(tag, attrs)), ...Array.isArray(children) ? children : [children]]);
454
+ });
455
+ //#endregion
456
+ //#region ../../node_modules/lucide-react/dist/esm/createLucideIcon.mjs
457
+ /**
458
+ * @license lucide-react v1.24.0 - ISC
459
+ *
460
+ * This source code is licensed under the ISC license.
461
+ * See the LICENSE file in the root directory of this source tree.
462
+ */
463
+ var createLucideIcon = (iconName, iconNode) => {
464
+ const Component = forwardRef(({ className, ...props }, ref) => createElement(Icon$1, {
465
+ ref,
466
+ iconNode,
467
+ className: mergeClasses(`lucide-${toKebabCase(toPascalCase(iconName))}`, `lucide-${iconName}`, className),
468
+ ...props
469
+ }));
470
+ Component.displayName = toPascalCase(iconName);
471
+ return Component;
472
+ };
473
+ /**
474
+ * @license lucide-react v1.24.0 - ISC
475
+ *
476
+ * This source code is licensed under the ISC license.
477
+ * See the LICENSE file in the root directory of this source tree.
478
+ */
479
+ var Check = createLucideIcon("check", [["path", {
480
+ d: "M20 6 9 17l-5-5",
481
+ key: "1gmf2c"
482
+ }]]);
483
+ /**
484
+ * @license lucide-react v1.24.0 - ISC
485
+ *
486
+ * This source code is licensed under the ISC license.
487
+ * See the LICENSE file in the root directory of this source tree.
488
+ */
489
+ var ChevronDown = createLucideIcon("chevron-down", [["path", {
490
+ d: "m6 9 6 6 6-6",
491
+ key: "qrunsl"
492
+ }]]);
493
+ /**
494
+ * @license lucide-react v1.24.0 - ISC
495
+ *
496
+ * This source code is licensed under the ISC license.
497
+ * See the LICENSE file in the root directory of this source tree.
498
+ */
499
+ var ChevronRight = createLucideIcon("chevron-right", [["path", {
500
+ d: "m9 18 6-6-6-6",
501
+ key: "mthhwq"
502
+ }]]);
503
+ /**
504
+ * @license lucide-react v1.24.0 - ISC
505
+ *
506
+ * This source code is licensed under the ISC license.
507
+ * See the LICENSE file in the root directory of this source tree.
508
+ */
509
+ var FileArchive = createLucideIcon("file-archive", [
510
+ ["path", {
511
+ d: "M13.659 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v11.5",
512
+ key: "4pqfef"
513
+ }],
514
+ ["path", {
515
+ d: "M14 2v5a1 1 0 0 0 1 1h5",
516
+ key: "wfsgrz"
517
+ }],
518
+ ["path", {
519
+ d: "M8 12v-1",
520
+ key: "1ej8lb"
521
+ }],
522
+ ["path", {
523
+ d: "M8 18v-2",
524
+ key: "qcmpov"
525
+ }],
526
+ ["path", {
527
+ d: "M8 7V6",
528
+ key: "1nbb54"
529
+ }],
530
+ ["circle", {
531
+ cx: "8",
532
+ cy: "20",
533
+ r: "2",
534
+ key: "ckkr5m"
535
+ }]
536
+ ]);
537
+ /**
538
+ * @license lucide-react v1.24.0 - ISC
539
+ *
540
+ * This source code is licensed under the ISC license.
541
+ * See the LICENSE file in the root directory of this source tree.
542
+ */
543
+ var FileCode = createLucideIcon("file-code", [
544
+ ["path", {
545
+ d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",
546
+ key: "1oefj6"
547
+ }],
548
+ ["path", {
549
+ d: "M14 2v5a1 1 0 0 0 1 1h5",
550
+ key: "wfsgrz"
551
+ }],
552
+ ["path", {
553
+ d: "M10 12.5 8 15l2 2.5",
554
+ key: "1tg20x"
555
+ }],
556
+ ["path", {
557
+ d: "m14 12.5 2 2.5-2 2.5",
558
+ key: "yinavb"
559
+ }]
560
+ ]);
561
+ /**
562
+ * @license lucide-react v1.24.0 - ISC
563
+ *
564
+ * This source code is licensed under the ISC license.
565
+ * See the LICENSE file in the root directory of this source tree.
566
+ */
567
+ var FileHeadphone = createLucideIcon("file-headphone", [
568
+ ["path", {
569
+ d: "M4 6.835V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2h-.343",
570
+ key: "1vfytu"
571
+ }],
572
+ ["path", {
573
+ d: "M14 2v5a1 1 0 0 0 1 1h5",
574
+ key: "wfsgrz"
575
+ }],
576
+ ["path", {
577
+ d: "M2 19a2 2 0 0 1 4 0v1a2 2 0 0 1-4 0v-4a6 6 0 0 1 12 0v4a2 2 0 0 1-4 0v-1a2 2 0 0 1 4 0",
578
+ key: "1etmh7"
579
+ }]
580
+ ]);
581
+ /**
582
+ * @license lucide-react v1.24.0 - ISC
583
+ *
584
+ * This source code is licensed under the ISC license.
585
+ * See the LICENSE file in the root directory of this source tree.
586
+ */
587
+ var FileImage = createLucideIcon("file-image", [
588
+ ["path", {
589
+ d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",
590
+ key: "1oefj6"
591
+ }],
592
+ ["path", {
593
+ d: "M14 2v5a1 1 0 0 0 1 1h5",
594
+ key: "wfsgrz"
595
+ }],
596
+ ["circle", {
597
+ cx: "10",
598
+ cy: "12",
599
+ r: "2",
600
+ key: "737tya"
601
+ }],
602
+ ["path", {
603
+ d: "m20 17-1.296-1.296a2.41 2.41 0 0 0-3.408 0L9 22",
604
+ key: "wt3hpn"
605
+ }]
606
+ ]);
607
+ /**
608
+ * @license lucide-react v1.24.0 - ISC
609
+ *
610
+ * This source code is licensed under the ISC license.
611
+ * See the LICENSE file in the root directory of this source tree.
612
+ */
613
+ var FilePlay = createLucideIcon("file-play", [
614
+ ["path", {
615
+ d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",
616
+ key: "1oefj6"
617
+ }],
618
+ ["path", {
619
+ d: "M14 2v5a1 1 0 0 0 1 1h5",
620
+ key: "wfsgrz"
621
+ }],
622
+ ["path", {
623
+ d: "M15.033 13.44a.647.647 0 0 1 0 1.12l-4.065 2.352a.645.645 0 0 1-.968-.56v-4.704a.645.645 0 0 1 .967-.56z",
624
+ key: "1tzo1f"
625
+ }]
626
+ ]);
627
+ /**
628
+ * @license lucide-react v1.24.0 - ISC
629
+ *
630
+ * This source code is licensed under the ISC license.
631
+ * See the LICENSE file in the root directory of this source tree.
632
+ */
633
+ var FileSpreadsheet = createLucideIcon("file-spreadsheet", [
634
+ ["path", {
635
+ d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",
636
+ key: "1oefj6"
637
+ }],
638
+ ["path", {
639
+ d: "M14 2v5a1 1 0 0 0 1 1h5",
640
+ key: "wfsgrz"
641
+ }],
642
+ ["path", {
643
+ d: "M8 13h2",
644
+ key: "yr2amv"
645
+ }],
646
+ ["path", {
647
+ d: "M14 13h2",
648
+ key: "un5t4a"
649
+ }],
650
+ ["path", {
651
+ d: "M8 17h2",
652
+ key: "2yhykz"
653
+ }],
654
+ ["path", {
655
+ d: "M14 17h2",
656
+ key: "10kma7"
657
+ }]
658
+ ]);
659
+ /**
660
+ * @license lucide-react v1.24.0 - ISC
661
+ *
662
+ * This source code is licensed under the ISC license.
663
+ * See the LICENSE file in the root directory of this source tree.
664
+ */
665
+ var FileText = createLucideIcon("file-text", [
666
+ ["path", {
667
+ d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",
668
+ key: "1oefj6"
669
+ }],
670
+ ["path", {
671
+ d: "M14 2v5a1 1 0 0 0 1 1h5",
672
+ key: "wfsgrz"
673
+ }],
674
+ ["path", {
675
+ d: "M10 9H8",
676
+ key: "b1mrlr"
677
+ }],
678
+ ["path", {
679
+ d: "M16 13H8",
680
+ key: "t4e002"
681
+ }],
682
+ ["path", {
683
+ d: "M16 17H8",
684
+ key: "z1uh3a"
685
+ }]
686
+ ]);
687
+ /**
688
+ * @license lucide-react v1.24.0 - ISC
689
+ *
690
+ * This source code is licensed under the ISC license.
691
+ * See the LICENSE file in the root directory of this source tree.
692
+ */
693
+ var List = createLucideIcon("list", [
694
+ ["path", {
695
+ d: "M3 5h.01",
696
+ key: "18ugdj"
697
+ }],
698
+ ["path", {
699
+ d: "M3 12h.01",
700
+ key: "nlz23k"
701
+ }],
702
+ ["path", {
703
+ d: "M3 19h.01",
704
+ key: "noohij"
705
+ }],
706
+ ["path", {
707
+ d: "M8 5h13",
708
+ key: "1pao27"
709
+ }],
710
+ ["path", {
711
+ d: "M8 12h13",
712
+ key: "1za7za"
713
+ }],
714
+ ["path", {
715
+ d: "M8 19h13",
716
+ key: "m83p4d"
717
+ }]
718
+ ]);
719
+ /**
720
+ * @license lucide-react v1.24.0 - ISC
721
+ *
722
+ * This source code is licensed under the ISC license.
723
+ * See the LICENSE file in the root directory of this source tree.
724
+ */
725
+ var LoaderCircle = createLucideIcon("loader-circle", [["path", {
726
+ d: "M21 12a9 9 0 1 1-6.219-8.56",
727
+ key: "13zald"
728
+ }]]);
729
+ /**
730
+ * @license lucide-react v1.24.0 - ISC
731
+ *
732
+ * This source code is licensed under the ISC license.
733
+ * See the LICENSE file in the root directory of this source tree.
734
+ */
735
+ var Maximize2 = createLucideIcon("maximize-2", [
736
+ ["path", {
737
+ d: "M15 3h6v6",
738
+ key: "1q9fwt"
739
+ }],
740
+ ["path", {
741
+ d: "m21 3-7 7",
742
+ key: "1l2asr"
743
+ }],
744
+ ["path", {
745
+ d: "m3 21 7-7",
746
+ key: "tjx5ai"
747
+ }],
748
+ ["path", {
749
+ d: "M9 21H3v-6",
750
+ key: "wtvkvv"
751
+ }]
752
+ ]);
753
+ /**
754
+ * @license lucide-react v1.24.0 - ISC
755
+ *
756
+ * This source code is licensed under the ISC license.
757
+ * See the LICENSE file in the root directory of this source tree.
758
+ */
759
+ var MessageCircle = createLucideIcon("message-circle", [["path", {
760
+ d: "M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719",
761
+ key: "1sd12s"
762
+ }]]);
763
+ /**
764
+ * @license lucide-react v1.24.0 - ISC
765
+ *
766
+ * This source code is licensed under the ISC license.
767
+ * See the LICENSE file in the root directory of this source tree.
768
+ */
769
+ var MessagesSquare = createLucideIcon("messages-square", [["path", {
770
+ d: "M16 10a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 14.286V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z",
771
+ key: "1n2ejm"
772
+ }], ["path", {
773
+ d: "M20 9a2 2 0 0 1 2 2v10.286a.71.71 0 0 1-1.212.502l-2.202-2.202A2 2 0 0 0 17.172 19H10a2 2 0 0 1-2-2v-1",
774
+ key: "1qfcsi"
775
+ }]]);
776
+ /**
777
+ * @license lucide-react v1.24.0 - ISC
778
+ *
779
+ * This source code is licensed under the ISC license.
780
+ * See the LICENSE file in the root directory of this source tree.
781
+ */
782
+ var Minimize2 = createLucideIcon("minimize-2", [
783
+ ["path", {
784
+ d: "m14 10 7-7",
785
+ key: "oa77jy"
786
+ }],
787
+ ["path", {
788
+ d: "M20 10h-6V4",
789
+ key: "mjg0md"
790
+ }],
791
+ ["path", {
792
+ d: "m3 21 7-7",
793
+ key: "tjx5ai"
794
+ }],
795
+ ["path", {
796
+ d: "M4 14h6v6",
797
+ key: "rmj7iw"
798
+ }]
799
+ ]);
800
+ /**
801
+ * @license lucide-react v1.24.0 - ISC
802
+ *
803
+ * This source code is licensed under the ISC license.
804
+ * See the LICENSE file in the root directory of this source tree.
805
+ */
806
+ var Paperclip = createLucideIcon("paperclip", [["path", {
807
+ d: "m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551",
808
+ key: "1miecu"
809
+ }]]);
810
+ /**
811
+ * @license lucide-react v1.24.0 - ISC
812
+ *
813
+ * This source code is licensed under the ISC license.
814
+ * See the LICENSE file in the root directory of this source tree.
815
+ */
816
+ var Plus = createLucideIcon("plus", [["path", {
817
+ d: "M5 12h14",
818
+ key: "1ays0h"
819
+ }], ["path", {
820
+ d: "M12 5v14",
821
+ key: "s699le"
822
+ }]]);
823
+ /**
824
+ * @license lucide-react v1.24.0 - ISC
825
+ *
826
+ * This source code is licensed under the ISC license.
827
+ * See the LICENSE file in the root directory of this source tree.
828
+ */
829
+ var RotateCcw = createLucideIcon("rotate-ccw", [["path", {
830
+ d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",
831
+ key: "1357e3"
832
+ }], ["path", {
833
+ d: "M3 3v5h5",
834
+ key: "1xhq8a"
835
+ }]]);
836
+ /**
837
+ * @license lucide-react v1.24.0 - ISC
838
+ *
839
+ * This source code is licensed under the ISC license.
840
+ * See the LICENSE file in the root directory of this source tree.
841
+ */
842
+ var Send = createLucideIcon("send", [["path", {
843
+ d: "M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z",
844
+ key: "1ffxy3"
845
+ }], ["path", {
846
+ d: "m21.854 2.147-10.94 10.939",
847
+ key: "12cjpa"
848
+ }]]);
849
+ /**
850
+ * @license lucide-react v1.24.0 - ISC
851
+ *
852
+ * This source code is licensed under the ISC license.
853
+ * See the LICENSE file in the root directory of this source tree.
854
+ */
855
+ var Square = createLucideIcon("square", [["rect", {
856
+ width: "18",
857
+ height: "18",
858
+ x: "3",
859
+ y: "3",
860
+ rx: "2",
861
+ key: "afitv7"
862
+ }]]);
863
+ /**
864
+ * @license lucide-react v1.24.0 - ISC
865
+ *
866
+ * This source code is licensed under the ISC license.
867
+ * See the LICENSE file in the root directory of this source tree.
868
+ */
869
+ var Upload = createLucideIcon("upload", [
870
+ ["path", {
871
+ d: "M12 3v12",
872
+ key: "1x0j5s"
873
+ }],
874
+ ["path", {
875
+ d: "m17 8-5-5-5 5",
876
+ key: "7q97r8"
877
+ }],
878
+ ["path", {
879
+ d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",
880
+ key: "ih7n3h"
881
+ }]
882
+ ]);
883
+ /**
884
+ * @license lucide-react v1.24.0 - ISC
885
+ *
886
+ * This source code is licensed under the ISC license.
887
+ * See the LICENSE file in the root directory of this source tree.
888
+ */
889
+ var X = createLucideIcon("x", [["path", {
890
+ d: "M18 6 6 18",
891
+ key: "1bl5f8"
892
+ }], ["path", {
893
+ d: "m6 6 12 12",
894
+ key: "d8bk6v"
895
+ }]]);
896
+ //#endregion
897
+ //#region src/ui/sse.ts
898
+ async function* readSse(body) {
899
+ const reader = body.getReader();
900
+ const decoder = new TextDecoder();
901
+ let buf = "";
902
+ for (;;) {
903
+ const { done, value } = await reader.read();
904
+ if (done) break;
905
+ buf += decoder.decode(value, { stream: true });
906
+ let idx;
907
+ while ((idx = buf.indexOf("\n\n")) !== -1) {
908
+ const frame = buf.slice(0, idx);
909
+ buf = buf.slice(idx + 2);
910
+ let event = "message";
911
+ const data = [];
912
+ for (const line of frame.split("\n")) {
913
+ if (!line || line.startsWith(":")) continue;
914
+ if (line.startsWith("event:")) event = line.slice(6).trim();
915
+ else if (line.startsWith("data:")) data.push(line.slice(5).replace(/^ /, ""));
916
+ }
917
+ if (data.length) yield {
918
+ event,
919
+ data: data.join("\n")
920
+ };
921
+ }
922
+ }
923
+ }
924
+ //#endregion
925
+ //#region src/ui/chat-api.ts
926
+ /** Web-chat transport. Session cookies are sent automatically (same-origin), so there is no
927
+ * separate authorization here; the gate is on the server. */
928
+ var BASE = "/api/plugins/webchat/user";
929
+ /** POST a member-level webchat action with a JSON body: the one request path every call
930
+ * in this file (except the SSE stream in `openChannel` and `stopTurn`'s empty-body POST)
931
+ * goes through. Throws `Error(action)` on any non-OK response — every caller below relies
932
+ * on that to build its own error message. */
933
+ async function postAction(action, body) {
934
+ const r = await fetch(`${BASE}/${action}`, {
935
+ method: "POST",
936
+ headers: { "content-type": "application/json" },
937
+ body: JSON.stringify(body)
938
+ });
939
+ if (!r.ok) throw new Error(action);
940
+ return await r.json();
941
+ }
942
+ /**
943
+ * Throws on any rejection, including 404 (“plugin was disabled AFTER the widget bundle
944
+ * loaded”). There is deliberately no separate `null` for this case: the caller keeps
945
+ * the current thread list either way, so two return codes would mean the same thing.
946
+ */
947
+ async function fetchThreads() {
948
+ return (await postAction("threads", {})).threads;
949
+ }
950
+ async function fetchHistory(convId) {
951
+ return postAction("history", { convId });
952
+ }
953
+ async function fetchAgents(convId) {
954
+ const out = await postAction("agents", { convId });
955
+ return {
956
+ agents: out.agents ?? [],
957
+ defaultAgentId: out.defaultAgentId ?? null,
958
+ selection: out.selection ?? {
959
+ agentId: null,
960
+ presetId: null
961
+ },
962
+ starters: out.starters ?? []
963
+ };
964
+ }
965
+ async function selectAgent(convId, patch) {
966
+ return (await postAction("selectAgent", {
967
+ convId,
968
+ ...patch
969
+ })).selection;
970
+ }
971
+ /** Mark a conversation private, or reopen it. Only ever meaningful for the
972
+ * caller's OWN conversation — the server refuses (returns the unchanged
973
+ * visibility) for anything else, so the UI only offers this control on the
974
+ * viewer's own rows in the first place. */
975
+ async function setThreadVisibility(convId, makePrivate) {
976
+ return postAction("setVisibility", {
977
+ convId,
978
+ private: makePrivate
979
+ });
980
+ }
981
+ /** Records the user turn and starts the agent turn running — an ordinary POST that
982
+ * resolves as soon as the turn has STARTED, not once it finishes (`sendAction`,
983
+ * `../runtime/send.ts`). It streams nothing any more: every live update — the
984
+ * answer as it is written, its completion, an error — arrives on the
985
+ * conversation's own channel (`openChannel`, below), which `use-chat.ts` keeps
986
+ * open for as long as this conversation is on screen, whether or not THIS call
987
+ * is what started the turn currently running. */
988
+ async function sendMessage(req) {
989
+ return postAction("send", {
990
+ convId: req.convId,
991
+ text: req.text,
992
+ replyTo: req.replyTo,
993
+ context: req.context,
994
+ ...req.attachments?.length ? { attachments: req.attachments } : {}
995
+ });
996
+ }
997
+ /** Abort the turn currently running on `convId`, if there is one (`stopAction`,
998
+ * `../runtime/actions.ts`). That action returns `Promise<void>` — the route sends
999
+ * an empty 200 body, the same uniform non-answer whether a live turn was
1000
+ * actually aborted, none was running, or the caller may not write to the
1001
+ * conversation — so this deliberately does NOT go through `postAction`, whose
1002
+ * `.json()` would throw on that empty body. */
1003
+ async function stopTurn(convId) {
1004
+ if (!(await fetch(`${BASE}/stop`, {
1005
+ method: "POST",
1006
+ headers: { "content-type": "application/json" },
1007
+ body: JSON.stringify({ convId })
1008
+ })).ok) throw new Error("stop");
1009
+ }
1010
+ /** Subscribe to `convId`'s channel and hold the connection open, delivering every
1011
+ * frame to `handlers` until the server ends the stream (the conversation went
1012
+ * private on this viewer, `setVisibility`'s eviction; a restart; a network drop)
1013
+ * or `signal` aborts it. Resolves (does not reject) when the server ends the
1014
+ * stream cleanly, same as a plain fetch whose body simply runs out — rejects only
1015
+ * on a genuine transport failure or `signal` aborting. Either way it is
1016
+ * `use-chat.ts`'s job to reconnect with backoff while the conversation is still
1017
+ * the one on screen: there is no `Last-Event-ID` bookkeeping and no server-side
1018
+ * buffer here, by design (see the design doc's "Reconnect, and the race that
1019
+ * comes with it") — missed frames are recovered by re-reading history, never by
1020
+ * replaying the stream. */
1021
+ async function openChannel(convId, handlers, signal) {
1022
+ const r = await fetch(`${BASE}/channel/stream`, {
1023
+ method: "POST",
1024
+ headers: { "content-type": "application/json" },
1025
+ body: JSON.stringify({ convId }),
1026
+ signal
1027
+ });
1028
+ if (!r.ok || !r.body) throw new Error(`http ${r.status}`);
1029
+ for await (const frame of readSse(r.body)) try {
1030
+ const data = JSON.parse(frame.data);
1031
+ if (frame.event === "user") handlers.onUser({
1032
+ msgId: String(data["msgId"] ?? ""),
1033
+ text: String(data["text"] ?? ""),
1034
+ ts: typeof data["ts"] === "number" ? data["ts"] : 0,
1035
+ ...Array.isArray(data["attachments"]) ? { attachments: data["attachments"] } : {}
1036
+ });
1037
+ else if (frame.event === "message") handlers.onMessage({
1038
+ msgId: String(data["msgId"] ?? ""),
1039
+ text: String(data["text"] ?? "")
1040
+ });
1041
+ else if (frame.event === "delta") handlers.onDelta({
1042
+ msgId: String(data["msgId"] ?? ""),
1043
+ text: String(data["text"] ?? "")
1044
+ });
1045
+ else if (frame.event === "reasoning") handlers.onReasoning({
1046
+ msgId: String(data["msgId"] ?? ""),
1047
+ text: String(data["text"] ?? "")
1048
+ });
1049
+ else if (frame.event === "done") handlers.onDone({
1050
+ msgId: data["msgId"] ?? null,
1051
+ parentMsgId: String(data["parentMsgId"] ?? ""),
1052
+ suggestions: Array.isArray(data["suggestions"]) ? data["suggestions"] : null
1053
+ });
1054
+ else if (frame.event === "error") handlers.onError({
1055
+ message: data["message"] ?? null,
1056
+ parentMsgId: typeof data["parentMsgId"] === "string" ? data["parentMsgId"] : null
1057
+ });
1058
+ } catch (e) {
1059
+ handlers.onError({
1060
+ message: `malformed frame: ${String(e)}`,
1061
+ parentMsgId: null
1062
+ });
1063
+ }
1064
+ }
1065
+ //#endregion
1066
+ //#region src/ui/use-chat.ts
1067
+ function newId() {
1068
+ return crypto.randomUUID();
1069
+ }
1070
+ /** The message id a frame reports as its OUTCOME, for dedup against history on
1071
+ * flush. `error` is never persisted (a failed turn stores nothing), so it is
1072
+ * never a duplicate of anything a history read could carry — its
1073
+ * `parentMsgId` exists only to scope the send-lock clear below to the
1074
+ * viewer's own turn, a different question from dedup. */
1075
+ function frameHistoryId(frame) {
1076
+ switch (frame.kind) {
1077
+ case "user":
1078
+ case "message":
1079
+ case "delta":
1080
+ case "reasoning": return frame.data.msgId;
1081
+ case "done": return frame.data.msgId;
1082
+ case "error": return null;
1083
+ }
1084
+ }
1085
+ var RECONNECT_BASE_MS = 1e3;
1086
+ var RECONNECT_MAX_MS = 3e4;
1087
+ function useChat() {
1088
+ const [threads, setThreads] = useState([]);
1089
+ const [convId, setConvId] = useState(null);
1090
+ const [messages, setMessages] = useState([]);
1091
+ const [pending, setPending] = useState(null);
1092
+ const [pendingReasoning, setPendingReasoning] = useState(null);
1093
+ const [error, setError] = useState(null);
1094
+ const [agents, setAgents] = useState([]);
1095
+ const [defaultAgentId, setDefaultAgentId] = useState(null);
1096
+ const [selection, setSelection] = useState({
1097
+ agentId: null,
1098
+ presetId: null
1099
+ });
1100
+ const [starters, setStarters] = useState([]);
1101
+ const [suggestions, setSuggestions] = useState(null);
1102
+ const headRef = useRef(null);
1103
+ const sendingRef = useRef(false);
1104
+ const myTurnMsgIdRef = useRef(null);
1105
+ const pendingTextRef = useRef("");
1106
+ const pendingReasoningRef = useRef(null);
1107
+ const turnRef = useRef(0);
1108
+ const aliveRef = useRef(true);
1109
+ useEffect(() => {
1110
+ aliveRef.current = true;
1111
+ return () => {
1112
+ aliveRef.current = false;
1113
+ turnRef.current++;
1114
+ };
1115
+ }, []);
1116
+ const loadThreads = useCallback(() => {
1117
+ fetchThreads().then((list) => {
1118
+ if (aliveRef.current) setThreads(list);
1119
+ }).catch(() => {});
1120
+ }, []);
1121
+ useEffect(() => {
1122
+ if (!convId) return;
1123
+ let cancelled = false;
1124
+ fetchAgents(convId).then((r) => {
1125
+ if (cancelled) return;
1126
+ setAgents(r.agents);
1127
+ setDefaultAgentId(r.defaultAgentId);
1128
+ setSelection(r.selection);
1129
+ setStarters(r.starters);
1130
+ }).catch(() => {});
1131
+ return () => {
1132
+ cancelled = true;
1133
+ };
1134
+ }, [convId]);
1135
+ /** Apply one channel frame to state. Called either live (as it arrives, once a
1136
+ * connection has finished its history flush) or from the buffer a fresh
1137
+ * (re)connect built while history was in flight — `historyIds` is non-null
1138
+ * ONLY for the latter, and gates every frame on it: a frame whose outcome the
1139
+ * just-fetched history already carries is dropped rather than reapplied, the
1140
+ * one piece of ordering the design doc calls out by name. Live frames never
1141
+ * need this check — their message id cannot already be in a history snapshot
1142
+ * read before they existed. */
1143
+ const applyFrame = useCallback((frame, historyIds) => {
1144
+ const outcomeId = frameHistoryId(frame);
1145
+ const isDuplicate = historyIds !== null && outcomeId !== null && historyIds.has(outcomeId);
1146
+ switch (frame.kind) {
1147
+ case "user": {
1148
+ if (isDuplicate) break;
1149
+ const { msgId, text, ts, attachments } = frame.data;
1150
+ setMessages((prev) => prev.some((m) => m.msgId === msgId) ? prev : [...prev, {
1151
+ msgId,
1152
+ role: "user",
1153
+ text,
1154
+ ts,
1155
+ ...attachments?.length ? { attachments } : {}
1156
+ }]);
1157
+ break;
1158
+ }
1159
+ case "message":
1160
+ case "delta":
1161
+ if (isDuplicate) break;
1162
+ pendingTextRef.current = frame.data.text;
1163
+ setPending(frame.data.text);
1164
+ break;
1165
+ case "reasoning":
1166
+ if (isDuplicate) break;
1167
+ pendingReasoningRef.current = frame.data.text;
1168
+ setPendingReasoning(frame.data.text);
1169
+ break;
1170
+ case "done": {
1171
+ const { msgId, parentMsgId, suggestions: nextSuggestions } = frame.data;
1172
+ headRef.current = msgId ?? parentMsgId;
1173
+ setSuggestions(nextSuggestions ?? null);
1174
+ if (!isDuplicate && msgId && pendingTextRef.current) {
1175
+ const text = pendingTextRef.current;
1176
+ const reasoning = pendingReasoningRef.current;
1177
+ setMessages((prev) => prev.some((m) => m.msgId === msgId) ? prev : [...prev, {
1178
+ msgId,
1179
+ role: "assistant",
1180
+ text,
1181
+ ts: Math.floor(Date.now() / 1e3),
1182
+ ...reasoning ? { reasoning } : {}
1183
+ }]);
1184
+ }
1185
+ pendingTextRef.current = "";
1186
+ pendingReasoningRef.current = null;
1187
+ setPending(null);
1188
+ setPendingReasoning(null);
1189
+ if (myTurnMsgIdRef.current !== null && myTurnMsgIdRef.current === parentMsgId) {
1190
+ sendingRef.current = false;
1191
+ myTurnMsgIdRef.current = null;
1192
+ }
1193
+ loadThreads();
1194
+ break;
1195
+ }
1196
+ case "error":
1197
+ setError(frame.data.message ?? "error");
1198
+ pendingTextRef.current = "";
1199
+ pendingReasoningRef.current = null;
1200
+ setPending(null);
1201
+ setPendingReasoning(null);
1202
+ if (myTurnMsgIdRef.current !== null && myTurnMsgIdRef.current === frame.data.parentMsgId) {
1203
+ sendingRef.current = false;
1204
+ myTurnMsgIdRef.current = null;
1205
+ }
1206
+ break;
1207
+ }
1208
+ }, [loadThreads]);
1209
+ useEffect(() => {
1210
+ if (!convId) return;
1211
+ const chatConvId = convId;
1212
+ let cancelled = false;
1213
+ let reconnectTimer = null;
1214
+ let attempt = 0;
1215
+ const ctl = new AbortController();
1216
+ async function connectOnce() {
1217
+ if (cancelled) return;
1218
+ let buffering = true;
1219
+ let receivedFrame = false;
1220
+ const buffer = [];
1221
+ const dispatch = (frame) => {
1222
+ if (cancelled) return;
1223
+ receivedFrame = true;
1224
+ if (buffering) buffer.push(frame);
1225
+ else applyFrame(frame, null);
1226
+ };
1227
+ const settled = openChannel(chatConvId, {
1228
+ onUser: (data) => dispatch({
1229
+ kind: "user",
1230
+ data
1231
+ }),
1232
+ onMessage: (data) => dispatch({
1233
+ kind: "message",
1234
+ data
1235
+ }),
1236
+ onDelta: (data) => dispatch({
1237
+ kind: "delta",
1238
+ data
1239
+ }),
1240
+ onReasoning: (data) => dispatch({
1241
+ kind: "reasoning",
1242
+ data
1243
+ }),
1244
+ onDone: (data) => dispatch({
1245
+ kind: "done",
1246
+ data
1247
+ }),
1248
+ onError: (data) => dispatch({
1249
+ kind: "error",
1250
+ data
1251
+ })
1252
+ }, ctl.signal).then(() => true, () => false);
1253
+ try {
1254
+ const h = await fetchHistory(chatConvId);
1255
+ if (cancelled) return;
1256
+ headRef.current = h.headMsgId;
1257
+ const historyIds = new Set(h.messages.map((m) => m.msgId));
1258
+ setMessages((prev) => {
1259
+ const ownPendingId = myTurnMsgIdRef.current;
1260
+ const stillUnconfirmed = prev.filter((m) => !historyIds.has(m.msgId) && (m.msgId.startsWith("local-") || m.msgId === ownPendingId));
1261
+ return stillUnconfirmed.length ? [...h.messages, ...stillUnconfirmed] : h.messages;
1262
+ });
1263
+ setSuggestions(h.messages.at(-1)?.suggestions ?? null);
1264
+ buffering = false;
1265
+ for (const frame of buffer) applyFrame(frame, historyIds);
1266
+ } catch (e) {
1267
+ if (cancelled) return;
1268
+ setError(e.message);
1269
+ buffering = false;
1270
+ for (const frame of buffer) applyFrame(frame, null);
1271
+ }
1272
+ await settled;
1273
+ if (cancelled) return;
1274
+ if (receivedFrame) attempt = 0;
1275
+ const delay = Math.min(RECONNECT_BASE_MS * 2 ** attempt, RECONNECT_MAX_MS);
1276
+ attempt++;
1277
+ reconnectTimer = setTimeout(() => void connectOnce(), delay);
1278
+ }
1279
+ connectOnce();
1280
+ return () => {
1281
+ cancelled = true;
1282
+ if (reconnectTimer) clearTimeout(reconnectTimer);
1283
+ ctl.abort();
1284
+ };
1285
+ }, [convId, applyFrame]);
1286
+ const chooseAgent = useCallback((next) => {
1287
+ const previous = selection;
1288
+ setSelection(next);
1289
+ if (convId) selectAgent(convId, next).then(setSelection).catch((err) => {
1290
+ setSelection(previous);
1291
+ setError(err instanceof Error ? err.message : String(err));
1292
+ });
1293
+ }, [convId, selection]);
1294
+ const togglePrivate = useCallback((convId, next) => {
1295
+ const previous = threads.find((t) => t.convId === convId)?.private ?? false;
1296
+ setThreads((prev) => prev.map((t) => t.convId === convId ? {
1297
+ ...t,
1298
+ private: next
1299
+ } : t));
1300
+ setThreadVisibility(convId, next).then((r) => {
1301
+ setThreads((prev) => prev.map((t) => t.convId === convId ? {
1302
+ ...t,
1303
+ private: r.visibility === "private"
1304
+ } : t));
1305
+ }).catch(() => {
1306
+ setThreads((prev) => prev.map((t) => t.convId === convId ? {
1307
+ ...t,
1308
+ private: previous
1309
+ } : t));
1310
+ });
1311
+ }, [threads]);
1312
+ const attachmentsAllowed = useMemo(() => {
1313
+ const media = agents.find((a) => a.id === selection.agentId)?.media;
1314
+ if (!media) return true;
1315
+ return media.image.accepts.length > 0 || media.document.accepts.length > 0;
1316
+ }, [agents, selection]);
1317
+ const newConversation = useCallback(() => {
1318
+ turnRef.current++;
1319
+ sendingRef.current = false;
1320
+ myTurnMsgIdRef.current = null;
1321
+ headRef.current = null;
1322
+ setConvId(newId());
1323
+ setMessages([]);
1324
+ setPending(null);
1325
+ setPendingReasoning(null);
1326
+ pendingTextRef.current = "";
1327
+ pendingReasoningRef.current = null;
1328
+ setError(null);
1329
+ setSuggestions(null);
1330
+ }, []);
1331
+ const open = useCallback(async (id) => {
1332
+ turnRef.current++;
1333
+ sendingRef.current = false;
1334
+ myTurnMsgIdRef.current = null;
1335
+ setConvId(id);
1336
+ headRef.current = null;
1337
+ setMessages([]);
1338
+ setPending(null);
1339
+ setPendingReasoning(null);
1340
+ pendingTextRef.current = "";
1341
+ pendingReasoningRef.current = null;
1342
+ setError(null);
1343
+ setSuggestions(null);
1344
+ }, []);
1345
+ const stop = useCallback(() => {
1346
+ if (!convId) return;
1347
+ stopTurn(convId).catch(() => {});
1348
+ }, [convId]);
1349
+ return {
1350
+ threads,
1351
+ convId,
1352
+ messages,
1353
+ pending,
1354
+ pendingReasoning,
1355
+ error,
1356
+ agents,
1357
+ defaultAgentId,
1358
+ selection,
1359
+ starters,
1360
+ suggestions,
1361
+ attachmentsAllowed,
1362
+ chooseAgent,
1363
+ togglePrivate,
1364
+ send: useCallback((text, context, attachments = []) => {
1365
+ if (!text.trim() || sendingRef.current) return false;
1366
+ sendingRef.current = true;
1367
+ const myTurn = turnRef.current;
1368
+ const id = convId ?? newId();
1369
+ if (convId === null) setConvId(id);
1370
+ setError(null);
1371
+ setSuggestions(null);
1372
+ const localMsgId = `local-${newId()}`;
1373
+ setMessages((prev) => [...prev, {
1374
+ msgId: localMsgId,
1375
+ role: "user",
1376
+ text,
1377
+ ts: Math.floor(Date.now() / 1e3),
1378
+ ...attachments.length ? { attachments } : {}
1379
+ }]);
1380
+ setPending("");
1381
+ (async () => {
1382
+ try {
1383
+ const { msgId } = await sendMessage({
1384
+ convId: id,
1385
+ text,
1386
+ replyTo: headRef.current,
1387
+ context,
1388
+ attachments
1389
+ });
1390
+ if (turnRef.current !== myTurn) return;
1391
+ if (msgId === null) {
1392
+ sendingRef.current = false;
1393
+ setPending(null);
1394
+ setPendingReasoning(null);
1395
+ return;
1396
+ }
1397
+ setMessages((prev) => prev.some((m) => m.msgId === msgId) ? prev.filter((m) => m.msgId !== localMsgId) : prev.map((m) => m.msgId === localMsgId ? {
1398
+ ...m,
1399
+ msgId
1400
+ } : m));
1401
+ myTurnMsgIdRef.current = msgId;
1402
+ } catch (e) {
1403
+ if (turnRef.current === myTurn) {
1404
+ sendingRef.current = false;
1405
+ setError(e.message);
1406
+ setPending(null);
1407
+ setPendingReasoning(null);
1408
+ }
1409
+ }
1410
+ })();
1411
+ return true;
1412
+ }, [convId]),
1413
+ open,
1414
+ newConversation,
1415
+ loadThreads,
1416
+ stop
1417
+ };
1418
+ }
1419
+ //#endregion
1420
+ //#region src/ui/use-page-context.ts
1421
+ /** The record page's own heading — a BEST-EFFORT read of the app's own rendered DOM,
1422
+ * not a data path. `RecordContent` (`packages/web/display/index.tsx`) marks the
1423
+ * TITLE element itself `role="heading" aria-level="1"` (the role sits on the title
1424
+ * element alone, never a wrapper — a wrapping row's role would pull every sibling
1425
+ * body field's text into the name, and the title is now one field among many in the
1426
+ * body); today that is the only element in the whole app carrying it,
1427
+ * so this selector cannot pick up some OTHER record's heading by mistake. It exists at
1428
+ * all because the chat overlay is a sibling of the routed page (mounted in the app
1429
+ * shell's own slot), never a descendant of it, so there is no React tree — no prop, no
1430
+ * context — to read the value from, and this package (`plugin-webchat`) has no data
1431
+ * path of its own to the open record either. Reading the painted DOM instead of
1432
+ * fetching the record: the SPA already has it open and displaying this text, and a
1433
+ * server-side lookup would be a query per turn for a value the browser is holding.
1434
+ *
1435
+ * WHEN it is read is the whole safety argument, so it is stated rather than assumed:
1436
+ * reading the DOM during React's RENDER phase sees the PREVIOUSLY COMMITTED tree, so a
1437
+ * navigation from one record to another produced the new path and id beside the OLD
1438
+ * record's name — a fact stated WRONG, which the base prompt tells the model to trust.
1439
+ * Hence `usePageContext` returns a READER the composer calls in its send handler: an
1440
+ * event handler runs after commit, so the heading and the path it is reported with
1441
+ * come from the same painted page. That is also what the design asks for — the page
1442
+ * the user was on AT THE MOMENT OF SENDING, and nowhere else.
1443
+ *
1444
+ * What is guaranteed, then: the title is read from the DOM as it stands when the turn
1445
+ * is sent. What is NOT guaranteed is that the title is present — a heading not yet
1446
+ * painted (a record still loading) and an untitled record's Dash placeholder both
1447
+ * yield no title, and an absent fact is the harmless case. One case yields a WRONG
1448
+ * one: if the app ever grows a SECOND element with this exact role/level (a preview
1449
+ * dialog, a relation picker, a plugin's own record renderer), `querySelector` returns
1450
+ * whichever comes first in document order, not necessarily the record the user is
1451
+ * actually looking at. Whoever eventually gives the overlay a real data path to the
1452
+ * open record should read this as the reason it was ever DOM-based, not rediscover it. */
1453
+ var EMPTY_TITLE = "—";
1454
+ function recordHeading() {
1455
+ if (typeof document === "undefined") return void 0;
1456
+ const text = document.querySelector("[role=\"heading\"][aria-level=\"1\"]")?.textContent?.trim();
1457
+ return text && text !== EMPTY_TITLE ? text : void 0;
1458
+ }
1459
+ /** SPA routes: `/:library/:type[/:id]`; system branches (settings, browse) are
1460
+ * returned by path only. Pure but for the heading read, which is why it takes the
1461
+ * path rather than calling the router itself. */
1462
+ function pageContextAt(pathname) {
1463
+ const parts = pathname.split("/").filter(Boolean);
1464
+ const system = /* @__PURE__ */ new Set([
1465
+ "settings",
1466
+ "browse",
1467
+ "plugins",
1468
+ "login",
1469
+ "setup"
1470
+ ]);
1471
+ if (parts.length < 2 || system.has(parts[0])) return { path: pathname };
1472
+ const [library, type, id] = parts;
1473
+ const isRecord = Boolean(id && id !== "new");
1474
+ const title = isRecord ? recordHeading() : void 0;
1475
+ return {
1476
+ path: pathname,
1477
+ library,
1478
+ type,
1479
+ ...isRecord ? { id } : {},
1480
+ ...title ? { title } : {}
1481
+ };
1482
+ }
1483
+ /** Current-page context for the agent, as a reader called AT SEND TIME rather than a
1484
+ * value computed while rendering — see the heading comment above for why that
1485
+ * distinction is the difference between an absent title and a wrong one. The path
1486
+ * comes from the render that is currently committed, which is the same tree the
1487
+ * reader's heading lookup walks. */
1488
+ function usePageContext() {
1489
+ const { pathname } = useLocation();
1490
+ return useCallback(() => pageContextAt(pathname), [pathname]);
1491
+ }
1492
+ //#endregion
1493
+ //#region src/ui/chat-nav.tsx
1494
+ /** What to do with the widget when the user follows a link from a response.
1495
+ * ChatWidget supplies the callback (it owns mode/open), avoiding a prop
1496
+ * threaded through ChatMessages and Bubble. */
1497
+ var ChatNavContext = createContext(() => {});
1498
+ function ChatNavProvider({ onNavigate, children }) {
1499
+ return /* @__PURE__ */ jsx(ChatNavContext.Provider, {
1500
+ value: onNavigate,
1501
+ children
1502
+ });
1503
+ }
1504
+ function useChatNav() {
1505
+ return useContext(ChatNavContext);
1506
+ }
1507
+ //#endregion
1508
+ //#region src/ui/chat-markdown.tsx
1509
+ /** Render markdown received FROM THE MODEL, an untrusted source.
1510
+ *
1511
+ * One renderer serves every markdown surface now — the record page and this bubble alike —
1512
+ * so this component is a density layer over the kit's `MarkdownView` and nothing more. What
1513
+ * it used to own itself now lives there, for both surfaces:
1514
+ *
1515
+ * - raw HTML never becomes markup. The vocabulary renderer has NO raw-HTML pass, so a tag
1516
+ * the model wrote is escaped before anything sees it; the barrier react-markdown gave
1517
+ * structurally is still structural, just one layer earlier. DOMPurify is defence in depth.
1518
+ * - an internal link goes through the router, so the chat stays mounted, and `onNavigate`
1519
+ * closes the panel on the way out.
1520
+ * - an address that leads nowhere (`javascript:`, a fabricated shelf) keeps its text and
1521
+ * loses its anchor — this bubble's old rule, now everyone's.
1522
+ * - a table gets its scroll wrapper from the web tag map itself.
1523
+ */
1524
+ function ChatMarkdown({ text, className }) {
1525
+ const onNavigate = useChatNav();
1526
+ return /* @__PURE__ */ jsx(MarkdownView, {
1527
+ value: text,
1528
+ className: className ?? "md-static md-chat",
1529
+ onNavigate
1530
+ });
1531
+ }
1532
+ //#endregion
1533
+ //#region src/ui/ChatAttachments.tsx
1534
+ /**
1535
+ * The ONE presentation of a chat attachment — the files staged in the composer and the files a
1536
+ * sent turn carries are drawn by this component, so the two cannot drift apart.
1537
+ *
1538
+ * Three decisions are worth stating, because each replaces something that was there before:
1539
+ *
1540
+ * - **A file is a ROW, not a word.** The bubble used to print `a.label ?? a.name` as bare text
1541
+ * in a wrapping flex, which for eight uploads meant eight lines of 40-character storage names
1542
+ * running to the bubble's edge and clipping mid-name. A row states the three facts a file has
1543
+ * — what kind it is, what it is called, how big it is — and states them in the same places
1544
+ * every time, which is what makes a stack of them scannable.
1545
+ * - **The name breaks in the MIDDLE, not at the end.** The extension is the most informative
1546
+ * part of a long filename and the part a tail-truncation always eats. The stem takes the
1547
+ * truncation (`truncate`), the extension is pinned beside it (`shrink-0`), so `…_2026.pdf`
1548
+ * keeps saying `.pdf` at any width. CSS does this, not a character budget — the same row
1549
+ * works in a 380px panel and in the 1040px modal without measuring anything.
1550
+ * - **Past two files the group COLLAPSES.** A turn carrying eight PDFs is a turn whose text a
1551
+ * reader still has to reach; the attachments are its luggage, not its content. Two rows say
1552
+ * what was sent, `+N` says how much more there is, and one click shows the rest.
1553
+ *
1554
+ * The rows are inert on purpose: an attachment names itself and does nothing else. Opening one
1555
+ * would be a second file-viewing surface beside the record page's, and the chat is not where a
1556
+ * file is read (decided 2026-09-15).
1557
+ */
1558
+ /** How many rows stand before the rest fold away. Two is what a bubble can carry without the
1559
+ * luggage outweighing the message — see the component's note. */
1560
+ var COLLAPSE_AFTER = 2;
1561
+ /** Extension families, in the order a name is tested against them. */
1562
+ var BY_EXT = [
1563
+ [/\.(png|jpe?g|gif|webp|avif|svg|bmp|heic|heif)$/i, FileImage],
1564
+ [/\.(mp3|wav|ogg|oga|flac|m4a|aac|opus)$/i, FileHeadphone],
1565
+ [/\.(mp4|mov|mkv|webm|avi|m4v)$/i, FilePlay],
1566
+ [/\.(zip|rar|7z|tar|gz|tgz|bz2|xz)$/i, FileArchive],
1567
+ [/\.(csv|tsv|xlsx?|ods|numbers)$/i, FileSpreadsheet],
1568
+ [/\.(json|xml|ya?ml|toml|ini|js|jsx|ts|tsx|css|html?|sh|py|sql)$/i, FileCode]
1569
+ ];
1570
+ /**
1571
+ * What KIND of file this is, at a glance. The mime type is the server's answer and is trusted
1572
+ * first; an upload that arrives with none (or with the `application/octet-stream` a browser
1573
+ * falls back to) is read from its extension instead, which is the only fact left.
1574
+ */
1575
+ function attachmentIcon(a) {
1576
+ const mime = a.mime?.toLowerCase().split(";", 1)[0] ?? "";
1577
+ if (mime.startsWith("image/")) return FileImage;
1578
+ if (mime.startsWith("audio/")) return FileHeadphone;
1579
+ if (mime.startsWith("video/")) return FilePlay;
1580
+ if (mime === "text/csv" || mime.includes("spreadsheet") || mime.includes("excel")) return FileSpreadsheet;
1581
+ if (mime === "application/json" || mime === "application/xml" || mime.startsWith("text/x")) return FileCode;
1582
+ if (mime === "application/zip" || mime.includes("compressed") || mime.includes("tar")) return FileArchive;
1583
+ const name = a.label ?? a.name;
1584
+ for (const [re, icon] of BY_EXT) if (re.test(name)) return icon;
1585
+ return FileText;
1586
+ }
1587
+ /**
1588
+ * Split a filename into the part that may be truncated and the part that may not. A leading dot
1589
+ * (`.gitignore`) is not an extension, and neither is a "dot" eleven characters from the end of a
1590
+ * sentence-like name — an extension is short, so anything longer is left in the stem where the
1591
+ * truncation can reach it.
1592
+ */
1593
+ function splitFileName(name) {
1594
+ const dot = name.lastIndexOf(".");
1595
+ if (dot <= 0 || name.length - dot > 11) return {
1596
+ stem: name,
1597
+ ext: ""
1598
+ };
1599
+ return {
1600
+ stem: name.slice(0, dot),
1601
+ ext: name.slice(dot)
1602
+ };
1603
+ }
1604
+ function AttachmentRow({ a, onRemove }) {
1605
+ const { t } = useTranslation();
1606
+ const name = a.label ?? a.name;
1607
+ const { stem, ext } = splitFileName(name);
1608
+ const size = fileSizeLabel(a.size);
1609
+ return /* @__PURE__ */ jsxs("li", {
1610
+ className: "flex min-w-0 items-center gap-2 rounded-[calc(var(--radius-md)+1px)] border border-border bg-background py-1 pr-1.5 pl-2",
1611
+ children: [
1612
+ /* @__PURE__ */ jsx(attachmentIcon(a), {
1613
+ size: 14,
1614
+ "aria-hidden": true,
1615
+ className: "shrink-0 text-muted-foreground"
1616
+ }),
1617
+ /* @__PURE__ */ jsxs("span", {
1618
+ className: "flex min-w-0 flex-1 text-xs",
1619
+ title: name,
1620
+ children: [/* @__PURE__ */ jsx("span", {
1621
+ className: "truncate",
1622
+ children: stem
1623
+ }), ext && /* @__PURE__ */ jsx("span", {
1624
+ className: "shrink-0 text-muted-foreground",
1625
+ children: ext
1626
+ })]
1627
+ }),
1628
+ size && /* @__PURE__ */ jsx("span", {
1629
+ className: "shrink-0 text-[0.6875rem] tabular-nums text-muted-foreground",
1630
+ children: size
1631
+ }),
1632
+ onRemove && /* @__PURE__ */ jsx("button", {
1633
+ type: "button",
1634
+ "aria-label": t("webchat.removeAttachment", { name }),
1635
+ title: t("webchat.removeAttachment", { name }),
1636
+ className: "shrink-0 rounded p-0.5 text-muted-foreground hover:text-destructive focus-visible:outline-2 focus-visible:outline-ring",
1637
+ onClick: onRemove,
1638
+ children: /* @__PURE__ */ jsx(X, { size: 13 })
1639
+ })
1640
+ ]
1641
+ });
1642
+ }
1643
+ function ChatAttachments({ items, onRemove, className = "" }) {
1644
+ const { t } = useTranslation();
1645
+ const [expanded, setExpanded] = useState(false);
1646
+ const listId = useId();
1647
+ if (!items.length) return null;
1648
+ const hidden = expanded ? 0 : Math.max(0, items.length - COLLAPSE_AFTER);
1649
+ const shown = hidden ? items.slice(0, COLLAPSE_AFTER) : items;
1650
+ return /* @__PURE__ */ jsxs("div", {
1651
+ className: `flex flex-col gap-1 ${className}`,
1652
+ "data-chat-attachments": true,
1653
+ children: [/* @__PURE__ */ jsx("ul", {
1654
+ id: listId,
1655
+ "aria-label": t("webchat.attachments"),
1656
+ className: "flex flex-col gap-1",
1657
+ children: shown.map((a, i) => /* @__PURE__ */ jsx(AttachmentRow, {
1658
+ a,
1659
+ ...onRemove ? { onRemove: () => onRemove(i) } : {}
1660
+ }, `${a.name}-${i}`))
1661
+ }), items.length > COLLAPSE_AFTER && /* @__PURE__ */ jsxs("button", {
1662
+ type: "button",
1663
+ "aria-expanded": expanded,
1664
+ "aria-controls": listId,
1665
+ className: "inline-flex items-center gap-1 self-start rounded px-1 py-0.5 text-xs text-muted-foreground hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring",
1666
+ onClick: () => setExpanded((v) => !v),
1667
+ children: [/* @__PURE__ */ jsx(ChevronDown, {
1668
+ size: 12,
1669
+ "aria-hidden": true,
1670
+ className: expanded ? "rotate-180 transition-transform" : "transition-transform"
1671
+ }), expanded ? t("webchat.fewerFiles") : t("webchat.moreFiles", { count: hidden })]
1672
+ })]
1673
+ });
1674
+ }
1675
+ //#endregion
1676
+ //#region src/ui/ChatMessages.tsx
1677
+ function ChatMessages({ messages, pending, pendingReasoning, trailing }) {
1678
+ const { t } = useTranslation();
1679
+ const listRef = useRef(null);
1680
+ useEffect(() => {
1681
+ const el = listRef.current;
1682
+ if (el) el.scrollTop = el.scrollHeight;
1683
+ }, [messages.length, pending]);
1684
+ return /* @__PURE__ */ jsxs("div", {
1685
+ ref: listRef,
1686
+ "data-chat-list": true,
1687
+ className: "flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-2",
1688
+ children: [!messages.length && pending === null && pendingReasoning === null ? /* @__PURE__ */ jsx("div", {
1689
+ className: "grid flex-1 place-items-center px-6 text-center text-sm text-muted-foreground",
1690
+ children: /* @__PURE__ */ jsxs("span", {
1691
+ className: "flex flex-col items-center gap-2",
1692
+ children: [/* @__PURE__ */ jsx(MessagesSquare, {
1693
+ size: 28,
1694
+ "aria-hidden": true,
1695
+ className: "text-faint"
1696
+ }), t("webchat.empty")]
1697
+ })
1698
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [messages.map((m) => /* @__PURE__ */ jsx(Bubble, {
1699
+ role: m.role,
1700
+ text: m.text,
1701
+ reasoning: m.reasoning,
1702
+ attachments: m.attachments
1703
+ }, m.msgId)), pending !== null && /* @__PURE__ */ jsx(Bubble, {
1704
+ role: "assistant",
1705
+ text: pending || t("webchat.thinking"),
1706
+ reasoning: pendingReasoning
1707
+ })] }), trailing]
1708
+ });
1709
+ }
1710
+ /**
1711
+ * memo: during streaming, the parent re-renders on EVERY delta (several times per
1712
+ * second). Without memo, each tick would re-render (and therefore re-parse markdown)
1713
+ * for every completed message while only one bubble at the bottom is changing.
1714
+ * The props are primitives, so the default shallow comparison is sufficient.
1715
+ */
1716
+ var Bubble = memo(function Bubble({ role, text, reasoning, attachments }) {
1717
+ const { t } = useTranslation();
1718
+ if (role === "user") return /* @__PURE__ */ jsxs("div", {
1719
+ className: "max-w-[85%] min-w-0 self-end rounded-2xl rounded-br-sm border border-border bg-accent px-3 py-2 text-[0.8125rem] break-words whitespace-pre-wrap",
1720
+ children: [attachments?.length ? /* @__PURE__ */ jsx(ChatAttachments, {
1721
+ items: attachments,
1722
+ className: `-mx-1 ${text ? "mb-2" : ""}`
1723
+ }) : null, text]
1724
+ });
1725
+ return /* @__PURE__ */ jsxs("div", {
1726
+ className: "max-w-[95%] min-w-0 self-start rounded-2xl rounded-bl-sm bg-muted px-3 py-2 text-sm break-words",
1727
+ children: [reasoning ? /* @__PURE__ */ jsxs("details", {
1728
+ className: "group/reason -mx-1 mb-2 rounded-[calc(var(--radius-md)+1px)] border border-border bg-background px-2 py-1",
1729
+ children: [/* @__PURE__ */ jsxs("summary", {
1730
+ className: "flex cursor-pointer list-none items-center gap-1 select-none text-xs text-muted-foreground hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring [&::-webkit-details-marker]:hidden",
1731
+ children: [/* @__PURE__ */ jsx(ChevronRight, {
1732
+ size: 12,
1733
+ "aria-hidden": true,
1734
+ className: "shrink-0 transition-transform group-open/reason:rotate-90"
1735
+ }), t("webchat.reasoning")]
1736
+ }), /* @__PURE__ */ jsx(ChatMarkdown, {
1737
+ text: reasoning,
1738
+ className: "md-static md-chat md-chat-sub mt-1 text-muted-foreground"
1739
+ })]
1740
+ }) : null, /* @__PURE__ */ jsx(ChatMarkdown, {
1741
+ text,
1742
+ className: "md-static md-chat"
1743
+ })]
1744
+ });
1745
+ });
1746
+ //#endregion
1747
+ //#region src/ui/AgentPicker.tsx
1748
+ /**
1749
+ * Flatten the catalog to one list: with a single registered agent the agent level is
1750
+ * noise, and with several the agent name simply prefixes its presets.
1751
+ *
1752
+ * ONE mode, a list of names. Until 2026-09-16 there was a second — a row of stars in the
1753
+ * composer, where a preset's `quality` rating addressed it by rung and the model name was
1754
+ * never printed. It is gone, with the ratings it read. A star count can only address a
1755
+ * preset when the presets form a single totally ordered line, and `claude-agent`'s do
1756
+ * not: nothing published ranks `sonnet-high` against `opus-low`, and the cheaper of the
1757
+ * two is not the weaker one. The list that used to be the FALLBACK — for several agents,
1758
+ * a gap in the ladder, a preset with no rating — was always the honest control, and is
1759
+ * now the only one. See
1760
+ * `docs/superpowers/specs/2026-09-16-agent-preset-is-a-model-choice-design.md`.
1761
+ */
1762
+ function AgentPicker({ agents, defaultAgentId, selection, onChange }) {
1763
+ const { t } = useTranslation();
1764
+ const [open, setOpen] = useState(false);
1765
+ const rootRef = useRef(null);
1766
+ const triggerRef = useRef(null);
1767
+ useEffect(() => {
1768
+ if (!open) return;
1769
+ function onPointerDown(e) {
1770
+ if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
1771
+ }
1772
+ function onKeyDown(e) {
1773
+ if (e.key === "Escape") {
1774
+ setOpen(false);
1775
+ triggerRef.current?.focus();
1776
+ }
1777
+ }
1778
+ document.addEventListener("pointerdown", onPointerDown);
1779
+ document.addEventListener("keydown", onKeyDown);
1780
+ return () => {
1781
+ document.removeEventListener("pointerdown", onPointerDown);
1782
+ document.removeEventListener("keydown", onKeyDown);
1783
+ };
1784
+ }, [open]);
1785
+ const multi = agents.length > 1;
1786
+ const rows = agents.flatMap((a) => a.presets.map((p) => ({
1787
+ agentId: a.id,
1788
+ presetId: p.id,
1789
+ label: multi ? `${a.title} · ${p.title}` : p.title,
1790
+ hint: p.hint,
1791
+ isDefault: p.default === true
1792
+ })));
1793
+ if (rows.length === 0) return null;
1794
+ const current = rows.find((r) => r.agentId === selection.agentId && r.presetId === selection.presetId) ?? rows.find((r) => r.agentId === defaultAgentId && r.isDefault) ?? rows.find((r) => r.isDefault) ?? rows[0];
1795
+ return /* @__PURE__ */ jsxs("div", {
1796
+ className: "relative",
1797
+ ref: rootRef,
1798
+ children: [/* @__PURE__ */ jsxs("button", {
1799
+ ref: triggerRef,
1800
+ type: "button",
1801
+ "aria-haspopup": "menu",
1802
+ "aria-expanded": open,
1803
+ className: "flex items-center gap-1 rounded-md px-2 py-1 text-xs text-muted-foreground hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring",
1804
+ onClick: () => setOpen((v) => !v),
1805
+ children: [/* @__PURE__ */ jsx("span", {
1806
+ className: "max-w-40 truncate",
1807
+ children: current.hint ? `${current.label} · ${current.hint}` : current.label
1808
+ }), /* @__PURE__ */ jsx(ChevronDown, {
1809
+ size: 12,
1810
+ className: "shrink-0"
1811
+ })]
1812
+ }), open && /* @__PURE__ */ jsx("ul", {
1813
+ "aria-label": t("webchat.agentPicker"),
1814
+ className: "absolute bottom-full left-0 z-10 mb-1 max-h-64 w-64 max-w-[calc(100vw-2rem)] overflow-y-auto rounded-md border border-border bg-muted py-1 shadow-lg",
1815
+ children: rows.map((r) => {
1816
+ const selected = r === current;
1817
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
1818
+ type: "button",
1819
+ ...selected ? { "aria-current": true } : {},
1820
+ className: "flex w-full items-center gap-2 px-2 py-1.5 text-left text-xs hover:bg-card focus-visible:outline-2 focus-visible:outline-ring",
1821
+ onClick: () => {
1822
+ setOpen(false);
1823
+ onChange({
1824
+ agentId: r.agentId,
1825
+ presetId: r.presetId
1826
+ });
1827
+ },
1828
+ children: [
1829
+ /* @__PURE__ */ jsx(Check, {
1830
+ size: 12,
1831
+ className: selected ? "shrink-0 text-primary" : "shrink-0 opacity-0"
1832
+ }),
1833
+ /* @__PURE__ */ jsx("span", {
1834
+ className: "min-w-0 flex-1 truncate",
1835
+ children: r.label
1836
+ }),
1837
+ r.hint ? /* @__PURE__ */ jsx("span", {
1838
+ className: "shrink-0 text-muted-foreground",
1839
+ children: r.hint
1840
+ }) : null
1841
+ ]
1842
+ }) }, `${r.agentId}/${r.presetId}`);
1843
+ })
1844
+ })]
1845
+ });
1846
+ }
1847
+ //#endregion
1848
+ //#region src/ui/ChatComposer.tsx
1849
+ function ChatComposer({ streaming, onSend, onStop, attachments, uploading, uploadError, uploadProgress, canRetryUpload, onRetryUpload, onPickFiles, onRemoveAttachment, agents, defaultAgentId, selection, onChooseAgent, attachmentsAllowed }) {
1850
+ const { t } = useTranslation();
1851
+ const [text, setText] = useState("");
1852
+ function submit() {
1853
+ if (!text.trim() || streaming) return;
1854
+ if (onSend(text.trim(), attachments)) setText("");
1855
+ }
1856
+ return /* @__PURE__ */ jsxs("div", {
1857
+ className: "flex shrink-0 flex-col gap-2 border-t border-border p-2",
1858
+ children: [(attachments.length > 0 || uploading || uploadError) && /* @__PURE__ */ jsxs("div", {
1859
+ className: "flex flex-col gap-1.5 px-1",
1860
+ children: [
1861
+ /* @__PURE__ */ jsx(ChatAttachments, {
1862
+ items: attachments,
1863
+ onRemove: onRemoveAttachment
1864
+ }),
1865
+ uploading && /* @__PURE__ */ jsxs("div", {
1866
+ className: "flex min-w-32 flex-1 items-center gap-2",
1867
+ "aria-label": t("webchat.uploading"),
1868
+ children: [
1869
+ /* @__PURE__ */ jsx(LoaderCircle, {
1870
+ size: 14,
1871
+ className: "shrink-0 animate-spin text-muted-foreground"
1872
+ }),
1873
+ /* @__PURE__ */ jsx("div", {
1874
+ role: "progressbar",
1875
+ "aria-label": t("webchat.uploadProgress"),
1876
+ "aria-valuemin": 0,
1877
+ "aria-valuemax": 100,
1878
+ "aria-valuenow": uploadProgress,
1879
+ className: "h-1.5 min-w-0 flex-1 overflow-hidden rounded-full bg-border",
1880
+ children: /* @__PURE__ */ jsx("div", {
1881
+ className: "h-full rounded-full bg-primary transition-[width]",
1882
+ style: { width: `${uploadProgress}%` }
1883
+ })
1884
+ }),
1885
+ /* @__PURE__ */ jsxs("span", {
1886
+ className: "text-xs tabular-nums text-muted-foreground",
1887
+ children: [uploadProgress, "%"]
1888
+ })
1889
+ ]
1890
+ }),
1891
+ uploadError && /* @__PURE__ */ jsxs("div", {
1892
+ className: "flex min-w-0 flex-1 items-center gap-2 text-xs text-destructive",
1893
+ children: [/* @__PURE__ */ jsx("span", {
1894
+ className: "min-w-0 flex-1",
1895
+ children: t(uploadError)
1896
+ }), canRetryUpload && /* @__PURE__ */ jsxs("button", {
1897
+ type: "button",
1898
+ className: "inline-flex shrink-0 items-center gap-1 rounded px-1.5 py-0.5 font-medium text-primary hover:bg-muted focus-visible:outline-2 focus-visible:outline-ring",
1899
+ onClick: onRetryUpload,
1900
+ children: [
1901
+ /* @__PURE__ */ jsx(RotateCcw, { size: 12 }),
1902
+ " ",
1903
+ t("webchat.retryUpload")
1904
+ ]
1905
+ })]
1906
+ })
1907
+ ]
1908
+ }), /* @__PURE__ */ jsxs("div", {
1909
+ className: "flex flex-col gap-1",
1910
+ children: [/* @__PURE__ */ jsx("textarea", {
1911
+ className: "max-h-32 w-full resize-none rounded-lg border border-border bg-background px-3 py-2 text-sm transition-colors outline-none placeholder:text-faint focus:border-ring focus:ring-2 focus:ring-ring",
1912
+ rows: 1,
1913
+ value: text,
1914
+ placeholder: t("webchat.placeholder"),
1915
+ onChange: (e) => setText(e.target.value),
1916
+ onKeyDown: (e) => {
1917
+ if (e.key === "Enter" && !e.shiftKey) {
1918
+ e.preventDefault();
1919
+ submit();
1920
+ }
1921
+ }
1922
+ }), /* @__PURE__ */ jsxs("div", {
1923
+ className: "flex items-center gap-1",
1924
+ children: [
1925
+ attachmentsAllowed && /* @__PURE__ */ jsxs("label", {
1926
+ title: t("webchat.attach"),
1927
+ className: `flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-within:ring-2 focus-within:ring-ring ${uploading ? "pointer-events-none opacity-60" : ""}`,
1928
+ children: [/* @__PURE__ */ jsx(Paperclip, {
1929
+ size: 18,
1930
+ "aria-hidden": true
1931
+ }), /* @__PURE__ */ jsx("input", {
1932
+ type: "file",
1933
+ multiple: true,
1934
+ "aria-label": t("webchat.attach"),
1935
+ className: "sr-only",
1936
+ disabled: uploading,
1937
+ onChange: (e) => {
1938
+ if (e.target.files?.length) onPickFiles(e.target.files);
1939
+ e.currentTarget.value = "";
1940
+ }
1941
+ })]
1942
+ }),
1943
+ /* @__PURE__ */ jsx(AgentPicker, {
1944
+ agents,
1945
+ defaultAgentId,
1946
+ selection,
1947
+ onChange: onChooseAgent
1948
+ }),
1949
+ /* @__PURE__ */ jsx("div", { className: "flex-1" }),
1950
+ streaming ? /* @__PURE__ */ jsx(Button, {
1951
+ type: "button",
1952
+ variant: "ghost",
1953
+ size: "iconSm",
1954
+ "aria-label": t("webchat.stop"),
1955
+ title: t("webchat.stop"),
1956
+ className: "shrink-0",
1957
+ onClick: onStop,
1958
+ children: /* @__PURE__ */ jsx(Square, { size: 18 })
1959
+ }) : /* @__PURE__ */ jsx(Button, {
1960
+ type: "button",
1961
+ variant: "ghost",
1962
+ size: "iconSm",
1963
+ "aria-label": t("webchat.send"),
1964
+ title: t("webchat.send"),
1965
+ disabled: !text.trim(),
1966
+ className: "shrink-0 hover:text-primary",
1967
+ onClick: submit,
1968
+ children: /* @__PURE__ */ jsx(Send, { size: 18 })
1969
+ })
1970
+ ]
1971
+ })]
1972
+ })]
1973
+ });
1974
+ }
1975
+ //#endregion
1976
+ //#region src/ui/ChatThreadList.tsx
1977
+ function ChatThreadList({ threads, activeId, onOpen, onNew, onTogglePrivate, className = "w-[220px] shrink-0" }) {
1978
+ const { t } = useTranslation();
1979
+ return /* @__PURE__ */ jsxs("aside", {
1980
+ "aria-label": t("webchat.threads"),
1981
+ className: `${className} flex flex-col border-r border-border`,
1982
+ children: [/* @__PURE__ */ jsxs("button", {
1983
+ className: "flex items-center gap-2 border-b border-border px-3 py-2 text-sm font-medium text-primary hover:bg-muted focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring",
1984
+ onClick: onNew,
1985
+ children: [
1986
+ /* @__PURE__ */ jsx(Plus, {
1987
+ size: 16,
1988
+ "aria-hidden": true
1989
+ }),
1990
+ " ",
1991
+ t("webchat.newConversation")
1992
+ ]
1993
+ }), /* @__PURE__ */ jsx("div", {
1994
+ className: "flex-1 overflow-y-auto",
1995
+ children: threads.map((th) => {
1996
+ const active = th.convId === activeId;
1997
+ return /* @__PURE__ */ jsxs("div", {
1998
+ className: `flex items-center gap-1 border-l-2 pr-2 pl-1 ${active ? "border-ring bg-accent" : "border-transparent hover:bg-muted"}`,
1999
+ children: [/* @__PURE__ */ jsxs("button", {
2000
+ onClick: () => onOpen(th.convId),
2001
+ className: `min-w-0 flex-1 rounded-sm py-2 pl-2 text-left text-sm focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring ${active ? "font-medium" : ""}`,
2002
+ children: [/* @__PURE__ */ jsx("span", {
2003
+ className: "block truncate",
2004
+ children: th.title || t("webchat.newConversation")
2005
+ }), (th.mine || th.ownerName) && /* @__PURE__ */ jsx("span", {
2006
+ className: "block truncate text-xs text-muted-foreground",
2007
+ children: th.mine ? t("webchat.you") : th.ownerName
2008
+ })]
2009
+ }), th.mine && /* @__PURE__ */ jsx("button", {
2010
+ type: "button",
2011
+ "aria-label": th.private ? t("webchat.makePublic") : t("webchat.makePrivate"),
2012
+ title: th.private ? t("webchat.makePublic") : t("webchat.makePrivate"),
2013
+ onClick: (e) => {
2014
+ e.stopPropagation();
2015
+ onTogglePrivate(th.convId, !th.private);
2016
+ },
2017
+ className: "shrink-0 rounded p-1 text-muted-foreground hover:bg-card hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring",
2018
+ children: /* @__PURE__ */ jsx(Icon, {
2019
+ name: th.private ? "lucide:shield" : "lucide:globe",
2020
+ size: 14
2021
+ })
2022
+ })]
2023
+ }, th.convId);
2024
+ })
2025
+ })]
2026
+ });
2027
+ }
2028
+ //#endregion
2029
+ //#region src/ui/SuggestionChips.tsx
2030
+ /**
2031
+ * A row of clickable quick-reply chips: either the static, global conversation starters
2032
+ * shown on an empty chat, or the contextual suggestions the agent proposed after its last
2033
+ * answer (`ChatState.suggestions`) — both render through this one component, distinguished
2034
+ * only by which list `ChatWidget` passes in. Renders nothing for an empty or null list, so
2035
+ * callers do not need their own guard.
2036
+ */
2037
+ function SuggestionChips({ items, onPick }) {
2038
+ const { t } = useTranslation();
2039
+ if (!items?.length) return null;
2040
+ return /* @__PURE__ */ jsx("div", {
2041
+ role: "group",
2042
+ "aria-label": t("webchat.suggestions"),
2043
+ className: "flex flex-wrap gap-1.5 px-2 pb-1.5",
2044
+ children: items.map((text, i) => /* @__PURE__ */ jsx("button", {
2045
+ type: "button",
2046
+ title: text,
2047
+ className: "max-w-full truncate rounded-full border border-border bg-muted px-3 py-1 text-xs text-foreground hover:border-border2 hover:bg-card focus-visible:outline-2 focus-visible:outline-ring",
2048
+ onClick: () => onPick(text),
2049
+ children: text
2050
+ }, `${i}-${text}`))
2051
+ });
2052
+ }
2053
+ //#endregion
2054
+ //#region src/ui/ChatWidget.tsx
2055
+ var MAX_CHAT_UPLOAD_BYTES = 25 * 1024 * 1024;
2056
+ function isFileDrag(e) {
2057
+ return Array.from(e.dataTransfer.types).includes("Files");
2058
+ }
2059
+ /** One drop surface per popup. The overlay deliberately covers the header, feed, and composer. */
2060
+ function ChatDropSurface({ children, onFiles, className }) {
2061
+ const { t } = useTranslation();
2062
+ const [dragging, setDragging] = useState(false);
2063
+ const depth = useRef(0);
2064
+ function enter(e) {
2065
+ if (!isFileDrag(e)) return;
2066
+ e.preventDefault();
2067
+ depth.current += 1;
2068
+ setDragging(true);
2069
+ }
2070
+ function leave(e) {
2071
+ if (!isFileDrag(e)) return;
2072
+ e.preventDefault();
2073
+ depth.current = Math.max(0, depth.current - 1);
2074
+ if (depth.current === 0) setDragging(false);
2075
+ }
2076
+ function drop(e) {
2077
+ if (!isFileDrag(e)) return;
2078
+ e.preventDefault();
2079
+ depth.current = 0;
2080
+ setDragging(false);
2081
+ onFiles(Array.from(e.dataTransfer.files));
2082
+ }
2083
+ return /* @__PURE__ */ jsxs("div", {
2084
+ className,
2085
+ onDragEnter: enter,
2086
+ onDragLeave: leave,
2087
+ onDragOver: (e) => {
2088
+ if (isFileDrag(e)) {
2089
+ e.preventDefault();
2090
+ e.dataTransfer.dropEffect = "copy";
2091
+ }
2092
+ },
2093
+ onDrop: drop,
2094
+ children: [children, dragging && /* @__PURE__ */ jsx("div", {
2095
+ "data-chat-drop-overlay": true,
2096
+ "data-testid": "chat-drop-overlay",
2097
+ className: "pointer-events-none absolute inset-0 z-50 grid place-items-center bg-card/95 p-6 text-center backdrop-blur-sm",
2098
+ children: /* @__PURE__ */ jsxs("div", {
2099
+ className: "flex h-full w-full flex-col items-center justify-center rounded-2xl border-2 border-dashed border-ring text-primary",
2100
+ children: [
2101
+ /* @__PURE__ */ jsx(Upload, { size: 34 }),
2102
+ /* @__PURE__ */ jsx("span", {
2103
+ className: "mt-3 text-base font-medium",
2104
+ children: t("webchat.dropFiles")
2105
+ }),
2106
+ /* @__PURE__ */ jsx("span", {
2107
+ className: "mt-1 text-xs text-muted-foreground",
2108
+ children: t("webchat.dropFilesHint")
2109
+ })
2110
+ ]
2111
+ })
2112
+ })]
2113
+ });
2114
+ }
2115
+ /** The states the VIEWPORT offers, ordered from least to most expanded. `panel` — the 380px
2116
+ * floating window — exists only where it does not obstruct the page: on a narrow screen the
2117
+ * widget has exactly two states, the bubble and the full sheet. Every mode transition is a
2118
+ * move along this ladder, so no call site can put the widget into a mode its screen lacks. */
2119
+ function chatModes(wide) {
2120
+ return wide ? [
2121
+ "bubble",
2122
+ "panel",
2123
+ "modal"
2124
+ ] : ["bubble", "modal"];
2125
+ }
2126
+ /** Where the widget rests once it is opened — expanded, but as little as the viewport allows.
2127
+ * Also where it returns after following a link from a response: on a wide screen the panel
2128
+ * does not obstruct the page the user just navigated to, while on a narrow one the sheet is
2129
+ * the only visible state, so the active conversation survives the navigation either way. */
2130
+ function restingMode(wide) {
2131
+ return chatModes(wide)[1];
2132
+ }
2133
+ /** One step along the ladder, clamped at both ends: `-1` is what collapse, Esc and a dialog
2134
+ * dismiss do, `+1` is expand. A mode this viewport no longer offers is re-homed by the effect
2135
+ * in the widget, not by stepping — the step always returns a mode from the current ladder. */
2136
+ function stepMode(mode, wide, delta) {
2137
+ const ladder = chatModes(wide);
2138
+ const next = ladder.indexOf(mode) + delta;
2139
+ return ladder[Math.min(ladder.length - 1, Math.max(0, next))];
2140
+ }
2141
+ function ChatWidget() {
2142
+ const { t } = useTranslation();
2143
+ const chat = useChat();
2144
+ const readPage = usePageContext();
2145
+ const [mode, setMode] = useState("bubble");
2146
+ const [threadsOpen, setThreadsOpen] = useState(false);
2147
+ const [attachments, setAttachments] = useState([]);
2148
+ const [uploading, setUploading] = useState(false);
2149
+ const [uploadProgress, setUploadProgress] = useState(0);
2150
+ const [uploadError, setUploadError] = useState(null);
2151
+ const [retryFiles, setRetryFiles] = useState([]);
2152
+ const wide = useMinWidth("sm", 640);
2153
+ const hasSidebar = wide && mode === "modal";
2154
+ const onChatNavigate = useCallback(() => {
2155
+ setMode(restingMode(wide));
2156
+ }, [wide]);
2157
+ const expandBtnRef = useRef(null);
2158
+ const prevModeRef = useRef("bubble");
2159
+ useEffect(() => {
2160
+ if (mode !== "modal") return;
2161
+ const onKey = (e) => {
2162
+ if (e.key === "Escape") setMode(stepMode("modal", wide, -1));
2163
+ };
2164
+ window.addEventListener("keydown", onKey);
2165
+ return () => window.removeEventListener("keydown", onKey);
2166
+ }, [mode, wide]);
2167
+ useEffect(() => {
2168
+ if (!chatModes(wide).includes(mode)) setMode(restingMode(wide));
2169
+ }, [mode, wide]);
2170
+ useEffect(() => {
2171
+ if (mode === "panel" && prevModeRef.current === "modal") expandBtnRef.current?.focus();
2172
+ prevModeRef.current = mode;
2173
+ }, [mode]);
2174
+ useEffect(() => {
2175
+ if (mode === "bubble" || hasSidebar) setThreadsOpen(false);
2176
+ }, [mode, hasSidebar]);
2177
+ useEffect(() => {
2178
+ if (mode !== "modal") return;
2179
+ const prev = document.body.style.overflow;
2180
+ document.body.style.overflow = "hidden";
2181
+ return () => {
2182
+ document.body.style.overflow = prev;
2183
+ };
2184
+ }, [mode]);
2185
+ const isOpen = mode !== "bubble";
2186
+ useEffect(() => {
2187
+ if (isOpen) chat.loadThreads();
2188
+ }, [isOpen, chat.loadThreads]);
2189
+ const { convId, newConversation } = chat;
2190
+ useEffect(() => {
2191
+ if (isOpen && convId === null) newConversation();
2192
+ }, [
2193
+ isOpen,
2194
+ convId,
2195
+ newConversation
2196
+ ]);
2197
+ function startNew() {
2198
+ chat.newConversation();
2199
+ setAttachments([]);
2200
+ setUploadError(null);
2201
+ setRetryFiles([]);
2202
+ setThreadsOpen(false);
2203
+ }
2204
+ const pickFiles = useCallback(async (files) => {
2205
+ if (!files.length) return;
2206
+ setUploadError(null);
2207
+ setRetryFiles([]);
2208
+ setUploading(true);
2209
+ setUploadProgress(0);
2210
+ const added = [];
2211
+ const failedFiles = [];
2212
+ let error = null;
2213
+ for (const file of files) {
2214
+ if (file.size > MAX_CHAT_UPLOAD_BYTES) {
2215
+ error ??= "webchat.uploadTooLarge";
2216
+ continue;
2217
+ }
2218
+ try {
2219
+ setUploadProgress(0);
2220
+ const rec = await uploadFile(file, setUploadProgress);
2221
+ if (rec) added.push({
2222
+ ...rec,
2223
+ label: file.name
2224
+ });
2225
+ else {
2226
+ failedFiles.push(file);
2227
+ error ??= "webchat.uploadFailed";
2228
+ }
2229
+ } catch {
2230
+ failedFiles.push(file);
2231
+ error ??= "webchat.uploadFailed";
2232
+ }
2233
+ }
2234
+ if (added.length) setAttachments((prev) => [...prev, ...added]);
2235
+ setUploading(false);
2236
+ setUploadProgress(0);
2237
+ setRetryFiles(failedFiles);
2238
+ if (error) setUploadError(error);
2239
+ }, []);
2240
+ const retryUploads = useCallback(() => {
2241
+ if (retryFiles.length) pickFiles(retryFiles);
2242
+ }, [pickFiles, retryFiles]);
2243
+ const sendTurn = useCallback((text, refs) => {
2244
+ const page = readPage();
2245
+ const accepted = refs.length ? chat.send(text, page, refs) : chat.send(text, page);
2246
+ if (accepted) {
2247
+ setAttachments([]);
2248
+ setUploadError(null);
2249
+ setRetryFiles([]);
2250
+ }
2251
+ return accepted;
2252
+ }, [chat, readPage]);
2253
+ const hasConversation = chat.messages.length > 0 || chat.pending !== null;
2254
+ const activeTitle = chat.threads.find((th) => th.convId === chat.convId)?.title;
2255
+ const header = /* @__PURE__ */ jsxs("div", {
2256
+ className: "flex shrink-0 items-center justify-between gap-2 border-b border-border px-3 py-1.5",
2257
+ children: [/* @__PURE__ */ jsx("span", {
2258
+ className: "min-w-0 truncate text-sm font-medium",
2259
+ children: activeTitle || t("webchat.title")
2260
+ }), /* @__PURE__ */ jsxs("div", {
2261
+ className: "-mr-1 flex shrink-0 items-center gap-0.5",
2262
+ children: [
2263
+ hasConversation && /* @__PURE__ */ jsx(Button, {
2264
+ type: "button",
2265
+ variant: "ghost",
2266
+ size: "iconSm",
2267
+ "aria-label": t("webchat.newConversation"),
2268
+ title: t("webchat.newConversation"),
2269
+ onClick: startNew,
2270
+ children: /* @__PURE__ */ jsx(Plus, { size: 16 })
2271
+ }),
2272
+ !hasSidebar && /* @__PURE__ */ jsx(Button, {
2273
+ type: "button",
2274
+ variant: "ghost",
2275
+ size: "iconSm",
2276
+ "aria-label": t("webchat.threads"),
2277
+ title: t("webchat.threads"),
2278
+ "aria-expanded": threadsOpen,
2279
+ className: threadsOpen ? "bg-muted text-foreground" : "",
2280
+ onClick: () => setThreadsOpen((v) => !v),
2281
+ children: /* @__PURE__ */ jsx(List, { size: 16 })
2282
+ }),
2283
+ chatModes(wide).includes("panel") && /* @__PURE__ */ jsx(Button, {
2284
+ ref: expandBtnRef,
2285
+ type: "button",
2286
+ variant: "ghost",
2287
+ size: "iconSm",
2288
+ "aria-label": mode === "modal" ? t("webchat.collapse") : t("webchat.expand"),
2289
+ title: mode === "modal" ? t("webchat.collapse") : t("webchat.expand"),
2290
+ onClick: () => setMode(stepMode(mode, wide, mode === "modal" ? -1 : 1)),
2291
+ children: mode === "modal" ? /* @__PURE__ */ jsx(Minimize2, { size: 16 }) : /* @__PURE__ */ jsx(Maximize2, { size: 16 })
2292
+ }),
2293
+ /* @__PURE__ */ jsx(Button, {
2294
+ type: "button",
2295
+ variant: "ghost",
2296
+ size: "iconSm",
2297
+ "aria-label": t("webchat.close"),
2298
+ title: t("webchat.close"),
2299
+ onClick: () => setMode("bubble"),
2300
+ children: /* @__PURE__ */ jsx(X, { size: 16 })
2301
+ })
2302
+ ]
2303
+ })]
2304
+ });
2305
+ const threadList = /* @__PURE__ */ jsx(ChatThreadList, {
2306
+ threads: chat.threads,
2307
+ activeId: chat.convId,
2308
+ onOpen: (id) => {
2309
+ chat.open(id);
2310
+ setAttachments([]);
2311
+ setUploadError(null);
2312
+ setRetryFiles([]);
2313
+ setThreadsOpen(false);
2314
+ },
2315
+ onNew: startNew,
2316
+ onTogglePrivate: chat.togglePrivate,
2317
+ className: hasSidebar ? "w-[220px] shrink-0" : "w-full"
2318
+ });
2319
+ const conversation = /* @__PURE__ */ jsx(ChatNavProvider, {
2320
+ onNavigate: onChatNavigate,
2321
+ children: /* @__PURE__ */ jsxs("div", {
2322
+ className: "flex min-h-0 min-w-0 flex-1 flex-col",
2323
+ children: [
2324
+ chat.error && /* @__PURE__ */ jsx("div", {
2325
+ className: "px-3 py-2 text-sm text-destructive",
2326
+ children: t("webchat.error", { message: chat.error })
2327
+ }),
2328
+ /* @__PURE__ */ jsx(ChatMessages, {
2329
+ messages: chat.messages,
2330
+ pending: chat.pending,
2331
+ pendingReasoning: chat.pendingReasoning,
2332
+ trailing: chat.messages.length === 0 ? /* @__PURE__ */ jsx(SuggestionChips, {
2333
+ items: chat.starters,
2334
+ onPick: (text) => sendTurn(text, attachments)
2335
+ }) : /* @__PURE__ */ jsx(SuggestionChips, {
2336
+ items: chat.suggestions,
2337
+ onPick: (text) => sendTurn(text, attachments)
2338
+ })
2339
+ }),
2340
+ /* @__PURE__ */ jsx(ChatComposer, {
2341
+ streaming: chat.pending !== null,
2342
+ attachments,
2343
+ uploading,
2344
+ uploadProgress,
2345
+ uploadError,
2346
+ canRetryUpload: retryFiles.length > 0 && !uploading,
2347
+ onRetryUpload: retryUploads,
2348
+ onPickFiles: (files) => void pickFiles(Array.from(files)),
2349
+ onRemoveAttachment: (index) => setAttachments((prev) => prev.filter((_, i) => i !== index)),
2350
+ agents: chat.agents,
2351
+ defaultAgentId: chat.defaultAgentId,
2352
+ selection: chat.selection,
2353
+ onChooseAgent: chat.chooseAgent,
2354
+ attachmentsAllowed: chat.attachmentsAllowed,
2355
+ onSend: sendTurn,
2356
+ onStop: chat.stop
2357
+ })
2358
+ ]
2359
+ })
2360
+ });
2361
+ if (mode === "bubble") return /* @__PURE__ */ jsx("button", {
2362
+ "aria-label": t("webchat.open"),
2363
+ onClick: () => setMode(restingMode(wide)),
2364
+ className: "fixed bottom-4 right-4 z-40 grid h-14 w-14 place-items-center rounded-full bg-primary text-on-accent ring-1 ring-border2 shadow-lg",
2365
+ children: /* @__PURE__ */ jsx(MessageCircle, { size: 24 })
2366
+ });
2367
+ if (mode === "panel") return /* @__PURE__ */ jsxs(ChatDropSurface, {
2368
+ onFiles: pickFiles,
2369
+ className: "fixed bottom-4 right-4 z-40 flex h-[560px] max-h-[80dvh] w-[380px] max-w-[calc(100vw-2rem)] flex-col overflow-hidden rounded-2xl border border-border bg-card shadow-xl",
2370
+ children: [header, threadsOpen ? threadList : conversation]
2371
+ });
2372
+ return /* @__PURE__ */ jsxs(Dialog, {
2373
+ open: true,
2374
+ modal: false,
2375
+ onOpenChange: (open) => !open && setMode(stepMode("modal", wide, -1)),
2376
+ children: [/* @__PURE__ */ jsx("div", {
2377
+ "data-chat-backdrop": true,
2378
+ "aria-hidden": "true",
2379
+ className: "fixed inset-0 z-[45] bg-black/50 backdrop-blur-sm"
2380
+ }), /* @__PURE__ */ jsx(DialogContent, {
2381
+ hideClose: true,
2382
+ className: "h-[85dvh] w-full p-0 md:h-[85dvh] md:w-[80vw] md:max-w-[1040px] md:overflow-y-hidden",
2383
+ children: /* @__PURE__ */ jsx(ChatDropSurface, {
2384
+ onFiles: pickFiles,
2385
+ className: "h-full",
2386
+ children: /* @__PURE__ */ jsxs("div", {
2387
+ className: "flex h-full min-h-0 flex-col",
2388
+ children: [header, /* @__PURE__ */ jsxs("div", {
2389
+ className: "flex min-h-0 flex-1",
2390
+ children: [(hasSidebar || threadsOpen) && threadList, (hasSidebar || !threadsOpen) && conversation]
2391
+ })]
2392
+ })
2393
+ })
2394
+ })]
2395
+ });
2396
+ }
2397
+ //#endregion
2398
+ export { ChatWidget, ChatWidget as default, chatModes, restingMode, stepMode };