dewee-webchat 0.1.1 → 0.3.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 +92 -3
- package/astro/DeweeChat.astro +21 -21
- package/dist/chunk-W6A4HVFB.js +111 -0
- package/dist/{chunk-D4JM2FOB.js → chunk-ZTVGWFXL.js} +650 -33
- package/dist/dewee-webchat.js +85 -14
- package/dist/index.d.ts +157 -41
- package/dist/index.js +4 -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 +67 -3
- package/dist/vue.js +14 -5
- package/package.json +1 -1
- package/dist/chunk-WD6V3TAX.js +0 -10
|
@@ -1,9 +1,282 @@
|
|
|
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-W6A4HVFB.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
|
|
279
|
+
var RESERVED_HEADERS = ["content-type", "authorization", HEADER_GATEWAY_URL, HEADER_GATEWAY_TOKEN];
|
|
7
280
|
var ApiError = class extends Error {
|
|
8
281
|
constructor(message, status, code) {
|
|
9
282
|
super(message);
|
|
@@ -50,8 +323,22 @@ var ApiClient = class {
|
|
|
50
323
|
get storageKey() {
|
|
51
324
|
return `dewee-webchat:session:${this.o.endpoint}`;
|
|
52
325
|
}
|
|
53
|
-
|
|
54
|
-
|
|
326
|
+
/** Session reuse: refresh once less than 10% of the token lifetime (at most 24 h, at least 30 s) remains. */
|
|
327
|
+
fresh(s) {
|
|
328
|
+
const lifetime = s.obtainedAt ? s.expiresAt - s.obtainedAt : Infinity;
|
|
329
|
+
const margin = Math.min(864e5, Math.max(3e4, lifetime * 0.1));
|
|
330
|
+
return s.expiresAt - Date.now() > margin;
|
|
331
|
+
}
|
|
332
|
+
request(url, init) {
|
|
333
|
+
return (this.o.fetch ?? globalThis.fetch)(url, init);
|
|
334
|
+
}
|
|
335
|
+
async headers(auth = false) {
|
|
336
|
+
const h = {};
|
|
337
|
+
const extra = typeof this.o.headers === "function" ? await this.o.headers() : this.o.headers;
|
|
338
|
+
for (const [k, v] of Object.entries(extra ?? {})) {
|
|
339
|
+
if (typeof v === "string" && !RESERVED_HEADERS.includes(k.toLowerCase())) h[k.toLowerCase()] = v;
|
|
340
|
+
}
|
|
341
|
+
h["content-type"] = "application/json";
|
|
55
342
|
if (this.o.gatewayUrl && this.o.gatewayToken) {
|
|
56
343
|
h[HEADER_GATEWAY_URL] = this.o.gatewayUrl;
|
|
57
344
|
h[HEADER_GATEWAY_TOKEN] = this.o.gatewayToken;
|
|
@@ -66,18 +353,18 @@ var ApiClient = class {
|
|
|
66
353
|
}
|
|
67
354
|
async getConfig(force = false) {
|
|
68
355
|
if (this.config && !force) return this.config;
|
|
69
|
-
this.config = await this.parse(await
|
|
356
|
+
this.config = await this.parse(await this.request(this.url("config"), { headers: await this.headers() }));
|
|
70
357
|
return this.config;
|
|
71
358
|
}
|
|
72
359
|
/** Returns a valid visitor session, creating or refreshing it when needed. */
|
|
73
360
|
ensureSession() {
|
|
74
|
-
if (this.session && this.session
|
|
361
|
+
if (this.session && this.fresh(this.session)) return Promise.resolve(this.session);
|
|
75
362
|
if (!this.session) {
|
|
76
363
|
const saved = store.get(this.storageKey);
|
|
77
364
|
if (saved) {
|
|
78
365
|
try {
|
|
79
366
|
const s = JSON.parse(saved);
|
|
80
|
-
if (
|
|
367
|
+
if (this.fresh(s)) return Promise.resolve(this.session = s);
|
|
81
368
|
this.session = s;
|
|
82
369
|
} catch {
|
|
83
370
|
}
|
|
@@ -94,11 +381,11 @@ var ApiClient = class {
|
|
|
94
381
|
body.turnstileToken = await this.o.turnstile(cfg.turnstileSiteKey);
|
|
95
382
|
}
|
|
96
383
|
const s = await this.parse(
|
|
97
|
-
await
|
|
384
|
+
await this.request(this.url("session"), { method: "POST", headers: await this.headers(), body: JSON.stringify(body) })
|
|
98
385
|
);
|
|
99
|
-
this.session = s;
|
|
100
|
-
store.set(this.storageKey, JSON.stringify(
|
|
101
|
-
return
|
|
386
|
+
this.session = { ...s, obtainedAt: Date.now() };
|
|
387
|
+
store.set(this.storageKey, JSON.stringify(this.session));
|
|
388
|
+
return this.session;
|
|
102
389
|
}
|
|
103
390
|
forgetSession() {
|
|
104
391
|
this.session = null;
|
|
@@ -106,17 +393,17 @@ var ApiClient = class {
|
|
|
106
393
|
}
|
|
107
394
|
async history(agent, conversationId) {
|
|
108
395
|
await this.ensureSession();
|
|
109
|
-
const res = await
|
|
396
|
+
const res = await this.request(this.url("history", { agent, conversationId }), { headers: await this.headers(true) });
|
|
110
397
|
return (await this.parse(res)).messages;
|
|
111
398
|
}
|
|
112
399
|
/** Sends a message and calls `onEvent` for every streamed event. Resolves when the run ends. */
|
|
113
400
|
async chat(input, onEvent, signal) {
|
|
114
401
|
await this.ensureSession();
|
|
115
|
-
let res = await
|
|
402
|
+
let res = await this.request(this.url("chat"), { method: "POST", headers: await this.headers(true), body: JSON.stringify(input), signal });
|
|
116
403
|
if (res.status === 401) {
|
|
117
404
|
this.forgetSession();
|
|
118
405
|
await this.ensureSession();
|
|
119
|
-
res = await
|
|
406
|
+
res = await this.request(this.url("chat"), { method: "POST", headers: await this.headers(true), body: JSON.stringify(input), signal });
|
|
120
407
|
}
|
|
121
408
|
if (!res.ok || !res.body) await this.parse(res);
|
|
122
409
|
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
@@ -140,6 +427,79 @@ var ApiClient = class {
|
|
|
140
427
|
}
|
|
141
428
|
};
|
|
142
429
|
|
|
430
|
+
// src/widget/confirm-dialog.ts
|
|
431
|
+
var seq = 0;
|
|
432
|
+
function showConfirmDialog(container, s, action, args, signal) {
|
|
433
|
+
const id = `dw-confirm-${++seq}`;
|
|
434
|
+
const root = container.getRootNode();
|
|
435
|
+
const previous = root.activeElement;
|
|
436
|
+
const backdrop = document.createElement("div");
|
|
437
|
+
backdrop.className = "confirm-backdrop";
|
|
438
|
+
backdrop.innerHTML = `
|
|
439
|
+
<div class="confirm" role="alertdialog" aria-modal="true" aria-labelledby="${id}-t" aria-describedby="${id}-d">
|
|
440
|
+
<h3 id="${id}-t"></h3>
|
|
441
|
+
<div id="${id}-d" class="confirm-desc">
|
|
442
|
+
<div class="confirm-action"><code></code><span class="sens"></span></div>
|
|
443
|
+
<p class="confirm-text"></p>
|
|
444
|
+
<p class="confirm-warn" hidden></p>
|
|
445
|
+
</div>
|
|
446
|
+
<pre class="confirm-args"></pre>
|
|
447
|
+
<div class="confirm-buttons">
|
|
448
|
+
<button type="button" class="btn deny"></button>
|
|
449
|
+
<button type="button" class="btn allow"></button>
|
|
450
|
+
</div>
|
|
451
|
+
</div>`;
|
|
452
|
+
const q = (sel) => backdrop.querySelector(sel);
|
|
453
|
+
q("h3").textContent = s.confirmTitle;
|
|
454
|
+
q("code").textContent = action.name;
|
|
455
|
+
const sens = q(".sens");
|
|
456
|
+
sens.textContent = action.sensitivity;
|
|
457
|
+
sens.dataset.level = action.sensitivity;
|
|
458
|
+
q(".confirm-text").textContent = action.description;
|
|
459
|
+
if (action.sensitivity === "destructive") {
|
|
460
|
+
const warn = q(".confirm-warn");
|
|
461
|
+
warn.hidden = false;
|
|
462
|
+
warn.textContent = s.confirmDestructive;
|
|
463
|
+
}
|
|
464
|
+
const argsText = JSON.stringify(args, null, 2);
|
|
465
|
+
const pre = q(".confirm-args");
|
|
466
|
+
if (argsText && argsText !== "{}") pre.textContent = argsText;
|
|
467
|
+
else pre.remove();
|
|
468
|
+
const deny = q(".deny");
|
|
469
|
+
const allow = q(".allow");
|
|
470
|
+
deny.textContent = s.deny;
|
|
471
|
+
allow.textContent = s.allow;
|
|
472
|
+
if (action.sensitivity === "destructive") allow.classList.add("danger");
|
|
473
|
+
return new Promise((resolve) => {
|
|
474
|
+
let done = false;
|
|
475
|
+
const finish = (ok) => {
|
|
476
|
+
if (done) return;
|
|
477
|
+
done = true;
|
|
478
|
+
signal?.removeEventListener("abort", onAbort);
|
|
479
|
+
backdrop.remove();
|
|
480
|
+
previous?.focus?.();
|
|
481
|
+
resolve(ok);
|
|
482
|
+
};
|
|
483
|
+
const onAbort = () => finish(false);
|
|
484
|
+
if (signal?.aborted) return finish(false);
|
|
485
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
486
|
+
deny.addEventListener("click", () => finish(false));
|
|
487
|
+
allow.addEventListener("click", () => finish(true));
|
|
488
|
+
backdrop.addEventListener("keydown", (ev) => {
|
|
489
|
+
if (ev.key === "Escape") {
|
|
490
|
+
ev.preventDefault();
|
|
491
|
+
ev.stopPropagation();
|
|
492
|
+
finish(false);
|
|
493
|
+
} else if (ev.key === "Tab") {
|
|
494
|
+
ev.preventDefault();
|
|
495
|
+
(root.activeElement === deny ? allow : deny).focus();
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
container.appendChild(backdrop);
|
|
499
|
+
deny.focus();
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
143
503
|
// src/widget/i18n.ts
|
|
144
504
|
var en = {
|
|
145
505
|
title: "Assistant",
|
|
@@ -162,7 +522,13 @@ var en = {
|
|
|
162
522
|
online: "Online",
|
|
163
523
|
poweredBy: "Powered by",
|
|
164
524
|
errorGeneric: "Something went wrong. Please try again.",
|
|
165
|
-
stopped: "Stopped."
|
|
525
|
+
stopped: "Stopped.",
|
|
526
|
+
actionStatus: { pending: "queued", "awaiting-confirmation": "needs approval", running: "running", done: "done", denied: "declined", error: "failed" },
|
|
527
|
+
confirmTitle: "Allow this action?",
|
|
528
|
+
confirmDestructive: "This can't be undone.",
|
|
529
|
+
allow: "Allow",
|
|
530
|
+
deny: "Deny",
|
|
531
|
+
actionLimit: (n) => `Stopped after ${n} automatic action rounds. Send a message to continue.`
|
|
166
532
|
};
|
|
167
533
|
var vi = {
|
|
168
534
|
title: "Tr\u1EE3 l\xFD",
|
|
@@ -185,13 +551,20 @@ var vi = {
|
|
|
185
551
|
online: "\u0110ang tr\u1EF1c tuy\u1EBFn",
|
|
186
552
|
poweredBy: "V\u1EADn h\xE0nh b\u1EDFi",
|
|
187
553
|
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."
|
|
554
|
+
stopped: "\u0110\xE3 d\u1EEBng.",
|
|
555
|
+
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" },
|
|
556
|
+
confirmTitle: "Cho ph\xE9p thao t\xE1c n\xE0y?",
|
|
557
|
+
confirmDestructive: "Thao t\xE1c n\xE0y kh\xF4ng th\u1EC3 ho\xE0n t\xE1c.",
|
|
558
|
+
allow: "Cho ph\xE9p",
|
|
559
|
+
deny: "T\u1EEB ch\u1ED1i",
|
|
560
|
+
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
561
|
};
|
|
190
562
|
function pickStrings(locale) {
|
|
191
563
|
const l = (locale || (typeof document !== "undefined" ? document.documentElement.lang : "") || (typeof navigator !== "undefined" ? navigator.language : "") || "en").toLowerCase();
|
|
192
564
|
return l.startsWith("vi") ? vi : en;
|
|
193
565
|
}
|
|
194
566
|
var icons = {
|
|
567
|
+
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
568
|
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
569
|
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
570
|
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 +777,7 @@ var styles = (
|
|
|
404
777
|
}
|
|
405
778
|
* { box-sizing: border-box; }
|
|
406
779
|
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; padding: 0; }
|
|
780
|
+
[hidden] { display: none !important; }
|
|
407
781
|
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
|
|
408
782
|
|
|
409
783
|
/* ---------- launcher ---------- */
|
|
@@ -569,6 +943,46 @@ details > summary svg.ic { width: 14px; height: 14px; flex: none; }
|
|
|
569
943
|
.typing i:nth-child(2) { animation-delay: .15s; } .typing i:nth-child(3) { animation-delay: .3s; }
|
|
570
944
|
@keyframes bounce { 0%, 60%, 100% { transform: none; opacity: .35; } 30% { transform: translateY(-4px); opacity: 1; } }
|
|
571
945
|
|
|
946
|
+
/* page actions */
|
|
947
|
+
.actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
|
948
|
+
.md[hidden] + .actions { margin-top: 0; }
|
|
949
|
+
.action {
|
|
950
|
+
display: inline-flex; align-items: center; gap: 6px; max-width: 100%; padding: 4px 9px 4px 8px; border-radius: 999px;
|
|
951
|
+
border: 1px solid var(--dw-border); background: var(--dw-surface); font-size: 12px; color: var(--dw-muted);
|
|
952
|
+
}
|
|
953
|
+
.action svg.ic { width: 13px; height: 13px; flex: none; color: var(--dw-accent); }
|
|
954
|
+
.action-name { font-family: var(--dw-mono); font-size: 11.5px; color: var(--dw-fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
955
|
+
.action-state { font-size: 11px; flex: none; }
|
|
956
|
+
.action[data-status="done"] .action-state { color: #13a06f; }
|
|
957
|
+
.action[data-status="awaiting-confirmation"] { border-color: color-mix(in srgb, #d98a00 45%, transparent); }
|
|
958
|
+
.action[data-status="awaiting-confirmation"] .action-state { color: #b06f00; }
|
|
959
|
+
.action[data-status="denied"] .action-state, .action[data-status="error"] .action-state { color: #c2334f; }
|
|
960
|
+
.notice { margin: -6px 0 16px; font-size: 12px; color: var(--dw-muted); text-align: center; }
|
|
961
|
+
.confirm-backdrop {
|
|
962
|
+
position: absolute; inset: 0; z-index: 2; display: flex; align-items: flex-end; justify-content: center; padding: 12px;
|
|
963
|
+
background: color-mix(in srgb, var(--dw-fg) 28%, transparent); animation: rise .18s ease both;
|
|
964
|
+
}
|
|
965
|
+
.confirm {
|
|
966
|
+
width: 100%; max-height: 100%; overflow: auto; background: var(--dw-bg); color: var(--dw-fg);
|
|
967
|
+
border: 1px solid var(--dw-border); border-radius: 16px; padding: 16px; box-shadow: var(--dw-shadow);
|
|
968
|
+
}
|
|
969
|
+
.confirm h3 { margin: 0 0 10px; font-size: 15px; font-weight: 650; }
|
|
970
|
+
.confirm-action { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
971
|
+
.confirm-action code { font-family: var(--dw-mono); font-size: 12.5px; background: var(--dw-surface); padding: 2px 6px; border-radius: 6px; }
|
|
972
|
+
.sens { font-size: 11px; padding: 1px 7px; border-radius: 999px; border: 1px solid var(--dw-border); color: var(--dw-muted); }
|
|
973
|
+
.sens[data-level="destructive"] { color: #c2334f; border-color: color-mix(in srgb, #c2334f 40%, transparent); }
|
|
974
|
+
.confirm-text { margin: 8px 0 0; font-size: 13.5px; line-height: 1.5; color: var(--dw-muted); }
|
|
975
|
+
.confirm-warn { margin: 8px 0 0; font-size: 13px; font-weight: 600; color: #c2334f; }
|
|
976
|
+
.confirm-args {
|
|
977
|
+
margin: 10px 0 0; font-family: var(--dw-mono); font-size: 11.5px; line-height: 1.5; background: var(--dw-surface);
|
|
978
|
+
border: 1px solid var(--dw-border); border-radius: 8px; padding: 8px; max-height: 160px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere;
|
|
979
|
+
}
|
|
980
|
+
.confirm-buttons { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }
|
|
981
|
+
.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; }
|
|
982
|
+
.btn.allow { background: var(--dw-accent); border-color: var(--dw-accent); color: var(--dw-accent-contrast); }
|
|
983
|
+
.btn.allow.danger { background: #c2334f; border-color: #c2334f; color: #fff; }
|
|
984
|
+
.btn:focus-visible { outline: 3px solid color-mix(in srgb, var(--dw-accent) 45%, transparent); outline-offset: 2px; }
|
|
985
|
+
|
|
572
986
|
/* ---------- composer ---------- */
|
|
573
987
|
.composer { padding: 10px 12px 12px; border-top: 1px solid var(--dw-border); background: var(--dw-bg); }
|
|
574
988
|
.box {
|
|
@@ -677,6 +1091,10 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
677
1091
|
activated = false;
|
|
678
1092
|
strings = pickStrings();
|
|
679
1093
|
mql = null;
|
|
1094
|
+
registry = new ActionRegistry();
|
|
1095
|
+
propActionNames = /* @__PURE__ */ new Set();
|
|
1096
|
+
hooks = {};
|
|
1097
|
+
acks = /* @__PURE__ */ new Map();
|
|
680
1098
|
els;
|
|
681
1099
|
constructor() {
|
|
682
1100
|
super();
|
|
@@ -694,13 +1112,54 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
694
1112
|
else if (a === "z-index") c.zIndex = Number(v);
|
|
695
1113
|
else c[camel(a)] = v;
|
|
696
1114
|
}
|
|
697
|
-
return { ...c, ...this.overrides };
|
|
1115
|
+
return { ...c, ...this.overrides, ...this.hooks };
|
|
698
1116
|
}
|
|
699
1117
|
/** Updates configuration at runtime (wins over attributes). */
|
|
700
1118
|
configure(partial) {
|
|
701
|
-
|
|
1119
|
+
const { actions, confirm, getPageContext, headers, fetch, ...rest } = partial;
|
|
1120
|
+
const has = (k) => Object.prototype.hasOwnProperty.call(partial, k);
|
|
1121
|
+
if (has("actions")) this.setPropActions(actions ?? []);
|
|
1122
|
+
if (has("confirm")) this.hooks.confirm = confirm;
|
|
1123
|
+
if (has("getPageContext")) this.hooks.getPageContext = getPageContext;
|
|
1124
|
+
if (has("headers")) this.hooks.headers = headers;
|
|
1125
|
+
if (has("fetch")) this.hooks.fetch = fetch;
|
|
1126
|
+
if (!Object.keys(rest).length) return;
|
|
1127
|
+
this.overrides = { ...this.overrides, ...rest };
|
|
702
1128
|
if (this.els) this.applyConfig();
|
|
703
1129
|
}
|
|
1130
|
+
/** Adds or replaces page actions the agent may invoke. Throws TypeError on an invalid definition. */
|
|
1131
|
+
registerActions(actions) {
|
|
1132
|
+
this.registry.register(actions);
|
|
1133
|
+
}
|
|
1134
|
+
/** Removes a page action. Returns false when it was not registered. */
|
|
1135
|
+
unregisterAction(name) {
|
|
1136
|
+
this.propActionNames.delete(name);
|
|
1137
|
+
return this.registry.unregister(name);
|
|
1138
|
+
}
|
|
1139
|
+
/** Extra request headers (property only; same as `configure({ headers })`). */
|
|
1140
|
+
get headers() {
|
|
1141
|
+
return this.hooks.headers;
|
|
1142
|
+
}
|
|
1143
|
+
set headers(v) {
|
|
1144
|
+
this.hooks.headers = v;
|
|
1145
|
+
}
|
|
1146
|
+
/** Custom fetch for widget requests (property only; same as `configure({ fetch })`). */
|
|
1147
|
+
get fetch() {
|
|
1148
|
+
return this.hooks.fetch;
|
|
1149
|
+
}
|
|
1150
|
+
set fetch(v) {
|
|
1151
|
+
this.hooks.fetch = v;
|
|
1152
|
+
}
|
|
1153
|
+
/** Names of the currently registered page actions. */
|
|
1154
|
+
get actionNames() {
|
|
1155
|
+
return this.registry.names();
|
|
1156
|
+
}
|
|
1157
|
+
setPropActions(actions) {
|
|
1158
|
+
const next = new Set(actions.map((a) => a.name));
|
|
1159
|
+
for (const name of this.propActionNames) if (!next.has(name)) this.registry.unregister(name);
|
|
1160
|
+
if (actions.length) this.registry.register(actions);
|
|
1161
|
+
this.propActionNames = next;
|
|
1162
|
+
}
|
|
704
1163
|
open() {
|
|
705
1164
|
if (this.config.mode === "inline") return;
|
|
706
1165
|
this.setAttribute("data-open", "");
|
|
@@ -727,21 +1186,40 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
727
1186
|
await this.activate();
|
|
728
1187
|
this.els.input.value = "";
|
|
729
1188
|
this.autosize();
|
|
730
|
-
|
|
1189
|
+
let next = { message, hidden: false };
|
|
1190
|
+
for (let round = 0; next; round++) {
|
|
1191
|
+
const reply = await this.runTurn(next.message, next.hidden);
|
|
1192
|
+
next = null;
|
|
1193
|
+
if (reply.status !== "done" || reply.stopped || !this.registry.size) break;
|
|
1194
|
+
const { calls } = parseDirectives(reply.content);
|
|
1195
|
+
if (!calls.length) break;
|
|
1196
|
+
reply.actions = calls.map((c) => ({ id: c.id, name: c.name, args: c.ok ? c.args : void 0, status: "pending" }));
|
|
1197
|
+
const limit = this.config.maxActionRounds ?? 5;
|
|
1198
|
+
if (round >= limit) {
|
|
1199
|
+
for (const a of reply.actions) Object.assign(a, { status: "error", error: "Automatic action limit reached" });
|
|
1200
|
+
this.updateMessage(reply);
|
|
1201
|
+
this.showNotice(this.strings.actionLimit(limit));
|
|
1202
|
+
break;
|
|
1203
|
+
}
|
|
1204
|
+
this.updateMessage(reply);
|
|
1205
|
+
const results = await this.runActions(reply, calls);
|
|
1206
|
+
if (results) next = { message: formatActionResults(results, this.server?.maxMessageLength ?? 4e3), hidden: true };
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
/** One request/response with the agent. */
|
|
1210
|
+
async runTurn(message, hidden) {
|
|
1211
|
+
this.pushMessage({ role: "user", content: message, hidden });
|
|
731
1212
|
const reply = { role: "assistant", content: "", status: "streaming", startedAt: Date.now() };
|
|
732
1213
|
this.pushMessage(reply);
|
|
733
1214
|
this.setRunning(new AbortController());
|
|
734
|
-
this.emit("dewee:send", { message, agent: this.agentId, conversationId: this.conversationId });
|
|
1215
|
+
this.emit("dewee:send", { message, agent: this.agentId, conversationId: this.conversationId, auto: hidden || void 0 });
|
|
735
1216
|
try {
|
|
736
|
-
await this.client().chat(
|
|
737
|
-
{ message, agent: this.agentId, conversationId: this.conversationId },
|
|
738
|
-
(e) => this.onStreamEvent(reply, e),
|
|
739
|
-
this.running.signal
|
|
740
|
-
);
|
|
1217
|
+
await this.client().chat(this.chatBody(message), (e) => this.onStreamEvent(reply, e), this.running.signal);
|
|
741
1218
|
if (reply.status === "streaming") reply.status = "done";
|
|
742
1219
|
} catch (e) {
|
|
743
1220
|
if (e.name === "AbortError") {
|
|
744
1221
|
reply.status = "done";
|
|
1222
|
+
reply.stopped = true;
|
|
745
1223
|
if (!reply.content) reply.content = `_${this.strings.stopped}_`;
|
|
746
1224
|
} else {
|
|
747
1225
|
reply.status = "error";
|
|
@@ -752,10 +1230,95 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
752
1230
|
this.updateMessage(reply);
|
|
753
1231
|
this.setRunning(null);
|
|
754
1232
|
if (reply.status === "done") {
|
|
1233
|
+
if (reply.actionsHash && !reply.stopped) this.setAck(reply.actionsHash);
|
|
755
1234
|
this.emit("dewee:reply", { content: reply.content, agent: this.agentId, conversationId: this.conversationId });
|
|
756
1235
|
if (this.config.mode !== "inline" && !this.hasAttribute("data-open")) this.setAttribute("data-unread", "");
|
|
757
1236
|
}
|
|
758
1237
|
}
|
|
1238
|
+
return reply;
|
|
1239
|
+
}
|
|
1240
|
+
chatBody(message) {
|
|
1241
|
+
const body = { message, agent: this.agentId, conversationId: this.conversationId };
|
|
1242
|
+
if (this.registry.size) {
|
|
1243
|
+
body.actions = this.registry.manifest();
|
|
1244
|
+
const ack = this.getAck();
|
|
1245
|
+
if (ack) body.actionsHash = ack;
|
|
1246
|
+
}
|
|
1247
|
+
const ctx = this.pageContext();
|
|
1248
|
+
if (ctx !== void 0) body.pageContext = ctx;
|
|
1249
|
+
return body;
|
|
1250
|
+
}
|
|
1251
|
+
/** Host snapshot, dropped (with a warning) when it fails or exceeds the backend cap. */
|
|
1252
|
+
pageContext() {
|
|
1253
|
+
const get = this.hooks.getPageContext;
|
|
1254
|
+
if (!get) return void 0;
|
|
1255
|
+
try {
|
|
1256
|
+
const v = get();
|
|
1257
|
+
if (v === void 0 || v === null) return void 0;
|
|
1258
|
+
const size = new TextEncoder().encode(JSON.stringify(v)).length;
|
|
1259
|
+
if (size <= ACTION_LIMITS.maxPageContextBytes) return v;
|
|
1260
|
+
console.warn(`[dewee-webchat] getPageContext() returned ${size} bytes; the limit is ${ACTION_LIMITS.maxPageContextBytes}. Context not sent.`);
|
|
1261
|
+
return { truncated: true };
|
|
1262
|
+
} catch (e) {
|
|
1263
|
+
console.warn("[dewee-webchat] getPageContext() failed:", e);
|
|
1264
|
+
return void 0;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
ackKey() {
|
|
1268
|
+
return `dewee-webchat:actions:${this.config.endpoint}:${this.agentId}:${this.conversationId}`;
|
|
1269
|
+
}
|
|
1270
|
+
getAck() {
|
|
1271
|
+
return this.acks.get(this.ackKey()) ?? (this.config.persist !== false ? store2.get(this.ackKey()) ?? void 0 : void 0);
|
|
1272
|
+
}
|
|
1273
|
+
setAck(hash) {
|
|
1274
|
+
this.acks.set(this.ackKey(), hash);
|
|
1275
|
+
if (this.config.persist !== false) store2.set(this.ackKey(), hash);
|
|
1276
|
+
}
|
|
1277
|
+
/** Executes the calls of one answer in order. Resolves null when the visitor stopped the chain. */
|
|
1278
|
+
async runActions(reply, calls) {
|
|
1279
|
+
const ctrl = new AbortController();
|
|
1280
|
+
this.setRunning(ctrl);
|
|
1281
|
+
const results = [];
|
|
1282
|
+
const base = { agent: this.agentId, conversationId: this.conversationId };
|
|
1283
|
+
try {
|
|
1284
|
+
for (const [i, call] of calls.entries()) {
|
|
1285
|
+
const view = reply.actions[i];
|
|
1286
|
+
const sensitivity = call.ok ? this.registry.get(call.name)?.sensitivity : void 0;
|
|
1287
|
+
const args = call.ok ? call.args : void 0;
|
|
1288
|
+
this.emit("dewee:action", { phase: "start", id: call.id, name: call.name, sensitivity, args, ...base });
|
|
1289
|
+
const r = await runActionCall(call, {
|
|
1290
|
+
registry: this.registry,
|
|
1291
|
+
confirm: (action, a) => this.confirmAction(action, a, ctrl.signal),
|
|
1292
|
+
signal: ctrl.signal,
|
|
1293
|
+
timeoutMs: this.config.actionTimeoutMs,
|
|
1294
|
+
...base,
|
|
1295
|
+
onStatus: (status) => {
|
|
1296
|
+
view.status = status;
|
|
1297
|
+
this.updateMessage(reply);
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
if (r.error) view.error = r.error;
|
|
1301
|
+
this.updateMessage(reply);
|
|
1302
|
+
results.push(r);
|
|
1303
|
+
this.emit("dewee:action", { phase: "result", id: r.id, name: r.name, sensitivity, args, status: r.status, result: r.result, error: r.error, ...base });
|
|
1304
|
+
}
|
|
1305
|
+
} finally {
|
|
1306
|
+
this.setRunning(null);
|
|
1307
|
+
}
|
|
1308
|
+
return ctrl.signal.aborted ? null : results;
|
|
1309
|
+
}
|
|
1310
|
+
confirmAction(action, args, signal) {
|
|
1311
|
+
if (this.hooks.confirm) return this.hooks.confirm(action, args);
|
|
1312
|
+
if (this.config.mode !== "inline" && !this.hasAttribute("data-open")) this.open();
|
|
1313
|
+
return showConfirmDialog(this.els.panel, this.strings, action, args, signal);
|
|
1314
|
+
}
|
|
1315
|
+
showNotice(text) {
|
|
1316
|
+
const div = document.createElement("div");
|
|
1317
|
+
div.className = "notice";
|
|
1318
|
+
div.setAttribute("role", "status");
|
|
1319
|
+
div.textContent = text;
|
|
1320
|
+
this.els.log.appendChild(div);
|
|
1321
|
+
this.scrollToBottom(true);
|
|
759
1322
|
}
|
|
760
1323
|
/** Stops the running reply. */
|
|
761
1324
|
stop() {
|
|
@@ -799,7 +1362,13 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
799
1362
|
endpoint: c.endpoint,
|
|
800
1363
|
gatewayUrl: c.gatewayUrl,
|
|
801
1364
|
gatewayToken: c.gatewayToken,
|
|
802
|
-
turnstile: (siteKey) => this.challenge(siteKey)
|
|
1365
|
+
turnstile: (siteKey) => this.challenge(siteKey),
|
|
1366
|
+
// Read the current hooks on every request so configure({ headers, fetch }) applies at once.
|
|
1367
|
+
headers: () => {
|
|
1368
|
+
const h = this.hooks.headers;
|
|
1369
|
+
return (typeof h === "function" ? h() : h) ?? {};
|
|
1370
|
+
},
|
|
1371
|
+
fetch: (input, init) => (this.hooks.fetch ?? globalThis.fetch)(input, init)
|
|
803
1372
|
});
|
|
804
1373
|
}
|
|
805
1374
|
return this.api;
|
|
@@ -999,7 +1568,7 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
999
1568
|
if (!saved) return;
|
|
1000
1569
|
try {
|
|
1001
1570
|
const history = await this.client().history(this.agentId, this.conversationId);
|
|
1002
|
-
this.messages = history.map((m) => ({ ...m, status: "done" }));
|
|
1571
|
+
this.messages = decorateHistory(history.map((m) => ({ ...m, status: "done" })));
|
|
1003
1572
|
this.renderLog();
|
|
1004
1573
|
} catch {
|
|
1005
1574
|
}
|
|
@@ -1045,6 +1614,7 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1045
1614
|
renderMessage(m) {
|
|
1046
1615
|
const root = document.createElement("div");
|
|
1047
1616
|
root.className = `msg ${m.role}${m.status === "error" ? " error" : ""}`;
|
|
1617
|
+
root.hidden = !!m.hidden;
|
|
1048
1618
|
const bubble = document.createElement("div");
|
|
1049
1619
|
bubble.className = "bubble";
|
|
1050
1620
|
root.appendChild(bubble);
|
|
@@ -1052,9 +1622,13 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1052
1622
|
trace.className = "trace";
|
|
1053
1623
|
const md = document.createElement("div");
|
|
1054
1624
|
md.className = m.role === "assistant" ? "md" : "";
|
|
1625
|
+
const actions = document.createElement("div");
|
|
1626
|
+
actions.className = "actions";
|
|
1627
|
+
actions.hidden = true;
|
|
1055
1628
|
if (m.role === "assistant") bubble.appendChild(trace);
|
|
1056
1629
|
bubble.appendChild(md);
|
|
1057
|
-
m.
|
|
1630
|
+
if (m.role === "assistant") bubble.appendChild(actions);
|
|
1631
|
+
m.view = { root, trace, md, tools: /* @__PURE__ */ new Map(), actions };
|
|
1058
1632
|
this.updateMessage(m);
|
|
1059
1633
|
return root;
|
|
1060
1634
|
}
|
|
@@ -1091,13 +1665,33 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1091
1665
|
v.tools.clear();
|
|
1092
1666
|
}
|
|
1093
1667
|
v.trace.hidden = !v.trace.childElementCount;
|
|
1094
|
-
const
|
|
1668
|
+
const shown = this.registry.size || m.actions ? stripDirectives(m.content) : m.content;
|
|
1669
|
+
const typing = m.status === "streaming" && !shown;
|
|
1670
|
+
v.md.hidden = !typing && !shown && !!m.actions?.length;
|
|
1095
1671
|
if (typing) {
|
|
1096
1672
|
v.md.innerHTML = `<span class="typing" aria-label="${this.strings.thinking}"><i></i><i></i><i></i></span>`;
|
|
1097
1673
|
} else {
|
|
1098
|
-
v.md.replaceChildren(renderMarkdown(document,
|
|
1674
|
+
v.md.replaceChildren(renderMarkdown(document, shown));
|
|
1099
1675
|
v.md.classList.toggle("cursor", m.status === "streaming");
|
|
1100
1676
|
}
|
|
1677
|
+
this.renderActions(v, m.actions ?? []);
|
|
1678
|
+
}
|
|
1679
|
+
renderActions(v, list) {
|
|
1680
|
+
v.actions.hidden = !list.length;
|
|
1681
|
+
v.actions.replaceChildren(
|
|
1682
|
+
...list.map((a) => {
|
|
1683
|
+
const chip = document.createElement("div");
|
|
1684
|
+
chip.className = "action";
|
|
1685
|
+
chip.dataset.status = a.status;
|
|
1686
|
+
const busy = a.status === "running" || a.status === "awaiting-confirmation";
|
|
1687
|
+
chip.innerHTML = `${busy ? '<span class="spin"></span>' : icons.action}<span class="action-name"></span><span class="action-state"></span>`;
|
|
1688
|
+
chip.querySelector(".action-name").textContent = a.name || "?";
|
|
1689
|
+
chip.querySelector(".action-state").textContent = this.strings.actionStatus[a.status];
|
|
1690
|
+
const tip = [a.args && Object.keys(a.args).length ? JSON.stringify(a.args) : "", a.error ?? ""].filter(Boolean).join("\n");
|
|
1691
|
+
if (tip) chip.title = tip;
|
|
1692
|
+
return chip;
|
|
1693
|
+
})
|
|
1694
|
+
);
|
|
1101
1695
|
}
|
|
1102
1696
|
renderTool(v, t) {
|
|
1103
1697
|
let d = v.tools.get(t.id);
|
|
@@ -1116,7 +1710,7 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1116
1710
|
d.querySelector(".tool-state").textContent = t.status === "running" ? this.strings.toolRunning : t.status === "error" ? this.strings.toolError : this.strings.toolDone;
|
|
1117
1711
|
const body = d.querySelector(".tool-body");
|
|
1118
1712
|
body.replaceChildren();
|
|
1119
|
-
const
|
|
1713
|
+
const block2 = (label, value) => {
|
|
1120
1714
|
if (value === void 0 || value === null || value === "") return;
|
|
1121
1715
|
const l = document.createElement("div");
|
|
1122
1716
|
l.className = "label";
|
|
@@ -1125,12 +1719,13 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1125
1719
|
pre.textContent = typeof value === "string" ? tryPretty(value) : JSON.stringify(value, null, 2);
|
|
1126
1720
|
body.append(l, pre);
|
|
1127
1721
|
};
|
|
1128
|
-
|
|
1129
|
-
|
|
1722
|
+
block2(this.strings.args, t.arguments);
|
|
1723
|
+
block2(this.strings.result, t.result);
|
|
1130
1724
|
}
|
|
1131
1725
|
onStreamEvent(m, e) {
|
|
1132
1726
|
switch (e.type) {
|
|
1133
1727
|
case "start":
|
|
1728
|
+
if (e.actionsHash) m.actionsHash = e.actionsHash;
|
|
1134
1729
|
if (e.conversationId && e.conversationId !== this.conversationId) {
|
|
1135
1730
|
this.conversationId = e.conversationId;
|
|
1136
1731
|
if (this.config.persist !== false) store2.set(this.convKey(), e.conversationId);
|
|
@@ -1228,6 +1823,27 @@ var DeweeChatElement = class extends ElementBase {
|
|
|
1228
1823
|
}
|
|
1229
1824
|
}
|
|
1230
1825
|
};
|
|
1826
|
+
function decorateHistory(messages) {
|
|
1827
|
+
messages.forEach((m, i) => {
|
|
1828
|
+
if (m.role === "user") {
|
|
1829
|
+
m.content = stripPageBlock(m.content);
|
|
1830
|
+
if (isActionResultMessage(m.content)) m.hidden = true;
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
if (!m.content.includes("```dewee-action")) return;
|
|
1834
|
+
const { calls } = parseDirectives(m.content);
|
|
1835
|
+
if (!calls.length) return;
|
|
1836
|
+
const next = messages[i + 1];
|
|
1837
|
+
const nextContent = next?.role === "user" ? stripPageBlock(next.content) : "";
|
|
1838
|
+
const results = isActionResultMessage(nextContent) ? parseActionResults(nextContent) : [];
|
|
1839
|
+
m.actions = calls.map((c) => {
|
|
1840
|
+
const r = results.find((x) => x.id === c.id);
|
|
1841
|
+
const status = !r ? "error" : r.status === "ok" ? "done" : r.status;
|
|
1842
|
+
return { id: c.id, name: c.name, args: c.ok ? c.args : void 0, status, error: r ? r.error : "Not run" };
|
|
1843
|
+
});
|
|
1844
|
+
});
|
|
1845
|
+
return messages;
|
|
1846
|
+
}
|
|
1231
1847
|
function safeJson(s, fallback) {
|
|
1232
1848
|
try {
|
|
1233
1849
|
return JSON.parse(s);
|
|
@@ -1262,6 +1878,7 @@ function mount(config = {}, target) {
|
|
|
1262
1878
|
defineDeweeChat();
|
|
1263
1879
|
|
|
1264
1880
|
export {
|
|
1881
|
+
RESERVED_HEADERS,
|
|
1265
1882
|
ApiError,
|
|
1266
1883
|
ApiClient,
|
|
1267
1884
|
renderMarkdown,
|