@coffer-org/plugin-webchat 2.2.7 → 3.0.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-4U7nKidI.js → ChatWidget-BvI5pRWb.js} +198 -16
- package/dist/runtime/actions.d.ts +10 -0
- package/dist/runtime/actions.js +30 -2
- package/dist/runtime/chain-store.d.ts +5 -0
- package/dist/runtime/chain-store.js +20 -2
- package/dist/runtime/connector.js +21 -4
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +15 -5
- package/dist/runtime/send.js +10 -4
- package/dist/schema.js +102 -3
- package/dist/web.js +1 -1
- package/package.json +4 -4
|
@@ -507,6 +507,26 @@ 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 Check = createLucideIcon("check", [["path", {
|
|
511
|
+
d: "M20 6 9 17l-5-5",
|
|
512
|
+
key: "1gmf2c"
|
|
513
|
+
}]]);
|
|
514
|
+
/**
|
|
515
|
+
* @license lucide-react v1.24.0 - ISC
|
|
516
|
+
*
|
|
517
|
+
* This source code is licensed under the ISC license.
|
|
518
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
519
|
+
*/
|
|
520
|
+
var ChevronDown = createLucideIcon("chevron-down", [["path", {
|
|
521
|
+
d: "m6 9 6 6 6-6",
|
|
522
|
+
key: "qrunsl"
|
|
523
|
+
}]]);
|
|
524
|
+
/**
|
|
525
|
+
* @license lucide-react v1.24.0 - ISC
|
|
526
|
+
*
|
|
527
|
+
* This source code is licensed under the ISC license.
|
|
528
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
529
|
+
*/
|
|
510
530
|
var FileText = createLucideIcon("file-text", [
|
|
511
531
|
["path", {
|
|
512
532
|
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",
|
|
@@ -6291,28 +6311,45 @@ async function* readSse(body) {
|
|
|
6291
6311
|
/** Web-chat transport. Session cookies are sent automatically (same-origin), so there is no
|
|
6292
6312
|
* separate authorization here; the gate is on the server. */
|
|
6293
6313
|
var BASE = "/api/plugins/webchat/user";
|
|
6314
|
+
/** POST a member-level webchat action with a JSON body: the one request path every call
|
|
6315
|
+
* in this file (except the SSE stream in `sendMessage`) goes through. Throws `Error(action)`
|
|
6316
|
+
* on any non-OK response — every caller below relies on that to build its own error message. */
|
|
6317
|
+
async function postAction(action, body) {
|
|
6318
|
+
const r = await fetch(`${BASE}/${action}`, {
|
|
6319
|
+
method: "POST",
|
|
6320
|
+
headers: { "content-type": "application/json" },
|
|
6321
|
+
body: JSON.stringify(body)
|
|
6322
|
+
});
|
|
6323
|
+
if (!r.ok) throw new Error(action);
|
|
6324
|
+
return await r.json();
|
|
6325
|
+
}
|
|
6294
6326
|
/**
|
|
6295
6327
|
* Throws on any rejection, including 404 (“plugin was disabled AFTER the widget bundle
|
|
6296
6328
|
* loaded”). There is deliberately no separate `null` for this case: the caller keeps
|
|
6297
6329
|
* the current thread list either way, so two return codes would mean the same thing.
|
|
6298
6330
|
*/
|
|
6299
6331
|
async function fetchThreads() {
|
|
6300
|
-
|
|
6301
|
-
method: "POST",
|
|
6302
|
-
headers: { "content-type": "application/json" },
|
|
6303
|
-
body: "{}"
|
|
6304
|
-
});
|
|
6305
|
-
if (!r.ok) throw new Error("threads");
|
|
6306
|
-
return (await r.json()).threads;
|
|
6332
|
+
return (await postAction("threads", {})).threads;
|
|
6307
6333
|
}
|
|
6308
6334
|
async function fetchHistory(convId) {
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6335
|
+
return postAction("history", { convId });
|
|
6336
|
+
}
|
|
6337
|
+
async function fetchAgents(convId) {
|
|
6338
|
+
const out = await postAction("agents", { convId });
|
|
6339
|
+
return {
|
|
6340
|
+
agents: out.agents ?? [],
|
|
6341
|
+
defaultAgentId: out.defaultAgentId ?? null,
|
|
6342
|
+
selection: out.selection ?? {
|
|
6343
|
+
agentId: null,
|
|
6344
|
+
presetId: null
|
|
6345
|
+
}
|
|
6346
|
+
};
|
|
6347
|
+
}
|
|
6348
|
+
async function selectAgent(convId, patch) {
|
|
6349
|
+
return (await postAction("selectAgent", {
|
|
6350
|
+
convId,
|
|
6351
|
+
...patch
|
|
6352
|
+
})).selection;
|
|
6316
6353
|
}
|
|
6317
6354
|
async function sendMessage(req) {
|
|
6318
6355
|
const r = await fetch(`${BASE}/send/stream`, {
|
|
@@ -6356,6 +6393,12 @@ function useChat() {
|
|
|
6356
6393
|
const [pending, setPending] = useState(null);
|
|
6357
6394
|
const [pendingReasoning, setPendingReasoning] = useState(null);
|
|
6358
6395
|
const [error, setError] = useState(null);
|
|
6396
|
+
const [agents, setAgents] = useState([]);
|
|
6397
|
+
const [defaultAgentId, setDefaultAgentId] = useState(null);
|
|
6398
|
+
const [selection, setSelection] = useState({
|
|
6399
|
+
agentId: null,
|
|
6400
|
+
presetId: null
|
|
6401
|
+
});
|
|
6359
6402
|
const headRef = useRef(null);
|
|
6360
6403
|
const abortRef = useRef(null);
|
|
6361
6404
|
const sendingRef = useRef(false);
|
|
@@ -6374,6 +6417,32 @@ function useChat() {
|
|
|
6374
6417
|
if (aliveRef.current) setThreads(list);
|
|
6375
6418
|
}).catch(() => {});
|
|
6376
6419
|
}, []);
|
|
6420
|
+
useEffect(() => {
|
|
6421
|
+
if (!convId) return;
|
|
6422
|
+
let cancelled = false;
|
|
6423
|
+
fetchAgents(convId).then((r) => {
|
|
6424
|
+
if (cancelled) return;
|
|
6425
|
+
setAgents(r.agents);
|
|
6426
|
+
setDefaultAgentId(r.defaultAgentId);
|
|
6427
|
+
setSelection(r.selection);
|
|
6428
|
+
}).catch(() => {});
|
|
6429
|
+
return () => {
|
|
6430
|
+
cancelled = true;
|
|
6431
|
+
};
|
|
6432
|
+
}, [convId]);
|
|
6433
|
+
const chooseAgent = useCallback((next) => {
|
|
6434
|
+
const previous = selection;
|
|
6435
|
+
setSelection(next);
|
|
6436
|
+
if (convId) selectAgent(convId, next).then(setSelection).catch((err) => {
|
|
6437
|
+
setSelection(previous);
|
|
6438
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
6439
|
+
});
|
|
6440
|
+
}, [convId, selection]);
|
|
6441
|
+
const attachmentsAllowed = useMemo(() => {
|
|
6442
|
+
const preset = agents.find((a) => a.id === selection.agentId)?.presets.find((p) => p.id === selection.presetId);
|
|
6443
|
+
if (!preset?.capabilities) return true;
|
|
6444
|
+
return preset.capabilities.vision !== false || preset.capabilities.documents !== false;
|
|
6445
|
+
}, [agents, selection]);
|
|
6377
6446
|
const newConversation = useCallback(() => {
|
|
6378
6447
|
abortRef.current?.abort();
|
|
6379
6448
|
abortRef.current = null;
|
|
@@ -6419,6 +6488,11 @@ function useChat() {
|
|
|
6419
6488
|
pending,
|
|
6420
6489
|
pendingReasoning,
|
|
6421
6490
|
error,
|
|
6491
|
+
agents,
|
|
6492
|
+
defaultAgentId,
|
|
6493
|
+
selection,
|
|
6494
|
+
attachmentsAllowed,
|
|
6495
|
+
chooseAgent,
|
|
6422
6496
|
send: useCallback((text, context, attachments = []) => {
|
|
6423
6497
|
if (!text.trim() || sendingRef.current) return false;
|
|
6424
6498
|
sendingRef.current = true;
|
|
@@ -29309,8 +29383,97 @@ var Bubble = memo(function Bubble({ role, text, reasoning, attachments }) {
|
|
|
29309
29383
|
});
|
|
29310
29384
|
});
|
|
29311
29385
|
//#endregion
|
|
29386
|
+
//#region src/ui/AgentPicker.tsx
|
|
29387
|
+
/** Flatten the catalog to one list: with a single registered agent the agent
|
|
29388
|
+
* level is noise, and with several the agent name simply prefixes its presets. */
|
|
29389
|
+
function AgentPicker({ agents, defaultAgentId, selection, onChange }) {
|
|
29390
|
+
const { t } = useTranslation();
|
|
29391
|
+
const [open, setOpen] = useState(false);
|
|
29392
|
+
const rootRef = useRef(null);
|
|
29393
|
+
const triggerRef = useRef(null);
|
|
29394
|
+
useEffect(() => {
|
|
29395
|
+
if (!open) return;
|
|
29396
|
+
function onPointerDown(e) {
|
|
29397
|
+
if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
|
|
29398
|
+
}
|
|
29399
|
+
function onKeyDown(e) {
|
|
29400
|
+
if (e.key === "Escape") {
|
|
29401
|
+
setOpen(false);
|
|
29402
|
+
triggerRef.current?.focus();
|
|
29403
|
+
}
|
|
29404
|
+
}
|
|
29405
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
29406
|
+
document.addEventListener("keydown", onKeyDown);
|
|
29407
|
+
return () => {
|
|
29408
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
29409
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
29410
|
+
};
|
|
29411
|
+
}, [open]);
|
|
29412
|
+
const multi = agents.length > 1;
|
|
29413
|
+
const rows = agents.flatMap((a) => a.presets.map((p) => ({
|
|
29414
|
+
agentId: a.id,
|
|
29415
|
+
presetId: p.id,
|
|
29416
|
+
label: multi ? `${a.title} · ${p.title}` : p.title,
|
|
29417
|
+
hint: p.hint,
|
|
29418
|
+
isDefault: p.default === true
|
|
29419
|
+
})));
|
|
29420
|
+
if (rows.length === 0) return null;
|
|
29421
|
+
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];
|
|
29422
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
29423
|
+
className: "relative",
|
|
29424
|
+
ref: rootRef,
|
|
29425
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
29426
|
+
ref: triggerRef,
|
|
29427
|
+
type: "button",
|
|
29428
|
+
"aria-haspopup": "menu",
|
|
29429
|
+
"aria-expanded": open,
|
|
29430
|
+
className: "flex items-center gap-1 rounded-md px-2 py-1 text-xs text-muted hover:text-text focus-visible:outline-2 focus-visible:outline-accent",
|
|
29431
|
+
onClick: () => setOpen((v) => !v),
|
|
29432
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
29433
|
+
className: "max-w-40 truncate",
|
|
29434
|
+
children: current.label
|
|
29435
|
+
}), /* @__PURE__ */ jsx(ChevronDown, {
|
|
29436
|
+
size: 12,
|
|
29437
|
+
className: "shrink-0"
|
|
29438
|
+
})]
|
|
29439
|
+
}), open && /* @__PURE__ */ jsx("ul", {
|
|
29440
|
+
"aria-label": t("webchat.agentPicker"),
|
|
29441
|
+
className: "absolute bottom-full z-10 mb-1 max-h-64 w-64 overflow-y-auto rounded-md border border-border bg-elevated py-1 shadow-lg",
|
|
29442
|
+
children: rows.map((r) => {
|
|
29443
|
+
const selected = r === current;
|
|
29444
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
|
|
29445
|
+
type: "button",
|
|
29446
|
+
...selected ? { "aria-current": true } : {},
|
|
29447
|
+
className: "flex w-full items-center gap-2 px-2 py-1.5 text-left text-xs hover:bg-surface focus-visible:outline-2 focus-visible:outline-accent",
|
|
29448
|
+
onClick: () => {
|
|
29449
|
+
setOpen(false);
|
|
29450
|
+
onChange({
|
|
29451
|
+
agentId: r.agentId,
|
|
29452
|
+
presetId: r.presetId
|
|
29453
|
+
});
|
|
29454
|
+
},
|
|
29455
|
+
children: [
|
|
29456
|
+
/* @__PURE__ */ jsx(Check, {
|
|
29457
|
+
size: 12,
|
|
29458
|
+
className: selected ? "shrink-0 text-accent" : "shrink-0 opacity-0"
|
|
29459
|
+
}),
|
|
29460
|
+
/* @__PURE__ */ jsx("span", {
|
|
29461
|
+
className: "min-w-0 flex-1 truncate",
|
|
29462
|
+
children: r.label
|
|
29463
|
+
}),
|
|
29464
|
+
r.hint && /* @__PURE__ */ jsx("span", {
|
|
29465
|
+
className: "shrink-0 text-muted",
|
|
29466
|
+
children: r.hint
|
|
29467
|
+
})
|
|
29468
|
+
]
|
|
29469
|
+
}) }, `${r.agentId}/${r.presetId}`);
|
|
29470
|
+
})
|
|
29471
|
+
})]
|
|
29472
|
+
});
|
|
29473
|
+
}
|
|
29474
|
+
//#endregion
|
|
29312
29475
|
//#region src/ui/ChatComposer.tsx
|
|
29313
|
-
function ChatComposer({ streaming, onSend, onStop, attachments, uploading, uploadError, uploadProgress, canRetryUpload, onRetryUpload, onPickFiles, onRemoveAttachment }) {
|
|
29476
|
+
function ChatComposer({ streaming, onSend, onStop, attachments, uploading, uploadError, uploadProgress, canRetryUpload, onRetryUpload, onPickFiles, onRemoveAttachment, agents, defaultAgentId, selection, onChooseAgent, attachmentsAllowed }) {
|
|
29314
29477
|
const { t } = useTranslation();
|
|
29315
29478
|
const [text, setText] = useState("");
|
|
29316
29479
|
function submit() {
|
|
@@ -29389,7 +29552,7 @@ function ChatComposer({ streaming, onSend, onStop, attachments, uploading, uploa
|
|
|
29389
29552
|
}), /* @__PURE__ */ jsxs("div", {
|
|
29390
29553
|
className: "flex items-end gap-2",
|
|
29391
29554
|
children: [
|
|
29392
|
-
/* @__PURE__ */ jsxs("label", {
|
|
29555
|
+
attachmentsAllowed && /* @__PURE__ */ jsxs("label", {
|
|
29393
29556
|
"aria-label": t("webchat.attach"),
|
|
29394
29557
|
className: `cursor-pointer rounded-lg p-2 text-muted hover:bg-elevated hover:text-accent ${uploading ? "pointer-events-none opacity-60" : ""}`,
|
|
29395
29558
|
children: [/* @__PURE__ */ jsx(Paperclip, { size: 18 }), /* @__PURE__ */ jsx("input", {
|
|
@@ -29416,6 +29579,12 @@ function ChatComposer({ streaming, onSend, onStop, attachments, uploading, uploa
|
|
|
29416
29579
|
}
|
|
29417
29580
|
}
|
|
29418
29581
|
}),
|
|
29582
|
+
/* @__PURE__ */ jsx(AgentPicker, {
|
|
29583
|
+
agents,
|
|
29584
|
+
defaultAgentId,
|
|
29585
|
+
selection,
|
|
29586
|
+
onChange: onChooseAgent
|
|
29587
|
+
}),
|
|
29419
29588
|
streaming ? /* @__PURE__ */ jsx("button", {
|
|
29420
29589
|
"aria-label": t("webchat.stop"),
|
|
29421
29590
|
className: "rounded-lg p-2 hover:bg-elevated",
|
|
@@ -29568,6 +29737,14 @@ function ChatWidget() {
|
|
|
29568
29737
|
useEffect(() => {
|
|
29569
29738
|
if (isOpen) chat.loadThreads();
|
|
29570
29739
|
}, [isOpen, chat.loadThreads]);
|
|
29740
|
+
const { convId, newConversation } = chat;
|
|
29741
|
+
useEffect(() => {
|
|
29742
|
+
if (isOpen && convId === null) newConversation();
|
|
29743
|
+
}, [
|
|
29744
|
+
isOpen,
|
|
29745
|
+
convId,
|
|
29746
|
+
newConversation
|
|
29747
|
+
]);
|
|
29571
29748
|
function startNew() {
|
|
29572
29749
|
chat.newConversation();
|
|
29573
29750
|
setAttachments([]);
|
|
@@ -29676,6 +29853,11 @@ function ChatWidget() {
|
|
|
29676
29853
|
onRetryUpload: retryUploads,
|
|
29677
29854
|
onPickFiles: (files) => void pickFiles(Array.from(files)),
|
|
29678
29855
|
onRemoveAttachment: (index) => setAttachments((prev) => prev.filter((_, i) => i !== index)),
|
|
29856
|
+
agents: chat.agents,
|
|
29857
|
+
defaultAgentId: chat.defaultAgentId,
|
|
29858
|
+
selection: chat.selection,
|
|
29859
|
+
onChooseAgent: chat.chooseAgent,
|
|
29860
|
+
attachmentsAllowed: chat.attachmentsAllowed,
|
|
29679
29861
|
onSend: (text, refs) => {
|
|
29680
29862
|
const accepted = refs.length ? chat.send(text, page, refs) : chat.send(text, page);
|
|
29681
29863
|
if (accepted) {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type ActionCaller } from '@coffer-org/server/plugin-hooks';
|
|
2
|
+
import { type AgentDescriptor } from '@coffer-org/plugin-orchestrator/runtime';
|
|
3
|
+
import { type ThreadSelection } from './chain-store.ts';
|
|
2
4
|
export interface ThreadSummary {
|
|
3
5
|
convId: string;
|
|
4
6
|
title: string;
|
|
@@ -26,3 +28,11 @@ export declare function historyAction(body: Record<string, unknown>, caller: Act
|
|
|
26
28
|
messages: HistoryMsg[];
|
|
27
29
|
headMsgId: string | null;
|
|
28
30
|
}>;
|
|
31
|
+
export declare function agentsAction(body: Record<string, unknown>, caller: ActionCaller): Promise<{
|
|
32
|
+
agents: AgentDescriptor[];
|
|
33
|
+
defaultAgentId: string | null;
|
|
34
|
+
selection: ThreadSelection;
|
|
35
|
+
}>;
|
|
36
|
+
export declare function selectAgentAction(body: Record<string, unknown>, caller: ActionCaller): Promise<{
|
|
37
|
+
selection: ThreadSelection;
|
|
38
|
+
}>;
|
package/dist/runtime/actions.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { HttpError } from '@coffer-org/server/plugin-hooks';
|
|
2
|
-
import {
|
|
2
|
+
import { listAgentCatalog, getDefaultAgentId, listRegisteredAgents, } from '@coffer-org/plugin-orchestrator/runtime';
|
|
3
|
+
import { chatIdFor, conversations, history, getSelection, setSelection } from "./chain-store.js";
|
|
3
4
|
const TITLE_MAX = 60;
|
|
4
5
|
function title(firstUserText) {
|
|
5
6
|
const t = firstUserText.trim().replace(/\s+/g, ' ');
|
|
@@ -17,10 +18,14 @@ export async function threadsAction(_body, caller) {
|
|
|
17
18
|
})),
|
|
18
19
|
};
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
+
function requireConvId(body) {
|
|
21
22
|
const convId = body['convId'];
|
|
22
23
|
if (typeof convId !== 'string' || !convId)
|
|
23
24
|
throw new HttpError(400, 'missing required field: convId');
|
|
25
|
+
return convId;
|
|
26
|
+
}
|
|
27
|
+
export async function historyAction(body, caller) {
|
|
28
|
+
const convId = requireConvId(body);
|
|
24
29
|
const rows = await history(chatIdFor(caller.id, convId));
|
|
25
30
|
const reasoningByBot = new Map(rows.filter((m) => m.role === 'reasoning').map((m) => [m.msgId.slice(0, -2), m.text]));
|
|
26
31
|
const visible = rows.filter((m) => m.role !== 'reasoning');
|
|
@@ -37,3 +42,26 @@ export async function historyAction(body, caller) {
|
|
|
37
42
|
headMsgId: visible.at(-1)?.msgId ?? null,
|
|
38
43
|
};
|
|
39
44
|
}
|
|
45
|
+
export async function agentsAction(body, caller) {
|
|
46
|
+
const convId = requireConvId(body);
|
|
47
|
+
return {
|
|
48
|
+
agents: await listAgentCatalog(),
|
|
49
|
+
defaultAgentId: getDefaultAgentId() ?? null,
|
|
50
|
+
selection: await getSelection(chatIdFor(caller.id, convId)),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export async function selectAgentAction(body, caller) {
|
|
54
|
+
const convId = requireConvId(body);
|
|
55
|
+
const chatId = chatIdFor(caller.id, convId);
|
|
56
|
+
const patch = {};
|
|
57
|
+
if ('agentId' in body) {
|
|
58
|
+
const agentId = typeof body['agentId'] === 'string' && body['agentId'] ? body['agentId'] : null;
|
|
59
|
+
if (agentId && !listRegisteredAgents().includes(agentId))
|
|
60
|
+
throw new HttpError(400, `unknown agent: ${agentId}`);
|
|
61
|
+
patch.agentId = agentId;
|
|
62
|
+
}
|
|
63
|
+
if ('presetId' in body)
|
|
64
|
+
patch.presetId = typeof body['presetId'] === 'string' && body['presetId'] ? body['presetId'] : null;
|
|
65
|
+
await setSelection(chatId, patch);
|
|
66
|
+
return { selection: await getSelection(chatId) };
|
|
67
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ThreadChat, type StoredMsg } from '@coffer-org/server/thread-store';
|
|
2
|
+
import { type ThreadSelection } from '@coffer-org/server/thread-state';
|
|
2
3
|
import type { ConvMessage } from '@coffer-org/plugin-orchestrator/runtime';
|
|
3
4
|
export declare const CONNECTOR = "webchat";
|
|
4
5
|
export declare function chatIdFor(userId: string, convId: string): string;
|
|
@@ -32,3 +33,7 @@ export declare function buildChain(headMsgId: string, opts: {
|
|
|
32
33
|
}): Promise<ConvMessage[]>;
|
|
33
34
|
export declare function history(chatId: string, limit?: number): Promise<StoredMsg[]>;
|
|
34
35
|
export declare function conversations(userId: string): Promise<ThreadChat[]>;
|
|
36
|
+
export type { ThreadSelection };
|
|
37
|
+
export declare function getSelection(chatId: string): Promise<ThreadSelection>;
|
|
38
|
+
export declare function setSelection(chatId: string, patch: Partial<ThreadSelection>): Promise<void>;
|
|
39
|
+
export declare function readSelectionForTurn(chatId: string): Promise<ThreadSelection>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { getThreadMessage, putThreadMessage, listThreadMessages, listThreadChats } from '@coffer-org/server/thread-store';
|
|
1
|
+
import { getThreadMessage, putThreadMessage, listThreadMessages, listThreadChats, } from '@coffer-org/server/thread-store';
|
|
2
|
+
import { getThreadState, setThreadState, readAndTouchThreadState, } from '@coffer-org/server/thread-state';
|
|
2
3
|
import { getLogger } from '@coffer-org/sdk/logger';
|
|
3
4
|
const log = getLogger('webchat');
|
|
4
5
|
export const CONNECTOR = 'webchat';
|
|
@@ -25,7 +26,15 @@ export async function recordUser(m) {
|
|
|
25
26
|
export async function recordAssistant(m) {
|
|
26
27
|
if (m.botMsgId == null || !m.text.trim())
|
|
27
28
|
return false;
|
|
28
|
-
await putThreadMessage({
|
|
29
|
+
await putThreadMessage({
|
|
30
|
+
connector: CONNECTOR,
|
|
31
|
+
chatId: m.chatId,
|
|
32
|
+
msgId: m.botMsgId,
|
|
33
|
+
role: 'assistant',
|
|
34
|
+
text: m.text,
|
|
35
|
+
ts: m.now,
|
|
36
|
+
replyToId: m.parentMsgId,
|
|
37
|
+
});
|
|
29
38
|
return true;
|
|
30
39
|
}
|
|
31
40
|
export async function recordReasoning(m) {
|
|
@@ -76,3 +85,12 @@ export function history(chatId, limit) {
|
|
|
76
85
|
export function conversations(userId) {
|
|
77
86
|
return listThreadChats(CONNECTOR, chatPrefixFor(userId));
|
|
78
87
|
}
|
|
88
|
+
export async function getSelection(chatId) {
|
|
89
|
+
return getThreadState(CONNECTOR, chatId);
|
|
90
|
+
}
|
|
91
|
+
export async function setSelection(chatId, patch) {
|
|
92
|
+
await setThreadState(CONNECTOR, chatId, patch);
|
|
93
|
+
}
|
|
94
|
+
export async function readSelectionForTurn(chatId) {
|
|
95
|
+
return readAndTouchThreadState(CONNECTOR, chatId);
|
|
96
|
+
}
|
|
@@ -11,8 +11,13 @@ export function makeStreamingConnector(opts) {
|
|
|
11
11
|
reply(_chatId, _ctx) {
|
|
12
12
|
const ch = makeLiveChannel({
|
|
13
13
|
ops: {
|
|
14
|
-
send: async (text) => {
|
|
15
|
-
|
|
14
|
+
send: async (text) => {
|
|
15
|
+
opts.emit('message', { msgId: opts.botMsgId, text });
|
|
16
|
+
return opts.botMsgId;
|
|
17
|
+
},
|
|
18
|
+
edit: async (msgId, text) => {
|
|
19
|
+
opts.emit('delta', { msgId, text });
|
|
20
|
+
},
|
|
16
21
|
},
|
|
17
22
|
throttleMs: WEBCHAT_THROTTLE_MS,
|
|
18
23
|
maxLength: WEBCHAT_MAX,
|
|
@@ -29,10 +34,22 @@ export function makeStreamingConnector(opts) {
|
|
|
29
34
|
},
|
|
30
35
|
async recordAssistant(m) {
|
|
31
36
|
const now = Math.floor(Date.now() / 1000);
|
|
32
|
-
const wrote = await record({
|
|
37
|
+
const wrote = await record({
|
|
38
|
+
chatId: opts.chatId,
|
|
39
|
+
parentMsgId: m.parentMsgId,
|
|
40
|
+
botMsgId: m.botMsgId,
|
|
41
|
+
text: m.text,
|
|
42
|
+
now,
|
|
43
|
+
});
|
|
33
44
|
recordedId = wrote ? m.botMsgId : null;
|
|
34
45
|
if (wrote && opts.display !== 'off' && m.reasoning && m.botMsgId) {
|
|
35
|
-
await recordReasoningFn({
|
|
46
|
+
await recordReasoningFn({
|
|
47
|
+
chatId: opts.chatId,
|
|
48
|
+
botMsgId: m.botMsgId,
|
|
49
|
+
parentMsgId: m.parentMsgId,
|
|
50
|
+
text: m.reasoning,
|
|
51
|
+
now,
|
|
52
|
+
});
|
|
36
53
|
}
|
|
37
54
|
},
|
|
38
55
|
};
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ import type { PluginHooks } from '@coffer-org/server/plugin-hooks';
|
|
|
2
2
|
export type { ThreadSummary, HistoryMsg } from './actions.ts';
|
|
3
3
|
export { WEB_FORMAT } from './format.ts';
|
|
4
4
|
export { makeStreamingConnector } from './connector.ts';
|
|
5
|
-
export { chatIdFor, chatPrefixFor, recordUser, recordAssistant, buildChain } from './chain-store.ts';
|
|
5
|
+
export { chatIdFor, chatPrefixFor, recordUser, recordAssistant, buildChain, getSelection, setSelection, } from './chain-store.ts';
|
|
6
6
|
export { sendAction, pageContext } from './send.ts';
|
|
7
7
|
export declare const serverHooks: PluginHooks;
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { pruneThreadMessages } from '@coffer-org/server/thread-store';
|
|
2
|
-
import {
|
|
2
|
+
import { pruneThreadState } from '@coffer-org/server/thread-state';
|
|
3
|
+
import { threadsAction, historyAction, agentsAction, selectAgentAction } from "./actions.js";
|
|
3
4
|
import { sendAction } from "./send.js";
|
|
4
5
|
import { CONNECTOR } from "./chain-store.js";
|
|
5
6
|
import { registerConnector } from '@coffer-org/plugin-orchestrator/runtime';
|
|
@@ -7,14 +8,21 @@ const THREAD_TTL_MS = Number(process.env['WEBCHAT_THREAD_TTL_MS'] ?? 30 * 86_400
|
|
|
7
8
|
let unregisterConnector;
|
|
8
9
|
export { WEB_FORMAT } from "./format.js";
|
|
9
10
|
export { makeStreamingConnector } from "./connector.js";
|
|
10
|
-
export { chatIdFor, chatPrefixFor, recordUser, recordAssistant, buildChain } from "./chain-store.js";
|
|
11
|
+
export { chatIdFor, chatPrefixFor, recordUser, recordAssistant, buildChain, getSelection, setSelection, } from "./chain-store.js";
|
|
11
12
|
export { sendAction, pageContext } from "./send.js";
|
|
12
13
|
export const serverHooks = {
|
|
13
|
-
init: () => {
|
|
14
|
-
|
|
14
|
+
init: () => {
|
|
15
|
+
unregisterConnector = registerConnector({ id: 'webchat' });
|
|
16
|
+
},
|
|
17
|
+
teardown: () => {
|
|
18
|
+
unregisterConnector?.();
|
|
19
|
+
unregisterConnector = undefined;
|
|
20
|
+
},
|
|
15
21
|
userActions: {
|
|
16
22
|
threads: threadsAction,
|
|
17
23
|
history: historyAction,
|
|
24
|
+
agents: agentsAction,
|
|
25
|
+
selectAgent: selectAgentAction,
|
|
18
26
|
},
|
|
19
27
|
streamActions: {
|
|
20
28
|
send: (body, ctx) => sendAction(body, ctx),
|
|
@@ -24,7 +32,9 @@ export const serverHooks = {
|
|
|
24
32
|
name: 'webchat-thread-prune',
|
|
25
33
|
intervalMs: 86_400_000,
|
|
26
34
|
run: async () => {
|
|
27
|
-
|
|
35
|
+
const cutoff = new Date(Date.now() - THREAD_TTL_MS);
|
|
36
|
+
await pruneThreadMessages(CONNECTOR, Math.floor(cutoff.getTime() / 1000));
|
|
37
|
+
await pruneThreadState(CONNECTOR, cutoff.toISOString());
|
|
28
38
|
},
|
|
29
39
|
},
|
|
30
40
|
],
|
package/dist/runtime/send.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { handleIncoming } from '@coffer-org/plugin-orchestrator/runtime';
|
|
3
|
-
import { chatIdFor, recordUser, buildChain, history } from "./chain-store.js";
|
|
2
|
+
import { handleIncoming, liveAgentId } from '@coffer-org/plugin-orchestrator/runtime';
|
|
3
|
+
import { chatIdFor, recordUser, buildChain, history, readSelectionForTurn } from "./chain-store.js";
|
|
4
4
|
import { policy } from "./config.js";
|
|
5
5
|
import { makeStreamingConnector } from "./connector.js";
|
|
6
6
|
import { WEB_FORMAT } from "./format.js";
|
|
@@ -31,6 +31,8 @@ export async function sendAction(body, ctx, deps = {}) {
|
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
33
|
const chatId = chatIdFor(ctx.caller.id, convId);
|
|
34
|
+
const selection = await readSelectionForTurn(chatId);
|
|
35
|
+
const agentId = liveAgentId(selection.agentId);
|
|
34
36
|
const msgId = randomUUID();
|
|
35
37
|
const botMsgId = randomUUID();
|
|
36
38
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
@@ -50,6 +52,8 @@ export async function sendAction(body, ctx, deps = {}) {
|
|
|
50
52
|
connectorId: 'webchat',
|
|
51
53
|
channelSystem: WEB_FORMAT,
|
|
52
54
|
...(volatileSystem ? { volatileSystem } : {}),
|
|
55
|
+
...(agentId ? { agentId } : {}),
|
|
56
|
+
...(selection.presetId ? { presetId: selection.presetId } : {}),
|
|
53
57
|
chatId,
|
|
54
58
|
sender: { id: ctx.caller.id },
|
|
55
59
|
messages,
|
|
@@ -65,12 +69,14 @@ function parseAttachments(value) {
|
|
|
65
69
|
const r = item;
|
|
66
70
|
if (typeof r['name'] !== 'string' || !r['name'])
|
|
67
71
|
return [];
|
|
68
|
-
return [
|
|
72
|
+
return [
|
|
73
|
+
{
|
|
69
74
|
name: r['name'],
|
|
70
75
|
...(typeof r['mime'] === 'string' ? { mime: r['mime'] } : {}),
|
|
71
76
|
...(typeof r['size'] === 'number' ? { size: r['size'] } : {}),
|
|
72
77
|
...(typeof r['label'] === 'string' ? { label: r['label'] } : {}),
|
|
73
|
-
}
|
|
78
|
+
},
|
|
79
|
+
];
|
|
74
80
|
});
|
|
75
81
|
return refs.length ? refs : undefined;
|
|
76
82
|
}
|
package/dist/schema.js
CHANGED
|
@@ -4502,6 +4502,8 @@ function normalizeOpts(rawIn) {
|
|
|
4502
4502
|
unique: r.unique,
|
|
4503
4503
|
fixed: r.fixed,
|
|
4504
4504
|
exts: r.exts,
|
|
4505
|
+
ext: r.ext,
|
|
4506
|
+
maxBytes: r.maxBytes,
|
|
4505
4507
|
lead: r.lead,
|
|
4506
4508
|
by: r.by,
|
|
4507
4509
|
min: r.min,
|
|
@@ -4913,6 +4915,42 @@ function snippet(raw) {
|
|
|
4913
4915
|
}
|
|
4914
4916
|
});
|
|
4915
4917
|
}
|
|
4918
|
+
/** A NUL byte, or U+FFFD left behind by decoding non-UTF-8 bytes as text. */
|
|
4919
|
+
function isBinaryText(v) {
|
|
4920
|
+
return v.includes("\0") || v.includes("�");
|
|
4921
|
+
}
|
|
4922
|
+
function source(raw) {
|
|
4923
|
+
const o = normalizeOpts(raw);
|
|
4924
|
+
const required = o.required ?? false;
|
|
4925
|
+
const ext = o.ext ?? "txt";
|
|
4926
|
+
const maxBytes = o.maxBytes ?? 262144;
|
|
4927
|
+
const s = string$1(reqTypeErr()).superRefine((v, ctx) => {
|
|
4928
|
+
if (isBinaryText(v)) {
|
|
4929
|
+
ctx.addIssue({
|
|
4930
|
+
code: ZodIssueCode.custom,
|
|
4931
|
+
message: vmsg("source_binary")
|
|
4932
|
+
});
|
|
4933
|
+
return;
|
|
4934
|
+
}
|
|
4935
|
+
if (new TextEncoder().encode(v).length > maxBytes) ctx.addIssue({
|
|
4936
|
+
code: ZodIssueCode.custom,
|
|
4937
|
+
message: vmsg("source_too_large", { maxBytes })
|
|
4938
|
+
});
|
|
4939
|
+
});
|
|
4940
|
+
return wrapKey(o, {
|
|
4941
|
+
kind: "source",
|
|
4942
|
+
label: o.label ?? "",
|
|
4943
|
+
required,
|
|
4944
|
+
prim: "text",
|
|
4945
|
+
column: "text",
|
|
4946
|
+
hints: {
|
|
4947
|
+
ext,
|
|
4948
|
+
maxBytes,
|
|
4949
|
+
noEditControl: true
|
|
4950
|
+
},
|
|
4951
|
+
zod: optionalize(s, required)
|
|
4952
|
+
});
|
|
4953
|
+
}
|
|
4916
4954
|
function rating(raw) {
|
|
4917
4955
|
const o = normalizeOpts(raw);
|
|
4918
4956
|
const required = o.required ?? false;
|
|
@@ -5049,6 +5087,7 @@ var presets = {
|
|
|
5049
5087
|
tags,
|
|
5050
5088
|
markdown,
|
|
5051
5089
|
snippet,
|
|
5090
|
+
source,
|
|
5052
5091
|
rating,
|
|
5053
5092
|
duration,
|
|
5054
5093
|
reminder,
|
|
@@ -6035,6 +6074,15 @@ function wrapKey(opts, meta) {
|
|
|
6035
6074
|
...m,
|
|
6036
6075
|
hidden: opts.hidden
|
|
6037
6076
|
};
|
|
6077
|
+
if (opts.derive) m = {
|
|
6078
|
+
...m,
|
|
6079
|
+
derive: opts.derive,
|
|
6080
|
+
derived: true,
|
|
6081
|
+
hints: {
|
|
6082
|
+
...m.hints,
|
|
6083
|
+
noEditControl: true
|
|
6084
|
+
}
|
|
6085
|
+
};
|
|
6038
6086
|
if (opts.key) return {
|
|
6039
6087
|
key: opts.key,
|
|
6040
6088
|
type: m
|
|
@@ -6349,11 +6397,18 @@ function triState(raw) {
|
|
|
6349
6397
|
zod: optionalize(s, required)
|
|
6350
6398
|
});
|
|
6351
6399
|
}
|
|
6400
|
+
/** Inline option entry → OptionItem (plain string = value and label at once). */
|
|
6401
|
+
function toOptionItem(o) {
|
|
6402
|
+
return typeof o === "string" ? {
|
|
6403
|
+
value: o,
|
|
6404
|
+
title: o
|
|
6405
|
+
} : o;
|
|
6406
|
+
}
|
|
6352
6407
|
function select(raw) {
|
|
6353
6408
|
const o = normalizeOpts(raw);
|
|
6354
6409
|
const required = o.required ?? false;
|
|
6355
6410
|
const source = typeof o.options === "string" ? o.options : null;
|
|
6356
|
-
const inlineOpts = Array.isArray(o.options) ? o.options : [];
|
|
6411
|
+
const inlineOpts = Array.isArray(o.options) ? o.options.map(toOptionItem) : [];
|
|
6357
6412
|
const s = inlineOpts.length > 0 ? _enum(inlineOpts.map((x) => x.value), { error: () => vmsg("enum") }) : string$1(reqErr());
|
|
6358
6413
|
return wrapKey(o, applyMultiple({
|
|
6359
6414
|
kind: "select",
|
|
@@ -6550,6 +6605,47 @@ function measured(raw) {
|
|
|
6550
6605
|
zod: optionalize(s, required)
|
|
6551
6606
|
});
|
|
6552
6607
|
}
|
|
6608
|
+
function unit(raw) {
|
|
6609
|
+
const o = normalizeOpts(raw);
|
|
6610
|
+
const required = o.required ?? false;
|
|
6611
|
+
const units = resolveUnits(o.options);
|
|
6612
|
+
const s = string$1(reqErr()).refine((v) => units.some((u) => u.value === v), { message: vmsg("measured_unit") });
|
|
6613
|
+
return wrapKey(o, {
|
|
6614
|
+
kind: "unit",
|
|
6615
|
+
label: o.label ?? "",
|
|
6616
|
+
required,
|
|
6617
|
+
prim: "select",
|
|
6618
|
+
column: "text",
|
|
6619
|
+
hints: { source: typeof o.options === "string" ? o.options : null },
|
|
6620
|
+
options: units.map((u) => ({
|
|
6621
|
+
value: u.value,
|
|
6622
|
+
title: u.label
|
|
6623
|
+
})),
|
|
6624
|
+
zod: optionalize(s, required)
|
|
6625
|
+
});
|
|
6626
|
+
}
|
|
6627
|
+
function amount(raw) {
|
|
6628
|
+
const o = normalizeOpts(raw);
|
|
6629
|
+
const required = o.required ?? false;
|
|
6630
|
+
const cfg = o.config ?? {};
|
|
6631
|
+
let s = number(typeErr());
|
|
6632
|
+
if (cfg.min != null) s = s.min(cfg.min, { message: vmsg("min", { min: cfg.min }) });
|
|
6633
|
+
if (cfg.max != null) s = s.max(cfg.max, { message: vmsg("max", { max: cfg.max }) });
|
|
6634
|
+
return wrapKey(o, {
|
|
6635
|
+
kind: "amount",
|
|
6636
|
+
label: o.label ?? "",
|
|
6637
|
+
required,
|
|
6638
|
+
prim: "number",
|
|
6639
|
+
column: "real",
|
|
6640
|
+
hints: {
|
|
6641
|
+
unitFrom: o.unitFrom,
|
|
6642
|
+
min: cfg.min,
|
|
6643
|
+
max: cfg.max,
|
|
6644
|
+
step: cfg.step ?? "any"
|
|
6645
|
+
},
|
|
6646
|
+
zod: optionalize(s, required)
|
|
6647
|
+
});
|
|
6648
|
+
}
|
|
6553
6649
|
function money(raw) {
|
|
6554
6650
|
const o = normalizeOpts(raw);
|
|
6555
6651
|
const required = o.required ?? false;
|
|
@@ -7027,6 +7123,8 @@ var PRIMITIVES = {
|
|
|
7027
7123
|
json,
|
|
7028
7124
|
check,
|
|
7029
7125
|
measured,
|
|
7126
|
+
unit,
|
|
7127
|
+
amount,
|
|
7030
7128
|
money,
|
|
7031
7129
|
code,
|
|
7032
7130
|
geo,
|
|
@@ -7072,7 +7170,7 @@ var field = new Proxy({}, {
|
|
|
7072
7170
|
has: (_t, k) => k in composedField()
|
|
7073
7171
|
});
|
|
7074
7172
|
function toClient(field) {
|
|
7075
|
-
const { kind, label, required, prim, hints, options, relation, virtual, json, hidden } = field;
|
|
7173
|
+
const { kind, label, required, prim, hints, options, relation, virtual, json, hidden, derived } = field;
|
|
7076
7174
|
return {
|
|
7077
7175
|
kind,
|
|
7078
7176
|
label,
|
|
@@ -7083,7 +7181,8 @@ function toClient(field) {
|
|
|
7083
7181
|
relation,
|
|
7084
7182
|
virtual,
|
|
7085
7183
|
json,
|
|
7086
|
-
hidden
|
|
7184
|
+
hidden,
|
|
7185
|
+
derived
|
|
7087
7186
|
};
|
|
7088
7187
|
}
|
|
7089
7188
|
//#endregion
|
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-BvI5pRWb.js")
|
|
15
15
|
}] });
|
|
16
16
|
//#endregion
|
|
17
17
|
export { ui_default as default };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffer-org/plugin-webchat",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24"
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"test:ui": "vitest run --root ."
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@coffer-org/plugin-orchestrator": "^
|
|
30
|
-
"@coffer-org/sdk": "^
|
|
31
|
-
"@coffer-org/server": "^
|
|
29
|
+
"@coffer-org/plugin-orchestrator": "^3.0.0",
|
|
30
|
+
"@coffer-org/sdk": "^3.0.0",
|
|
31
|
+
"@coffer-org/server": "^3.0.0",
|
|
32
32
|
"react-markdown": "^10.1.0",
|
|
33
33
|
"remark-gfm": "^4.0.1"
|
|
34
34
|
},
|