@coffer-org/plugin-webchat 7.0.1 → 7.1.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,2179 @@
1
+ import * as __fedReact from "react";
2
+ import { Dialog, DialogContent, Icon, MarkdownView, toneVar, uploadFile, useMinWidth } from "@coffer-org/web-ui";
3
+ import { createContext, createElement, forwardRef, memo, useCallback, useContext, useEffect, 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 Clock = createLucideIcon("clock", [["circle", {
500
+ cx: "12",
501
+ cy: "12",
502
+ r: "10",
503
+ key: "1mglay"
504
+ }], ["path", {
505
+ d: "M12 6v6l4 2",
506
+ key: "mmk7yg"
507
+ }]]);
508
+ /**
509
+ * @license lucide-react v1.24.0 - ISC
510
+ *
511
+ * This source code is licensed under the ISC license.
512
+ * See the LICENSE file in the root directory of this source tree.
513
+ */
514
+ var FileText = createLucideIcon("file-text", [
515
+ ["path", {
516
+ 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",
517
+ key: "1oefj6"
518
+ }],
519
+ ["path", {
520
+ d: "M14 2v5a1 1 0 0 0 1 1h5",
521
+ key: "wfsgrz"
522
+ }],
523
+ ["path", {
524
+ d: "M10 9H8",
525
+ key: "b1mrlr"
526
+ }],
527
+ ["path", {
528
+ d: "M16 13H8",
529
+ key: "t4e002"
530
+ }],
531
+ ["path", {
532
+ d: "M16 17H8",
533
+ key: "z1uh3a"
534
+ }]
535
+ ]);
536
+ /**
537
+ * @license lucide-react v1.24.0 - ISC
538
+ *
539
+ * This source code is licensed under the ISC license.
540
+ * See the LICENSE file in the root directory of this source tree.
541
+ */
542
+ var List = createLucideIcon("list", [
543
+ ["path", {
544
+ d: "M3 5h.01",
545
+ key: "18ugdj"
546
+ }],
547
+ ["path", {
548
+ d: "M3 12h.01",
549
+ key: "nlz23k"
550
+ }],
551
+ ["path", {
552
+ d: "M3 19h.01",
553
+ key: "noohij"
554
+ }],
555
+ ["path", {
556
+ d: "M8 5h13",
557
+ key: "1pao27"
558
+ }],
559
+ ["path", {
560
+ d: "M8 12h13",
561
+ key: "1za7za"
562
+ }],
563
+ ["path", {
564
+ d: "M8 19h13",
565
+ key: "m83p4d"
566
+ }]
567
+ ]);
568
+ /**
569
+ * @license lucide-react v1.24.0 - ISC
570
+ *
571
+ * This source code is licensed under the ISC license.
572
+ * See the LICENSE file in the root directory of this source tree.
573
+ */
574
+ var LoaderCircle = createLucideIcon("loader-circle", [["path", {
575
+ d: "M21 12a9 9 0 1 1-6.219-8.56",
576
+ key: "13zald"
577
+ }]]);
578
+ /**
579
+ * @license lucide-react v1.24.0 - ISC
580
+ *
581
+ * This source code is licensed under the ISC license.
582
+ * See the LICENSE file in the root directory of this source tree.
583
+ */
584
+ var Maximize2 = createLucideIcon("maximize-2", [
585
+ ["path", {
586
+ d: "M15 3h6v6",
587
+ key: "1q9fwt"
588
+ }],
589
+ ["path", {
590
+ d: "m21 3-7 7",
591
+ key: "1l2asr"
592
+ }],
593
+ ["path", {
594
+ d: "m3 21 7-7",
595
+ key: "tjx5ai"
596
+ }],
597
+ ["path", {
598
+ d: "M9 21H3v-6",
599
+ key: "wtvkvv"
600
+ }]
601
+ ]);
602
+ /**
603
+ * @license lucide-react v1.24.0 - ISC
604
+ *
605
+ * This source code is licensed under the ISC license.
606
+ * See the LICENSE file in the root directory of this source tree.
607
+ */
608
+ var MessageCircle = createLucideIcon("message-circle", [["path", {
609
+ 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",
610
+ key: "1sd12s"
611
+ }]]);
612
+ /**
613
+ * @license lucide-react v1.24.0 - ISC
614
+ *
615
+ * This source code is licensed under the ISC license.
616
+ * See the LICENSE file in the root directory of this source tree.
617
+ */
618
+ var Minimize2 = createLucideIcon("minimize-2", [
619
+ ["path", {
620
+ d: "m14 10 7-7",
621
+ key: "oa77jy"
622
+ }],
623
+ ["path", {
624
+ d: "M20 10h-6V4",
625
+ key: "mjg0md"
626
+ }],
627
+ ["path", {
628
+ d: "m3 21 7-7",
629
+ key: "tjx5ai"
630
+ }],
631
+ ["path", {
632
+ d: "M4 14h6v6",
633
+ key: "rmj7iw"
634
+ }]
635
+ ]);
636
+ /**
637
+ * @license lucide-react v1.24.0 - ISC
638
+ *
639
+ * This source code is licensed under the ISC license.
640
+ * See the LICENSE file in the root directory of this source tree.
641
+ */
642
+ var Paperclip = createLucideIcon("paperclip", [["path", {
643
+ 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",
644
+ key: "1miecu"
645
+ }]]);
646
+ /**
647
+ * @license lucide-react v1.24.0 - ISC
648
+ *
649
+ * This source code is licensed under the ISC license.
650
+ * See the LICENSE file in the root directory of this source tree.
651
+ */
652
+ var Plus = createLucideIcon("plus", [["path", {
653
+ d: "M5 12h14",
654
+ key: "1ays0h"
655
+ }], ["path", {
656
+ d: "M12 5v14",
657
+ key: "s699le"
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 RotateCcw = createLucideIcon("rotate-ccw", [["path", {
666
+ d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",
667
+ key: "1357e3"
668
+ }], ["path", {
669
+ d: "M3 3v5h5",
670
+ key: "1xhq8a"
671
+ }]]);
672
+ /**
673
+ * @license lucide-react v1.24.0 - ISC
674
+ *
675
+ * This source code is licensed under the ISC license.
676
+ * See the LICENSE file in the root directory of this source tree.
677
+ */
678
+ var Send = createLucideIcon("send", [["path", {
679
+ 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",
680
+ key: "1ffxy3"
681
+ }], ["path", {
682
+ d: "m21.854 2.147-10.94 10.939",
683
+ key: "12cjpa"
684
+ }]]);
685
+ /**
686
+ * @license lucide-react v1.24.0 - ISC
687
+ *
688
+ * This source code is licensed under the ISC license.
689
+ * See the LICENSE file in the root directory of this source tree.
690
+ */
691
+ var Square = createLucideIcon("square", [["rect", {
692
+ width: "18",
693
+ height: "18",
694
+ x: "3",
695
+ y: "3",
696
+ rx: "2",
697
+ key: "afitv7"
698
+ }]]);
699
+ /**
700
+ * @license lucide-react v1.24.0 - ISC
701
+ *
702
+ * This source code is licensed under the ISC license.
703
+ * See the LICENSE file in the root directory of this source tree.
704
+ */
705
+ var Star = createLucideIcon("star", [["path", {
706
+ d: "M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z",
707
+ key: "r04s7s"
708
+ }]]);
709
+ /**
710
+ * @license lucide-react v1.24.0 - ISC
711
+ *
712
+ * This source code is licensed under the ISC license.
713
+ * See the LICENSE file in the root directory of this source tree.
714
+ */
715
+ var Upload = createLucideIcon("upload", [
716
+ ["path", {
717
+ d: "M12 3v12",
718
+ key: "1x0j5s"
719
+ }],
720
+ ["path", {
721
+ d: "m17 8-5-5-5 5",
722
+ key: "7q97r8"
723
+ }],
724
+ ["path", {
725
+ d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",
726
+ key: "ih7n3h"
727
+ }]
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 X = createLucideIcon("x", [["path", {
736
+ d: "M18 6 6 18",
737
+ key: "1bl5f8"
738
+ }], ["path", {
739
+ d: "m6 6 12 12",
740
+ key: "d8bk6v"
741
+ }]]);
742
+ //#endregion
743
+ //#region src/ui/sse.ts
744
+ async function* readSse(body) {
745
+ const reader = body.getReader();
746
+ const decoder = new TextDecoder();
747
+ let buf = "";
748
+ for (;;) {
749
+ const { done, value } = await reader.read();
750
+ if (done) break;
751
+ buf += decoder.decode(value, { stream: true });
752
+ let idx;
753
+ while ((idx = buf.indexOf("\n\n")) !== -1) {
754
+ const frame = buf.slice(0, idx);
755
+ buf = buf.slice(idx + 2);
756
+ let event = "message";
757
+ const data = [];
758
+ for (const line of frame.split("\n")) {
759
+ if (!line || line.startsWith(":")) continue;
760
+ if (line.startsWith("event:")) event = line.slice(6).trim();
761
+ else if (line.startsWith("data:")) data.push(line.slice(5).replace(/^ /, ""));
762
+ }
763
+ if (data.length) yield {
764
+ event,
765
+ data: data.join("\n")
766
+ };
767
+ }
768
+ }
769
+ }
770
+ //#endregion
771
+ //#region src/ui/chat-api.ts
772
+ /** Web-chat transport. Session cookies are sent automatically (same-origin), so there is no
773
+ * separate authorization here; the gate is on the server. */
774
+ var BASE = "/api/plugins/webchat/user";
775
+ /** POST a member-level webchat action with a JSON body: the one request path every call
776
+ * in this file (except the SSE stream in `openChannel` and `stopTurn`'s empty-body POST)
777
+ * goes through. Throws `Error(action)` on any non-OK response — every caller below relies
778
+ * on that to build its own error message. */
779
+ async function postAction(action, body) {
780
+ const r = await fetch(`${BASE}/${action}`, {
781
+ method: "POST",
782
+ headers: { "content-type": "application/json" },
783
+ body: JSON.stringify(body)
784
+ });
785
+ if (!r.ok) throw new Error(action);
786
+ return await r.json();
787
+ }
788
+ /**
789
+ * Throws on any rejection, including 404 (“plugin was disabled AFTER the widget bundle
790
+ * loaded”). There is deliberately no separate `null` for this case: the caller keeps
791
+ * the current thread list either way, so two return codes would mean the same thing.
792
+ */
793
+ async function fetchThreads() {
794
+ return (await postAction("threads", {})).threads;
795
+ }
796
+ async function fetchHistory(convId) {
797
+ return postAction("history", { convId });
798
+ }
799
+ async function fetchAgents(convId) {
800
+ const out = await postAction("agents", { convId });
801
+ return {
802
+ agents: out.agents ?? [],
803
+ defaultAgentId: out.defaultAgentId ?? null,
804
+ selection: out.selection ?? {
805
+ agentId: null,
806
+ presetId: null
807
+ },
808
+ starters: out.starters ?? []
809
+ };
810
+ }
811
+ async function selectAgent(convId, patch) {
812
+ return (await postAction("selectAgent", {
813
+ convId,
814
+ ...patch
815
+ })).selection;
816
+ }
817
+ /** Mark a conversation private, or reopen it. Only ever meaningful for the
818
+ * caller's OWN conversation — the server refuses (returns the unchanged
819
+ * visibility) for anything else, so the UI only offers this control on the
820
+ * viewer's own rows in the first place. */
821
+ async function setThreadVisibility(convId, makePrivate) {
822
+ return postAction("setVisibility", {
823
+ convId,
824
+ private: makePrivate
825
+ });
826
+ }
827
+ /** Records the user turn and starts the agent turn running — an ordinary POST that
828
+ * resolves as soon as the turn has STARTED, not once it finishes (`sendAction`,
829
+ * `../runtime/send.ts`). It streams nothing any more: every live update — the
830
+ * answer as it is written, its completion, an error — arrives on the
831
+ * conversation's own channel (`openChannel`, below), which `use-chat.ts` keeps
832
+ * open for as long as this conversation is on screen, whether or not THIS call
833
+ * is what started the turn currently running. */
834
+ async function sendMessage(req) {
835
+ return postAction("send", {
836
+ convId: req.convId,
837
+ text: req.text,
838
+ replyTo: req.replyTo,
839
+ context: req.context,
840
+ ...req.attachments?.length ? { attachments: req.attachments } : {}
841
+ });
842
+ }
843
+ /** Abort the turn currently running on `convId`, if there is one (`stopAction`,
844
+ * `../runtime/actions.ts`). That action returns `Promise<void>` — the route sends
845
+ * an empty 200 body, the same uniform non-answer whether a live turn was
846
+ * actually aborted, none was running, or the caller may not write to the
847
+ * conversation — so this deliberately does NOT go through `postAction`, whose
848
+ * `.json()` would throw on that empty body. */
849
+ async function stopTurn(convId) {
850
+ if (!(await fetch(`${BASE}/stop`, {
851
+ method: "POST",
852
+ headers: { "content-type": "application/json" },
853
+ body: JSON.stringify({ convId })
854
+ })).ok) throw new Error("stop");
855
+ }
856
+ /** Subscribe to `convId`'s channel and hold the connection open, delivering every
857
+ * frame to `handlers` until the server ends the stream (the conversation went
858
+ * private on this viewer, `setVisibility`'s eviction; a restart; a network drop)
859
+ * or `signal` aborts it. Resolves (does not reject) when the server ends the
860
+ * stream cleanly, same as a plain fetch whose body simply runs out — rejects only
861
+ * on a genuine transport failure or `signal` aborting. Either way it is
862
+ * `use-chat.ts`'s job to reconnect with backoff while the conversation is still
863
+ * the one on screen: there is no `Last-Event-ID` bookkeeping and no server-side
864
+ * buffer here, by design (see the design doc's "Reconnect, and the race that
865
+ * comes with it") — missed frames are recovered by re-reading history, never by
866
+ * replaying the stream. */
867
+ async function openChannel(convId, handlers, signal) {
868
+ const r = await fetch(`${BASE}/channel/stream`, {
869
+ method: "POST",
870
+ headers: { "content-type": "application/json" },
871
+ body: JSON.stringify({ convId }),
872
+ signal
873
+ });
874
+ if (!r.ok || !r.body) throw new Error(`http ${r.status}`);
875
+ for await (const frame of readSse(r.body)) try {
876
+ const data = JSON.parse(frame.data);
877
+ if (frame.event === "user") handlers.onUser({
878
+ msgId: String(data["msgId"] ?? ""),
879
+ text: String(data["text"] ?? ""),
880
+ ts: typeof data["ts"] === "number" ? data["ts"] : 0,
881
+ ...Array.isArray(data["attachments"]) ? { attachments: data["attachments"] } : {}
882
+ });
883
+ else if (frame.event === "message") handlers.onMessage({
884
+ msgId: String(data["msgId"] ?? ""),
885
+ text: String(data["text"] ?? "")
886
+ });
887
+ else if (frame.event === "delta") handlers.onDelta({
888
+ msgId: String(data["msgId"] ?? ""),
889
+ text: String(data["text"] ?? "")
890
+ });
891
+ else if (frame.event === "reasoning") handlers.onReasoning({
892
+ msgId: String(data["msgId"] ?? ""),
893
+ text: String(data["text"] ?? "")
894
+ });
895
+ else if (frame.event === "done") handlers.onDone({
896
+ msgId: data["msgId"] ?? null,
897
+ parentMsgId: String(data["parentMsgId"] ?? ""),
898
+ suggestions: Array.isArray(data["suggestions"]) ? data["suggestions"] : null
899
+ });
900
+ else if (frame.event === "error") handlers.onError({
901
+ message: data["message"] ?? null,
902
+ parentMsgId: typeof data["parentMsgId"] === "string" ? data["parentMsgId"] : null
903
+ });
904
+ } catch (e) {
905
+ handlers.onError({
906
+ message: `malformed frame: ${String(e)}`,
907
+ parentMsgId: null
908
+ });
909
+ }
910
+ }
911
+ //#endregion
912
+ //#region src/ui/use-chat.ts
913
+ function newId() {
914
+ return crypto.randomUUID();
915
+ }
916
+ /** The message id a frame reports as its OUTCOME, for dedup against history on
917
+ * flush. `error` is never persisted (a failed turn stores nothing), so it is
918
+ * never a duplicate of anything a history read could carry — its
919
+ * `parentMsgId` exists only to scope the send-lock clear below to the
920
+ * viewer's own turn, a different question from dedup. */
921
+ function frameHistoryId(frame) {
922
+ switch (frame.kind) {
923
+ case "user":
924
+ case "message":
925
+ case "delta":
926
+ case "reasoning": return frame.data.msgId;
927
+ case "done": return frame.data.msgId;
928
+ case "error": return null;
929
+ }
930
+ }
931
+ var RECONNECT_BASE_MS = 1e3;
932
+ var RECONNECT_MAX_MS = 3e4;
933
+ function useChat() {
934
+ const [threads, setThreads] = useState([]);
935
+ const [convId, setConvId] = useState(null);
936
+ const [messages, setMessages] = useState([]);
937
+ const [pending, setPending] = useState(null);
938
+ const [pendingReasoning, setPendingReasoning] = useState(null);
939
+ const [error, setError] = useState(null);
940
+ const [agents, setAgents] = useState([]);
941
+ const [defaultAgentId, setDefaultAgentId] = useState(null);
942
+ const [selection, setSelection] = useState({
943
+ agentId: null,
944
+ presetId: null
945
+ });
946
+ const [starters, setStarters] = useState([]);
947
+ const [suggestions, setSuggestions] = useState(null);
948
+ const headRef = useRef(null);
949
+ const sendingRef = useRef(false);
950
+ const myTurnMsgIdRef = useRef(null);
951
+ const pendingTextRef = useRef("");
952
+ const pendingReasoningRef = useRef(null);
953
+ const turnRef = useRef(0);
954
+ const aliveRef = useRef(true);
955
+ useEffect(() => {
956
+ aliveRef.current = true;
957
+ return () => {
958
+ aliveRef.current = false;
959
+ turnRef.current++;
960
+ };
961
+ }, []);
962
+ const loadThreads = useCallback(() => {
963
+ fetchThreads().then((list) => {
964
+ if (aliveRef.current) setThreads(list);
965
+ }).catch(() => {});
966
+ }, []);
967
+ useEffect(() => {
968
+ if (!convId) return;
969
+ let cancelled = false;
970
+ fetchAgents(convId).then((r) => {
971
+ if (cancelled) return;
972
+ setAgents(r.agents);
973
+ setDefaultAgentId(r.defaultAgentId);
974
+ setSelection(r.selection);
975
+ setStarters(r.starters);
976
+ }).catch(() => {});
977
+ return () => {
978
+ cancelled = true;
979
+ };
980
+ }, [convId]);
981
+ /** Apply one channel frame to state. Called either live (as it arrives, once a
982
+ * connection has finished its history flush) or from the buffer a fresh
983
+ * (re)connect built while history was in flight — `historyIds` is non-null
984
+ * ONLY for the latter, and gates every frame on it: a frame whose outcome the
985
+ * just-fetched history already carries is dropped rather than reapplied, the
986
+ * one piece of ordering the design doc calls out by name. Live frames never
987
+ * need this check — their message id cannot already be in a history snapshot
988
+ * read before they existed. */
989
+ const applyFrame = useCallback((frame, historyIds) => {
990
+ const outcomeId = frameHistoryId(frame);
991
+ const isDuplicate = historyIds !== null && outcomeId !== null && historyIds.has(outcomeId);
992
+ switch (frame.kind) {
993
+ case "user": {
994
+ if (isDuplicate) break;
995
+ const { msgId, text, ts, attachments } = frame.data;
996
+ setMessages((prev) => prev.some((m) => m.msgId === msgId) ? prev : [...prev, {
997
+ msgId,
998
+ role: "user",
999
+ text,
1000
+ ts,
1001
+ ...attachments?.length ? { attachments } : {}
1002
+ }]);
1003
+ break;
1004
+ }
1005
+ case "message":
1006
+ case "delta":
1007
+ if (isDuplicate) break;
1008
+ pendingTextRef.current = frame.data.text;
1009
+ setPending(frame.data.text);
1010
+ break;
1011
+ case "reasoning":
1012
+ if (isDuplicate) break;
1013
+ pendingReasoningRef.current = frame.data.text;
1014
+ setPendingReasoning(frame.data.text);
1015
+ break;
1016
+ case "done": {
1017
+ const { msgId, parentMsgId, suggestions: nextSuggestions } = frame.data;
1018
+ headRef.current = msgId ?? parentMsgId;
1019
+ setSuggestions(nextSuggestions ?? null);
1020
+ if (!isDuplicate && msgId && pendingTextRef.current) {
1021
+ const text = pendingTextRef.current;
1022
+ const reasoning = pendingReasoningRef.current;
1023
+ setMessages((prev) => prev.some((m) => m.msgId === msgId) ? prev : [...prev, {
1024
+ msgId,
1025
+ role: "assistant",
1026
+ text,
1027
+ ts: Math.floor(Date.now() / 1e3),
1028
+ ...reasoning ? { reasoning } : {}
1029
+ }]);
1030
+ }
1031
+ pendingTextRef.current = "";
1032
+ pendingReasoningRef.current = null;
1033
+ setPending(null);
1034
+ setPendingReasoning(null);
1035
+ if (myTurnMsgIdRef.current !== null && myTurnMsgIdRef.current === parentMsgId) {
1036
+ sendingRef.current = false;
1037
+ myTurnMsgIdRef.current = null;
1038
+ }
1039
+ loadThreads();
1040
+ break;
1041
+ }
1042
+ case "error":
1043
+ setError(frame.data.message ?? "error");
1044
+ pendingTextRef.current = "";
1045
+ pendingReasoningRef.current = null;
1046
+ setPending(null);
1047
+ setPendingReasoning(null);
1048
+ if (myTurnMsgIdRef.current !== null && myTurnMsgIdRef.current === frame.data.parentMsgId) {
1049
+ sendingRef.current = false;
1050
+ myTurnMsgIdRef.current = null;
1051
+ }
1052
+ break;
1053
+ }
1054
+ }, [loadThreads]);
1055
+ useEffect(() => {
1056
+ if (!convId) return;
1057
+ const chatConvId = convId;
1058
+ let cancelled = false;
1059
+ let reconnectTimer = null;
1060
+ let attempt = 0;
1061
+ const ctl = new AbortController();
1062
+ async function connectOnce() {
1063
+ if (cancelled) return;
1064
+ let buffering = true;
1065
+ let receivedFrame = false;
1066
+ const buffer = [];
1067
+ const dispatch = (frame) => {
1068
+ if (cancelled) return;
1069
+ receivedFrame = true;
1070
+ if (buffering) buffer.push(frame);
1071
+ else applyFrame(frame, null);
1072
+ };
1073
+ const settled = openChannel(chatConvId, {
1074
+ onUser: (data) => dispatch({
1075
+ kind: "user",
1076
+ data
1077
+ }),
1078
+ onMessage: (data) => dispatch({
1079
+ kind: "message",
1080
+ data
1081
+ }),
1082
+ onDelta: (data) => dispatch({
1083
+ kind: "delta",
1084
+ data
1085
+ }),
1086
+ onReasoning: (data) => dispatch({
1087
+ kind: "reasoning",
1088
+ data
1089
+ }),
1090
+ onDone: (data) => dispatch({
1091
+ kind: "done",
1092
+ data
1093
+ }),
1094
+ onError: (data) => dispatch({
1095
+ kind: "error",
1096
+ data
1097
+ })
1098
+ }, ctl.signal).then(() => true, () => false);
1099
+ try {
1100
+ const h = await fetchHistory(chatConvId);
1101
+ if (cancelled) return;
1102
+ headRef.current = h.headMsgId;
1103
+ const historyIds = new Set(h.messages.map((m) => m.msgId));
1104
+ setMessages((prev) => {
1105
+ const ownPendingId = myTurnMsgIdRef.current;
1106
+ const stillUnconfirmed = prev.filter((m) => !historyIds.has(m.msgId) && (m.msgId.startsWith("local-") || m.msgId === ownPendingId));
1107
+ return stillUnconfirmed.length ? [...h.messages, ...stillUnconfirmed] : h.messages;
1108
+ });
1109
+ setSuggestions(h.messages.at(-1)?.suggestions ?? null);
1110
+ buffering = false;
1111
+ for (const frame of buffer) applyFrame(frame, historyIds);
1112
+ } catch (e) {
1113
+ if (cancelled) return;
1114
+ setError(e.message);
1115
+ buffering = false;
1116
+ for (const frame of buffer) applyFrame(frame, null);
1117
+ }
1118
+ await settled;
1119
+ if (cancelled) return;
1120
+ if (receivedFrame) attempt = 0;
1121
+ const delay = Math.min(RECONNECT_BASE_MS * 2 ** attempt, RECONNECT_MAX_MS);
1122
+ attempt++;
1123
+ reconnectTimer = setTimeout(() => void connectOnce(), delay);
1124
+ }
1125
+ connectOnce();
1126
+ return () => {
1127
+ cancelled = true;
1128
+ if (reconnectTimer) clearTimeout(reconnectTimer);
1129
+ ctl.abort();
1130
+ };
1131
+ }, [convId, applyFrame]);
1132
+ const chooseAgent = useCallback((next) => {
1133
+ const previous = selection;
1134
+ setSelection(next);
1135
+ if (convId) selectAgent(convId, next).then(setSelection).catch((err) => {
1136
+ setSelection(previous);
1137
+ setError(err instanceof Error ? err.message : String(err));
1138
+ });
1139
+ }, [convId, selection]);
1140
+ const togglePrivate = useCallback((convId, next) => {
1141
+ const previous = threads.find((t) => t.convId === convId)?.private ?? false;
1142
+ setThreads((prev) => prev.map((t) => t.convId === convId ? {
1143
+ ...t,
1144
+ private: next
1145
+ } : t));
1146
+ setThreadVisibility(convId, next).then((r) => {
1147
+ setThreads((prev) => prev.map((t) => t.convId === convId ? {
1148
+ ...t,
1149
+ private: r.visibility === "private"
1150
+ } : t));
1151
+ }).catch(() => {
1152
+ setThreads((prev) => prev.map((t) => t.convId === convId ? {
1153
+ ...t,
1154
+ private: previous
1155
+ } : t));
1156
+ });
1157
+ }, [threads]);
1158
+ const attachmentsAllowed = useMemo(() => {
1159
+ const media = agents.find((a) => a.id === selection.agentId)?.media;
1160
+ if (!media) return true;
1161
+ return media.image.accepts.length > 0 || media.document.accepts.length > 0;
1162
+ }, [agents, selection]);
1163
+ const newConversation = useCallback(() => {
1164
+ turnRef.current++;
1165
+ sendingRef.current = false;
1166
+ myTurnMsgIdRef.current = null;
1167
+ headRef.current = null;
1168
+ setConvId(newId());
1169
+ setMessages([]);
1170
+ setPending(null);
1171
+ setPendingReasoning(null);
1172
+ pendingTextRef.current = "";
1173
+ pendingReasoningRef.current = null;
1174
+ setError(null);
1175
+ setSuggestions(null);
1176
+ }, []);
1177
+ const open = useCallback(async (id) => {
1178
+ turnRef.current++;
1179
+ sendingRef.current = false;
1180
+ myTurnMsgIdRef.current = null;
1181
+ setConvId(id);
1182
+ headRef.current = null;
1183
+ setMessages([]);
1184
+ setPending(null);
1185
+ setPendingReasoning(null);
1186
+ pendingTextRef.current = "";
1187
+ pendingReasoningRef.current = null;
1188
+ setError(null);
1189
+ setSuggestions(null);
1190
+ }, []);
1191
+ const stop = useCallback(() => {
1192
+ if (!convId) return;
1193
+ stopTurn(convId).catch(() => {});
1194
+ }, [convId]);
1195
+ return {
1196
+ threads,
1197
+ convId,
1198
+ messages,
1199
+ pending,
1200
+ pendingReasoning,
1201
+ error,
1202
+ agents,
1203
+ defaultAgentId,
1204
+ selection,
1205
+ starters,
1206
+ suggestions,
1207
+ attachmentsAllowed,
1208
+ chooseAgent,
1209
+ togglePrivate,
1210
+ send: useCallback((text, context, attachments = []) => {
1211
+ if (!text.trim() || sendingRef.current) return false;
1212
+ sendingRef.current = true;
1213
+ const myTurn = turnRef.current;
1214
+ const id = convId ?? newId();
1215
+ if (convId === null) setConvId(id);
1216
+ setError(null);
1217
+ setSuggestions(null);
1218
+ const localMsgId = `local-${newId()}`;
1219
+ setMessages((prev) => [...prev, {
1220
+ msgId: localMsgId,
1221
+ role: "user",
1222
+ text,
1223
+ ts: Math.floor(Date.now() / 1e3),
1224
+ ...attachments.length ? { attachments } : {}
1225
+ }]);
1226
+ setPending("");
1227
+ (async () => {
1228
+ try {
1229
+ const { msgId } = await sendMessage({
1230
+ convId: id,
1231
+ text,
1232
+ replyTo: headRef.current,
1233
+ context,
1234
+ attachments
1235
+ });
1236
+ if (turnRef.current !== myTurn) return;
1237
+ if (msgId === null) {
1238
+ sendingRef.current = false;
1239
+ setPending(null);
1240
+ setPendingReasoning(null);
1241
+ return;
1242
+ }
1243
+ setMessages((prev) => prev.some((m) => m.msgId === msgId) ? prev.filter((m) => m.msgId !== localMsgId) : prev.map((m) => m.msgId === localMsgId ? {
1244
+ ...m,
1245
+ msgId
1246
+ } : m));
1247
+ myTurnMsgIdRef.current = msgId;
1248
+ } catch (e) {
1249
+ if (turnRef.current === myTurn) {
1250
+ sendingRef.current = false;
1251
+ setError(e.message);
1252
+ setPending(null);
1253
+ setPendingReasoning(null);
1254
+ }
1255
+ }
1256
+ })();
1257
+ return true;
1258
+ }, [convId]),
1259
+ open,
1260
+ newConversation,
1261
+ loadThreads,
1262
+ stop
1263
+ };
1264
+ }
1265
+ //#endregion
1266
+ //#region src/ui/use-page-context.ts
1267
+ /** The record page's own heading — a BEST-EFFORT read of the app's own rendered DOM,
1268
+ * not a data path. `RecordContent` (`packages/web/display/index.tsx`) marks the
1269
+ * TITLE element itself `role="heading" aria-level="1"` (the role sits on the title
1270
+ * element alone, never a wrapper — a wrapping row's role would pull every sibling
1271
+ * body field's text into the name, and the title is now one field among many in the
1272
+ * body); today that is the only element in the whole app carrying it,
1273
+ * so this selector cannot pick up some OTHER record's heading by mistake. It exists at
1274
+ * all because the chat overlay is a sibling of the routed page (mounted in the app
1275
+ * shell's own slot), never a descendant of it, so there is no React tree — no prop, no
1276
+ * context — to read the value from, and this package (`plugin-webchat`) has no data
1277
+ * path of its own to the open record either. Reading the painted DOM instead of
1278
+ * fetching the record: the SPA already has it open and displaying this text, and a
1279
+ * server-side lookup would be a query per turn for a value the browser is holding.
1280
+ *
1281
+ * WHEN it is read is the whole safety argument, so it is stated rather than assumed:
1282
+ * reading the DOM during React's RENDER phase sees the PREVIOUSLY COMMITTED tree, so a
1283
+ * navigation from one record to another produced the new path and id beside the OLD
1284
+ * record's name — a fact stated WRONG, which the base prompt tells the model to trust.
1285
+ * Hence `usePageContext` returns a READER the composer calls in its send handler: an
1286
+ * event handler runs after commit, so the heading and the path it is reported with
1287
+ * come from the same painted page. That is also what the design asks for — the page
1288
+ * the user was on AT THE MOMENT OF SENDING, and nowhere else.
1289
+ *
1290
+ * What is guaranteed, then: the title is read from the DOM as it stands when the turn
1291
+ * is sent. What is NOT guaranteed is that the title is present — a heading not yet
1292
+ * painted (a record still loading) and an untitled record's Dash placeholder both
1293
+ * yield no title, and an absent fact is the harmless case. One case yields a WRONG
1294
+ * one: if the app ever grows a SECOND element with this exact role/level (a preview
1295
+ * dialog, a relation picker, a plugin's own record renderer), `querySelector` returns
1296
+ * whichever comes first in document order, not necessarily the record the user is
1297
+ * actually looking at. Whoever eventually gives the overlay a real data path to the
1298
+ * open record should read this as the reason it was ever DOM-based, not rediscover it. */
1299
+ var EMPTY_TITLE = "—";
1300
+ function recordHeading() {
1301
+ if (typeof document === "undefined") return void 0;
1302
+ const text = document.querySelector("[role=\"heading\"][aria-level=\"1\"]")?.textContent?.trim();
1303
+ return text && text !== EMPTY_TITLE ? text : void 0;
1304
+ }
1305
+ /** SPA routes: `/:library/:type[/:id]`; system branches (settings, browse) are
1306
+ * returned by path only. Pure but for the heading read, which is why it takes the
1307
+ * path rather than calling the router itself. */
1308
+ function pageContextAt(pathname) {
1309
+ const parts = pathname.split("/").filter(Boolean);
1310
+ const system = /* @__PURE__ */ new Set([
1311
+ "settings",
1312
+ "browse",
1313
+ "plugins",
1314
+ "login",
1315
+ "setup"
1316
+ ]);
1317
+ if (parts.length < 2 || system.has(parts[0])) return { path: pathname };
1318
+ const [library, type, id] = parts;
1319
+ const isRecord = Boolean(id && id !== "new");
1320
+ const title = isRecord ? recordHeading() : void 0;
1321
+ return {
1322
+ path: pathname,
1323
+ library,
1324
+ type,
1325
+ ...isRecord ? { id } : {},
1326
+ ...title ? { title } : {}
1327
+ };
1328
+ }
1329
+ /** Current-page context for the agent, as a reader called AT SEND TIME rather than a
1330
+ * value computed while rendering — see the heading comment above for why that
1331
+ * distinction is the difference between an absent title and a wrong one. The path
1332
+ * comes from the render that is currently committed, which is the same tree the
1333
+ * reader's heading lookup walks. */
1334
+ function usePageContext() {
1335
+ const { pathname } = useLocation();
1336
+ return useCallback(() => pageContextAt(pathname), [pathname]);
1337
+ }
1338
+ //#endregion
1339
+ //#region src/ui/chat-nav.tsx
1340
+ /** What to do with the widget when the user follows a link from a response.
1341
+ * ChatWidget supplies the callback (it owns mode/open), avoiding a prop
1342
+ * threaded through ChatMessages and Bubble. */
1343
+ var ChatNavContext = createContext(() => {});
1344
+ function ChatNavProvider({ onNavigate, children }) {
1345
+ return /* @__PURE__ */ jsx(ChatNavContext.Provider, {
1346
+ value: onNavigate,
1347
+ children
1348
+ });
1349
+ }
1350
+ function useChatNav() {
1351
+ return useContext(ChatNavContext);
1352
+ }
1353
+ //#endregion
1354
+ //#region src/ui/chat-markdown.tsx
1355
+ /** Render markdown received FROM THE MODEL, an untrusted source.
1356
+ *
1357
+ * One renderer serves every markdown surface now — the record page and this bubble alike —
1358
+ * so this component is a density layer over the kit's `MarkdownView` and nothing more. What
1359
+ * it used to own itself now lives there, for both surfaces:
1360
+ *
1361
+ * - raw HTML never becomes markup. The vocabulary renderer has NO raw-HTML pass, so a tag
1362
+ * the model wrote is escaped before anything sees it; the barrier react-markdown gave
1363
+ * structurally is still structural, just one layer earlier. DOMPurify is defence in depth.
1364
+ * - an internal link goes through the router, so the chat stays mounted, and `onNavigate`
1365
+ * closes the panel on the way out.
1366
+ * - an address that leads nowhere (`javascript:`, a fabricated shelf) keeps its text and
1367
+ * loses its anchor — this bubble's old rule, now everyone's.
1368
+ * - a table gets its scroll wrapper from the web tag map itself.
1369
+ */
1370
+ function ChatMarkdown({ text, className }) {
1371
+ const onNavigate = useChatNav();
1372
+ return /* @__PURE__ */ jsx(MarkdownView, {
1373
+ value: text,
1374
+ className: className ?? "md-static md-chat",
1375
+ onNavigate
1376
+ });
1377
+ }
1378
+ //#endregion
1379
+ //#region src/ui/ChatMessages.tsx
1380
+ function ChatMessages({ messages, pending, pendingReasoning, trailing }) {
1381
+ const { t } = useTranslation();
1382
+ const listRef = useRef(null);
1383
+ useEffect(() => {
1384
+ const el = listRef.current;
1385
+ if (el) el.scrollTop = el.scrollHeight;
1386
+ }, [messages.length, pending]);
1387
+ return /* @__PURE__ */ jsxs("div", {
1388
+ ref: listRef,
1389
+ "data-chat-list": true,
1390
+ className: "flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-2",
1391
+ children: [!messages.length && pending === null && pendingReasoning === null ? /* @__PURE__ */ jsx("div", {
1392
+ className: "flex-1 grid place-items-center text-muted-foreground text-sm",
1393
+ children: t("webchat.empty")
1394
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [messages.map((m) => /* @__PURE__ */ jsx(Bubble, {
1395
+ role: m.role,
1396
+ text: m.text,
1397
+ reasoning: m.reasoning,
1398
+ attachments: m.attachments
1399
+ }, m.msgId)), pending !== null && /* @__PURE__ */ jsx(Bubble, {
1400
+ role: "assistant",
1401
+ text: pending || t("webchat.thinking"),
1402
+ reasoning: pendingReasoning
1403
+ })] }), trailing]
1404
+ });
1405
+ }
1406
+ /**
1407
+ * memo: during streaming, the parent re-renders on EVERY delta (several times per
1408
+ * second). Without memo, each tick would re-render (and therefore re-parse markdown)
1409
+ * for every completed message while only one bubble at the bottom is changing.
1410
+ * The props are primitives, so the default shallow comparison is sufficient.
1411
+ */
1412
+ var Bubble = memo(function Bubble({ role, text, reasoning, attachments }) {
1413
+ const { t } = useTranslation();
1414
+ if (role === "user") return /* @__PURE__ */ jsxs("div", {
1415
+ className: "self-end max-w-[85%] rounded-2xl border border-ring bg-muted px-3 py-2 text-[0.8125rem] whitespace-pre-wrap break-words",
1416
+ children: [attachments?.length ? /* @__PURE__ */ jsx("div", {
1417
+ className: "mb-1.5 flex flex-wrap gap-1",
1418
+ children: attachments.map((a) => /* @__PURE__ */ jsx("span", {
1419
+ className: "inline-flex max-w-full items-center rounded-md bg-background px-1.5 py-0.5 text-xs",
1420
+ title: a.name,
1421
+ children: a.label ?? a.name
1422
+ }, a.name))
1423
+ }) : null, text]
1424
+ });
1425
+ return /* @__PURE__ */ jsxs("div", {
1426
+ className: "self-start max-w-[95%] min-w-0 rounded-2xl bg-muted px-3 py-2 text-sm break-words",
1427
+ children: [reasoning ? /* @__PURE__ */ jsxs("details", {
1428
+ className: "mb-2 rounded-lg border border-border px-2 py-1",
1429
+ children: [/* @__PURE__ */ jsx("summary", {
1430
+ className: "cursor-pointer select-none text-xs text-muted-foreground",
1431
+ children: t("webchat.reasoning")
1432
+ }), /* @__PURE__ */ jsx(ChatMarkdown, {
1433
+ text: reasoning,
1434
+ className: "md-static md-chat md-chat-sub mt-1 text-muted-foreground"
1435
+ })]
1436
+ }) : null, /* @__PURE__ */ jsx(ChatMarkdown, {
1437
+ text,
1438
+ className: "md-static md-chat"
1439
+ })]
1440
+ });
1441
+ });
1442
+ //#endregion
1443
+ //#region src/ui/AgentPicker.tsx
1444
+ var MAX_STARS = 5;
1445
+ /** Rating pair: "3 star, 3 clock". Two digits read at a glance and stay the same
1446
+ * width on every row, which a free-text hint ("fastest and cheapest" next to
1447
+ * "quick") does not. Both are read the same way — the star is what you get, the
1448
+ * clock is what it costs you in waiting — so a heavier model raises both numbers.
1449
+ * Falls back to the hint when the agent publishes no ratings. */
1450
+ function PresetRating({ quality, time, hint }) {
1451
+ const { t } = useTranslation();
1452
+ if (quality === void 0 || time === void 0) return hint ? /* @__PURE__ */ jsx("span", {
1453
+ className: "shrink-0 text-muted-foreground",
1454
+ children: hint
1455
+ }) : null;
1456
+ const both = quality !== time;
1457
+ return /* @__PURE__ */ jsxs("span", {
1458
+ className: "flex shrink-0 items-center gap-1.5 text-muted-foreground tabular-nums",
1459
+ children: [/* @__PURE__ */ jsxs("span", {
1460
+ className: "flex items-center gap-0.5",
1461
+ title: t("webchat.quality", { value: quality }),
1462
+ children: [
1463
+ quality,
1464
+ /* @__PURE__ */ jsx(Star, {
1465
+ size: 11,
1466
+ "aria-hidden": true,
1467
+ className: "shrink-0"
1468
+ }),
1469
+ /* @__PURE__ */ jsx("span", {
1470
+ className: "sr-only",
1471
+ children: t("webchat.quality", { value: quality })
1472
+ })
1473
+ ]
1474
+ }), both ? /* @__PURE__ */ jsxs("span", {
1475
+ className: "flex items-center gap-0.5",
1476
+ title: t("webchat.time", { value: time }),
1477
+ children: [
1478
+ time,
1479
+ /* @__PURE__ */ jsx(Clock, {
1480
+ size: 11,
1481
+ "aria-hidden": true,
1482
+ className: "shrink-0"
1483
+ }),
1484
+ /* @__PURE__ */ jsx("span", {
1485
+ className: "sr-only",
1486
+ children: t("webchat.time", { value: time })
1487
+ })
1488
+ ]
1489
+ }) : /* @__PURE__ */ jsx("span", {
1490
+ className: "sr-only",
1491
+ children: t("webchat.time", { value: time })
1492
+ })]
1493
+ });
1494
+ }
1495
+ /**
1496
+ * The presets of ONE agent, ordered by their level, when they form a complete ladder:
1497
+ * a unique `quality` per preset, contiguous from 1, at most five rungs. Only then does a
1498
+ * star count address a preset without ambiguity, and only then may the picker stop
1499
+ * printing model names. Anything else — two agents, a gap in the ladder, a preset with
1500
+ * no rating — returns null and the picker keeps its list of names.
1501
+ */
1502
+ function starLadder(rows, multi) {
1503
+ if (multi || rows.length < 2 || rows.length > MAX_STARS) return null;
1504
+ const byLevel = Array.from({ length: rows.length });
1505
+ for (const r of rows) {
1506
+ const q = r.quality;
1507
+ if (q === void 0 || !Number.isInteger(q) || q < 1 || q > rows.length) return null;
1508
+ if (byLevel[q - 1]) return null;
1509
+ byLevel[q - 1] = r;
1510
+ }
1511
+ return byLevel.every((r) => r !== void 0) ? byLevel : null;
1512
+ }
1513
+ /** Flatten the catalog to one list: with a single registered agent the agent
1514
+ * level is noise, and with several the agent name simply prefixes its presets. */
1515
+ function AgentPicker({ agents, defaultAgentId, selection, onChange }) {
1516
+ const { t } = useTranslation();
1517
+ const [open, setOpen] = useState(false);
1518
+ const [preview, setPreview] = useState(null);
1519
+ const rootRef = useRef(null);
1520
+ const triggerRef = useRef(null);
1521
+ useEffect(() => {
1522
+ if (!open) return;
1523
+ function onPointerDown(e) {
1524
+ if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
1525
+ }
1526
+ function onKeyDown(e) {
1527
+ if (e.key === "Escape") {
1528
+ setOpen(false);
1529
+ triggerRef.current?.focus();
1530
+ }
1531
+ }
1532
+ document.addEventListener("pointerdown", onPointerDown);
1533
+ document.addEventListener("keydown", onKeyDown);
1534
+ return () => {
1535
+ document.removeEventListener("pointerdown", onPointerDown);
1536
+ document.removeEventListener("keydown", onKeyDown);
1537
+ };
1538
+ }, [open]);
1539
+ const multi = agents.length > 1;
1540
+ const rows = agents.flatMap((a) => a.presets.map((p) => ({
1541
+ agentId: a.id,
1542
+ presetId: p.id,
1543
+ label: multi ? `${a.title} · ${p.title}` : p.title,
1544
+ hint: p.hint,
1545
+ quality: p.quality,
1546
+ time: p.time,
1547
+ isDefault: p.default === true
1548
+ })));
1549
+ if (rows.length === 0) return null;
1550
+ 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];
1551
+ const ladder = starLadder(rows, multi);
1552
+ if (ladder) {
1553
+ const level = ladder.indexOf(current) + 1;
1554
+ const shown = preview ?? level;
1555
+ const levelName = (row, n) => `${row.label} · ${t("webchat.level", {
1556
+ value: n,
1557
+ max: ladder.length
1558
+ })}`;
1559
+ return /* @__PURE__ */ jsx("ul", {
1560
+ "aria-label": t("webchat.agentPicker"),
1561
+ className: "flex shrink-0 items-center",
1562
+ onPointerLeave: () => setPreview(null),
1563
+ children: ladder.map((r, i) => {
1564
+ const n = i + 1;
1565
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
1566
+ type: "button",
1567
+ ...r === current ? { "aria-current": true } : {},
1568
+ "aria-label": levelName(r, n),
1569
+ className: "flex rounded p-1 focus-visible:outline-2 focus-visible:outline-ring",
1570
+ onPointerEnter: () => setPreview(n),
1571
+ onFocus: () => setPreview(n),
1572
+ onBlur: () => setPreview(null),
1573
+ onClick: () => onChange({
1574
+ agentId: r.agentId,
1575
+ presetId: r.presetId
1576
+ }),
1577
+ children: /* @__PURE__ */ jsx(Star, {
1578
+ size: 16,
1579
+ "aria-hidden": true,
1580
+ className: n <= shown ? "fill-current" : "text-border",
1581
+ style: n <= shown ? { color: toneVar("yellow") } : void 0
1582
+ })
1583
+ }) }, `${r.agentId}/${r.presetId}`);
1584
+ })
1585
+ });
1586
+ }
1587
+ return /* @__PURE__ */ jsxs("div", {
1588
+ className: "relative",
1589
+ ref: rootRef,
1590
+ children: [/* @__PURE__ */ jsxs("button", {
1591
+ ref: triggerRef,
1592
+ type: "button",
1593
+ "aria-haspopup": "menu",
1594
+ "aria-expanded": open,
1595
+ 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",
1596
+ onClick: () => setOpen((v) => !v),
1597
+ children: [/* @__PURE__ */ jsx("span", {
1598
+ className: "max-w-40 truncate",
1599
+ children: current.label
1600
+ }), /* @__PURE__ */ jsx(ChevronDown, {
1601
+ size: 12,
1602
+ className: "shrink-0"
1603
+ })]
1604
+ }), open && /* @__PURE__ */ jsx("ul", {
1605
+ "aria-label": t("webchat.agentPicker"),
1606
+ 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",
1607
+ children: rows.map((r) => {
1608
+ const selected = r === current;
1609
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
1610
+ type: "button",
1611
+ ...selected ? { "aria-current": true } : {},
1612
+ 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",
1613
+ onClick: () => {
1614
+ setOpen(false);
1615
+ onChange({
1616
+ agentId: r.agentId,
1617
+ presetId: r.presetId
1618
+ });
1619
+ },
1620
+ children: [
1621
+ /* @__PURE__ */ jsx(Check, {
1622
+ size: 12,
1623
+ className: selected ? "shrink-0 text-primary" : "shrink-0 opacity-0"
1624
+ }),
1625
+ /* @__PURE__ */ jsx("span", {
1626
+ className: "min-w-0 flex-1 truncate",
1627
+ children: r.label
1628
+ }),
1629
+ /* @__PURE__ */ jsx(PresetRating, {
1630
+ quality: r.quality,
1631
+ time: r.time,
1632
+ hint: r.hint
1633
+ })
1634
+ ]
1635
+ }) }, `${r.agentId}/${r.presetId}`);
1636
+ })
1637
+ })]
1638
+ });
1639
+ }
1640
+ //#endregion
1641
+ //#region src/ui/ChatComposer.tsx
1642
+ function ChatComposer({ streaming, onSend, onStop, attachments, uploading, uploadError, uploadProgress, canRetryUpload, onRetryUpload, onPickFiles, onRemoveAttachment, agents, defaultAgentId, selection, onChooseAgent, attachmentsAllowed }) {
1643
+ const { t } = useTranslation();
1644
+ const [text, setText] = useState("");
1645
+ function submit() {
1646
+ if (!text.trim() || streaming) return;
1647
+ if (onSend(text.trim(), attachments)) setText("");
1648
+ }
1649
+ return /* @__PURE__ */ jsxs("div", {
1650
+ className: "flex shrink-0 flex-col gap-2 border-t border-border p-2",
1651
+ children: [(attachments.length > 0 || uploading || uploadError) && /* @__PURE__ */ jsxs("div", {
1652
+ className: "flex flex-wrap items-center gap-1.5 px-1",
1653
+ "data-chat-attachments": true,
1654
+ children: [
1655
+ attachments.map((a, i) => /* @__PURE__ */ jsxs("span", {
1656
+ className: "inline-flex min-w-0 max-w-full items-center gap-1 rounded-md bg-muted px-2 py-1 text-xs",
1657
+ children: [
1658
+ /* @__PURE__ */ jsx(FileText, {
1659
+ size: 13,
1660
+ className: "shrink-0 text-muted-foreground"
1661
+ }),
1662
+ /* @__PURE__ */ jsx("span", {
1663
+ className: "max-w-48 truncate",
1664
+ children: a.label ?? a.name
1665
+ }),
1666
+ /* @__PURE__ */ jsx("button", {
1667
+ type: "button",
1668
+ "aria-label": t("webchat.removeAttachment", { name: a.label ?? a.name }),
1669
+ className: "shrink-0 rounded p-0.5 text-muted-foreground hover:text-destructive",
1670
+ onClick: () => onRemoveAttachment(i),
1671
+ children: /* @__PURE__ */ jsx(X, { size: 13 })
1672
+ })
1673
+ ]
1674
+ }, `${a.name}-${i}`)),
1675
+ uploading && /* @__PURE__ */ jsxs("div", {
1676
+ className: "flex min-w-32 flex-1 items-center gap-2",
1677
+ "aria-label": t("webchat.uploading"),
1678
+ children: [
1679
+ /* @__PURE__ */ jsx(LoaderCircle, {
1680
+ size: 14,
1681
+ className: "shrink-0 animate-spin text-muted-foreground"
1682
+ }),
1683
+ /* @__PURE__ */ jsx("div", {
1684
+ role: "progressbar",
1685
+ "aria-label": t("webchat.uploadProgress"),
1686
+ "aria-valuemin": 0,
1687
+ "aria-valuemax": 100,
1688
+ "aria-valuenow": uploadProgress,
1689
+ className: "h-1.5 min-w-0 flex-1 overflow-hidden rounded-full bg-border",
1690
+ children: /* @__PURE__ */ jsx("div", {
1691
+ className: "h-full rounded-full bg-primary transition-[width]",
1692
+ style: { width: `${uploadProgress}%` }
1693
+ })
1694
+ }),
1695
+ /* @__PURE__ */ jsxs("span", {
1696
+ className: "text-xs tabular-nums text-muted-foreground",
1697
+ children: [uploadProgress, "%"]
1698
+ })
1699
+ ]
1700
+ }),
1701
+ uploadError && /* @__PURE__ */ jsxs("div", {
1702
+ className: "flex min-w-0 flex-1 items-center gap-2 text-xs text-destructive",
1703
+ children: [/* @__PURE__ */ jsx("span", {
1704
+ className: "min-w-0 flex-1",
1705
+ children: t(uploadError)
1706
+ }), canRetryUpload && /* @__PURE__ */ jsxs("button", {
1707
+ type: "button",
1708
+ className: "inline-flex shrink-0 items-center gap-1 rounded px-1.5 py-0.5 font-medium text-primary hover:bg-muted",
1709
+ onClick: onRetryUpload,
1710
+ children: [
1711
+ /* @__PURE__ */ jsx(RotateCcw, { size: 12 }),
1712
+ " ",
1713
+ t("webchat.retryUpload")
1714
+ ]
1715
+ })]
1716
+ })
1717
+ ]
1718
+ }), /* @__PURE__ */ jsxs("div", {
1719
+ className: "flex flex-col gap-1",
1720
+ children: [/* @__PURE__ */ jsx("textarea", {
1721
+ className: "w-full resize-none rounded-lg border border-border bg-background px-3 py-2 text-sm max-h-32",
1722
+ rows: 1,
1723
+ value: text,
1724
+ placeholder: t("webchat.placeholder"),
1725
+ onChange: (e) => setText(e.target.value),
1726
+ onKeyDown: (e) => {
1727
+ if (e.key === "Enter" && !e.shiftKey) {
1728
+ e.preventDefault();
1729
+ submit();
1730
+ }
1731
+ }
1732
+ }), /* @__PURE__ */ jsxs("div", {
1733
+ className: "flex items-center gap-1",
1734
+ children: [
1735
+ attachmentsAllowed && /* @__PURE__ */ jsxs("label", {
1736
+ "aria-label": t("webchat.attach"),
1737
+ className: `shrink-0 cursor-pointer rounded-lg p-1.5 text-muted-foreground hover:bg-muted hover:text-primary ${uploading ? "pointer-events-none opacity-60" : ""}`,
1738
+ children: [/* @__PURE__ */ jsx(Paperclip, { size: 18 }), /* @__PURE__ */ jsx("input", {
1739
+ type: "file",
1740
+ multiple: true,
1741
+ className: "hidden",
1742
+ disabled: uploading,
1743
+ onChange: (e) => {
1744
+ if (e.target.files?.length) onPickFiles(e.target.files);
1745
+ e.currentTarget.value = "";
1746
+ }
1747
+ })]
1748
+ }),
1749
+ /* @__PURE__ */ jsx(AgentPicker, {
1750
+ agents,
1751
+ defaultAgentId,
1752
+ selection,
1753
+ onChange: onChooseAgent
1754
+ }),
1755
+ /* @__PURE__ */ jsx("div", { className: "flex-1" }),
1756
+ streaming ? /* @__PURE__ */ jsx("button", {
1757
+ "aria-label": t("webchat.stop"),
1758
+ className: "shrink-0 rounded-lg p-1.5 hover:bg-muted",
1759
+ onClick: onStop,
1760
+ children: /* @__PURE__ */ jsx(Square, { size: 18 })
1761
+ }) : /* @__PURE__ */ jsx("button", {
1762
+ "aria-label": t("webchat.send"),
1763
+ className: "shrink-0 rounded-lg p-1.5 hover:bg-muted",
1764
+ onClick: submit,
1765
+ children: /* @__PURE__ */ jsx(Send, { size: 18 })
1766
+ })
1767
+ ]
1768
+ })]
1769
+ })]
1770
+ });
1771
+ }
1772
+ //#endregion
1773
+ //#region src/ui/ChatThreadList.tsx
1774
+ function ChatThreadList({ threads, activeId, onOpen, onNew, onTogglePrivate, className = "w-[220px] shrink-0" }) {
1775
+ const { t } = useTranslation();
1776
+ return /* @__PURE__ */ jsxs("aside", {
1777
+ "aria-label": t("webchat.threads"),
1778
+ className: `${className} border-r border-border flex flex-col`,
1779
+ children: [/* @__PURE__ */ jsxs("button", {
1780
+ className: "flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted",
1781
+ onClick: onNew,
1782
+ children: [
1783
+ /* @__PURE__ */ jsx(Plus, { size: 16 }),
1784
+ " ",
1785
+ t("webchat.newConversation")
1786
+ ]
1787
+ }), /* @__PURE__ */ jsx("div", {
1788
+ className: "flex-1 overflow-y-auto",
1789
+ children: threads.map((th) => {
1790
+ const active = th.convId === activeId;
1791
+ return /* @__PURE__ */ jsxs("div", {
1792
+ className: `flex items-center gap-1 pl-1 pr-2 ${active ? "bg-muted" : "hover:bg-muted"}`,
1793
+ children: [/* @__PURE__ */ jsxs("button", {
1794
+ onClick: () => onOpen(th.convId),
1795
+ className: `min-w-0 flex-1 py-2 pl-2 text-left text-sm ${active ? "font-medium" : ""}`,
1796
+ children: [/* @__PURE__ */ jsx("span", {
1797
+ className: "block truncate",
1798
+ children: th.title || t("webchat.newConversation")
1799
+ }), (th.mine || th.ownerName) && /* @__PURE__ */ jsx("span", {
1800
+ className: "block truncate text-xs text-muted-foreground",
1801
+ children: th.mine ? t("webchat.you") : th.ownerName
1802
+ })]
1803
+ }), th.mine && /* @__PURE__ */ jsx("button", {
1804
+ type: "button",
1805
+ "aria-label": th.private ? t("webchat.makePublic") : t("webchat.makePrivate"),
1806
+ title: th.private ? t("webchat.makePublic") : t("webchat.makePrivate"),
1807
+ onClick: (e) => {
1808
+ e.stopPropagation();
1809
+ onTogglePrivate(th.convId, !th.private);
1810
+ },
1811
+ className: "shrink-0 rounded p-1 text-muted-foreground hover:bg-card hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring",
1812
+ children: /* @__PURE__ */ jsx(Icon, {
1813
+ name: th.private ? "lucide:shield" : "lucide:globe",
1814
+ size: 14
1815
+ })
1816
+ })]
1817
+ }, th.convId);
1818
+ })
1819
+ })]
1820
+ });
1821
+ }
1822
+ //#endregion
1823
+ //#region src/ui/SuggestionChips.tsx
1824
+ /**
1825
+ * A row of clickable quick-reply chips: either the static, global conversation starters
1826
+ * shown on an empty chat, or the contextual suggestions the agent proposed after its last
1827
+ * answer (`ChatState.suggestions`) — both render through this one component, distinguished
1828
+ * only by which list `ChatWidget` passes in. Renders nothing for an empty or null list, so
1829
+ * callers do not need their own guard.
1830
+ */
1831
+ function SuggestionChips({ items, onPick }) {
1832
+ const { t } = useTranslation();
1833
+ if (!items?.length) return null;
1834
+ return /* @__PURE__ */ jsx("div", {
1835
+ role: "group",
1836
+ "aria-label": t("webchat.suggestions"),
1837
+ className: "flex flex-wrap gap-1.5 px-2 pb-1.5",
1838
+ children: items.map((text, i) => /* @__PURE__ */ jsx("button", {
1839
+ type: "button",
1840
+ title: text,
1841
+ 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",
1842
+ onClick: () => onPick(text),
1843
+ children: text
1844
+ }, `${i}-${text}`))
1845
+ });
1846
+ }
1847
+ //#endregion
1848
+ //#region src/ui/ChatWidget.tsx
1849
+ var MAX_CHAT_UPLOAD_BYTES = 25 * 1024 * 1024;
1850
+ function isFileDrag(e) {
1851
+ return Array.from(e.dataTransfer.types).includes("Files");
1852
+ }
1853
+ /** One drop surface per popup. The overlay deliberately covers the header, feed, and composer. */
1854
+ function ChatDropSurface({ children, onFiles, className }) {
1855
+ const { t } = useTranslation();
1856
+ const [dragging, setDragging] = useState(false);
1857
+ const depth = useRef(0);
1858
+ function enter(e) {
1859
+ if (!isFileDrag(e)) return;
1860
+ e.preventDefault();
1861
+ depth.current += 1;
1862
+ setDragging(true);
1863
+ }
1864
+ function leave(e) {
1865
+ if (!isFileDrag(e)) return;
1866
+ e.preventDefault();
1867
+ depth.current = Math.max(0, depth.current - 1);
1868
+ if (depth.current === 0) setDragging(false);
1869
+ }
1870
+ function drop(e) {
1871
+ if (!isFileDrag(e)) return;
1872
+ e.preventDefault();
1873
+ depth.current = 0;
1874
+ setDragging(false);
1875
+ onFiles(Array.from(e.dataTransfer.files));
1876
+ }
1877
+ return /* @__PURE__ */ jsxs("div", {
1878
+ className,
1879
+ onDragEnter: enter,
1880
+ onDragLeave: leave,
1881
+ onDragOver: (e) => {
1882
+ if (isFileDrag(e)) {
1883
+ e.preventDefault();
1884
+ e.dataTransfer.dropEffect = "copy";
1885
+ }
1886
+ },
1887
+ onDrop: drop,
1888
+ children: [children, dragging && /* @__PURE__ */ jsx("div", {
1889
+ "data-chat-drop-overlay": true,
1890
+ "data-testid": "chat-drop-overlay",
1891
+ className: "pointer-events-none absolute inset-0 z-50 grid place-items-center bg-card/95 p-6 text-center backdrop-blur-sm",
1892
+ children: /* @__PURE__ */ jsxs("div", {
1893
+ className: "flex h-full w-full flex-col items-center justify-center rounded-2xl border-2 border-dashed border-ring text-primary",
1894
+ children: [
1895
+ /* @__PURE__ */ jsx(Upload, { size: 34 }),
1896
+ /* @__PURE__ */ jsx("span", {
1897
+ className: "mt-3 text-base font-medium",
1898
+ children: t("webchat.dropFiles")
1899
+ }),
1900
+ /* @__PURE__ */ jsx("span", {
1901
+ className: "mt-1 text-xs text-muted-foreground",
1902
+ children: t("webchat.dropFilesHint")
1903
+ })
1904
+ ]
1905
+ })
1906
+ })]
1907
+ });
1908
+ }
1909
+ /** The states the VIEWPORT offers, ordered from least to most expanded. `panel` — the 380px
1910
+ * floating window — exists only where it does not obstruct the page: on a narrow screen the
1911
+ * widget has exactly two states, the bubble and the full sheet. Every mode transition is a
1912
+ * move along this ladder, so no call site can put the widget into a mode its screen lacks. */
1913
+ function chatModes(wide) {
1914
+ return wide ? [
1915
+ "bubble",
1916
+ "panel",
1917
+ "modal"
1918
+ ] : ["bubble", "modal"];
1919
+ }
1920
+ /** Where the widget rests once it is opened — expanded, but as little as the viewport allows.
1921
+ * Also where it returns after following a link from a response: on a wide screen the panel
1922
+ * does not obstruct the page the user just navigated to, while on a narrow one the sheet is
1923
+ * the only visible state, so the active conversation survives the navigation either way. */
1924
+ function restingMode(wide) {
1925
+ return chatModes(wide)[1];
1926
+ }
1927
+ /** One step along the ladder, clamped at both ends: `-1` is what collapse, Esc and a dialog
1928
+ * dismiss do, `+1` is expand. A mode this viewport no longer offers is re-homed by the effect
1929
+ * in the widget, not by stepping — the step always returns a mode from the current ladder. */
1930
+ function stepMode(mode, wide, delta) {
1931
+ const ladder = chatModes(wide);
1932
+ const next = ladder.indexOf(mode) + delta;
1933
+ return ladder[Math.min(ladder.length - 1, Math.max(0, next))];
1934
+ }
1935
+ function ChatWidget() {
1936
+ const { t } = useTranslation();
1937
+ const chat = useChat();
1938
+ const readPage = usePageContext();
1939
+ const [mode, setMode] = useState("bubble");
1940
+ const [threadsOpen, setThreadsOpen] = useState(false);
1941
+ const [attachments, setAttachments] = useState([]);
1942
+ const [uploading, setUploading] = useState(false);
1943
+ const [uploadProgress, setUploadProgress] = useState(0);
1944
+ const [uploadError, setUploadError] = useState(null);
1945
+ const [retryFiles, setRetryFiles] = useState([]);
1946
+ const wide = useMinWidth("sm", 640);
1947
+ const hasSidebar = wide && mode === "modal";
1948
+ const onChatNavigate = useCallback(() => {
1949
+ setMode(restingMode(wide));
1950
+ }, [wide]);
1951
+ const expandBtnRef = useRef(null);
1952
+ const prevModeRef = useRef("bubble");
1953
+ useEffect(() => {
1954
+ if (mode !== "modal") return;
1955
+ const onKey = (e) => {
1956
+ if (e.key === "Escape") setMode(stepMode("modal", wide, -1));
1957
+ };
1958
+ window.addEventListener("keydown", onKey);
1959
+ return () => window.removeEventListener("keydown", onKey);
1960
+ }, [mode, wide]);
1961
+ useEffect(() => {
1962
+ if (!chatModes(wide).includes(mode)) setMode(restingMode(wide));
1963
+ }, [mode, wide]);
1964
+ useEffect(() => {
1965
+ if (mode === "panel" && prevModeRef.current === "modal") expandBtnRef.current?.focus();
1966
+ prevModeRef.current = mode;
1967
+ }, [mode]);
1968
+ useEffect(() => {
1969
+ if (mode === "bubble" || hasSidebar) setThreadsOpen(false);
1970
+ }, [mode, hasSidebar]);
1971
+ useEffect(() => {
1972
+ if (mode !== "modal") return;
1973
+ const prev = document.body.style.overflow;
1974
+ document.body.style.overflow = "hidden";
1975
+ return () => {
1976
+ document.body.style.overflow = prev;
1977
+ };
1978
+ }, [mode]);
1979
+ const isOpen = mode !== "bubble";
1980
+ useEffect(() => {
1981
+ if (isOpen) chat.loadThreads();
1982
+ }, [isOpen, chat.loadThreads]);
1983
+ const { convId, newConversation } = chat;
1984
+ useEffect(() => {
1985
+ if (isOpen && convId === null) newConversation();
1986
+ }, [
1987
+ isOpen,
1988
+ convId,
1989
+ newConversation
1990
+ ]);
1991
+ function startNew() {
1992
+ chat.newConversation();
1993
+ setAttachments([]);
1994
+ setUploadError(null);
1995
+ setRetryFiles([]);
1996
+ setThreadsOpen(false);
1997
+ }
1998
+ const pickFiles = useCallback(async (files) => {
1999
+ if (!files.length) return;
2000
+ setUploadError(null);
2001
+ setRetryFiles([]);
2002
+ setUploading(true);
2003
+ setUploadProgress(0);
2004
+ const added = [];
2005
+ const failedFiles = [];
2006
+ let error = null;
2007
+ for (const file of files) {
2008
+ if (file.size > MAX_CHAT_UPLOAD_BYTES) {
2009
+ error ??= "webchat.uploadTooLarge";
2010
+ continue;
2011
+ }
2012
+ try {
2013
+ setUploadProgress(0);
2014
+ const rec = await uploadFile(file, setUploadProgress);
2015
+ if (rec) added.push({
2016
+ ...rec,
2017
+ label: file.name
2018
+ });
2019
+ else {
2020
+ failedFiles.push(file);
2021
+ error ??= "webchat.uploadFailed";
2022
+ }
2023
+ } catch {
2024
+ failedFiles.push(file);
2025
+ error ??= "webchat.uploadFailed";
2026
+ }
2027
+ }
2028
+ if (added.length) setAttachments((prev) => [...prev, ...added]);
2029
+ setUploading(false);
2030
+ setUploadProgress(0);
2031
+ setRetryFiles(failedFiles);
2032
+ if (error) setUploadError(error);
2033
+ }, []);
2034
+ const retryUploads = useCallback(() => {
2035
+ if (retryFiles.length) pickFiles(retryFiles);
2036
+ }, [pickFiles, retryFiles]);
2037
+ const sendTurn = useCallback((text, refs) => {
2038
+ const page = readPage();
2039
+ const accepted = refs.length ? chat.send(text, page, refs) : chat.send(text, page);
2040
+ if (accepted) {
2041
+ setAttachments([]);
2042
+ setUploadError(null);
2043
+ setRetryFiles([]);
2044
+ }
2045
+ return accepted;
2046
+ }, [chat, readPage]);
2047
+ const hasConversation = chat.messages.length > 0 || chat.pending !== null;
2048
+ const activeTitle = chat.threads.find((th) => th.convId === chat.convId)?.title;
2049
+ const header = /* @__PURE__ */ jsxs("div", {
2050
+ className: "flex shrink-0 items-center justify-between gap-2 border-b border-border px-3 py-2",
2051
+ children: [/* @__PURE__ */ jsx("span", {
2052
+ className: "min-w-0 truncate text-sm font-medium",
2053
+ children: activeTitle || t("webchat.title")
2054
+ }), /* @__PURE__ */ jsxs("div", {
2055
+ className: "flex shrink-0 items-center gap-1",
2056
+ children: [
2057
+ !hasSidebar && /* @__PURE__ */ jsx("button", {
2058
+ "aria-label": t("webchat.threads"),
2059
+ "aria-expanded": threadsOpen,
2060
+ className: "rounded p-1 hover:bg-muted",
2061
+ onClick: () => setThreadsOpen((v) => !v),
2062
+ children: /* @__PURE__ */ jsx(List, { size: 16 })
2063
+ }),
2064
+ hasConversation && /* @__PURE__ */ jsx("button", {
2065
+ "aria-label": t("webchat.newConversation"),
2066
+ className: "rounded p-1 hover:bg-muted",
2067
+ onClick: startNew,
2068
+ children: /* @__PURE__ */ jsx(Plus, { size: 16 })
2069
+ }),
2070
+ chatModes(wide).includes("panel") && /* @__PURE__ */ jsx("button", {
2071
+ ref: expandBtnRef,
2072
+ "aria-label": mode === "modal" ? t("webchat.collapse") : t("webchat.expand"),
2073
+ className: "rounded p-1 hover:bg-muted",
2074
+ onClick: () => setMode(stepMode(mode, wide, mode === "modal" ? -1 : 1)),
2075
+ children: mode === "modal" ? /* @__PURE__ */ jsx(Minimize2, { size: 16 }) : /* @__PURE__ */ jsx(Maximize2, { size: 16 })
2076
+ }),
2077
+ /* @__PURE__ */ jsx("button", {
2078
+ "aria-label": t("webchat.close"),
2079
+ className: "rounded p-1 hover:bg-muted",
2080
+ onClick: () => setMode("bubble"),
2081
+ children: /* @__PURE__ */ jsx(X, { size: 16 })
2082
+ })
2083
+ ]
2084
+ })]
2085
+ });
2086
+ const threadList = /* @__PURE__ */ jsx(ChatThreadList, {
2087
+ threads: chat.threads,
2088
+ activeId: chat.convId,
2089
+ onOpen: (id) => {
2090
+ chat.open(id);
2091
+ setAttachments([]);
2092
+ setUploadError(null);
2093
+ setRetryFiles([]);
2094
+ setThreadsOpen(false);
2095
+ },
2096
+ onNew: startNew,
2097
+ onTogglePrivate: chat.togglePrivate,
2098
+ className: hasSidebar ? "w-[220px] shrink-0" : "w-full"
2099
+ });
2100
+ const conversation = /* @__PURE__ */ jsx(ChatNavProvider, {
2101
+ onNavigate: onChatNavigate,
2102
+ children: /* @__PURE__ */ jsxs("div", {
2103
+ className: "flex min-h-0 min-w-0 flex-1 flex-col",
2104
+ children: [
2105
+ chat.error && /* @__PURE__ */ jsx("div", {
2106
+ className: "px-3 py-2 text-sm text-destructive",
2107
+ children: t("webchat.error", { message: chat.error })
2108
+ }),
2109
+ /* @__PURE__ */ jsx(ChatMessages, {
2110
+ messages: chat.messages,
2111
+ pending: chat.pending,
2112
+ pendingReasoning: chat.pendingReasoning,
2113
+ trailing: chat.messages.length === 0 ? /* @__PURE__ */ jsx(SuggestionChips, {
2114
+ items: chat.starters,
2115
+ onPick: (text) => sendTurn(text, attachments)
2116
+ }) : /* @__PURE__ */ jsx(SuggestionChips, {
2117
+ items: chat.suggestions,
2118
+ onPick: (text) => sendTurn(text, attachments)
2119
+ })
2120
+ }),
2121
+ /* @__PURE__ */ jsx(ChatComposer, {
2122
+ streaming: chat.pending !== null,
2123
+ attachments,
2124
+ uploading,
2125
+ uploadProgress,
2126
+ uploadError,
2127
+ canRetryUpload: retryFiles.length > 0 && !uploading,
2128
+ onRetryUpload: retryUploads,
2129
+ onPickFiles: (files) => void pickFiles(Array.from(files)),
2130
+ onRemoveAttachment: (index) => setAttachments((prev) => prev.filter((_, i) => i !== index)),
2131
+ agents: chat.agents,
2132
+ defaultAgentId: chat.defaultAgentId,
2133
+ selection: chat.selection,
2134
+ onChooseAgent: chat.chooseAgent,
2135
+ attachmentsAllowed: chat.attachmentsAllowed,
2136
+ onSend: sendTurn,
2137
+ onStop: chat.stop
2138
+ })
2139
+ ]
2140
+ })
2141
+ });
2142
+ if (mode === "bubble") return /* @__PURE__ */ jsx("button", {
2143
+ "aria-label": t("webchat.open"),
2144
+ onClick: () => setMode(restingMode(wide)),
2145
+ 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",
2146
+ children: /* @__PURE__ */ jsx(MessageCircle, { size: 24 })
2147
+ });
2148
+ if (mode === "panel") return /* @__PURE__ */ jsxs(ChatDropSurface, {
2149
+ onFiles: pickFiles,
2150
+ 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",
2151
+ children: [header, threadsOpen ? threadList : conversation]
2152
+ });
2153
+ return /* @__PURE__ */ jsxs(Dialog, {
2154
+ open: true,
2155
+ modal: false,
2156
+ onOpenChange: (open) => !open && setMode(stepMode("modal", wide, -1)),
2157
+ children: [/* @__PURE__ */ jsx("div", {
2158
+ "data-chat-backdrop": true,
2159
+ "aria-hidden": "true",
2160
+ className: "fixed inset-0 z-[45] bg-black/50 backdrop-blur-sm"
2161
+ }), /* @__PURE__ */ jsx(DialogContent, {
2162
+ hideClose: true,
2163
+ className: "h-[85dvh] w-full p-0 md:h-[85dvh] md:w-[80vw] md:max-w-[1040px] md:overflow-y-hidden",
2164
+ children: /* @__PURE__ */ jsx(ChatDropSurface, {
2165
+ onFiles: pickFiles,
2166
+ className: "h-full",
2167
+ children: /* @__PURE__ */ jsxs("div", {
2168
+ className: "flex h-full min-h-0 flex-col",
2169
+ children: [header, /* @__PURE__ */ jsxs("div", {
2170
+ className: "flex min-h-0 flex-1",
2171
+ children: [(hasSidebar || threadsOpen) && threadList, (hasSidebar || !threadsOpen) && conversation]
2172
+ })]
2173
+ })
2174
+ })
2175
+ })]
2176
+ });
2177
+ }
2178
+ //#endregion
2179
+ export { ChatWidget, ChatWidget as default, chatModes, restingMode, stepMode };