@artooi/ag-ui-web-component 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +471 -0
- package/dist/ag-ui-web-component.bundle.js +319 -0
- package/dist/ag-ui-web-component.bundle.js.map +7 -0
- package/dist/ag_ui_chat.d.ts +85 -0
- package/dist/ag_ui_chat.d.ts.map +1 -0
- package/dist/agui_client.d.ts +99 -0
- package/dist/agui_client.d.ts.map +1 -0
- package/dist/animations.d.ts +33 -0
- package/dist/animations.d.ts.map +1 -0
- package/dist/client_tool_registry.d.ts +32 -0
- package/dist/client_tool_registry.d.ts.map +1 -0
- package/dist/confirmation_modal.d.ts +14 -0
- package/dist/confirmation_modal.d.ts.map +1 -0
- package/dist/constants.d.ts +40 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/conversation_store.d.ts +54 -0
- package/dist/conversation_store.d.ts.map +1 -0
- package/dist/create_http_agent.d.ts +22 -0
- package/dist/create_http_agent.d.ts.map +1 -0
- package/dist/define_ag_ui_chat.d.ts +9 -0
- package/dist/define_ag_ui_chat.d.ts.map +1 -0
- package/dist/dom_driver.d.ts +24 -0
- package/dist/dom_driver.d.ts.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1159 -0
- package/dist/index.js.map +7 -0
- package/dist/is_destructive.d.ts +8 -0
- package/dist/is_destructive.d.ts.map +1 -0
- package/dist/is_navigates.d.ts +9 -0
- package/dist/is_navigates.d.ts.map +1 -0
- package/dist/page_map.d.ts +16 -0
- package/dist/page_map.d.ts.map +1 -0
- package/dist/route_map.d.ts +27 -0
- package/dist/route_map.d.ts.map +1 -0
- package/dist/state_hook.d.ts +23 -0
- package/dist/state_hook.d.ts.map +1 -0
- package/dist/styles.d.ts +2 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/tool_call_card.d.ts +29 -0
- package/dist/tool_call_card.d.ts.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/package.json +79 -0
- package/src/ag_ui_chat.ts +411 -0
- package/src/agui_client.ts +212 -0
- package/src/animations.ts +86 -0
- package/src/client_tool_registry.ts +56 -0
- package/src/confirmation_modal.ts +69 -0
- package/src/constants.ts +48 -0
- package/src/conversation_store.ts +103 -0
- package/src/create_http_agent.ts +40 -0
- package/src/define_ag_ui_chat.ts +15 -0
- package/src/dom_driver.ts +60 -0
- package/src/index.ts +60 -0
- package/src/is_destructive.ts +11 -0
- package/src/is_navigates.ts +12 -0
- package/src/page_map.ts +25 -0
- package/src/route_map.ts +83 -0
- package/src/state_hook.ts +44 -0
- package/src/styles.ts +296 -0
- package/src/tool_call_card.ts +95 -0
- package/src/version.ts +1 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1159 @@
|
|
|
1
|
+
// src/agui_client.ts
|
|
2
|
+
import { randomUUID } from "@ag-ui/client";
|
|
3
|
+
|
|
4
|
+
// src/constants.ts
|
|
5
|
+
var ELEMENT_TAG = "ag-ui-chat";
|
|
6
|
+
var SUBMIT_EVENT = "ag-ui-submit";
|
|
7
|
+
var MESSAGE_ROLE = {
|
|
8
|
+
USER: "user",
|
|
9
|
+
ASSISTANT: "assistant"
|
|
10
|
+
};
|
|
11
|
+
var X_DESTRUCTIVE_KEY = "x-destructive";
|
|
12
|
+
var X_NAVIGATES_KEY = "x-navigates";
|
|
13
|
+
var MAX_TOOL_ROUNDS = 10;
|
|
14
|
+
var TOOL_CALL_STATUS = {
|
|
15
|
+
PENDING: "pending",
|
|
16
|
+
DONE: "done",
|
|
17
|
+
ERROR: "error",
|
|
18
|
+
DECLINED: "declined"
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// src/agui_client.ts
|
|
22
|
+
var AgUiClient = class {
|
|
23
|
+
#agent;
|
|
24
|
+
#handlers;
|
|
25
|
+
#getTools;
|
|
26
|
+
#getContext;
|
|
27
|
+
#executeTool;
|
|
28
|
+
#onPersist;
|
|
29
|
+
constructor(config) {
|
|
30
|
+
this.#agent = config.agent;
|
|
31
|
+
this.#handlers = config.handlers;
|
|
32
|
+
this.#getTools = config.getTools ?? (() => []);
|
|
33
|
+
this.#getContext = config.getContext ?? (() => []);
|
|
34
|
+
this.#executeTool = config.executeTool ?? null;
|
|
35
|
+
this.#onPersist = config.onPersist ?? (() => {
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** Whether a run is currently in flight. */
|
|
39
|
+
get running() {
|
|
40
|
+
return this.#agent.isRunning;
|
|
41
|
+
}
|
|
42
|
+
/** The current conversation history (for persistence / rehydration). */
|
|
43
|
+
get messages() {
|
|
44
|
+
return this.#agent.messages;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Append a user message and run the agent to completion.
|
|
48
|
+
*
|
|
49
|
+
* When the agent calls frontend tools, this executes them and re-runs the
|
|
50
|
+
* agent with the results, looping until the agent stops calling frontend
|
|
51
|
+
* tools (bounded by {@link MAX_TOOL_ROUNDS}).
|
|
52
|
+
*/
|
|
53
|
+
async send(content) {
|
|
54
|
+
this.#agent.addMessage({ id: randomUUID(), role: "user", content });
|
|
55
|
+
this.#onPersist(this.#agent.messages);
|
|
56
|
+
await this.#run();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Resume the run loop after a navigating tool's result was supplied
|
|
60
|
+
* post-reload (via {@link addToolResult}). Unlike {@link send}, adds no user
|
|
61
|
+
* message — it simply continues the conversation already in history.
|
|
62
|
+
*/
|
|
63
|
+
async resume() {
|
|
64
|
+
await this.#run();
|
|
65
|
+
}
|
|
66
|
+
/** Append a frontend tool result to history (used by the resume path). */
|
|
67
|
+
addToolResult(toolCallId, content) {
|
|
68
|
+
this.#agent.addMessage({ id: randomUUID(), role: "tool", content, toolCallId });
|
|
69
|
+
this.#onPersist(this.#agent.messages);
|
|
70
|
+
}
|
|
71
|
+
async #run() {
|
|
72
|
+
try {
|
|
73
|
+
await this.#runLoop();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
this.#handlers.onError(error instanceof Error ? error.message : String(error));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async #runLoop() {
|
|
79
|
+
for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) {
|
|
80
|
+
const pending = [];
|
|
81
|
+
await this.#agent.runAgent(
|
|
82
|
+
{ tools: this.#getTools(), context: this.#getContext() },
|
|
83
|
+
this.#buildSubscriber(pending)
|
|
84
|
+
);
|
|
85
|
+
this.#onPersist(this.#agent.messages);
|
|
86
|
+
if (this.#executeTool === null || pending.length === 0) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
let executed = false;
|
|
90
|
+
for (const call of pending) {
|
|
91
|
+
const result = await this.#executeTool(call);
|
|
92
|
+
if (result === null) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (result.halt === true) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
this.#agent.addMessage({
|
|
99
|
+
id: randomUUID(),
|
|
100
|
+
role: "tool",
|
|
101
|
+
content: result.content,
|
|
102
|
+
toolCallId: call.id
|
|
103
|
+
});
|
|
104
|
+
this.#onPersist(this.#agent.messages);
|
|
105
|
+
executed = true;
|
|
106
|
+
}
|
|
107
|
+
if (!executed) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
#buildSubscriber(pending) {
|
|
113
|
+
const h = this.#handlers;
|
|
114
|
+
return {
|
|
115
|
+
onRunInitialized() {
|
|
116
|
+
h.onRunStart();
|
|
117
|
+
},
|
|
118
|
+
onTextMessageContentEvent({ textMessageBuffer }) {
|
|
119
|
+
h.onTextDelta(textMessageBuffer);
|
|
120
|
+
},
|
|
121
|
+
onTextMessageEndEvent({ textMessageBuffer }) {
|
|
122
|
+
h.onTextEnd(textMessageBuffer);
|
|
123
|
+
},
|
|
124
|
+
onToolCallEndEvent({ event, toolCallName, toolCallArgs }) {
|
|
125
|
+
const call = {
|
|
126
|
+
id: event.toolCallId,
|
|
127
|
+
name: toolCallName,
|
|
128
|
+
args: toolCallArgs
|
|
129
|
+
};
|
|
130
|
+
pending.push(call);
|
|
131
|
+
h.onToolCall(call);
|
|
132
|
+
},
|
|
133
|
+
onRunErrorEvent({ event }) {
|
|
134
|
+
h.onError(event.message);
|
|
135
|
+
},
|
|
136
|
+
onRunFinalized() {
|
|
137
|
+
h.onRunEnd();
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// src/client_tool_registry.ts
|
|
144
|
+
var ClientToolRegistry = class {
|
|
145
|
+
#tools = /* @__PURE__ */ new Map();
|
|
146
|
+
/** Register a tool. Throws if the name is already taken. */
|
|
147
|
+
register(tool) {
|
|
148
|
+
if (this.#tools.has(tool.name)) {
|
|
149
|
+
throw new Error(`tool "${tool.name}" already registered`);
|
|
150
|
+
}
|
|
151
|
+
this.#tools.set(tool.name, tool);
|
|
152
|
+
}
|
|
153
|
+
has(name) {
|
|
154
|
+
return this.#tools.has(name);
|
|
155
|
+
}
|
|
156
|
+
/** Return a registered tool or throw. */
|
|
157
|
+
get(name) {
|
|
158
|
+
const tool = this.#tools.get(name);
|
|
159
|
+
if (tool === void 0) {
|
|
160
|
+
throw new Error(`tool "${name}" is not registered`);
|
|
161
|
+
}
|
|
162
|
+
return tool;
|
|
163
|
+
}
|
|
164
|
+
/** AG-UI tool definitions for `RunAgentInput.tools`. */
|
|
165
|
+
tools() {
|
|
166
|
+
return [...this.#tools.values()].map((tool) => ({
|
|
167
|
+
name: tool.name,
|
|
168
|
+
description: tool.description,
|
|
169
|
+
parameters: tool.parameters
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// src/confirmation_modal.ts
|
|
175
|
+
function requestConfirmation(host, request) {
|
|
176
|
+
return new Promise((resolve) => {
|
|
177
|
+
const overlay = document.createElement("div");
|
|
178
|
+
overlay.className = "modal-overlay";
|
|
179
|
+
const dialog = document.createElement("div");
|
|
180
|
+
dialog.className = "modal";
|
|
181
|
+
const title = document.createElement("div");
|
|
182
|
+
title.className = "modal-title";
|
|
183
|
+
title.textContent = "Confirm action";
|
|
184
|
+
const body = document.createElement("div");
|
|
185
|
+
body.className = "modal-body";
|
|
186
|
+
body.textContent = `Run \u201C${request.toolName}\u201D?`;
|
|
187
|
+
const args = document.createElement("pre");
|
|
188
|
+
args.className = "modal-args";
|
|
189
|
+
args.textContent = JSON.stringify(request.args, null, 2);
|
|
190
|
+
const actions = document.createElement("div");
|
|
191
|
+
actions.className = "modal-actions";
|
|
192
|
+
const cancel = document.createElement("button");
|
|
193
|
+
cancel.className = "modal-btn modal-btn--cancel";
|
|
194
|
+
cancel.type = "button";
|
|
195
|
+
cancel.textContent = "Cancel";
|
|
196
|
+
const confirm = document.createElement("button");
|
|
197
|
+
confirm.className = "modal-btn modal-btn--confirm";
|
|
198
|
+
confirm.type = "button";
|
|
199
|
+
confirm.textContent = "Run";
|
|
200
|
+
const close = (accepted) => {
|
|
201
|
+
overlay.remove();
|
|
202
|
+
resolve(accepted);
|
|
203
|
+
};
|
|
204
|
+
cancel.addEventListener("click", () => close(false));
|
|
205
|
+
confirm.addEventListener("click", () => close(true));
|
|
206
|
+
overlay.addEventListener("click", (event) => {
|
|
207
|
+
if (event.target === overlay) {
|
|
208
|
+
close(false);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
actions.append(cancel, confirm);
|
|
212
|
+
dialog.append(title, body, args, actions);
|
|
213
|
+
overlay.append(dialog);
|
|
214
|
+
host.appendChild(overlay);
|
|
215
|
+
confirm.focus();
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/conversation_store.ts
|
|
220
|
+
import { randomUUID as randomUUID2 } from "@ag-ui/client";
|
|
221
|
+
var THREAD_KEY = "ag-ui-chat:thread";
|
|
222
|
+
var MESSAGES_PREFIX = "ag-ui-chat:messages:";
|
|
223
|
+
var CHECKPOINT_PREFIX = "ag-ui-chat:checkpoint:";
|
|
224
|
+
var SessionStorageStore = class {
|
|
225
|
+
threadId() {
|
|
226
|
+
const existing = sessionStorage.getItem(THREAD_KEY);
|
|
227
|
+
if (existing !== null) {
|
|
228
|
+
return existing;
|
|
229
|
+
}
|
|
230
|
+
const id = randomUUID2();
|
|
231
|
+
sessionStorage.setItem(THREAD_KEY, id);
|
|
232
|
+
return id;
|
|
233
|
+
}
|
|
234
|
+
loadMessages(threadId) {
|
|
235
|
+
return Promise.resolve(this.#readJson(MESSAGES_PREFIX + threadId));
|
|
236
|
+
}
|
|
237
|
+
saveMessages(threadId, messages) {
|
|
238
|
+
sessionStorage.setItem(MESSAGES_PREFIX + threadId, JSON.stringify(messages));
|
|
239
|
+
}
|
|
240
|
+
loadCheckpoint(threadId) {
|
|
241
|
+
return this.#readJson(CHECKPOINT_PREFIX + threadId);
|
|
242
|
+
}
|
|
243
|
+
saveCheckpoint(threadId, checkpoint) {
|
|
244
|
+
const key = CHECKPOINT_PREFIX + threadId;
|
|
245
|
+
if (checkpoint === null) {
|
|
246
|
+
sessionStorage.removeItem(key);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
sessionStorage.setItem(key, JSON.stringify(checkpoint));
|
|
250
|
+
}
|
|
251
|
+
clear(threadId) {
|
|
252
|
+
sessionStorage.removeItem(MESSAGES_PREFIX + threadId);
|
|
253
|
+
sessionStorage.removeItem(CHECKPOINT_PREFIX + threadId);
|
|
254
|
+
sessionStorage.removeItem(THREAD_KEY);
|
|
255
|
+
}
|
|
256
|
+
/** Parse a stored JSON value, returning `null` when absent or corrupt. */
|
|
257
|
+
#readJson(key) {
|
|
258
|
+
const raw = sessionStorage.getItem(key);
|
|
259
|
+
if (raw === null) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
return JSON.parse(raw);
|
|
264
|
+
} catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// src/create_http_agent.ts
|
|
271
|
+
import { HttpAgent } from "@ag-ui/client";
|
|
272
|
+
function createHttpAgent(options) {
|
|
273
|
+
return new HttpAgent({
|
|
274
|
+
url: options.endpoint,
|
|
275
|
+
headers: options.headers ?? {},
|
|
276
|
+
// HttpAgent invokes its configured fetch as a method (`this.fetch(...)`),
|
|
277
|
+
// which would rebind the global `fetch` to the agent instance and trigger
|
|
278
|
+
// "Illegal invocation" in browsers. Wrap it so `fetch` is always called as
|
|
279
|
+
// a free function with the correct receiver.
|
|
280
|
+
fetch: (url, init) => fetch(url, init),
|
|
281
|
+
// Spread conditionally: under `exactOptionalPropertyTypes` an explicit
|
|
282
|
+
// `undefined` is not assignable to these optional config fields.
|
|
283
|
+
...options.threadId !== void 0 ? { threadId: options.threadId } : {},
|
|
284
|
+
...options.initialMessages !== void 0 ? { initialMessages: [...options.initialMessages] } : {}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/is_destructive.ts
|
|
289
|
+
function isDestructive(parameters) {
|
|
290
|
+
return parameters[X_DESTRUCTIVE_KEY] === true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/is_navigates.ts
|
|
294
|
+
function isNavigates(parameters) {
|
|
295
|
+
return parameters[X_NAVIGATES_KEY] === true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/page_map.ts
|
|
299
|
+
function createPageMapContext(getPageMap, autoInject) {
|
|
300
|
+
if (!autoInject || getPageMap === null) {
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
return [{ description: "page_map", value: JSON.stringify(getPageMap()) }];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/route_map.ts
|
|
307
|
+
function withQuery(path, params) {
|
|
308
|
+
if (params === void 0) {
|
|
309
|
+
return path;
|
|
310
|
+
}
|
|
311
|
+
const usp = new URLSearchParams();
|
|
312
|
+
for (const [key, value] of Object.entries(params)) {
|
|
313
|
+
usp.set(key, String(value));
|
|
314
|
+
}
|
|
315
|
+
const query = usp.toString();
|
|
316
|
+
return query === "" ? path : `${path}?${query}`;
|
|
317
|
+
}
|
|
318
|
+
function createRouteTools(getRouteMap, getNavigate) {
|
|
319
|
+
return [
|
|
320
|
+
{
|
|
321
|
+
name: "list_routes",
|
|
322
|
+
description: "List the routes the app can navigate to.",
|
|
323
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
324
|
+
handler: () => getRouteMap()
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: "navigate_to_route",
|
|
328
|
+
description: "Navigate to one of the app's routes by its id.",
|
|
329
|
+
parameters: {
|
|
330
|
+
type: "object",
|
|
331
|
+
properties: {
|
|
332
|
+
route_id: { type: "string" },
|
|
333
|
+
params: { type: "object" }
|
|
334
|
+
},
|
|
335
|
+
required: ["route_id"],
|
|
336
|
+
[X_NAVIGATES_KEY]: true
|
|
337
|
+
},
|
|
338
|
+
handler: (args) => {
|
|
339
|
+
const routeId = args["route_id"];
|
|
340
|
+
const route = getRouteMap().find((r) => r.id === routeId);
|
|
341
|
+
if (route === void 0) {
|
|
342
|
+
throw new Error(`unknown route "${String(routeId)}"`);
|
|
343
|
+
}
|
|
344
|
+
const path = withQuery(route.path, args["params"]);
|
|
345
|
+
const navigate = getNavigate();
|
|
346
|
+
if (navigate !== null) {
|
|
347
|
+
navigate(path);
|
|
348
|
+
} else {
|
|
349
|
+
window.location.assign(path);
|
|
350
|
+
}
|
|
351
|
+
return { navigated: true, path };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/state_hook.ts
|
|
358
|
+
function createStateHookTools(hook) {
|
|
359
|
+
const tools = [
|
|
360
|
+
{
|
|
361
|
+
name: `read_${hook.name}`,
|
|
362
|
+
description: `Read the "${hook.name}" state.`,
|
|
363
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
364
|
+
handler: () => hook.read()
|
|
365
|
+
}
|
|
366
|
+
];
|
|
367
|
+
const write = hook.write;
|
|
368
|
+
if (write !== void 0) {
|
|
369
|
+
tools.push({
|
|
370
|
+
name: `set_${hook.name}`,
|
|
371
|
+
description: `Update the "${hook.name}" state.`,
|
|
372
|
+
parameters: { ...hook.schema ?? { type: "object" }, [X_DESTRUCTIVE_KEY]: true },
|
|
373
|
+
handler: (args) => write(args)
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
return tools;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/styles.ts
|
|
380
|
+
var STYLES = `
|
|
381
|
+
:host {
|
|
382
|
+
/* Colors */
|
|
383
|
+
--ag-ui-bg: #ffffff;
|
|
384
|
+
--ag-ui-fg: #1a1a2e;
|
|
385
|
+
--ag-ui-accent: #4f46e5;
|
|
386
|
+
--ag-ui-user-bg: #4f46e5;
|
|
387
|
+
--ag-ui-user-fg: #ffffff;
|
|
388
|
+
--ag-ui-assistant-bg: #f1f1f6;
|
|
389
|
+
--ag-ui-input-bg: var(--ag-ui-bg);
|
|
390
|
+
--ag-ui-tool-bg: var(--ag-ui-assistant-bg);
|
|
391
|
+
--ag-ui-tool-fg: var(--ag-ui-accent);
|
|
392
|
+
--ag-ui-header-bg: var(--ag-ui-accent);
|
|
393
|
+
--ag-ui-header-fg: #ffffff;
|
|
394
|
+
--ag-ui-border: #e2e2ec;
|
|
395
|
+
--ag-ui-radius: 12px;
|
|
396
|
+
|
|
397
|
+
/* Status accents for tool-call cards. */
|
|
398
|
+
--ag-ui-success: #15803d;
|
|
399
|
+
--ag-ui-danger: #b91c1c;
|
|
400
|
+
--ag-ui-muted: #6b7280;
|
|
401
|
+
|
|
402
|
+
/* Surface \u2014 set --ag-ui-shadow: none for a flush, embedded panel. */
|
|
403
|
+
--ag-ui-shadow: 0 12px 32px rgba(20, 20, 50, 0.18);
|
|
404
|
+
--ag-ui-font: inherit;
|
|
405
|
+
--ag-ui-font-size: 14px;
|
|
406
|
+
|
|
407
|
+
/* Layout \u2014 override from outside to dock the widget anywhere.
|
|
408
|
+
Set --ag-ui-position: static (and place this element in your own
|
|
409
|
+
grid/flex layout) to embed it in the page flow instead of floating. */
|
|
410
|
+
--ag-ui-position: fixed;
|
|
411
|
+
--ag-ui-z-index: 2147483000;
|
|
412
|
+
--ag-ui-width: 380px;
|
|
413
|
+
--ag-ui-height: 560px;
|
|
414
|
+
--ag-ui-inset: auto 24px 24px auto;
|
|
415
|
+
--ag-ui-max-width: calc(100vw - 48px);
|
|
416
|
+
--ag-ui-max-height: calc(100vh - 48px);
|
|
417
|
+
|
|
418
|
+
position: var(--ag-ui-position);
|
|
419
|
+
inset: var(--ag-ui-inset);
|
|
420
|
+
z-index: var(--ag-ui-z-index);
|
|
421
|
+
width: var(--ag-ui-width);
|
|
422
|
+
max-width: var(--ag-ui-max-width);
|
|
423
|
+
height: var(--ag-ui-height);
|
|
424
|
+
max-height: var(--ag-ui-max-height);
|
|
425
|
+
display: flex;
|
|
426
|
+
font-family: var(--ag-ui-font);
|
|
427
|
+
font-size: var(--ag-ui-font-size);
|
|
428
|
+
color: var(--ag-ui-fg);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.chat {
|
|
432
|
+
position: relative;
|
|
433
|
+
display: flex;
|
|
434
|
+
flex-direction: column;
|
|
435
|
+
flex: 1;
|
|
436
|
+
min-height: 0;
|
|
437
|
+
background: var(--ag-ui-bg);
|
|
438
|
+
border: 1px solid var(--ag-ui-border);
|
|
439
|
+
border-radius: var(--ag-ui-radius);
|
|
440
|
+
box-shadow: var(--ag-ui-shadow);
|
|
441
|
+
overflow: hidden;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.header {
|
|
445
|
+
padding: 12px 16px;
|
|
446
|
+
font-weight: 600;
|
|
447
|
+
border-bottom: 1px solid var(--ag-ui-border);
|
|
448
|
+
background: var(--ag-ui-header-bg);
|
|
449
|
+
color: var(--ag-ui-header-fg);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.messages {
|
|
453
|
+
flex: 1;
|
|
454
|
+
overflow-y: auto;
|
|
455
|
+
padding: 16px;
|
|
456
|
+
display: flex;
|
|
457
|
+
flex-direction: column;
|
|
458
|
+
gap: 10px;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.message {
|
|
462
|
+
max-width: 80%;
|
|
463
|
+
padding: 8px 12px;
|
|
464
|
+
border-radius: 14px;
|
|
465
|
+
line-height: 1.4;
|
|
466
|
+
white-space: pre-wrap;
|
|
467
|
+
word-break: break-word;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.message--user {
|
|
471
|
+
align-self: flex-end;
|
|
472
|
+
background: var(--ag-ui-user-bg);
|
|
473
|
+
color: var(--ag-ui-user-fg);
|
|
474
|
+
border-bottom-right-radius: 4px;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.message--assistant {
|
|
478
|
+
align-self: flex-start;
|
|
479
|
+
background: var(--ag-ui-assistant-bg);
|
|
480
|
+
border-bottom-left-radius: 4px;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.tool-call {
|
|
484
|
+
align-self: flex-start;
|
|
485
|
+
max-width: 80%;
|
|
486
|
+
box-sizing: border-box;
|
|
487
|
+
display: flex;
|
|
488
|
+
flex-direction: column;
|
|
489
|
+
gap: 6px;
|
|
490
|
+
font-size: 12px;
|
|
491
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
492
|
+
padding: 8px 10px;
|
|
493
|
+
border-radius: 8px;
|
|
494
|
+
background: var(--ag-ui-tool-bg);
|
|
495
|
+
border: 1px solid var(--ag-ui-border);
|
|
496
|
+
color: var(--ag-ui-tool-fg);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.tool-call-head {
|
|
500
|
+
display: flex;
|
|
501
|
+
align-items: center;
|
|
502
|
+
justify-content: space-between;
|
|
503
|
+
gap: 8px;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.tool-call-name {
|
|
507
|
+
font-weight: 600;
|
|
508
|
+
word-break: break-word;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.tool-call-status {
|
|
512
|
+
flex: none;
|
|
513
|
+
padding: 1px 8px;
|
|
514
|
+
border-radius: 999px;
|
|
515
|
+
font-size: 11px;
|
|
516
|
+
font-weight: 600;
|
|
517
|
+
background: rgba(127, 127, 127, 0.16);
|
|
518
|
+
color: var(--ag-ui-muted);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.tool-call[data-status="done"] .tool-call-status {
|
|
522
|
+
color: var(--ag-ui-success);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.tool-call[data-status="error"] .tool-call-status {
|
|
526
|
+
color: var(--ag-ui-danger);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.tool-call[data-status="declined"] .tool-call-status {
|
|
530
|
+
color: var(--ag-ui-muted);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
.tool-call-args,
|
|
534
|
+
.tool-call-result {
|
|
535
|
+
margin: 0;
|
|
536
|
+
padding: 6px 8px;
|
|
537
|
+
max-height: 160px;
|
|
538
|
+
overflow: auto;
|
|
539
|
+
background: var(--ag-ui-bg);
|
|
540
|
+
border: 1px solid var(--ag-ui-border);
|
|
541
|
+
border-radius: 6px;
|
|
542
|
+
white-space: pre-wrap;
|
|
543
|
+
word-break: break-word;
|
|
544
|
+
color: var(--ag-ui-fg);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.tool-call-toggle {
|
|
548
|
+
align-self: flex-start;
|
|
549
|
+
border: none;
|
|
550
|
+
padding: 0;
|
|
551
|
+
background: none;
|
|
552
|
+
font: inherit;
|
|
553
|
+
font-weight: 600;
|
|
554
|
+
color: var(--ag-ui-accent);
|
|
555
|
+
cursor: pointer;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
.tool-call-toggle::before {
|
|
559
|
+
content: "\u25B8 ";
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.tool-call-toggle[aria-expanded="true"]::before {
|
|
563
|
+
content: "\u25BE ";
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.input-row {
|
|
567
|
+
display: flex;
|
|
568
|
+
gap: 8px;
|
|
569
|
+
padding: 12px;
|
|
570
|
+
border-top: 1px solid var(--ag-ui-border);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.input {
|
|
574
|
+
flex: 1;
|
|
575
|
+
resize: none;
|
|
576
|
+
background: var(--ag-ui-input-bg);
|
|
577
|
+
border: 1px solid var(--ag-ui-border);
|
|
578
|
+
border-radius: 8px;
|
|
579
|
+
padding: 8px 10px;
|
|
580
|
+
font: inherit;
|
|
581
|
+
color: inherit;
|
|
582
|
+
outline: none;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.input:focus {
|
|
586
|
+
border-color: var(--ag-ui-accent);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.send {
|
|
590
|
+
border: none;
|
|
591
|
+
border-radius: 8px;
|
|
592
|
+
padding: 0 16px;
|
|
593
|
+
background: var(--ag-ui-accent);
|
|
594
|
+
color: #ffffff;
|
|
595
|
+
font: inherit;
|
|
596
|
+
font-weight: 600;
|
|
597
|
+
cursor: pointer;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
.send:disabled {
|
|
601
|
+
opacity: 0.5;
|
|
602
|
+
cursor: default;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.modal-overlay {
|
|
606
|
+
position: absolute;
|
|
607
|
+
inset: 0;
|
|
608
|
+
display: flex;
|
|
609
|
+
align-items: center;
|
|
610
|
+
justify-content: center;
|
|
611
|
+
padding: 16px;
|
|
612
|
+
background: rgba(20, 20, 50, 0.4);
|
|
613
|
+
backdrop-filter: blur(2px);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.modal {
|
|
617
|
+
width: 100%;
|
|
618
|
+
max-width: 320px;
|
|
619
|
+
background: var(--ag-ui-bg);
|
|
620
|
+
border-radius: 12px;
|
|
621
|
+
box-shadow: 0 16px 40px rgba(20, 20, 50, 0.3);
|
|
622
|
+
overflow: hidden;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
.modal-title {
|
|
626
|
+
padding: 12px 16px;
|
|
627
|
+
font-weight: 600;
|
|
628
|
+
border-bottom: 1px solid var(--ag-ui-border);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.modal-body {
|
|
632
|
+
padding: 12px 16px 4px;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.modal-args {
|
|
636
|
+
margin: 0 16px 12px;
|
|
637
|
+
padding: 8px 10px;
|
|
638
|
+
max-height: 140px;
|
|
639
|
+
overflow: auto;
|
|
640
|
+
font-size: 12px;
|
|
641
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
642
|
+
background: var(--ag-ui-assistant-bg);
|
|
643
|
+
border-radius: 8px;
|
|
644
|
+
white-space: pre-wrap;
|
|
645
|
+
word-break: break-word;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.modal-actions {
|
|
649
|
+
display: flex;
|
|
650
|
+
gap: 8px;
|
|
651
|
+
padding: 0 16px 16px;
|
|
652
|
+
justify-content: flex-end;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
.modal-btn {
|
|
656
|
+
border: 1px solid var(--ag-ui-border);
|
|
657
|
+
border-radius: 8px;
|
|
658
|
+
padding: 8px 14px;
|
|
659
|
+
font: inherit;
|
|
660
|
+
font-weight: 600;
|
|
661
|
+
cursor: pointer;
|
|
662
|
+
background: var(--ag-ui-bg);
|
|
663
|
+
color: var(--ag-ui-fg);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.modal-btn--confirm {
|
|
667
|
+
border-color: var(--ag-ui-accent);
|
|
668
|
+
background: var(--ag-ui-accent);
|
|
669
|
+
color: #ffffff;
|
|
670
|
+
}
|
|
671
|
+
`;
|
|
672
|
+
|
|
673
|
+
// src/tool_call_card.ts
|
|
674
|
+
var STATUS_LABEL = {
|
|
675
|
+
[TOOL_CALL_STATUS.PENDING]: "running\u2026",
|
|
676
|
+
[TOOL_CALL_STATUS.DONE]: "\u2713 done",
|
|
677
|
+
[TOOL_CALL_STATUS.ERROR]: "\u26A0 error",
|
|
678
|
+
[TOOL_CALL_STATUS.DECLINED]: "\u2298 declined"
|
|
679
|
+
};
|
|
680
|
+
var RESULT_LABEL = {
|
|
681
|
+
[TOOL_CALL_STATUS.DONE]: "Result",
|
|
682
|
+
[TOOL_CALL_STATUS.ERROR]: "Error",
|
|
683
|
+
[TOOL_CALL_STATUS.DECLINED]: "Declined"
|
|
684
|
+
};
|
|
685
|
+
var ToolCallCard = class {
|
|
686
|
+
/** The card's root element; append this into the message list. */
|
|
687
|
+
element;
|
|
688
|
+
#status;
|
|
689
|
+
constructor(name, args) {
|
|
690
|
+
this.element = document.createElement("div");
|
|
691
|
+
this.element.className = "tool-call";
|
|
692
|
+
this.element.setAttribute("data-tool-name", name);
|
|
693
|
+
this.element.setAttribute("data-status", TOOL_CALL_STATUS.PENDING);
|
|
694
|
+
const head = document.createElement("div");
|
|
695
|
+
head.className = "tool-call-head";
|
|
696
|
+
const label = document.createElement("span");
|
|
697
|
+
label.className = "tool-call-name";
|
|
698
|
+
label.textContent = `\u{1F527} ${name}`;
|
|
699
|
+
this.#status = document.createElement("span");
|
|
700
|
+
this.#status.className = "tool-call-status";
|
|
701
|
+
this.#status.textContent = STATUS_LABEL[TOOL_CALL_STATUS.PENDING];
|
|
702
|
+
head.append(label, this.#status);
|
|
703
|
+
const argsEl = document.createElement("pre");
|
|
704
|
+
argsEl.className = "tool-call-args";
|
|
705
|
+
argsEl.textContent = JSON.stringify(args, null, 2);
|
|
706
|
+
this.element.append(head, argsEl);
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Flip the status pill to ``status`` and append a collapsed body holding
|
|
710
|
+
* ``text`` (the JSON result, an error message, a decline notice, or a
|
|
711
|
+
* server-executed note) behind a click-to-expand toggle.
|
|
712
|
+
*/
|
|
713
|
+
settle(status, text) {
|
|
714
|
+
this.element.setAttribute("data-status", status);
|
|
715
|
+
this.#status.textContent = STATUS_LABEL[status];
|
|
716
|
+
const toggle = document.createElement("button");
|
|
717
|
+
toggle.type = "button";
|
|
718
|
+
toggle.className = "tool-call-toggle";
|
|
719
|
+
toggle.setAttribute("aria-expanded", "false");
|
|
720
|
+
toggle.textContent = RESULT_LABEL[status];
|
|
721
|
+
const output = document.createElement("pre");
|
|
722
|
+
output.className = "tool-call-result";
|
|
723
|
+
output.textContent = text;
|
|
724
|
+
output.hidden = true;
|
|
725
|
+
toggle.addEventListener("click", () => {
|
|
726
|
+
const expand = output.hidden;
|
|
727
|
+
output.hidden = !expand;
|
|
728
|
+
toggle.setAttribute("aria-expanded", String(expand));
|
|
729
|
+
});
|
|
730
|
+
this.element.append(toggle, output);
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
// src/ag_ui_chat.ts
|
|
735
|
+
var AgUiChat = class extends HTMLElement {
|
|
736
|
+
/** Agent factory; override to inject a custom or fake agent (tests). */
|
|
737
|
+
agentFactory = createHttpAgent;
|
|
738
|
+
/** Extra HTTP headers for the AG-UI endpoint (e.g. CSRF). */
|
|
739
|
+
headers = {};
|
|
740
|
+
/** When true, destructive tools execute without a confirmation modal. */
|
|
741
|
+
autoConfirm = false;
|
|
742
|
+
/**
|
|
743
|
+
* Per-run frontend tool catalog provider. Defaults to the built-in
|
|
744
|
+
* `route.*` tools (when a {@link routeMap} is set) plus the tools registered
|
|
745
|
+
* via {@link registerTool} / {@link registerStateHook}; override to supply a
|
|
746
|
+
* fully custom catalog.
|
|
747
|
+
*/
|
|
748
|
+
getTools = () => [
|
|
749
|
+
...this.#routeTools().map((t) => ({
|
|
750
|
+
name: t.name,
|
|
751
|
+
description: t.description,
|
|
752
|
+
parameters: t.parameters
|
|
753
|
+
})),
|
|
754
|
+
...this.#toolRegistry.tools()
|
|
755
|
+
];
|
|
756
|
+
/**
|
|
757
|
+
* Per-run context provider. Defaults to the compact page map (when a
|
|
758
|
+
* {@link getPageMap} provider is set and {@link autoInjectPageMap} is on).
|
|
759
|
+
*/
|
|
760
|
+
getContext = () => createPageMapContext(this.getPageMap, this.autoInjectPageMap);
|
|
761
|
+
/**
|
|
762
|
+
* Navigable routes the agent can jump to via the built-in `route.*` tools.
|
|
763
|
+
* A compact summary also rides in each run's context.
|
|
764
|
+
*/
|
|
765
|
+
routeMap = [];
|
|
766
|
+
/**
|
|
767
|
+
* Optional client-side router. When set (an SPA), `navigate_to_route` routes
|
|
768
|
+
* in-page and the run loop continues; when unset (an MPA like the admin), it
|
|
769
|
+
* falls back to `window.location` and the resumable-loop machinery applies.
|
|
770
|
+
*/
|
|
771
|
+
navigate = null;
|
|
772
|
+
/** Provider for the per-run page map; see {@link getContext}. */
|
|
773
|
+
getPageMap = null;
|
|
774
|
+
/** Whether to auto-inject the page map into context each run. */
|
|
775
|
+
autoInjectPageMap = true;
|
|
776
|
+
/**
|
|
777
|
+
* Persistence for the conversation + navigation checkpoint. Defaults to
|
|
778
|
+
* per-tab `sessionStorage` so the chat survives full page reloads; inject a
|
|
779
|
+
* server-backed store for cross-tab/device durability.
|
|
780
|
+
*/
|
|
781
|
+
conversationStore = new SessionStorageStore();
|
|
782
|
+
/**
|
|
783
|
+
* Builds the tool result a navigating tool resumes with after the page
|
|
784
|
+
* reloads. Defaults to the landed URL; a host (e.g. the admin package) can
|
|
785
|
+
* override to include a page snapshot or post-reload validation errors.
|
|
786
|
+
*/
|
|
787
|
+
navigationResult = () => ({
|
|
788
|
+
navigated: true,
|
|
789
|
+
url: window.location.href
|
|
790
|
+
});
|
|
791
|
+
#toolRegistry = new ClientToolRegistry();
|
|
792
|
+
/** Tool-call cards awaiting execution, keyed by call id. */
|
|
793
|
+
#toolCards = /* @__PURE__ */ new Map();
|
|
794
|
+
#root;
|
|
795
|
+
#chat;
|
|
796
|
+
#messages;
|
|
797
|
+
#input;
|
|
798
|
+
#send;
|
|
799
|
+
#client = null;
|
|
800
|
+
#streamingBubble = null;
|
|
801
|
+
#threadId = "";
|
|
802
|
+
#initialMessages = [];
|
|
803
|
+
constructor() {
|
|
804
|
+
super();
|
|
805
|
+
this.#root = this.attachShadow({ mode: "open" });
|
|
806
|
+
this.#chat = document.createElement("div");
|
|
807
|
+
this.#messages = document.createElement("div");
|
|
808
|
+
this.#input = document.createElement("textarea");
|
|
809
|
+
this.#send = document.createElement("button");
|
|
810
|
+
}
|
|
811
|
+
/** Declare a frontend tool the agent may call. */
|
|
812
|
+
registerTool(tool) {
|
|
813
|
+
this.#toolRegistry.register(tool);
|
|
814
|
+
}
|
|
815
|
+
/** Bind a piece of host state to `read_<name>` / `set_<name>` tools. */
|
|
816
|
+
registerStateHook(hook) {
|
|
817
|
+
for (const tool of createStateHookTools(hook)) {
|
|
818
|
+
this.#toolRegistry.register(tool);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
/** The built-in `route.*` tools, present only when a route map is set. */
|
|
822
|
+
#routeTools() {
|
|
823
|
+
if (this.routeMap.length === 0) {
|
|
824
|
+
return [];
|
|
825
|
+
}
|
|
826
|
+
return createRouteTools(
|
|
827
|
+
() => this.routeMap,
|
|
828
|
+
() => this.navigate
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
/** Resolve a tool by name: built-in route tools first, then the registry. */
|
|
832
|
+
#resolveTool(name) {
|
|
833
|
+
const route = this.#routeTools().find((t) => t.name === name);
|
|
834
|
+
if (route !== void 0) {
|
|
835
|
+
return route;
|
|
836
|
+
}
|
|
837
|
+
return this.#toolRegistry.has(name) ? this.#toolRegistry.get(name) : null;
|
|
838
|
+
}
|
|
839
|
+
/** The AG-UI endpoint URL, read from the `endpoint` attribute. */
|
|
840
|
+
get endpoint() {
|
|
841
|
+
return this.getAttribute("endpoint") ?? "";
|
|
842
|
+
}
|
|
843
|
+
connectedCallback() {
|
|
844
|
+
this.#render();
|
|
845
|
+
this.#threadId = this.conversationStore.threadId();
|
|
846
|
+
void this.#rehydrate();
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Restore the conversation from the store on mount, then — if a navigating
|
|
850
|
+
* tool reloaded the page mid-run — resume the loop by supplying that tool's
|
|
851
|
+
* result from the page we landed on.
|
|
852
|
+
*/
|
|
853
|
+
async #rehydrate() {
|
|
854
|
+
const messages = await this.conversationStore.loadMessages(this.#threadId);
|
|
855
|
+
if (messages !== null) {
|
|
856
|
+
this.#initialMessages = messages;
|
|
857
|
+
for (const message of messages) {
|
|
858
|
+
this.#renderHistoricMessage(message);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
const checkpoint = this.conversationStore.loadCheckpoint(this.#threadId);
|
|
862
|
+
if (checkpoint !== null) {
|
|
863
|
+
await this.#resumeFrom(checkpoint);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
/** Render a restored message as a chat bubble (text turns only). */
|
|
867
|
+
#renderHistoricMessage(message) {
|
|
868
|
+
if (typeof message.content !== "string" || message.content === "") {
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if (message.role === MESSAGE_ROLE.USER) {
|
|
872
|
+
this.appendMessage(MESSAGE_ROLE.USER, message.content);
|
|
873
|
+
} else if (message.role === MESSAGE_ROLE.ASSISTANT) {
|
|
874
|
+
this.appendMessage(MESSAGE_ROLE.ASSISTANT, message.content);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
/** Complete the checkpointed navigating tool call and continue the run. */
|
|
878
|
+
async #resumeFrom(checkpoint) {
|
|
879
|
+
this.conversationStore.saveCheckpoint(this.#threadId, null);
|
|
880
|
+
const client = this.#ensureClient();
|
|
881
|
+
client.addToolResult(checkpoint.toolCallId, JSON.stringify(this.navigationResult(checkpoint)));
|
|
882
|
+
await client.resume();
|
|
883
|
+
}
|
|
884
|
+
/** Append a message bubble and return it. */
|
|
885
|
+
appendMessage(role, content) {
|
|
886
|
+
const bubble = document.createElement("div");
|
|
887
|
+
bubble.className = `message message--${role}`;
|
|
888
|
+
bubble.textContent = content;
|
|
889
|
+
this.#messages.appendChild(bubble);
|
|
890
|
+
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
891
|
+
return bubble;
|
|
892
|
+
}
|
|
893
|
+
#render() {
|
|
894
|
+
const style = document.createElement("style");
|
|
895
|
+
style.textContent = STYLES;
|
|
896
|
+
this.#chat.className = "chat";
|
|
897
|
+
const header = document.createElement("div");
|
|
898
|
+
header.className = "header";
|
|
899
|
+
header.textContent = this.getAttribute("title-text") ?? "Assistant";
|
|
900
|
+
this.#messages.className = "messages";
|
|
901
|
+
const inputRow = document.createElement("div");
|
|
902
|
+
inputRow.className = "input-row";
|
|
903
|
+
this.#input.className = "input";
|
|
904
|
+
this.#input.rows = 2;
|
|
905
|
+
this.#input.placeholder = "Ask anything\u2026";
|
|
906
|
+
this.#input.addEventListener("keydown", (event) => this.#onKeydown(event));
|
|
907
|
+
this.#send.className = "send";
|
|
908
|
+
this.#send.type = "button";
|
|
909
|
+
this.#send.textContent = "Send";
|
|
910
|
+
this.#send.addEventListener("click", () => {
|
|
911
|
+
void this.#submit();
|
|
912
|
+
});
|
|
913
|
+
inputRow.append(this.#input, this.#send);
|
|
914
|
+
this.#chat.append(header, this.#messages, inputRow);
|
|
915
|
+
this.#root.append(style, this.#chat);
|
|
916
|
+
}
|
|
917
|
+
#onKeydown(event) {
|
|
918
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
919
|
+
event.preventDefault();
|
|
920
|
+
void this.#submit();
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
async #submit() {
|
|
924
|
+
const content = this.#input.value.trim();
|
|
925
|
+
if (content === "") {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
this.appendMessage(MESSAGE_ROLE.USER, content);
|
|
929
|
+
this.#input.value = "";
|
|
930
|
+
this.dispatchEvent(
|
|
931
|
+
new CustomEvent(SUBMIT_EVENT, {
|
|
932
|
+
detail: { content },
|
|
933
|
+
bubbles: true,
|
|
934
|
+
composed: true
|
|
935
|
+
})
|
|
936
|
+
);
|
|
937
|
+
await this.#client_send(content);
|
|
938
|
+
}
|
|
939
|
+
async #client_send(content) {
|
|
940
|
+
if (this.endpoint === "") {
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
await this.#ensureClient().send(content);
|
|
944
|
+
}
|
|
945
|
+
#ensureClient() {
|
|
946
|
+
if (this.#client === null) {
|
|
947
|
+
const agent = this.agentFactory({
|
|
948
|
+
endpoint: this.endpoint,
|
|
949
|
+
headers: this.headers,
|
|
950
|
+
threadId: this.#threadId,
|
|
951
|
+
initialMessages: this.#initialMessages
|
|
952
|
+
});
|
|
953
|
+
this.#client = new AgUiClient({
|
|
954
|
+
agent,
|
|
955
|
+
handlers: this.#handlers(),
|
|
956
|
+
getTools: () => this.getTools(),
|
|
957
|
+
getContext: () => this.getContext(),
|
|
958
|
+
executeTool: (call) => this.#executeTool(call),
|
|
959
|
+
onPersist: (messages) => this.conversationStore.saveMessages(this.#threadId, messages)
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
return this.#client;
|
|
963
|
+
}
|
|
964
|
+
async #executeTool(call) {
|
|
965
|
+
const card = this.#cardFor(call);
|
|
966
|
+
this.#toolCards.delete(call.id);
|
|
967
|
+
const tool = this.#resolveTool(call.name);
|
|
968
|
+
if (tool === null) {
|
|
969
|
+
card.settle(TOOL_CALL_STATUS.DONE, "Executed on the server.");
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
972
|
+
if (isDestructive(tool.parameters) && !this.autoConfirm) {
|
|
973
|
+
const accepted = await requestConfirmation(this.#chat, {
|
|
974
|
+
toolName: call.name,
|
|
975
|
+
args: call.args
|
|
976
|
+
});
|
|
977
|
+
if (!accepted) {
|
|
978
|
+
const message = "User declined the action.";
|
|
979
|
+
card.settle(TOOL_CALL_STATUS.DECLINED, message);
|
|
980
|
+
return { content: message };
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
const navigates = isNavigates(tool.parameters) && this.navigate === null;
|
|
984
|
+
if (navigates) {
|
|
985
|
+
this.conversationStore.saveCheckpoint(this.#threadId, { toolCallId: call.id });
|
|
986
|
+
}
|
|
987
|
+
try {
|
|
988
|
+
const result = await tool.handler(call.args);
|
|
989
|
+
if (navigates) {
|
|
990
|
+
card.settle(TOOL_CALL_STATUS.DONE, "Navigating\u2026");
|
|
991
|
+
return { content: "", halt: true };
|
|
992
|
+
}
|
|
993
|
+
const content = JSON.stringify(result ?? null);
|
|
994
|
+
card.settle(TOOL_CALL_STATUS.DONE, content);
|
|
995
|
+
return { content };
|
|
996
|
+
} catch (error) {
|
|
997
|
+
if (navigates) {
|
|
998
|
+
this.conversationStore.saveCheckpoint(this.#threadId, null);
|
|
999
|
+
}
|
|
1000
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1001
|
+
card.settle(TOOL_CALL_STATUS.ERROR, message);
|
|
1002
|
+
return { content: `Error: ${message}`, error: message };
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
#handlers() {
|
|
1006
|
+
return {
|
|
1007
|
+
onRunStart: () => {
|
|
1008
|
+
this.#send.disabled = true;
|
|
1009
|
+
},
|
|
1010
|
+
onTextDelta: (buffer) => {
|
|
1011
|
+
this.#streamInto(buffer);
|
|
1012
|
+
},
|
|
1013
|
+
onTextEnd: (buffer) => {
|
|
1014
|
+
this.#streamInto(buffer);
|
|
1015
|
+
this.#streamingBubble = null;
|
|
1016
|
+
},
|
|
1017
|
+
onToolCall: (call) => {
|
|
1018
|
+
this.#cardFor(call);
|
|
1019
|
+
},
|
|
1020
|
+
onRunEnd: () => {
|
|
1021
|
+
this.#send.disabled = false;
|
|
1022
|
+
this.#streamingBubble = null;
|
|
1023
|
+
},
|
|
1024
|
+
onError: (message) => {
|
|
1025
|
+
this.appendMessage(MESSAGE_ROLE.ASSISTANT, `\u26A0\uFE0F ${message}`);
|
|
1026
|
+
this.#send.disabled = false;
|
|
1027
|
+
this.#streamingBubble = null;
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
#streamInto(buffer) {
|
|
1032
|
+
if (this.#streamingBubble === null) {
|
|
1033
|
+
this.#streamingBubble = this.appendMessage(MESSAGE_ROLE.ASSISTANT, "");
|
|
1034
|
+
}
|
|
1035
|
+
this.#streamingBubble.textContent = buffer;
|
|
1036
|
+
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* The card for ``call``, creating and appending it on first sight.
|
|
1040
|
+
*
|
|
1041
|
+
* {@link AgUiClientHandlers.onToolCall} creates the card (pending) during the
|
|
1042
|
+
* run; {@link #executeTool} later retrieves the same card to settle it.
|
|
1043
|
+
*/
|
|
1044
|
+
#cardFor(call) {
|
|
1045
|
+
const existing = this.#toolCards.get(call.id);
|
|
1046
|
+
if (existing !== void 0) {
|
|
1047
|
+
return existing;
|
|
1048
|
+
}
|
|
1049
|
+
const card = new ToolCallCard(call.name, call.args);
|
|
1050
|
+
this.#toolCards.set(call.id, card);
|
|
1051
|
+
this.#messages.appendChild(card.element);
|
|
1052
|
+
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
1053
|
+
return card;
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
// src/animations.ts
|
|
1058
|
+
var ACCENT = "#4f46e5";
|
|
1059
|
+
function delay(ms) {
|
|
1060
|
+
return new Promise((resolve) => {
|
|
1061
|
+
setTimeout(resolve, ms);
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
async function typeInto(el, value, options = {}) {
|
|
1065
|
+
const charDelayMs = options.charDelayMs ?? 35;
|
|
1066
|
+
el.value = "";
|
|
1067
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1068
|
+
for (const char of value) {
|
|
1069
|
+
el.value += char;
|
|
1070
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1071
|
+
if (charDelayMs > 0) {
|
|
1072
|
+
await delay(charDelayMs);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1076
|
+
}
|
|
1077
|
+
async function highlightThenClick(el, options = {}) {
|
|
1078
|
+
const highlightMs = options.highlightMs ?? 280;
|
|
1079
|
+
const previousOutline = el.style.outline;
|
|
1080
|
+
const previousOffset = el.style.outlineOffset;
|
|
1081
|
+
el.style.outline = `2px solid ${ACCENT}`;
|
|
1082
|
+
el.style.outlineOffset = "2px";
|
|
1083
|
+
await delay(highlightMs);
|
|
1084
|
+
el.style.outline = previousOutline;
|
|
1085
|
+
el.style.outlineOffset = previousOffset;
|
|
1086
|
+
el.click();
|
|
1087
|
+
}
|
|
1088
|
+
function scrollIntoCenterView(el) {
|
|
1089
|
+
el.scrollIntoView({ block: "center", inline: "nearest", behavior: "smooth" });
|
|
1090
|
+
}
|
|
1091
|
+
async function focusWithFlash(el, options = {}) {
|
|
1092
|
+
const flashMs = options.flashMs ?? 200;
|
|
1093
|
+
el.focus();
|
|
1094
|
+
const previousShadow = el.style.boxShadow;
|
|
1095
|
+
el.style.boxShadow = `0 0 0 3px rgba(79, 70, 229, 0.4)`;
|
|
1096
|
+
await delay(flashMs);
|
|
1097
|
+
el.style.boxShadow = previousShadow;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// src/define_ag_ui_chat.ts
|
|
1101
|
+
function defineAgUiChat() {
|
|
1102
|
+
if (customElements.get(ELEMENT_TAG) === void 0) {
|
|
1103
|
+
customElements.define(ELEMENT_TAG, AgUiChat);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// src/dom_driver.ts
|
|
1108
|
+
async function fillField(el, value, options = {}) {
|
|
1109
|
+
scrollIntoCenterView(el);
|
|
1110
|
+
await focusWithFlash(el, { flashMs: options.flashMs ?? 0 });
|
|
1111
|
+
await typeInto(el, value, options);
|
|
1112
|
+
}
|
|
1113
|
+
async function clickElement(el, options = {}) {
|
|
1114
|
+
scrollIntoCenterView(el);
|
|
1115
|
+
await highlightThenClick(el, options);
|
|
1116
|
+
}
|
|
1117
|
+
function setControlValue(el, value) {
|
|
1118
|
+
if (el instanceof HTMLInputElement && el.type === "checkbox") {
|
|
1119
|
+
el.checked = Boolean(value);
|
|
1120
|
+
} else {
|
|
1121
|
+
el.value = String(value);
|
|
1122
|
+
}
|
|
1123
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1124
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// src/version.ts
|
|
1128
|
+
var VERSION = "0.1.0";
|
|
1129
|
+
export {
|
|
1130
|
+
AgUiChat,
|
|
1131
|
+
AgUiClient,
|
|
1132
|
+
ClientToolRegistry,
|
|
1133
|
+
ELEMENT_TAG,
|
|
1134
|
+
MAX_TOOL_ROUNDS,
|
|
1135
|
+
MESSAGE_ROLE,
|
|
1136
|
+
SUBMIT_EVENT,
|
|
1137
|
+
SessionStorageStore,
|
|
1138
|
+
TOOL_CALL_STATUS,
|
|
1139
|
+
ToolCallCard,
|
|
1140
|
+
VERSION,
|
|
1141
|
+
X_DESTRUCTIVE_KEY,
|
|
1142
|
+
X_NAVIGATES_KEY,
|
|
1143
|
+
clickElement,
|
|
1144
|
+
createHttpAgent,
|
|
1145
|
+
createPageMapContext,
|
|
1146
|
+
createRouteTools,
|
|
1147
|
+
createStateHookTools,
|
|
1148
|
+
defineAgUiChat,
|
|
1149
|
+
fillField,
|
|
1150
|
+
focusWithFlash,
|
|
1151
|
+
highlightThenClick,
|
|
1152
|
+
isDestructive,
|
|
1153
|
+
isNavigates,
|
|
1154
|
+
requestConfirmation,
|
|
1155
|
+
scrollIntoCenterView,
|
|
1156
|
+
setControlValue,
|
|
1157
|
+
typeInto
|
|
1158
|
+
};
|
|
1159
|
+
//# sourceMappingURL=index.js.map
|