@coffer-org/plugin-webchat 2.1.1 → 2.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.
- package/dist/{ChatWidget-U4vyG1gt.js → ChatWidget-Cew9znAf.js} +316 -49
- package/dist/runtime/actions.d.ts +6 -0
- package/dist/runtime/actions.js +1 -0
- package/dist/runtime/chain-store.d.ts +1 -0
- package/dist/runtime/chain-store.js +19 -2
- package/dist/runtime/send.js +20 -1
- package/dist/web.js +1 -1
- package/package.json +1 -1
|
@@ -507,6 +507,34 @@ var createLucideIcon = (iconName, iconNode) => {
|
|
|
507
507
|
* This source code is licensed under the ISC license.
|
|
508
508
|
* See the LICENSE file in the root directory of this source tree.
|
|
509
509
|
*/
|
|
510
|
+
var FileText = createLucideIcon("file-text", [
|
|
511
|
+
["path", {
|
|
512
|
+
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",
|
|
513
|
+
key: "1oefj6"
|
|
514
|
+
}],
|
|
515
|
+
["path", {
|
|
516
|
+
d: "M14 2v5a1 1 0 0 0 1 1h5",
|
|
517
|
+
key: "wfsgrz"
|
|
518
|
+
}],
|
|
519
|
+
["path", {
|
|
520
|
+
d: "M10 9H8",
|
|
521
|
+
key: "b1mrlr"
|
|
522
|
+
}],
|
|
523
|
+
["path", {
|
|
524
|
+
d: "M16 13H8",
|
|
525
|
+
key: "t4e002"
|
|
526
|
+
}],
|
|
527
|
+
["path", {
|
|
528
|
+
d: "M16 17H8",
|
|
529
|
+
key: "z1uh3a"
|
|
530
|
+
}]
|
|
531
|
+
]);
|
|
532
|
+
/**
|
|
533
|
+
* @license lucide-react v1.24.0 - ISC
|
|
534
|
+
*
|
|
535
|
+
* This source code is licensed under the ISC license.
|
|
536
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
537
|
+
*/
|
|
510
538
|
var List = createLucideIcon("list", [
|
|
511
539
|
["path", {
|
|
512
540
|
d: "M3 5h.01",
|
|
@@ -539,6 +567,16 @@ var List = createLucideIcon("list", [
|
|
|
539
567
|
* This source code is licensed under the ISC license.
|
|
540
568
|
* See the LICENSE file in the root directory of this source tree.
|
|
541
569
|
*/
|
|
570
|
+
var LoaderCircle = createLucideIcon("loader-circle", [["path", {
|
|
571
|
+
d: "M21 12a9 9 0 1 1-6.219-8.56",
|
|
572
|
+
key: "13zald"
|
|
573
|
+
}]]);
|
|
574
|
+
/**
|
|
575
|
+
* @license lucide-react v1.24.0 - ISC
|
|
576
|
+
*
|
|
577
|
+
* This source code is licensed under the ISC license.
|
|
578
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
579
|
+
*/
|
|
542
580
|
var Maximize2 = createLucideIcon("maximize-2", [
|
|
543
581
|
["path", {
|
|
544
582
|
d: "M15 3h6v6",
|
|
@@ -597,6 +635,16 @@ var Minimize2 = createLucideIcon("minimize-2", [
|
|
|
597
635
|
* This source code is licensed under the ISC license.
|
|
598
636
|
* See the LICENSE file in the root directory of this source tree.
|
|
599
637
|
*/
|
|
638
|
+
var Paperclip = createLucideIcon("paperclip", [["path", {
|
|
639
|
+
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",
|
|
640
|
+
key: "1miecu"
|
|
641
|
+
}]]);
|
|
642
|
+
/**
|
|
643
|
+
* @license lucide-react v1.24.0 - ISC
|
|
644
|
+
*
|
|
645
|
+
* This source code is licensed under the ISC license.
|
|
646
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
647
|
+
*/
|
|
600
648
|
var Plus = createLucideIcon("plus", [["path", {
|
|
601
649
|
d: "M5 12h14",
|
|
602
650
|
key: "1ays0h"
|
|
@@ -637,6 +685,26 @@ var Square = createLucideIcon("square", [["rect", {
|
|
|
637
685
|
* This source code is licensed under the ISC license.
|
|
638
686
|
* See the LICENSE file in the root directory of this source tree.
|
|
639
687
|
*/
|
|
688
|
+
var Upload = createLucideIcon("upload", [
|
|
689
|
+
["path", {
|
|
690
|
+
d: "M12 3v12",
|
|
691
|
+
key: "1x0j5s"
|
|
692
|
+
}],
|
|
693
|
+
["path", {
|
|
694
|
+
d: "m17 8-5-5-5 5",
|
|
695
|
+
key: "7q97r8"
|
|
696
|
+
}],
|
|
697
|
+
["path", {
|
|
698
|
+
d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",
|
|
699
|
+
key: "ih7n3h"
|
|
700
|
+
}]
|
|
701
|
+
]);
|
|
702
|
+
/**
|
|
703
|
+
* @license lucide-react v1.24.0 - ISC
|
|
704
|
+
*
|
|
705
|
+
* This source code is licensed under the ISC license.
|
|
706
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
707
|
+
*/
|
|
640
708
|
var X = createLucideIcon("x", [["path", {
|
|
641
709
|
d: "M18 6 6 18",
|
|
642
710
|
key: "1bl5f8"
|
|
@@ -6117,6 +6185,30 @@ function useMinWidth(name, fallback) {
|
|
|
6117
6185
|
return useMediaQuery(`(min-width: ${breakpointPx(name, fallback)}px)`, true);
|
|
6118
6186
|
}
|
|
6119
6187
|
//#endregion
|
|
6188
|
+
//#region ../web/api.ts
|
|
6189
|
+
/**
|
|
6190
|
+
* Upload a file → descriptor from the server (or null).
|
|
6191
|
+
* Take mime/size from the response, not the browser File: the server overwrites
|
|
6192
|
+
* them from disk anyway, so client values would only be noise.
|
|
6193
|
+
*/
|
|
6194
|
+
async function uploadFile(file) {
|
|
6195
|
+
const fd = new FormData();
|
|
6196
|
+
fd.append("file", file);
|
|
6197
|
+
const r = await fetch("/api/upload", {
|
|
6198
|
+
method: "POST",
|
|
6199
|
+
body: fd
|
|
6200
|
+
});
|
|
6201
|
+
if (!r.ok) return null;
|
|
6202
|
+
const j = await r.json();
|
|
6203
|
+
const name = j.name ?? j.filename;
|
|
6204
|
+
if (!name) return null;
|
|
6205
|
+
return {
|
|
6206
|
+
name,
|
|
6207
|
+
...j.mime ? { mime: j.mime } : {},
|
|
6208
|
+
...j.size != null ? { size: j.size } : {}
|
|
6209
|
+
};
|
|
6210
|
+
}
|
|
6211
|
+
//#endregion
|
|
6120
6212
|
//#region src/ui/sse.ts
|
|
6121
6213
|
async function* readSse(body) {
|
|
6122
6214
|
const reader = body.getReader();
|
|
@@ -6180,7 +6272,8 @@ async function sendMessage(req) {
|
|
|
6180
6272
|
convId: req.convId,
|
|
6181
6273
|
text: req.text,
|
|
6182
6274
|
replyTo: req.replyTo,
|
|
6183
|
-
context: req.context
|
|
6275
|
+
context: req.context,
|
|
6276
|
+
...req.attachments?.length ? { attachments: req.attachments } : {}
|
|
6184
6277
|
}),
|
|
6185
6278
|
signal: req.signal
|
|
6186
6279
|
});
|
|
@@ -6276,7 +6369,7 @@ function useChat() {
|
|
|
6276
6369
|
pending,
|
|
6277
6370
|
pendingReasoning,
|
|
6278
6371
|
error,
|
|
6279
|
-
send: useCallback((text, context) => {
|
|
6372
|
+
send: useCallback((text, context, attachments = []) => {
|
|
6280
6373
|
if (!text.trim() || sendingRef.current) return false;
|
|
6281
6374
|
sendingRef.current = true;
|
|
6282
6375
|
const myTurn = turnRef.current;
|
|
@@ -6287,7 +6380,8 @@ function useChat() {
|
|
|
6287
6380
|
msgId: `local-${newId()}`,
|
|
6288
6381
|
role: "user",
|
|
6289
6382
|
text,
|
|
6290
|
-
ts: Math.floor(Date.now() / 1e3)
|
|
6383
|
+
ts: Math.floor(Date.now() / 1e3),
|
|
6384
|
+
...attachments.length ? { attachments } : {}
|
|
6291
6385
|
}]);
|
|
6292
6386
|
setPending("");
|
|
6293
6387
|
const ctl = new AbortController();
|
|
@@ -6301,6 +6395,7 @@ function useChat() {
|
|
|
6301
6395
|
text,
|
|
6302
6396
|
replyTo: headRef.current,
|
|
6303
6397
|
context,
|
|
6398
|
+
attachments,
|
|
6304
6399
|
signal: ctl.signal,
|
|
6305
6400
|
onDelta: (t) => {
|
|
6306
6401
|
if (turnRef.current !== myTurn) return;
|
|
@@ -29112,7 +29207,8 @@ function ChatMessages({ messages, pending, pendingReasoning }) {
|
|
|
29112
29207
|
children: [messages.map((m) => /* @__PURE__ */ jsx(Bubble, {
|
|
29113
29208
|
role: m.role,
|
|
29114
29209
|
text: m.text,
|
|
29115
|
-
reasoning: m.reasoning
|
|
29210
|
+
reasoning: m.reasoning,
|
|
29211
|
+
attachments: m.attachments
|
|
29116
29212
|
}, m.msgId)), pending !== null && /* @__PURE__ */ jsx(Bubble, {
|
|
29117
29213
|
role: "assistant",
|
|
29118
29214
|
text: pending || t("webchat.thinking"),
|
|
@@ -29126,11 +29222,18 @@ function ChatMessages({ messages, pending, pendingReasoning }) {
|
|
|
29126
29222
|
* for every completed message while only one bubble at the bottom is changing.
|
|
29127
29223
|
* The props are primitives, so the default shallow comparison is sufficient.
|
|
29128
29224
|
*/
|
|
29129
|
-
var Bubble = memo(function Bubble({ role, text, reasoning }) {
|
|
29225
|
+
var Bubble = memo(function Bubble({ role, text, reasoning, attachments }) {
|
|
29130
29226
|
const { t } = useTranslation();
|
|
29131
|
-
if (role === "user") return /* @__PURE__ */
|
|
29227
|
+
if (role === "user") return /* @__PURE__ */ jsxs("div", {
|
|
29132
29228
|
className: "self-end max-w-[85%] rounded-2xl rounded-br-sm bg-accent/15 px-3 py-2 text-sm whitespace-pre-wrap break-words",
|
|
29133
|
-
children:
|
|
29229
|
+
children: [attachments?.length ? /* @__PURE__ */ jsx("div", {
|
|
29230
|
+
className: "mb-1.5 flex flex-wrap gap-1",
|
|
29231
|
+
children: attachments.map((a) => /* @__PURE__ */ jsx("span", {
|
|
29232
|
+
className: "inline-flex max-w-full items-center rounded-md bg-bg/60 px-1.5 py-0.5 text-xs",
|
|
29233
|
+
title: a.name,
|
|
29234
|
+
children: a.label ?? a.name
|
|
29235
|
+
}, a.name))
|
|
29236
|
+
}) : null, text]
|
|
29134
29237
|
});
|
|
29135
29238
|
return /* @__PURE__ */ jsxs("div", {
|
|
29136
29239
|
className: "self-start max-w-[95%] min-w-0 rounded-2xl rounded-bl-sm bg-elevated px-3 py-2 text-sm break-words",
|
|
@@ -29151,37 +29254,91 @@ var Bubble = memo(function Bubble({ role, text, reasoning }) {
|
|
|
29151
29254
|
});
|
|
29152
29255
|
//#endregion
|
|
29153
29256
|
//#region src/ui/ChatComposer.tsx
|
|
29154
|
-
function ChatComposer({ streaming, onSend, onStop }) {
|
|
29257
|
+
function ChatComposer({ streaming, onSend, onStop, attachments, uploading, uploadError, onPickFiles, onRemoveAttachment }) {
|
|
29155
29258
|
const { t } = useTranslation();
|
|
29156
29259
|
const [text, setText] = useState("");
|
|
29157
29260
|
function submit() {
|
|
29158
29261
|
if (!text.trim() || streaming) return;
|
|
29159
|
-
if (onSend(text.trim())) setText("");
|
|
29262
|
+
if (onSend(text.trim(), attachments)) setText("");
|
|
29160
29263
|
}
|
|
29161
29264
|
return /* @__PURE__ */ jsxs("div", {
|
|
29162
|
-
className: "flex shrink-0
|
|
29163
|
-
children: [/* @__PURE__ */
|
|
29164
|
-
className: "flex
|
|
29165
|
-
|
|
29166
|
-
|
|
29167
|
-
|
|
29168
|
-
|
|
29169
|
-
|
|
29170
|
-
|
|
29171
|
-
|
|
29172
|
-
|
|
29173
|
-
|
|
29174
|
-
|
|
29175
|
-
|
|
29176
|
-
|
|
29177
|
-
|
|
29178
|
-
|
|
29179
|
-
|
|
29180
|
-
|
|
29181
|
-
|
|
29182
|
-
|
|
29183
|
-
|
|
29184
|
-
|
|
29265
|
+
className: "flex shrink-0 flex-col gap-2 border-t border-border p-2",
|
|
29266
|
+
children: [(attachments.length > 0 || uploadError) && /* @__PURE__ */ jsxs("div", {
|
|
29267
|
+
className: "flex flex-wrap items-center gap-1.5 px-1",
|
|
29268
|
+
"data-chat-attachments": true,
|
|
29269
|
+
children: [
|
|
29270
|
+
attachments.map((a, i) => /* @__PURE__ */ jsxs("span", {
|
|
29271
|
+
className: "inline-flex min-w-0 max-w-full items-center gap-1 rounded-md bg-elevated px-2 py-1 text-xs",
|
|
29272
|
+
children: [
|
|
29273
|
+
/* @__PURE__ */ jsx(FileText, {
|
|
29274
|
+
size: 13,
|
|
29275
|
+
className: "shrink-0 text-muted"
|
|
29276
|
+
}),
|
|
29277
|
+
/* @__PURE__ */ jsx("span", {
|
|
29278
|
+
className: "max-w-48 truncate",
|
|
29279
|
+
children: a.label ?? a.name
|
|
29280
|
+
}),
|
|
29281
|
+
/* @__PURE__ */ jsx("button", {
|
|
29282
|
+
type: "button",
|
|
29283
|
+
"aria-label": t("webchat.removeAttachment", { name: a.label ?? a.name }),
|
|
29284
|
+
className: "shrink-0 rounded p-0.5 text-muted hover:text-danger",
|
|
29285
|
+
onClick: () => onRemoveAttachment(i),
|
|
29286
|
+
children: /* @__PURE__ */ jsx(X, { size: 13 })
|
|
29287
|
+
})
|
|
29288
|
+
]
|
|
29289
|
+
}, `${a.name}-${i}`)),
|
|
29290
|
+
uploading && /* @__PURE__ */ jsx(LoaderCircle, {
|
|
29291
|
+
size: 14,
|
|
29292
|
+
className: "animate-spin text-muted",
|
|
29293
|
+
"aria-label": t("webchat.uploading")
|
|
29294
|
+
}),
|
|
29295
|
+
uploadError && /* @__PURE__ */ jsx("span", {
|
|
29296
|
+
className: "text-xs text-danger",
|
|
29297
|
+
children: t(uploadError)
|
|
29298
|
+
})
|
|
29299
|
+
]
|
|
29300
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
29301
|
+
className: "flex items-end gap-2",
|
|
29302
|
+
children: [
|
|
29303
|
+
/* @__PURE__ */ jsxs("label", {
|
|
29304
|
+
"aria-label": t("webchat.attach"),
|
|
29305
|
+
className: `cursor-pointer rounded-lg p-2 text-muted hover:bg-elevated hover:text-accent ${uploading ? "pointer-events-none opacity-60" : ""}`,
|
|
29306
|
+
children: [/* @__PURE__ */ jsx(Paperclip, { size: 18 }), /* @__PURE__ */ jsx("input", {
|
|
29307
|
+
type: "file",
|
|
29308
|
+
multiple: true,
|
|
29309
|
+
className: "hidden",
|
|
29310
|
+
disabled: uploading,
|
|
29311
|
+
onChange: (e) => {
|
|
29312
|
+
if (e.target.files?.length) onPickFiles(e.target.files);
|
|
29313
|
+
e.currentTarget.value = "";
|
|
29314
|
+
}
|
|
29315
|
+
})]
|
|
29316
|
+
}),
|
|
29317
|
+
/* @__PURE__ */ jsx("textarea", {
|
|
29318
|
+
className: "flex-1 resize-none rounded-lg border border-border bg-bg px-3 py-2 text-sm max-h-32",
|
|
29319
|
+
rows: 1,
|
|
29320
|
+
value: text,
|
|
29321
|
+
placeholder: t("webchat.placeholder"),
|
|
29322
|
+
onChange: (e) => setText(e.target.value),
|
|
29323
|
+
onKeyDown: (e) => {
|
|
29324
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
29325
|
+
e.preventDefault();
|
|
29326
|
+
submit();
|
|
29327
|
+
}
|
|
29328
|
+
}
|
|
29329
|
+
}),
|
|
29330
|
+
streaming ? /* @__PURE__ */ jsx("button", {
|
|
29331
|
+
"aria-label": t("webchat.stop"),
|
|
29332
|
+
className: "rounded-lg p-2 hover:bg-elevated",
|
|
29333
|
+
onClick: onStop,
|
|
29334
|
+
children: /* @__PURE__ */ jsx(Square, { size: 18 })
|
|
29335
|
+
}) : /* @__PURE__ */ jsx("button", {
|
|
29336
|
+
"aria-label": t("webchat.send"),
|
|
29337
|
+
className: "rounded-lg p-2 hover:bg-elevated",
|
|
29338
|
+
onClick: submit,
|
|
29339
|
+
children: /* @__PURE__ */ jsx(Send, { size: 18 })
|
|
29340
|
+
})
|
|
29341
|
+
]
|
|
29185
29342
|
})]
|
|
29186
29343
|
});
|
|
29187
29344
|
}
|
|
@@ -29212,6 +29369,66 @@ function ChatThreadList({ threads, activeId, onOpen, onNew, className = "w-[220p
|
|
|
29212
29369
|
}
|
|
29213
29370
|
//#endregion
|
|
29214
29371
|
//#region src/ui/ChatWidget.tsx
|
|
29372
|
+
var MAX_CHAT_UPLOAD_BYTES = 25 * 1024 * 1024;
|
|
29373
|
+
function isFileDrag(e) {
|
|
29374
|
+
return Array.from(e.dataTransfer.types).includes("Files");
|
|
29375
|
+
}
|
|
29376
|
+
/** One drop surface per popup. The overlay deliberately covers the header, feed, and composer. */
|
|
29377
|
+
function ChatDropSurface({ children, onFiles, className }) {
|
|
29378
|
+
const { t } = useTranslation();
|
|
29379
|
+
const [dragging, setDragging] = useState(false);
|
|
29380
|
+
const depth = useRef(0);
|
|
29381
|
+
function enter(e) {
|
|
29382
|
+
if (!isFileDrag(e)) return;
|
|
29383
|
+
e.preventDefault();
|
|
29384
|
+
depth.current += 1;
|
|
29385
|
+
setDragging(true);
|
|
29386
|
+
}
|
|
29387
|
+
function leave(e) {
|
|
29388
|
+
if (!isFileDrag(e)) return;
|
|
29389
|
+
e.preventDefault();
|
|
29390
|
+
depth.current = Math.max(0, depth.current - 1);
|
|
29391
|
+
if (depth.current === 0) setDragging(false);
|
|
29392
|
+
}
|
|
29393
|
+
function drop(e) {
|
|
29394
|
+
if (!isFileDrag(e)) return;
|
|
29395
|
+
e.preventDefault();
|
|
29396
|
+
depth.current = 0;
|
|
29397
|
+
setDragging(false);
|
|
29398
|
+
onFiles(Array.from(e.dataTransfer.files));
|
|
29399
|
+
}
|
|
29400
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
29401
|
+
className,
|
|
29402
|
+
onDragEnter: enter,
|
|
29403
|
+
onDragLeave: leave,
|
|
29404
|
+
onDragOver: (e) => {
|
|
29405
|
+
if (isFileDrag(e)) {
|
|
29406
|
+
e.preventDefault();
|
|
29407
|
+
e.dataTransfer.dropEffect = "copy";
|
|
29408
|
+
}
|
|
29409
|
+
},
|
|
29410
|
+
onDrop: drop,
|
|
29411
|
+
children: [children, dragging && /* @__PURE__ */ jsx("div", {
|
|
29412
|
+
"data-chat-drop-overlay": true,
|
|
29413
|
+
"data-testid": "chat-drop-overlay",
|
|
29414
|
+
className: "pointer-events-none absolute inset-0 z-50 grid place-items-center bg-surface/95 p-6 text-center backdrop-blur-sm",
|
|
29415
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
29416
|
+
className: "flex h-full w-full flex-col items-center justify-center rounded-2xl border-2 border-dashed border-accent bg-accent/10 text-accent",
|
|
29417
|
+
children: [
|
|
29418
|
+
/* @__PURE__ */ jsx(Upload, { size: 34 }),
|
|
29419
|
+
/* @__PURE__ */ jsx("span", {
|
|
29420
|
+
className: "mt-3 text-base font-medium",
|
|
29421
|
+
children: t("webchat.dropFiles")
|
|
29422
|
+
}),
|
|
29423
|
+
/* @__PURE__ */ jsx("span", {
|
|
29424
|
+
className: "mt-1 text-xs text-muted",
|
|
29425
|
+
children: t("webchat.dropFilesHint")
|
|
29426
|
+
})
|
|
29427
|
+
]
|
|
29428
|
+
})
|
|
29429
|
+
})]
|
|
29430
|
+
});
|
|
29431
|
+
}
|
|
29215
29432
|
/** What to do with the widget after following a link from a response. On desktop a 380px
|
|
29216
29433
|
* panel does not obstruct the page (a darkened modal does, so it collapses into it);
|
|
29217
29434
|
* below the nav breakpoint the panel consumes nearly the whole screen. */
|
|
@@ -29224,6 +29441,9 @@ function ChatWidget() {
|
|
|
29224
29441
|
const page = usePageContext();
|
|
29225
29442
|
const [mode, setMode] = useState("bubble");
|
|
29226
29443
|
const [threadsOpen, setThreadsOpen] = useState(false);
|
|
29444
|
+
const [attachments, setAttachments] = useState([]);
|
|
29445
|
+
const [uploading, setUploading] = useState(false);
|
|
29446
|
+
const [uploadError, setUploadError] = useState(null);
|
|
29227
29447
|
const wide = useMinWidth("sm", 640);
|
|
29228
29448
|
const wideNav = useMinWidth("nav", 768);
|
|
29229
29449
|
const onChatNavigate = useCallback(() => {
|
|
@@ -29258,8 +29478,36 @@ function ChatWidget() {
|
|
|
29258
29478
|
}, [isOpen, chat.loadThreads]);
|
|
29259
29479
|
function startNew() {
|
|
29260
29480
|
chat.newConversation();
|
|
29481
|
+
setAttachments([]);
|
|
29482
|
+
setUploadError(null);
|
|
29261
29483
|
setThreadsOpen(false);
|
|
29262
29484
|
}
|
|
29485
|
+
const pickFiles = useCallback(async (files) => {
|
|
29486
|
+
if (!files.length) return;
|
|
29487
|
+
setUploadError(null);
|
|
29488
|
+
setUploading(true);
|
|
29489
|
+
const added = [];
|
|
29490
|
+
let error = null;
|
|
29491
|
+
for (const file of files) {
|
|
29492
|
+
if (file.size > MAX_CHAT_UPLOAD_BYTES) {
|
|
29493
|
+
error ??= "webchat.uploadTooLarge";
|
|
29494
|
+
continue;
|
|
29495
|
+
}
|
|
29496
|
+
try {
|
|
29497
|
+
const rec = await uploadFile(file);
|
|
29498
|
+
if (rec) added.push({
|
|
29499
|
+
...rec,
|
|
29500
|
+
label: file.name
|
|
29501
|
+
});
|
|
29502
|
+
else error ??= "webchat.uploadFailed";
|
|
29503
|
+
} catch {
|
|
29504
|
+
error ??= "webchat.uploadFailed";
|
|
29505
|
+
}
|
|
29506
|
+
}
|
|
29507
|
+
if (added.length) setAttachments((prev) => [...prev, ...added]);
|
|
29508
|
+
setUploading(false);
|
|
29509
|
+
if (error) setUploadError(error);
|
|
29510
|
+
}, []);
|
|
29263
29511
|
const hasConversation = chat.messages.length > 0 || chat.pending !== null;
|
|
29264
29512
|
const header = /* @__PURE__ */ jsxs("div", {
|
|
29265
29513
|
className: "flex shrink-0 items-center justify-between border-b border-border px-3 py-2",
|
|
@@ -29314,7 +29562,19 @@ function ChatWidget() {
|
|
|
29314
29562
|
}),
|
|
29315
29563
|
/* @__PURE__ */ jsx(ChatComposer, {
|
|
29316
29564
|
streaming: chat.pending !== null,
|
|
29317
|
-
|
|
29565
|
+
attachments,
|
|
29566
|
+
uploading,
|
|
29567
|
+
uploadError,
|
|
29568
|
+
onPickFiles: (files) => void pickFiles(Array.from(files)),
|
|
29569
|
+
onRemoveAttachment: (index) => setAttachments((prev) => prev.filter((_, i) => i !== index)),
|
|
29570
|
+
onSend: (text, refs) => {
|
|
29571
|
+
const accepted = refs.length ? chat.send(text, page, refs) : chat.send(text, page);
|
|
29572
|
+
if (accepted) {
|
|
29573
|
+
setAttachments([]);
|
|
29574
|
+
setUploadError(null);
|
|
29575
|
+
}
|
|
29576
|
+
return accepted;
|
|
29577
|
+
},
|
|
29318
29578
|
onStop: chat.stop
|
|
29319
29579
|
})
|
|
29320
29580
|
]
|
|
@@ -29326,7 +29586,8 @@ function ChatWidget() {
|
|
|
29326
29586
|
className: "fixed bottom-20 right-4 z-40 grid h-14 w-14 place-items-center rounded-full bg-accent text-white shadow-lg nav:bottom-4",
|
|
29327
29587
|
children: /* @__PURE__ */ jsx(MessageCircle, { size: 24 })
|
|
29328
29588
|
});
|
|
29329
|
-
if (mode === "panel") return /* @__PURE__ */ jsxs(
|
|
29589
|
+
if (mode === "panel") return /* @__PURE__ */ jsxs(ChatDropSurface, {
|
|
29590
|
+
onFiles: pickFiles,
|
|
29330
29591
|
className: "fixed bottom-20 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-surface shadow-xl nav:bottom-4",
|
|
29331
29592
|
children: [header, conversation]
|
|
29332
29593
|
});
|
|
@@ -29341,21 +29602,27 @@ function ChatWidget() {
|
|
|
29341
29602
|
}), /* @__PURE__ */ jsx(DialogContent, {
|
|
29342
29603
|
hideClose: true,
|
|
29343
29604
|
className: "h-[85dvh] w-full p-0 md:h-[85dvh] md:w-[80vw] md:max-w-[1040px] md:overflow-y-hidden",
|
|
29344
|
-
children: /* @__PURE__ */
|
|
29345
|
-
|
|
29346
|
-
|
|
29347
|
-
|
|
29348
|
-
|
|
29349
|
-
|
|
29350
|
-
|
|
29351
|
-
|
|
29352
|
-
chat.
|
|
29353
|
-
|
|
29354
|
-
|
|
29355
|
-
|
|
29356
|
-
|
|
29357
|
-
|
|
29358
|
-
|
|
29605
|
+
children: /* @__PURE__ */ jsx(ChatDropSurface, {
|
|
29606
|
+
onFiles: pickFiles,
|
|
29607
|
+
className: "h-full",
|
|
29608
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
29609
|
+
className: "flex h-full min-h-0 flex-col",
|
|
29610
|
+
children: [header, /* @__PURE__ */ jsxs("div", {
|
|
29611
|
+
className: "flex min-h-0 flex-1",
|
|
29612
|
+
children: [(wide || threadsOpen) && /* @__PURE__ */ jsx(ChatThreadList, {
|
|
29613
|
+
threads: chat.threads,
|
|
29614
|
+
activeId: chat.convId,
|
|
29615
|
+
onOpen: (id) => {
|
|
29616
|
+
chat.open(id);
|
|
29617
|
+
setAttachments([]);
|
|
29618
|
+
setUploadError(null);
|
|
29619
|
+
setThreadsOpen(false);
|
|
29620
|
+
},
|
|
29621
|
+
onNew: startNew,
|
|
29622
|
+
className: wide ? "w-[220px] shrink-0" : "w-full"
|
|
29623
|
+
}), (wide || !threadsOpen) && conversation]
|
|
29624
|
+
})]
|
|
29625
|
+
})
|
|
29359
29626
|
})
|
|
29360
29627
|
})]
|
|
29361
29628
|
});
|
|
@@ -11,6 +11,12 @@ export interface HistoryMsg {
|
|
|
11
11
|
text: string;
|
|
12
12
|
ts: number;
|
|
13
13
|
reasoning?: string;
|
|
14
|
+
attachments?: {
|
|
15
|
+
name: string;
|
|
16
|
+
mime?: string;
|
|
17
|
+
size?: number;
|
|
18
|
+
label?: string;
|
|
19
|
+
}[];
|
|
14
20
|
}
|
|
15
21
|
export declare function threadsAction(_body: Record<string, unknown>, caller: ActionCaller): Promise<{
|
|
16
22
|
threads: ThreadSummary[];
|
package/dist/runtime/actions.js
CHANGED
|
@@ -32,6 +32,7 @@ export async function historyAction(body, caller) {
|
|
|
32
32
|
text: m.text,
|
|
33
33
|
ts: m.ts,
|
|
34
34
|
...(reasoningByBot.has(m.msgId) ? { reasoning: reasoningByBot.get(m.msgId) } : {}),
|
|
35
|
+
...(m.attachments?.length ? { attachments: m.attachments } : {}),
|
|
35
36
|
})),
|
|
36
37
|
headMsgId: visible.at(-1)?.msgId ?? null,
|
|
37
38
|
};
|
|
@@ -10,7 +10,17 @@ export function chatPrefixFor(userId) {
|
|
|
10
10
|
return `u:${userId}:`;
|
|
11
11
|
}
|
|
12
12
|
export async function recordUser(m) {
|
|
13
|
-
await putThreadMessage({
|
|
13
|
+
await putThreadMessage({
|
|
14
|
+
connector: CONNECTOR,
|
|
15
|
+
chatId: m.chatId,
|
|
16
|
+
msgId: m.msgId,
|
|
17
|
+
role: 'user',
|
|
18
|
+
sender: m.sender ?? null,
|
|
19
|
+
...(m.attachments?.length ? { attachments: m.attachments } : {}),
|
|
20
|
+
text: m.text,
|
|
21
|
+
ts: m.ts,
|
|
22
|
+
replyToId: m.replyToId,
|
|
23
|
+
});
|
|
14
24
|
}
|
|
15
25
|
export async function recordAssistant(m) {
|
|
16
26
|
if (m.botMsgId == null || !m.text.trim())
|
|
@@ -43,7 +53,14 @@ export async function buildChain(headMsgId, opts) {
|
|
|
43
53
|
if (!stored)
|
|
44
54
|
break;
|
|
45
55
|
if (stored.role !== 'reasoning') {
|
|
46
|
-
acc.push({
|
|
56
|
+
acc.push({
|
|
57
|
+
role: stored.role,
|
|
58
|
+
content: stored.text,
|
|
59
|
+
sender: stored.sender,
|
|
60
|
+
...(stored.attachments ? { attachments: stored.attachments } : {}),
|
|
61
|
+
msgId: stored.msgId,
|
|
62
|
+
ts: stored.ts,
|
|
63
|
+
});
|
|
47
64
|
}
|
|
48
65
|
cur = stored.replyToId;
|
|
49
66
|
}
|
package/dist/runtime/send.js
CHANGED
|
@@ -34,9 +34,10 @@ export async function sendAction(body, ctx, deps = {}) {
|
|
|
34
34
|
const msgId = randomUUID();
|
|
35
35
|
const botMsgId = randomUUID();
|
|
36
36
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
37
|
+
const attachments = parseAttachments(body['attachments']);
|
|
37
38
|
const explicitReplyTo = typeof body['replyTo'] === 'string' && body['replyTo'] ? body['replyTo'] : null;
|
|
38
39
|
const replyTo = explicitReplyTo ?? (await history(chatId, 1)).at(-1)?.msgId ?? null;
|
|
39
|
-
await recordUser({ chatId, msgId, sender: ctx.caller.id, text, ts: nowSec, replyToId: replyTo });
|
|
40
|
+
await recordUser({ chatId, msgId, sender: ctx.caller.id, text, attachments, ts: nowSec, replyToId: replyTo });
|
|
40
41
|
const messages = await buildChain(msgId, { chatId });
|
|
41
42
|
const { connector, recorded } = makeStreamingConnector({
|
|
42
43
|
chatId,
|
|
@@ -55,3 +56,21 @@ export async function sendAction(body, ctx, deps = {}) {
|
|
|
55
56
|
}, { policy: policy() });
|
|
56
57
|
ctx.emit('done', { msgId: recorded(), parentMsgId: msgId });
|
|
57
58
|
}
|
|
59
|
+
function parseAttachments(value) {
|
|
60
|
+
if (!Array.isArray(value))
|
|
61
|
+
return undefined;
|
|
62
|
+
const refs = value.flatMap((item) => {
|
|
63
|
+
if (typeof item !== 'object' || item === null)
|
|
64
|
+
return [];
|
|
65
|
+
const r = item;
|
|
66
|
+
if (typeof r['name'] !== 'string' || !r['name'])
|
|
67
|
+
return [];
|
|
68
|
+
return [{
|
|
69
|
+
name: r['name'],
|
|
70
|
+
...(typeof r['mime'] === 'string' ? { mime: r['mime'] } : {}),
|
|
71
|
+
...(typeof r['size'] === 'number' ? { size: r['size'] } : {}),
|
|
72
|
+
...(typeof r['label'] === 'string' ? { label: r['label'] } : {}),
|
|
73
|
+
}];
|
|
74
|
+
});
|
|
75
|
+
return refs.length ? refs : undefined;
|
|
76
|
+
}
|
package/dist/web.js
CHANGED
|
@@ -11,7 +11,7 @@ import { definePluginUI } from "@coffer-org/web-sdk";
|
|
|
11
11
|
var ui_default = definePluginUI({ slots: [{
|
|
12
12
|
slot: "overlay",
|
|
13
13
|
id: "webchat",
|
|
14
|
-
load: () => import("./ChatWidget-
|
|
14
|
+
load: () => import("./ChatWidget-Cew9znAf.js")
|
|
15
15
|
}] });
|
|
16
16
|
//#endregion
|
|
17
17
|
export { ui_default as default };
|