dewee-webchat 0.1.0 → 0.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/README.md +69 -3
- package/astro/DeweeChat.astro +21 -21
- package/dist/chunk-JX6AH37X.js +111 -0
- package/dist/{chunk-RGAYYTL7.js → chunk-YV6PXYJZ.js} +607 -22
- package/dist/dewee-webchat.js +92 -15
- package/dist/index.d.ts +97 -5
- package/dist/index.js +2 -2
- package/dist/{protocol-CS8_4zC0.d.ts → protocol-B72Ry-fY.d.ts} +22 -1
- package/dist/react.d.ts +6 -3
- package/dist/react.js +10 -5
- package/dist/server.d.ts +27 -3
- package/dist/server.js +28 -5
- package/dist/vue.d.ts +50 -4
- package/dist/vue.js +12 -5
- package/package.json +99 -99
- package/dist/chunk-MKTHKYLY.js +0 -10
|
@@ -1,7 +1,279 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ACTION_LIMITS,
|
|
3
|
+
ACTION_NAME_RE,
|
|
2
4
|
HEADER_GATEWAY_TOKEN,
|
|
3
|
-
HEADER_GATEWAY_URL
|
|
4
|
-
|
|
5
|
+
HEADER_GATEWAY_URL,
|
|
6
|
+
SENSITIVITIES,
|
|
7
|
+
stripPageBlock
|
|
8
|
+
} from "./chunk-JX6AH37X.js";
|
|
9
|
+
|
|
10
|
+
// src/actions/directives.ts
|
|
11
|
+
var DIRECTIVE_LANG = "dewee-action";
|
|
12
|
+
var RESULT_LANG = "dewee-action-result";
|
|
13
|
+
var DIRECTIVE_LIMITS = { maxCalls: 8, maxBlockBytes: 8192 };
|
|
14
|
+
var CALL_ID_RE = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
15
|
+
var FENCE_CLOSE_RE = /^\s{0,3}```\s*$/;
|
|
16
|
+
var fenceOpen = (lang) => new RegExp("^\\s{0,3}```" + lang.replace(/[.-]/g, "\\$&") + "\\s*$");
|
|
17
|
+
var DIRECTIVE_OPEN_RE = fenceOpen(DIRECTIVE_LANG);
|
|
18
|
+
var RESULT_OPEN_RE = fenceOpen(RESULT_LANG);
|
|
19
|
+
function findBlocks(lines, open) {
|
|
20
|
+
const blocks = [];
|
|
21
|
+
for (let i = 0; i < lines.length; i++) {
|
|
22
|
+
if (!open.test(lines[i])) continue;
|
|
23
|
+
const start = i;
|
|
24
|
+
const body = [];
|
|
25
|
+
let closed = false;
|
|
26
|
+
for (i++; i < lines.length; i++) {
|
|
27
|
+
if (FENCE_CLOSE_RE.test(lines[i])) {
|
|
28
|
+
closed = true;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
body.push(lines[i]);
|
|
32
|
+
}
|
|
33
|
+
blocks.push({ start, end: Math.min(i, lines.length - 1), body, closed });
|
|
34
|
+
}
|
|
35
|
+
return blocks;
|
|
36
|
+
}
|
|
37
|
+
var removeBlocks = (lines, blocks) => {
|
|
38
|
+
const drop = /* @__PURE__ */ new Set();
|
|
39
|
+
for (const b of blocks) for (let i = b.start; i <= b.end; i++) drop.add(i);
|
|
40
|
+
return lines.filter((_, i) => !drop.has(i)).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
41
|
+
};
|
|
42
|
+
function parseDirectives(content) {
|
|
43
|
+
const lines = content.replace(/\r\n?/g, "\n").split("\n");
|
|
44
|
+
const blocks = findBlocks(lines, DIRECTIVE_OPEN_RE);
|
|
45
|
+
const calls = [];
|
|
46
|
+
const ids = /* @__PURE__ */ new Set();
|
|
47
|
+
blocks.forEach((b, n) => {
|
|
48
|
+
const fallbackId = `call-${n + 1}`;
|
|
49
|
+
const raw = b.body.join("\n");
|
|
50
|
+
const bad = (error, id2 = fallbackId, name2 = "") => calls.push({ ok: false, id: id2, name: name2, error });
|
|
51
|
+
if (n >= DIRECTIVE_LIMITS.maxCalls) return bad(`Too many action calls in one reply (max ${DIRECTIVE_LIMITS.maxCalls})`);
|
|
52
|
+
if (!b.closed) return bad("Unclosed dewee-action block");
|
|
53
|
+
if (new TextEncoder().encode(raw).length > DIRECTIVE_LIMITS.maxBlockBytes) return bad(`Action block exceeds ${DIRECTIVE_LIMITS.maxBlockBytes} bytes`);
|
|
54
|
+
let data;
|
|
55
|
+
try {
|
|
56
|
+
data = JSON.parse(raw);
|
|
57
|
+
} catch {
|
|
58
|
+
return bad("Action block is not valid JSON");
|
|
59
|
+
}
|
|
60
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) return bad("Action block must be a JSON object");
|
|
61
|
+
let id = typeof data.id === "string" && CALL_ID_RE.test(data.id) ? data.id : fallbackId;
|
|
62
|
+
const name = typeof data.name === "string" ? data.name.slice(0, 64) : "";
|
|
63
|
+
if (ids.has(id)) return bad(`Duplicate action call id "${id}"`, `${id}-${n + 1}`, name);
|
|
64
|
+
ids.add(id);
|
|
65
|
+
if (!name) return bad("Action name is required", id);
|
|
66
|
+
const args = data.args ?? {};
|
|
67
|
+
if (typeof args !== "object" || args === null || Array.isArray(args)) return bad("args must be a JSON object", id, name);
|
|
68
|
+
calls.push({ ok: true, id, name, args });
|
|
69
|
+
});
|
|
70
|
+
return { text: blocks.length ? removeBlocks(lines, blocks) : content, calls };
|
|
71
|
+
}
|
|
72
|
+
function stripDirectives(content) {
|
|
73
|
+
if (!content.includes("```" + DIRECTIVE_LANG)) return content;
|
|
74
|
+
const lines = content.replace(/\r\n?/g, "\n").split("\n");
|
|
75
|
+
return removeBlocks(lines, findBlocks(lines, DIRECTIVE_OPEN_RE));
|
|
76
|
+
}
|
|
77
|
+
function isActionResultMessage(content) {
|
|
78
|
+
return RESULT_OPEN_RE.test(content.trimStart().split("\n", 1)[0] ?? "");
|
|
79
|
+
}
|
|
80
|
+
function parseActionResults(content) {
|
|
81
|
+
const lines = content.replace(/\r\n?/g, "\n").split("\n");
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const b of findBlocks(lines, RESULT_OPEN_RE)) {
|
|
84
|
+
try {
|
|
85
|
+
const r = JSON.parse(b.body.join("\n"));
|
|
86
|
+
if (r && typeof r.id === "string" && ["ok", "denied", "error"].includes(r.status)) out.push(r);
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
var block = (r) => "```" + RESULT_LANG + "\n" + JSON.stringify(r) + "\n```";
|
|
93
|
+
function formatActionResults(results, maxLength = 4e3) {
|
|
94
|
+
const budget = Math.max(64, Math.floor((maxLength - 16) / Math.max(1, results.length)));
|
|
95
|
+
const fitted = results.map((r) => {
|
|
96
|
+
const safe = { ...r, result: toJsonValue(r.result) };
|
|
97
|
+
if (safe.result === void 0) delete safe.result;
|
|
98
|
+
if (block(safe).length <= budget) return block(safe);
|
|
99
|
+
const preview = JSON.stringify(safe.result ?? null);
|
|
100
|
+
const room = budget - block({ ...safe, result: { truncated: true, preview: "" } }).length - 8;
|
|
101
|
+
const shrunk = block({ ...safe, result: { truncated: true, preview: preview.slice(0, Math.max(0, room)) } });
|
|
102
|
+
return shrunk.length <= budget ? shrunk : block({ id: r.id, name: r.name.slice(0, 64), status: r.status, result: { truncated: true } });
|
|
103
|
+
});
|
|
104
|
+
return fitted.join("\n\n").slice(0, maxLength);
|
|
105
|
+
}
|
|
106
|
+
function toJsonValue(v) {
|
|
107
|
+
if (v === void 0) return void 0;
|
|
108
|
+
try {
|
|
109
|
+
const s = JSON.stringify(v);
|
|
110
|
+
return s === void 0 ? null : JSON.parse(s);
|
|
111
|
+
} catch {
|
|
112
|
+
return "[unserializable result]";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/actions/json-schema-subset.ts
|
|
117
|
+
var typeOf = (v) => v === null ? "null" : Array.isArray(v) ? "array" : typeof v === "number" ? Number.isInteger(v) ? "integer" : "number" : typeof v;
|
|
118
|
+
var matchesType = (v, t) => {
|
|
119
|
+
const actual = typeOf(v);
|
|
120
|
+
return actual === t || t === "number" && actual === "integer";
|
|
121
|
+
};
|
|
122
|
+
function validateJsonSchema(value, schema, path = "args", depth = 0) {
|
|
123
|
+
if (!schema || typeof schema !== "object") return null;
|
|
124
|
+
if (depth > 32) return `${path}: schema is nested too deeply`;
|
|
125
|
+
const s = schema;
|
|
126
|
+
if (s.type !== void 0) {
|
|
127
|
+
const types = Array.isArray(s.type) ? s.type : [s.type];
|
|
128
|
+
if (!types.some((t) => matchesType(value, t))) return `${path}: expected ${types.join(" or ")}, got ${typeOf(value)}`;
|
|
129
|
+
}
|
|
130
|
+
if (Array.isArray(s.enum) && !s.enum.some((e) => sameJson(e, value))) {
|
|
131
|
+
return `${path}: must be one of ${s.enum.map((e) => JSON.stringify(e)).join(", ")}`;
|
|
132
|
+
}
|
|
133
|
+
if ("const" in s && !sameJson(s.const, value)) return `${path}: must equal ${JSON.stringify(s.const)}`;
|
|
134
|
+
if (typeof value === "string") {
|
|
135
|
+
if (typeof s.minLength === "number" && value.length < s.minLength) return `${path}: shorter than ${s.minLength} characters`;
|
|
136
|
+
if (typeof s.maxLength === "number" && value.length > s.maxLength) return `${path}: longer than ${s.maxLength} characters`;
|
|
137
|
+
if (typeof s.pattern === "string") {
|
|
138
|
+
let re = null;
|
|
139
|
+
try {
|
|
140
|
+
re = new RegExp(s.pattern, "u");
|
|
141
|
+
} catch {
|
|
142
|
+
return `${path}: schema pattern is invalid`;
|
|
143
|
+
}
|
|
144
|
+
if (!re.test(value)) return `${path}: does not match ${s.pattern}`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (typeof value === "number") {
|
|
148
|
+
if (typeof s.minimum === "number" && value < s.minimum) return `${path}: must be >= ${s.minimum}`;
|
|
149
|
+
if (typeof s.maximum === "number" && value > s.maximum) return `${path}: must be <= ${s.maximum}`;
|
|
150
|
+
}
|
|
151
|
+
if (Array.isArray(value)) {
|
|
152
|
+
if (typeof s.minItems === "number" && value.length < s.minItems) return `${path}: needs at least ${s.minItems} items`;
|
|
153
|
+
if (typeof s.maxItems === "number" && value.length > s.maxItems) return `${path}: allows at most ${s.maxItems} items`;
|
|
154
|
+
if (s.items && typeof s.items === "object") {
|
|
155
|
+
for (let i = 0; i < value.length; i++) {
|
|
156
|
+
const err = validateJsonSchema(value[i], s.items, `${path}[${i}]`, depth + 1);
|
|
157
|
+
if (err) return err;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (typeOf(value) === "object") {
|
|
162
|
+
const obj = value;
|
|
163
|
+
const props = s.properties && typeof s.properties === "object" ? s.properties : {};
|
|
164
|
+
for (const key of Array.isArray(s.required) ? s.required : []) {
|
|
165
|
+
if (!(key in obj)) return `${path}.${key}: is required`;
|
|
166
|
+
}
|
|
167
|
+
for (const [key, v] of Object.entries(obj)) {
|
|
168
|
+
if (Object.prototype.hasOwnProperty.call(props, key)) {
|
|
169
|
+
const err = validateJsonSchema(v, props[key], `${path}.${key}`, depth + 1);
|
|
170
|
+
if (err) return err;
|
|
171
|
+
} else if (s.additionalProperties === false) {
|
|
172
|
+
return `${path}.${key}: is not allowed`;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
function sameJson(a, b) {
|
|
179
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/actions/action-runner.ts
|
|
183
|
+
var needsConfirmation = (s) => s === "mutate" || s === "destructive";
|
|
184
|
+
var ActionRegistry = class {
|
|
185
|
+
map = /* @__PURE__ */ new Map();
|
|
186
|
+
get size() {
|
|
187
|
+
return this.map.size;
|
|
188
|
+
}
|
|
189
|
+
get(name) {
|
|
190
|
+
return this.map.get(name);
|
|
191
|
+
}
|
|
192
|
+
names() {
|
|
193
|
+
return [...this.map.keys()];
|
|
194
|
+
}
|
|
195
|
+
/** Adds or replaces actions by name. Throws TypeError on an invalid definition. */
|
|
196
|
+
register(actions) {
|
|
197
|
+
const list = Array.isArray(actions) ? actions : [actions];
|
|
198
|
+
for (const a of list) assertAction(a);
|
|
199
|
+
if ((/* @__PURE__ */ new Set([...this.map.keys(), ...list.map((a) => a.name)])).size > ACTION_LIMITS.maxActions) {
|
|
200
|
+
throw new TypeError(`dewee-webchat: at most ${ACTION_LIMITS.maxActions} page actions`);
|
|
201
|
+
}
|
|
202
|
+
for (const a of list) this.map.set(a.name, a);
|
|
203
|
+
}
|
|
204
|
+
unregister(name) {
|
|
205
|
+
return this.map.delete(name);
|
|
206
|
+
}
|
|
207
|
+
/** What is sent to the backend: everything but the handler. */
|
|
208
|
+
manifest() {
|
|
209
|
+
return [...this.map.values()].map((a) => ({
|
|
210
|
+
name: a.name,
|
|
211
|
+
description: a.description,
|
|
212
|
+
sensitivity: a.sensitivity,
|
|
213
|
+
parameters: a.parameters ?? { type: "object", properties: {} }
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
function assertAction(a) {
|
|
218
|
+
const fail = (why) => {
|
|
219
|
+
throw new TypeError(`dewee-webchat: invalid page action ${JSON.stringify(a?.name)}: ${why}`);
|
|
220
|
+
};
|
|
221
|
+
if (!a || typeof a !== "object") fail("not an object");
|
|
222
|
+
if (typeof a.name !== "string" || !ACTION_NAME_RE.test(a.name)) fail("name must match ^[a-z][a-z0-9_.-]{0,63}$");
|
|
223
|
+
if (typeof a.description !== "string" || a.description.length > ACTION_LIMITS.maxDescription) fail(`description must be a string up to ${ACTION_LIMITS.maxDescription} chars`);
|
|
224
|
+
if (!SENSITIVITIES.includes(a.sensitivity)) fail(`sensitivity must be one of ${SENSITIVITIES.join(", ")}`);
|
|
225
|
+
if (typeof a.handler !== "function") fail("handler must be a function");
|
|
226
|
+
if (a.parameters !== void 0 && (typeof a.parameters !== "object" || a.parameters === null || Array.isArray(a.parameters))) fail("parameters must be a JSON Schema object");
|
|
227
|
+
}
|
|
228
|
+
async function runActionCall(call, o) {
|
|
229
|
+
const fail = (error) => {
|
|
230
|
+
o.onStatus?.("error");
|
|
231
|
+
return { id: call.id, name: call.name, status: "error", error };
|
|
232
|
+
};
|
|
233
|
+
if (!call.ok) return fail(call.error);
|
|
234
|
+
if (o.signal.aborted) return fail("Cancelled");
|
|
235
|
+
const action = o.registry.get(call.name);
|
|
236
|
+
if (!action) return fail(`Unknown action "${call.name}". Available: ${o.registry.names().join(", ") || "none"}`);
|
|
237
|
+
const invalid = validateJsonSchema(call.args, action.parameters ?? { type: "object" });
|
|
238
|
+
if (invalid) return fail(`Invalid arguments: ${invalid}`);
|
|
239
|
+
if (needsConfirmation(action.sensitivity)) {
|
|
240
|
+
o.onStatus?.("awaiting-confirmation");
|
|
241
|
+
const { handler: _omit, ...info } = action;
|
|
242
|
+
let approved = false;
|
|
243
|
+
try {
|
|
244
|
+
approved = await raceAbort(Promise.resolve(o.confirm({ ...info, callId: call.id }, call.args)), o.signal) === true;
|
|
245
|
+
} catch {
|
|
246
|
+
approved = false;
|
|
247
|
+
}
|
|
248
|
+
if (!approved || o.signal.aborted) {
|
|
249
|
+
o.onStatus?.("denied");
|
|
250
|
+
return { id: call.id, name: call.name, status: "denied", error: "The user declined this action." };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
o.onStatus?.("running");
|
|
254
|
+
const ctrl = new AbortController();
|
|
255
|
+
const stop = () => ctrl.abort();
|
|
256
|
+
o.signal.addEventListener("abort", stop, { once: true });
|
|
257
|
+
const timer = setTimeout(stop, o.timeoutMs ?? 3e4);
|
|
258
|
+
try {
|
|
259
|
+
const value = await raceAbort(Promise.resolve().then(() => action.handler(call.args, { callId: call.id, conversationId: o.conversationId, agent: o.agent, signal: ctrl.signal })), ctrl.signal);
|
|
260
|
+
o.onStatus?.("done");
|
|
261
|
+
return { id: call.id, name: call.name, status: "ok", result: toJsonValue(value) ?? null };
|
|
262
|
+
} catch (e) {
|
|
263
|
+
return fail(ctrl.signal.aborted ? o.signal.aborted ? "Cancelled" : "Timed out" : String(e?.message ?? e).slice(0, 500) || "Action failed");
|
|
264
|
+
} finally {
|
|
265
|
+
clearTimeout(timer);
|
|
266
|
+
o.signal.removeEventListener("abort", stop);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function raceAbort(p, signal) {
|
|
270
|
+
if (signal.aborted) return Promise.reject(new Error("aborted"));
|
|
271
|
+
return new Promise((resolve, reject) => {
|
|
272
|
+
const onAbort = () => reject(new Error("aborted"));
|
|
273
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
274
|
+
p.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
275
|
+
});
|
|
276
|
+
}
|
|
5
277
|
|
|
6
278
|
// src/widget/api-client.ts
|
|
7
279
|
var ApiError = class extends Error {
|
|
@@ -140,6 +412,79 @@ var ApiClient = class {
|
|
|
140
412
|
}
|
|
141
413
|
};
|
|
142
414
|
|
|
415
|
+
// src/widget/confirm-dialog.ts
|
|
416
|
+
var seq = 0;
|
|
417
|
+
function showConfirmDialog(container, s, action, args, signal) {
|
|
418
|
+
const id = `dw-confirm-${++seq}`;
|
|
419
|
+
const root = container.getRootNode();
|
|
420
|
+
const previous = root.activeElement;
|
|
421
|
+
const backdrop = document.createElement("div");
|
|
422
|
+
backdrop.className = "confirm-backdrop";
|
|
423
|
+
backdrop.innerHTML = `
|
|
424
|
+
<div class="confirm" role="alertdialog" aria-modal="true" aria-labelledby="${id}-t" aria-describedby="${id}-d">
|
|
425
|
+
<h3 id="${id}-t"></h3>
|
|
426
|
+
<div id="${id}-d" class="confirm-desc">
|
|
427
|
+
<div class="confirm-action"><code></code><span class="sens"></span></div>
|
|
428
|
+
<p class="confirm-text"></p>
|
|
429
|
+
<p class="confirm-warn" hidden></p>
|
|
430
|
+
</div>
|
|
431
|
+
<pre class="confirm-args"></pre>
|
|
432
|
+
<div class="confirm-buttons">
|
|
433
|
+
<button type="button" class="btn deny"></button>
|
|
434
|
+
<button type="button" class="btn allow"></button>
|
|
435
|
+
</div>
|
|
436
|
+
</div>`;
|
|
437
|
+
const q = (sel) => backdrop.querySelector(sel);
|
|
438
|
+
q("h3").textContent = s.confirmTitle;
|
|
439
|
+
q("code").textContent = action.name;
|
|
440
|
+
const sens = q(".sens");
|
|
441
|
+
sens.textContent = action.sensitivity;
|
|
442
|
+
sens.dataset.level = action.sensitivity;
|
|
443
|
+
q(".confirm-text").textContent = action.description;
|
|
444
|
+
if (action.sensitivity === "destructive") {
|
|
445
|
+
const warn = q(".confirm-warn");
|
|
446
|
+
warn.hidden = false;
|
|
447
|
+
warn.textContent = s.confirmDestructive;
|
|
448
|
+
}
|
|
449
|
+
const argsText = JSON.stringify(args, null, 2);
|
|
450
|
+
const pre = q(".confirm-args");
|
|
451
|
+
if (argsText && argsText !== "{}") pre.textContent = argsText;
|
|
452
|
+
else pre.remove();
|
|
453
|
+
const deny = q(".deny");
|
|
454
|
+
const allow = q(".allow");
|
|
455
|
+
deny.textContent = s.deny;
|
|
456
|
+
allow.textContent = s.allow;
|
|
457
|
+
if (action.sensitivity === "destructive") allow.classList.add("danger");
|
|
458
|
+
return new Promise((resolve) => {
|
|
459
|
+
let done = false;
|
|
460
|
+
const finish = (ok) => {
|
|
461
|
+
if (done) return;
|
|
462
|
+
done = true;
|
|
463
|
+
signal?.removeEventListener("abort", onAbort);
|
|
464
|
+
backdrop.remove();
|
|
465
|
+
previous?.focus?.();
|
|
466
|
+
resolve(ok);
|
|
467
|
+
};
|
|
468
|
+
const onAbort = () => finish(false);
|
|
469
|
+
if (signal?.aborted) return finish(false);
|
|
470
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
471
|
+
deny.addEventListener("click", () => finish(false));
|
|
472
|
+
allow.addEventListener("click", () => finish(true));
|
|
473
|
+
backdrop.addEventListener("keydown", (ev) => {
|
|
474
|
+
if (ev.key === "Escape") {
|
|
475
|
+
ev.preventDefault();
|
|
476
|
+
ev.stopPropagation();
|
|
477
|
+
finish(false);
|
|
478
|
+
} else if (ev.key === "Tab") {
|
|
479
|
+
ev.preventDefault();
|
|
480
|
+
(root.activeElement === deny ? allow : deny).focus();
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
container.appendChild(backdrop);
|
|
484
|
+
deny.focus();
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
143
488
|
// src/widget/i18n.ts
|
|
144
489
|
var en = {
|
|
145
490
|
title: "Assistant",
|
|
@@ -162,7 +507,13 @@ var en = {
|
|
|
162
507
|
online: "Online",
|
|
163
508
|
poweredBy: "Powered by",
|
|
164
509
|
errorGeneric: "Something went wrong. Please try again.",
|
|
165
|
-
stopped: "Stopped."
|
|
510
|
+
stopped: "Stopped.",
|
|
511
|
+
actionStatus: { pending: "queued", "awaiting-confirmation": "needs approval", running: "running", done: "done", denied: "declined", error: "failed" },
|
|
512
|
+
confirmTitle: "Allow this action?",
|
|
513
|
+
confirmDestructive: "This can't be undone.",
|
|
514
|
+
allow: "Allow",
|
|
515
|
+
deny: "Deny",
|
|
516
|
+
actionLimit: (n) => `Stopped after ${n} automatic action rounds. Send a message to continue.`
|
|
166
517
|
};
|
|
167
518
|
var vi = {
|
|
168
519
|
title: "Tr\u1EE3 l\xFD",
|
|
@@ -185,13 +536,20 @@ var vi = {
|
|
|
185
536
|
online: "\u0110ang tr\u1EF1c tuy\u1EBFn",
|
|
186
537
|
poweredBy: "V\u1EADn h\xE0nh b\u1EDFi",
|
|
187
538
|
errorGeneric: "C\xF3 l\u1ED7i x\u1EA3y ra. B\u1EA1n th\u1EED l\u1EA1i gi\xFAp m\xECnh nh\xE9.",
|
|
188
|
-
stopped: "\u0110\xE3 d\u1EEBng."
|
|
539
|
+
stopped: "\u0110\xE3 d\u1EEBng.",
|
|
540
|
+
actionStatus: { pending: "ch\u1EDD", "awaiting-confirmation": "c\u1EA7n x\xE1c nh\u1EADn", running: "\u0111ang ch\u1EA1y", done: "xong", denied: "\u0111\xE3 t\u1EEB ch\u1ED1i", error: "l\u1ED7i" },
|
|
541
|
+
confirmTitle: "Cho ph\xE9p thao t\xE1c n\xE0y?",
|
|
542
|
+
confirmDestructive: "Thao t\xE1c n\xE0y kh\xF4ng th\u1EC3 ho\xE0n t\xE1c.",
|
|
543
|
+
allow: "Cho ph\xE9p",
|
|
544
|
+
deny: "T\u1EEB ch\u1ED1i",
|
|
545
|
+
actionLimit: (n) => `\u0110\xE3 d\u1EEBng sau ${n} l\u01B0\u1EE3t thao t\xE1c t\u1EF1 \u0111\u1ED9ng. G\u1EEDi tin nh\u1EAFn \u0111\u1EC3 ti\u1EBFp t\u1EE5c.`
|
|
189
546
|
};
|
|
190
547
|
function pickStrings(locale) {
|
|
191
548
|
const l = (locale || (typeof document !== "undefined" ? document.documentElement.lang : "") || (typeof navigator !== "undefined" ? navigator.language : "") || "en").toLowerCase();
|
|
192
549
|
return l.startsWith("vi") ? vi : en;
|
|
193
550
|
}
|
|
194
551
|
var icons = {
|
|
552
|
+
action: `<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 2 4 14h7l-1 8 9-12h-7l1-8Z"/></svg>`,
|
|
195
553
|
chat: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a8.5 8.5 0 0 1-12.4 7.6L3 21l1.4-5.1A8.5 8.5 0 1 1 21 12Z"/><path d="M8.5 11h.01M12 11h.01M15.5 11h.01"/></svg>`,
|
|
196
554
|
close: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg>`,
|
|
197
555
|
down: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>`,
|
|
@@ -404,6 +762,7 @@ var styles = (
|
|
|
404
762
|
}
|
|
405
763
|
* { box-sizing: border-box; }
|
|
406
764
|
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; padding: 0; }
|
|
765
|
+
[hidden] { display: none !important; }
|
|
407
766
|
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
|
|
408
767
|
|
|
409
768
|
/* ---------- launcher ---------- */
|
|
@@ -449,6 +808,8 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
|
|
|
449
808
|
@media (max-width: 520px) {
|
|
450
809
|
:host(:not([data-mode="inline"])) .panel { inset: 0; width: 100vw; height: 100dvh; border-radius: 0; border: 0; }
|
|
451
810
|
:host([data-open]:not([data-mode="inline"])) .launcher { display: none; }
|
|
811
|
+
:host(:not([data-mode="inline"])) .head { padding-top: max(14px, env(safe-area-inset-top)); }
|
|
812
|
+
:host(:not([data-mode="inline"])) .composer { padding-bottom: max(12px, env(safe-area-inset-bottom)); }
|
|
452
813
|
}
|
|
453
814
|
|
|
454
815
|
/* ---------- header ---------- */
|
|
@@ -466,7 +827,7 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
|
|
|
466
827
|
.who { flex: 1; min-width: 0; }
|
|
467
828
|
.title { font-weight: 650; font-size: 15px; line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
468
829
|
.subtitle { font-size: 12px; color: var(--dw-muted); display: flex; align-items: center; gap: 6px; margin-top: 2px; }
|
|
469
|
-
.dot { width: 7px; height: 7px; border-radius: 50%; background: #22c58b; box-shadow: 0 0 0 3px color-mix(in srgb, #22c58b 22%, transparent); }
|
|
830
|
+
.dot { flex: none; width: 7px; height: 7px; border-radius: 50%; background: #22c58b; box-shadow: 0 0 0 3px color-mix(in srgb, #22c58b 22%, transparent); }
|
|
470
831
|
.agent-select {
|
|
471
832
|
font: inherit; font-size: 12px; color: var(--dw-muted); background: transparent; border: 0;
|
|
472
833
|
padding: 0 14px 0 0; margin: 0; max-width: 170px; cursor: pointer; appearance: none; field-sizing: content; text-overflow: ellipsis;
|
|
@@ -567,6 +928,46 @@ details > summary svg.ic { width: 14px; height: 14px; flex: none; }
|
|
|
567
928
|
.typing i:nth-child(2) { animation-delay: .15s; } .typing i:nth-child(3) { animation-delay: .3s; }
|
|
568
929
|
@keyframes bounce { 0%, 60%, 100% { transform: none; opacity: .35; } 30% { transform: translateY(-4px); opacity: 1; } }
|
|
569
930
|
|
|
931
|
+
/* page actions */
|
|
932
|
+
.actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
|
933
|
+
.md[hidden] + .actions { margin-top: 0; }
|
|
934
|
+
.action {
|
|
935
|
+
display: inline-flex; align-items: center; gap: 6px; max-width: 100%; padding: 4px 9px 4px 8px; border-radius: 999px;
|
|
936
|
+
border: 1px solid var(--dw-border); background: var(--dw-surface); font-size: 12px; color: var(--dw-muted);
|
|
937
|
+
}
|
|
938
|
+
.action svg.ic { width: 13px; height: 13px; flex: none; color: var(--dw-accent); }
|
|
939
|
+
.action-name { font-family: var(--dw-mono); font-size: 11.5px; color: var(--dw-fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
940
|
+
.action-state { font-size: 11px; flex: none; }
|
|
941
|
+
.action[data-status="done"] .action-state { color: #13a06f; }
|
|
942
|
+
.action[data-status="awaiting-confirmation"] { border-color: color-mix(in srgb, #d98a00 45%, transparent); }
|
|
943
|
+
.action[data-status="awaiting-confirmation"] .action-state { color: #b06f00; }
|
|
944
|
+
.action[data-status="denied"] .action-state, .action[data-status="error"] .action-state { color: #c2334f; }
|
|
945
|
+
.notice { margin: -6px 0 16px; font-size: 12px; color: var(--dw-muted); text-align: center; }
|
|
946
|
+
.confirm-backdrop {
|
|
947
|
+
position: absolute; inset: 0; z-index: 2; display: flex; align-items: flex-end; justify-content: center; padding: 12px;
|
|
948
|
+
background: color-mix(in srgb, var(--dw-fg) 28%, transparent); animation: rise .18s ease both;
|
|
949
|
+
}
|
|
950
|
+
.confirm {
|
|
951
|
+
width: 100%; max-height: 100%; overflow: auto; background: var(--dw-bg); color: var(--dw-fg);
|
|
952
|
+
border: 1px solid var(--dw-border); border-radius: 16px; padding: 16px; box-shadow: var(--dw-shadow);
|
|
953
|
+
}
|
|
954
|
+
.confirm h3 { margin: 0 0 10px; font-size: 15px; font-weight: 650; }
|
|
955
|
+
.confirm-action { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
956
|
+
.confirm-action code { font-family: var(--dw-mono); font-size: 12.5px; background: var(--dw-surface); padding: 2px 6px; border-radius: 6px; }
|
|
957
|
+
.sens { font-size: 11px; padding: 1px 7px; border-radius: 999px; border: 1px solid var(--dw-border); color: var(--dw-muted); }
|
|
958
|
+
.sens[data-level="destructive"] { color: #c2334f; border-color: color-mix(in srgb, #c2334f 40%, transparent); }
|
|
959
|
+
.confirm-text { margin: 8px 0 0; font-size: 13.5px; line-height: 1.5; color: var(--dw-muted); }
|
|
960
|
+
.confirm-warn { margin: 8px 0 0; font-size: 13px; font-weight: 600; color: #c2334f; }
|
|
961
|
+
.confirm-args {
|
|
962
|
+
margin: 10px 0 0; font-family: var(--dw-mono); font-size: 11.5px; line-height: 1.5; background: var(--dw-surface);
|
|
963
|
+
border: 1px solid var(--dw-border); border-radius: 8px; padding: 8px; max-height: 160px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere;
|
|
964
|
+
}
|
|
965
|
+
.confirm-buttons { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }
|
|
966
|
+
.btn { font: inherit; font-size: 13.5px; font-weight: 600; padding: 8px 14px; border-radius: 10px; border: 1px solid var(--dw-border); background: var(--dw-bg); color: var(--dw-fg); min-height: 38px; }
|
|
967
|
+
.btn.allow { background: var(--dw-accent); border-color: var(--dw-accent); color: var(--dw-accent-contrast); }
|
|
968
|
+
.btn.allow.danger { background: #c2334f; border-color: #c2334f; color: #fff; }
|
|
969
|
+
.btn:focus-visible { outline: 3px solid color-mix(in srgb, var(--dw-accent) 45%, transparent); outline-offset: 2px; }
|
|
970
|
+
|
|
570
971
|
/* ---------- composer ---------- */
|
|
571
972
|
.composer { padding: 10px 12px 12px; border-top: 1px solid var(--dw-border); background: var(--dw-bg); }
|
|
572
973
|
.box {
|
|
@@ -576,7 +977,7 @@ details > summary svg.ic { width: 14px; height: 14px; flex: none; }
|
|
|
576
977
|
.box:focus-within { border-color: var(--dw-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--dw-accent) 16%, transparent); }
|
|
577
978
|
textarea {
|
|
578
979
|
flex: 1; resize: none; border: 0; outline: 0; background: transparent; color: var(--dw-fg);
|
|
579
|
-
font: inherit; font-size:
|
|
980
|
+
font: inherit; font-size: 16px; line-height: 1.45; max-height: 140px; padding: 5px 0; min-height: 34px;
|
|
580
981
|
}
|
|
581
982
|
textarea::placeholder { color: var(--dw-muted); }
|
|
582
983
|
.send {
|
|
@@ -591,6 +992,10 @@ textarea::placeholder { color: var(--dw-muted); }
|
|
|
591
992
|
.foot a:hover { color: var(--dw-accent); }
|
|
592
993
|
.challenge:empty { display: none; }
|
|
593
994
|
.challenge { display: flex; justify-content: center; padding: 8px 0; }
|
|
995
|
+
/* iOS zooms into any focused field under 16px; keep touch-device fields at 16px. */
|
|
996
|
+
@media (pointer: coarse) {
|
|
997
|
+
.agent-select { font-size: 16px; max-width: 200px; }
|
|
998
|
+
}
|
|
594
999
|
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none !important; transition: none !important; } }
|
|
595
1000
|
`
|
|
596
1001
|
);
|
|
@@ -671,6 +1076,10 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
671
1076
|
activated = false;
|
|
672
1077
|
strings = pickStrings();
|
|
673
1078
|
mql = null;
|
|
1079
|
+
registry = new ActionRegistry();
|
|
1080
|
+
propActionNames = /* @__PURE__ */ new Set();
|
|
1081
|
+
hooks = {};
|
|
1082
|
+
acks = /* @__PURE__ */ new Map();
|
|
674
1083
|
els;
|
|
675
1084
|
constructor() {
|
|
676
1085
|
super();
|
|
@@ -688,13 +1097,38 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
688
1097
|
else if (a === "z-index") c.zIndex = Number(v);
|
|
689
1098
|
else c[camel(a)] = v;
|
|
690
1099
|
}
|
|
691
|
-
return { ...c, ...this.overrides };
|
|
1100
|
+
return { ...c, ...this.overrides, ...this.hooks };
|
|
692
1101
|
}
|
|
693
1102
|
/** Updates configuration at runtime (wins over attributes). */
|
|
694
1103
|
configure(partial) {
|
|
695
|
-
|
|
1104
|
+
const { actions, confirm, getPageContext, ...rest } = partial;
|
|
1105
|
+
const has = (k) => Object.prototype.hasOwnProperty.call(partial, k);
|
|
1106
|
+
if (has("actions")) this.setPropActions(actions ?? []);
|
|
1107
|
+
if (has("confirm")) this.hooks.confirm = confirm;
|
|
1108
|
+
if (has("getPageContext")) this.hooks.getPageContext = getPageContext;
|
|
1109
|
+
if (!Object.keys(rest).length) return;
|
|
1110
|
+
this.overrides = { ...this.overrides, ...rest };
|
|
696
1111
|
if (this.els) this.applyConfig();
|
|
697
1112
|
}
|
|
1113
|
+
/** Adds or replaces page actions the agent may invoke. Throws TypeError on an invalid definition. */
|
|
1114
|
+
registerActions(actions) {
|
|
1115
|
+
this.registry.register(actions);
|
|
1116
|
+
}
|
|
1117
|
+
/** Removes a page action. Returns false when it was not registered. */
|
|
1118
|
+
unregisterAction(name) {
|
|
1119
|
+
this.propActionNames.delete(name);
|
|
1120
|
+
return this.registry.unregister(name);
|
|
1121
|
+
}
|
|
1122
|
+
/** Names of the currently registered page actions. */
|
|
1123
|
+
get actionNames() {
|
|
1124
|
+
return this.registry.names();
|
|
1125
|
+
}
|
|
1126
|
+
setPropActions(actions) {
|
|
1127
|
+
const next = new Set(actions.map((a) => a.name));
|
|
1128
|
+
for (const name of this.propActionNames) if (!next.has(name)) this.registry.unregister(name);
|
|
1129
|
+
if (actions.length) this.registry.register(actions);
|
|
1130
|
+
this.propActionNames = next;
|
|
1131
|
+
}
|
|
698
1132
|
open() {
|
|
699
1133
|
if (this.config.mode === "inline") return;
|
|
700
1134
|
this.setAttribute("data-open", "");
|
|
@@ -721,21 +1155,40 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
721
1155
|
await this.activate();
|
|
722
1156
|
this.els.input.value = "";
|
|
723
1157
|
this.autosize();
|
|
724
|
-
|
|
1158
|
+
let next = { message, hidden: false };
|
|
1159
|
+
for (let round = 0; next; round++) {
|
|
1160
|
+
const reply = await this.runTurn(next.message, next.hidden);
|
|
1161
|
+
next = null;
|
|
1162
|
+
if (reply.status !== "done" || reply.stopped || !this.registry.size) break;
|
|
1163
|
+
const { calls } = parseDirectives(reply.content);
|
|
1164
|
+
if (!calls.length) break;
|
|
1165
|
+
reply.actions = calls.map((c) => ({ id: c.id, name: c.name, args: c.ok ? c.args : void 0, status: "pending" }));
|
|
1166
|
+
const limit = this.config.maxActionRounds ?? 5;
|
|
1167
|
+
if (round >= limit) {
|
|
1168
|
+
for (const a of reply.actions) Object.assign(a, { status: "error", error: "Automatic action limit reached" });
|
|
1169
|
+
this.updateMessage(reply);
|
|
1170
|
+
this.showNotice(this.strings.actionLimit(limit));
|
|
1171
|
+
break;
|
|
1172
|
+
}
|
|
1173
|
+
this.updateMessage(reply);
|
|
1174
|
+
const results = await this.runActions(reply, calls);
|
|
1175
|
+
if (results) next = { message: formatActionResults(results, this.server?.maxMessageLength ?? 4e3), hidden: true };
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
/** One request/response with the agent. */
|
|
1179
|
+
async runTurn(message, hidden) {
|
|
1180
|
+
this.pushMessage({ role: "user", content: message, hidden });
|
|
725
1181
|
const reply = { role: "assistant", content: "", status: "streaming", startedAt: Date.now() };
|
|
726
1182
|
this.pushMessage(reply);
|
|
727
1183
|
this.setRunning(new AbortController());
|
|
728
|
-
this.emit("dewee:send", { message, agent: this.agentId, conversationId: this.conversationId });
|
|
1184
|
+
this.emit("dewee:send", { message, agent: this.agentId, conversationId: this.conversationId, auto: hidden || void 0 });
|
|
729
1185
|
try {
|
|
730
|
-
await this.client().chat(
|
|
731
|
-
{ message, agent: this.agentId, conversationId: this.conversationId },
|
|
732
|
-
(e) => this.onStreamEvent(reply, e),
|
|
733
|
-
this.running.signal
|
|
734
|
-
);
|
|
1186
|
+
await this.client().chat(this.chatBody(message), (e) => this.onStreamEvent(reply, e), this.running.signal);
|
|
735
1187
|
if (reply.status === "streaming") reply.status = "done";
|
|
736
1188
|
} catch (e) {
|
|
737
1189
|
if (e.name === "AbortError") {
|
|
738
1190
|
reply.status = "done";
|
|
1191
|
+
reply.stopped = true;
|
|
739
1192
|
if (!reply.content) reply.content = `_${this.strings.stopped}_`;
|
|
740
1193
|
} else {
|
|
741
1194
|
reply.status = "error";
|
|
@@ -746,10 +1199,95 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
746
1199
|
this.updateMessage(reply);
|
|
747
1200
|
this.setRunning(null);
|
|
748
1201
|
if (reply.status === "done") {
|
|
1202
|
+
if (reply.actionsHash && !reply.stopped) this.setAck(reply.actionsHash);
|
|
749
1203
|
this.emit("dewee:reply", { content: reply.content, agent: this.agentId, conversationId: this.conversationId });
|
|
750
1204
|
if (this.config.mode !== "inline" && !this.hasAttribute("data-open")) this.setAttribute("data-unread", "");
|
|
751
1205
|
}
|
|
752
1206
|
}
|
|
1207
|
+
return reply;
|
|
1208
|
+
}
|
|
1209
|
+
chatBody(message) {
|
|
1210
|
+
const body = { message, agent: this.agentId, conversationId: this.conversationId };
|
|
1211
|
+
if (this.registry.size) {
|
|
1212
|
+
body.actions = this.registry.manifest();
|
|
1213
|
+
const ack = this.getAck();
|
|
1214
|
+
if (ack) body.actionsHash = ack;
|
|
1215
|
+
}
|
|
1216
|
+
const ctx = this.pageContext();
|
|
1217
|
+
if (ctx !== void 0) body.pageContext = ctx;
|
|
1218
|
+
return body;
|
|
1219
|
+
}
|
|
1220
|
+
/** Host snapshot, dropped (with a warning) when it fails or exceeds the backend cap. */
|
|
1221
|
+
pageContext() {
|
|
1222
|
+
const get = this.hooks.getPageContext;
|
|
1223
|
+
if (!get) return void 0;
|
|
1224
|
+
try {
|
|
1225
|
+
const v = get();
|
|
1226
|
+
if (v === void 0 || v === null) return void 0;
|
|
1227
|
+
const size = new TextEncoder().encode(JSON.stringify(v)).length;
|
|
1228
|
+
if (size <= ACTION_LIMITS.maxPageContextBytes) return v;
|
|
1229
|
+
console.warn(`[dewee-webchat] getPageContext() returned ${size} bytes; the limit is ${ACTION_LIMITS.maxPageContextBytes}. Context not sent.`);
|
|
1230
|
+
return { truncated: true };
|
|
1231
|
+
} catch (e) {
|
|
1232
|
+
console.warn("[dewee-webchat] getPageContext() failed:", e);
|
|
1233
|
+
return void 0;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
ackKey() {
|
|
1237
|
+
return `dewee-webchat:actions:${this.config.endpoint}:${this.agentId}:${this.conversationId}`;
|
|
1238
|
+
}
|
|
1239
|
+
getAck() {
|
|
1240
|
+
return this.acks.get(this.ackKey()) ?? (this.config.persist !== false ? store2.get(this.ackKey()) ?? void 0 : void 0);
|
|
1241
|
+
}
|
|
1242
|
+
setAck(hash) {
|
|
1243
|
+
this.acks.set(this.ackKey(), hash);
|
|
1244
|
+
if (this.config.persist !== false) store2.set(this.ackKey(), hash);
|
|
1245
|
+
}
|
|
1246
|
+
/** Executes the calls of one answer in order. Resolves null when the visitor stopped the chain. */
|
|
1247
|
+
async runActions(reply, calls) {
|
|
1248
|
+
const ctrl = new AbortController();
|
|
1249
|
+
this.setRunning(ctrl);
|
|
1250
|
+
const results = [];
|
|
1251
|
+
const base = { agent: this.agentId, conversationId: this.conversationId };
|
|
1252
|
+
try {
|
|
1253
|
+
for (const [i, call] of calls.entries()) {
|
|
1254
|
+
const view = reply.actions[i];
|
|
1255
|
+
const sensitivity = call.ok ? this.registry.get(call.name)?.sensitivity : void 0;
|
|
1256
|
+
const args = call.ok ? call.args : void 0;
|
|
1257
|
+
this.emit("dewee:action", { phase: "start", id: call.id, name: call.name, sensitivity, args, ...base });
|
|
1258
|
+
const r = await runActionCall(call, {
|
|
1259
|
+
registry: this.registry,
|
|
1260
|
+
confirm: (action, a) => this.confirmAction(action, a, ctrl.signal),
|
|
1261
|
+
signal: ctrl.signal,
|
|
1262
|
+
timeoutMs: this.config.actionTimeoutMs,
|
|
1263
|
+
...base,
|
|
1264
|
+
onStatus: (status) => {
|
|
1265
|
+
view.status = status;
|
|
1266
|
+
this.updateMessage(reply);
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1269
|
+
if (r.error) view.error = r.error;
|
|
1270
|
+
this.updateMessage(reply);
|
|
1271
|
+
results.push(r);
|
|
1272
|
+
this.emit("dewee:action", { phase: "result", id: r.id, name: r.name, sensitivity, args, status: r.status, result: r.result, error: r.error, ...base });
|
|
1273
|
+
}
|
|
1274
|
+
} finally {
|
|
1275
|
+
this.setRunning(null);
|
|
1276
|
+
}
|
|
1277
|
+
return ctrl.signal.aborted ? null : results;
|
|
1278
|
+
}
|
|
1279
|
+
confirmAction(action, args, signal) {
|
|
1280
|
+
if (this.hooks.confirm) return this.hooks.confirm(action, args);
|
|
1281
|
+
if (this.config.mode !== "inline" && !this.hasAttribute("data-open")) this.open();
|
|
1282
|
+
return showConfirmDialog(this.els.panel, this.strings, action, args, signal);
|
|
1283
|
+
}
|
|
1284
|
+
showNotice(text) {
|
|
1285
|
+
const div = document.createElement("div");
|
|
1286
|
+
div.className = "notice";
|
|
1287
|
+
div.setAttribute("role", "status");
|
|
1288
|
+
div.textContent = text;
|
|
1289
|
+
this.els.log.appendChild(div);
|
|
1290
|
+
this.scrollToBottom(true);
|
|
753
1291
|
}
|
|
754
1292
|
/** Stops the running reply. */
|
|
755
1293
|
stop() {
|
|
@@ -993,7 +1531,7 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
993
1531
|
if (!saved) return;
|
|
994
1532
|
try {
|
|
995
1533
|
const history = await this.client().history(this.agentId, this.conversationId);
|
|
996
|
-
this.messages = history.map((m) => ({ ...m, status: "done" }));
|
|
1534
|
+
this.messages = decorateHistory(history.map((m) => ({ ...m, status: "done" })));
|
|
997
1535
|
this.renderLog();
|
|
998
1536
|
} catch {
|
|
999
1537
|
}
|
|
@@ -1039,6 +1577,7 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1039
1577
|
renderMessage(m) {
|
|
1040
1578
|
const root = document.createElement("div");
|
|
1041
1579
|
root.className = `msg ${m.role}${m.status === "error" ? " error" : ""}`;
|
|
1580
|
+
root.hidden = !!m.hidden;
|
|
1042
1581
|
const bubble = document.createElement("div");
|
|
1043
1582
|
bubble.className = "bubble";
|
|
1044
1583
|
root.appendChild(bubble);
|
|
@@ -1046,9 +1585,13 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1046
1585
|
trace.className = "trace";
|
|
1047
1586
|
const md = document.createElement("div");
|
|
1048
1587
|
md.className = m.role === "assistant" ? "md" : "";
|
|
1588
|
+
const actions = document.createElement("div");
|
|
1589
|
+
actions.className = "actions";
|
|
1590
|
+
actions.hidden = true;
|
|
1049
1591
|
if (m.role === "assistant") bubble.appendChild(trace);
|
|
1050
1592
|
bubble.appendChild(md);
|
|
1051
|
-
m.
|
|
1593
|
+
if (m.role === "assistant") bubble.appendChild(actions);
|
|
1594
|
+
m.view = { root, trace, md, tools: /* @__PURE__ */ new Map(), actions };
|
|
1052
1595
|
this.updateMessage(m);
|
|
1053
1596
|
return root;
|
|
1054
1597
|
}
|
|
@@ -1085,13 +1628,33 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1085
1628
|
v.tools.clear();
|
|
1086
1629
|
}
|
|
1087
1630
|
v.trace.hidden = !v.trace.childElementCount;
|
|
1088
|
-
const
|
|
1631
|
+
const shown = this.registry.size || m.actions ? stripDirectives(m.content) : m.content;
|
|
1632
|
+
const typing = m.status === "streaming" && !shown;
|
|
1633
|
+
v.md.hidden = !typing && !shown && !!m.actions?.length;
|
|
1089
1634
|
if (typing) {
|
|
1090
1635
|
v.md.innerHTML = `<span class="typing" aria-label="${this.strings.thinking}"><i></i><i></i><i></i></span>`;
|
|
1091
1636
|
} else {
|
|
1092
|
-
v.md.replaceChildren(renderMarkdown(document,
|
|
1637
|
+
v.md.replaceChildren(renderMarkdown(document, shown));
|
|
1093
1638
|
v.md.classList.toggle("cursor", m.status === "streaming");
|
|
1094
1639
|
}
|
|
1640
|
+
this.renderActions(v, m.actions ?? []);
|
|
1641
|
+
}
|
|
1642
|
+
renderActions(v, list) {
|
|
1643
|
+
v.actions.hidden = !list.length;
|
|
1644
|
+
v.actions.replaceChildren(
|
|
1645
|
+
...list.map((a) => {
|
|
1646
|
+
const chip = document.createElement("div");
|
|
1647
|
+
chip.className = "action";
|
|
1648
|
+
chip.dataset.status = a.status;
|
|
1649
|
+
const busy = a.status === "running" || a.status === "awaiting-confirmation";
|
|
1650
|
+
chip.innerHTML = `${busy ? '<span class="spin"></span>' : icons.action}<span class="action-name"></span><span class="action-state"></span>`;
|
|
1651
|
+
chip.querySelector(".action-name").textContent = a.name || "?";
|
|
1652
|
+
chip.querySelector(".action-state").textContent = this.strings.actionStatus[a.status];
|
|
1653
|
+
const tip = [a.args && Object.keys(a.args).length ? JSON.stringify(a.args) : "", a.error ?? ""].filter(Boolean).join("\n");
|
|
1654
|
+
if (tip) chip.title = tip;
|
|
1655
|
+
return chip;
|
|
1656
|
+
})
|
|
1657
|
+
);
|
|
1095
1658
|
}
|
|
1096
1659
|
renderTool(v, t) {
|
|
1097
1660
|
let d = v.tools.get(t.id);
|
|
@@ -1110,7 +1673,7 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1110
1673
|
d.querySelector(".tool-state").textContent = t.status === "running" ? this.strings.toolRunning : t.status === "error" ? this.strings.toolError : this.strings.toolDone;
|
|
1111
1674
|
const body = d.querySelector(".tool-body");
|
|
1112
1675
|
body.replaceChildren();
|
|
1113
|
-
const
|
|
1676
|
+
const block2 = (label, value) => {
|
|
1114
1677
|
if (value === void 0 || value === null || value === "") return;
|
|
1115
1678
|
const l = document.createElement("div");
|
|
1116
1679
|
l.className = "label";
|
|
@@ -1119,12 +1682,13 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1119
1682
|
pre.textContent = typeof value === "string" ? tryPretty(value) : JSON.stringify(value, null, 2);
|
|
1120
1683
|
body.append(l, pre);
|
|
1121
1684
|
};
|
|
1122
|
-
|
|
1123
|
-
|
|
1685
|
+
block2(this.strings.args, t.arguments);
|
|
1686
|
+
block2(this.strings.result, t.result);
|
|
1124
1687
|
}
|
|
1125
1688
|
onStreamEvent(m, e) {
|
|
1126
1689
|
switch (e.type) {
|
|
1127
1690
|
case "start":
|
|
1691
|
+
if (e.actionsHash) m.actionsHash = e.actionsHash;
|
|
1128
1692
|
if (e.conversationId && e.conversationId !== this.conversationId) {
|
|
1129
1693
|
this.conversationId = e.conversationId;
|
|
1130
1694
|
if (this.config.persist !== false) store2.set(this.convKey(), e.conversationId);
|
|
@@ -1222,6 +1786,27 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1222
1786
|
}
|
|
1223
1787
|
}
|
|
1224
1788
|
};
|
|
1789
|
+
function decorateHistory(messages) {
|
|
1790
|
+
messages.forEach((m, i) => {
|
|
1791
|
+
if (m.role === "user") {
|
|
1792
|
+
m.content = stripPageBlock(m.content);
|
|
1793
|
+
if (isActionResultMessage(m.content)) m.hidden = true;
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
if (!m.content.includes("```dewee-action")) return;
|
|
1797
|
+
const { calls } = parseDirectives(m.content);
|
|
1798
|
+
if (!calls.length) return;
|
|
1799
|
+
const next = messages[i + 1];
|
|
1800
|
+
const nextContent = next?.role === "user" ? stripPageBlock(next.content) : "";
|
|
1801
|
+
const results = isActionResultMessage(nextContent) ? parseActionResults(nextContent) : [];
|
|
1802
|
+
m.actions = calls.map((c) => {
|
|
1803
|
+
const r = results.find((x) => x.id === c.id);
|
|
1804
|
+
const status = !r ? "error" : r.status === "ok" ? "done" : r.status;
|
|
1805
|
+
return { id: c.id, name: c.name, args: c.ok ? c.args : void 0, status, error: r ? r.error : "Not run" };
|
|
1806
|
+
});
|
|
1807
|
+
});
|
|
1808
|
+
return messages;
|
|
1809
|
+
}
|
|
1225
1810
|
function safeJson(s, fallback) {
|
|
1226
1811
|
try {
|
|
1227
1812
|
return JSON.parse(s);
|