@artooi/ag-ui-web-component 0.6.0 → 0.8.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 +73 -1
- package/README.md +180 -9
- package/dist/ag-ui-web-component.bundle.js +268 -47
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +5 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +20 -0
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +16 -0
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +847 -237
- package/dist/index.js.map +3 -3
- package/dist/tools/page_action_tools.d.ts +31 -0
- package/dist/tools/page_action_tools.d.ts.map +1 -0
- package/dist/ui/attachment_tray.d.ts +3 -0
- package/dist/ui/attachment_tray.d.ts.map +1 -1
- package/dist/ui/confirmation_card.d.ts +4 -1
- package/dist/ui/confirmation_card.d.ts.map +1 -1
- package/dist/ui/relative_time.d.ts +5 -3
- package/dist/ui/relative_time.d.ts.map +1 -1
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/thread_drawer.d.ts +7 -1
- package/dist/ui/thread_drawer.d.ts.map +1 -1
- package/dist/ui/tool_call_card.d.ts +19 -8
- package/dist/ui/tool_call_card.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +126 -0
- package/dist/ui/ui_strings.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/constants.ts +5 -0
- package/src/core/ag_ui_chat.ts +267 -46
- package/src/core/agui_client.ts +33 -2
- package/src/index.ts +7 -0
- package/src/tools/page_action_tools.ts +130 -0
- package/src/ui/attachment_tray.ts +13 -7
- package/src/ui/confirmation_card.ts +15 -5
- package/src/ui/relative_time.ts +15 -8
- package/src/ui/styles.ts +221 -0
- package/src/ui/thread_drawer.ts +53 -25
- package/src/ui/tool_call_card.ts +63 -25
- package/src/ui/ui_strings.ts +208 -0
- package/src/version.ts +1 -1
package/src/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ export {
|
|
|
25
25
|
type AgUiClientHandlers,
|
|
26
26
|
type AgUiRunInputs,
|
|
27
27
|
type AgUiToolCall,
|
|
28
|
+
ConnectionLostError,
|
|
28
29
|
type ExecuteTool,
|
|
29
30
|
type ToolExecution,
|
|
30
31
|
} from "./core/agui_client.js";
|
|
@@ -78,6 +79,11 @@ export type { Skill } from "./skills/skill.js";
|
|
|
78
79
|
export { type ClientTool, ClientToolRegistry } from "./tools/client_tool_registry.js";
|
|
79
80
|
export { isDestructive } from "./tools/is_destructive.js";
|
|
80
81
|
export { isNavigates } from "./tools/is_navigates.js";
|
|
82
|
+
export {
|
|
83
|
+
createPageActionTools,
|
|
84
|
+
PAGE_ACTIONS,
|
|
85
|
+
type ResolvePageTarget,
|
|
86
|
+
} from "./tools/page_action_tools.js";
|
|
81
87
|
export { createPageMapContext, type PageMap } from "./tools/page_map.js";
|
|
82
88
|
export { parseToolCatalog, type ToolCatalogEntry } from "./tools/parse_tool_catalog.js";
|
|
83
89
|
export {
|
|
@@ -100,4 +106,5 @@ export {
|
|
|
100
106
|
type ToolCallStatus,
|
|
101
107
|
type ToolDisplayMode,
|
|
102
108
|
} from "./ui/tool_call_card.js";
|
|
109
|
+
export { DEFAULT_UI_STRINGS, mergeUiStrings, type UiStrings } from "./ui/ui_strings.js";
|
|
103
110
|
export { VERSION } from "./version.js";
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { X_SUMMARY_KEY } from "../constants.js";
|
|
2
|
+
import { scrollIntoCenterView } from "../dom/animations.js";
|
|
3
|
+
import type { ClientTool } from "./client_tool_registry.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a page-action target string to a host-page element, or `null` when it
|
|
7
|
+
* matches nothing. The default (`document.querySelector`) treats the string as a
|
|
8
|
+
* CSS selector; a host with a page map overrides it to map its own element ids,
|
|
9
|
+
* the same way host packages wrap the DOM-driver primitives with environment-
|
|
10
|
+
* aware lookups.
|
|
11
|
+
*/
|
|
12
|
+
export type ResolvePageTarget = (target: string) => HTMLElement | null;
|
|
13
|
+
|
|
14
|
+
/** The built-in page-action tool tokens, opted in via `data-page-actions`. */
|
|
15
|
+
export const PAGE_ACTIONS = {
|
|
16
|
+
SCROLL: "scroll",
|
|
17
|
+
DRAG: "drag",
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The opt-in page-action tools selected by `enabled` (a set of
|
|
22
|
+
* {@link PAGE_ACTIONS} tokens), bound to a {@link ResolvePageTarget}.
|
|
23
|
+
*
|
|
24
|
+
* - `scroll_to` — scroll a target (`top` / `bottom` / a resolver target) into
|
|
25
|
+
* view. Benign; no confirmation.
|
|
26
|
+
* - `drag_and_drop` — drag one element onto another, firing the standard HTML5
|
|
27
|
+
* drag sequence so the host page's own drop handler reacts. Not stamped
|
|
28
|
+
* destructive: a drag rearranges transient state and the durable change
|
|
29
|
+
* happens at the page's explicit commit. A host whose page persists *on drop*
|
|
30
|
+
* gates it with the element's `confirmPredicate`.
|
|
31
|
+
*
|
|
32
|
+
* Both report a clean, model-readable error when a target resolves to nothing,
|
|
33
|
+
* rather than throwing an opaque crash.
|
|
34
|
+
*/
|
|
35
|
+
export function createPageActionTools(
|
|
36
|
+
enabled: ReadonlySet<string>,
|
|
37
|
+
resolveTarget: ResolvePageTarget,
|
|
38
|
+
): ClientTool[] {
|
|
39
|
+
const tools: ClientTool[] = [];
|
|
40
|
+
if (enabled.has(PAGE_ACTIONS.SCROLL)) {
|
|
41
|
+
tools.push(scrollTool(resolveTarget));
|
|
42
|
+
}
|
|
43
|
+
if (enabled.has(PAGE_ACTIONS.DRAG)) {
|
|
44
|
+
tools.push(dragTool(resolveTarget));
|
|
45
|
+
}
|
|
46
|
+
return tools;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function scrollTool(resolveTarget: ResolvePageTarget): ClientTool {
|
|
50
|
+
return {
|
|
51
|
+
name: "scroll_to",
|
|
52
|
+
description:
|
|
53
|
+
"Scroll a target into view. `target` is `top`, `bottom`, or a CSS " +
|
|
54
|
+
"selector / page-map element id. Read-only: it changes nothing on the page.",
|
|
55
|
+
parameters: {
|
|
56
|
+
type: "object",
|
|
57
|
+
properties: { target: { type: "string" } },
|
|
58
|
+
required: ["target"],
|
|
59
|
+
[X_SUMMARY_KEY]: "Scroll into view",
|
|
60
|
+
},
|
|
61
|
+
handler: (args) => {
|
|
62
|
+
const target = String(args["target"] ?? "");
|
|
63
|
+
if (target === "top" || target === "bottom") {
|
|
64
|
+
const top = target === "top" ? 0 : document.body.scrollHeight;
|
|
65
|
+
window.scrollTo({ top, behavior: "smooth" });
|
|
66
|
+
return { scrolled: true, target };
|
|
67
|
+
}
|
|
68
|
+
const element = resolveTarget(target);
|
|
69
|
+
if (element === null) {
|
|
70
|
+
throw new Error(`no element matching "${target}"`);
|
|
71
|
+
}
|
|
72
|
+
scrollIntoCenterView(element);
|
|
73
|
+
return { scrolled: true, target };
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function dragTool(resolveTarget: ResolvePageTarget): ClientTool {
|
|
79
|
+
return {
|
|
80
|
+
name: "drag_and_drop",
|
|
81
|
+
description:
|
|
82
|
+
"Drag the `from` element onto the `to` element (CSS selectors or page-map " +
|
|
83
|
+
"element ids), firing the page's native drag-and-drop. Use for reordering " +
|
|
84
|
+
"sortable lists. The page decides what the drop commits.",
|
|
85
|
+
parameters: {
|
|
86
|
+
type: "object",
|
|
87
|
+
properties: { from: { type: "string" }, to: { type: "string" } },
|
|
88
|
+
required: ["from", "to"],
|
|
89
|
+
[X_SUMMARY_KEY]: "Drag and drop",
|
|
90
|
+
},
|
|
91
|
+
handler: (args) => {
|
|
92
|
+
const fromTarget = String(args["from"] ?? "");
|
|
93
|
+
const toTarget = String(args["to"] ?? "");
|
|
94
|
+
const from = resolveTarget(fromTarget);
|
|
95
|
+
if (from === null) {
|
|
96
|
+
throw new Error(`no element matching "${fromTarget}"`);
|
|
97
|
+
}
|
|
98
|
+
const to = resolveTarget(toTarget);
|
|
99
|
+
if (to === null) {
|
|
100
|
+
throw new Error(`no element matching "${toTarget}"`);
|
|
101
|
+
}
|
|
102
|
+
dispatchDragSequence(from, to);
|
|
103
|
+
return { dragged: true, from: fromTarget, to: toTarget };
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Fire the standard HTML5 drag sequence — `dragstart` on the source, then
|
|
110
|
+
* `dragenter` / `dragover` / `drop` on the target, then `dragend` on the source —
|
|
111
|
+
* sharing one {@link DataTransfer}. Dispatched as typed, bubbling events (the
|
|
112
|
+
* `dataTransfer` is attached explicitly so a drop handler reads it in every
|
|
113
|
+
* environment).
|
|
114
|
+
*/
|
|
115
|
+
function dispatchDragSequence(from: HTMLElement, to: HTMLElement): void {
|
|
116
|
+
const dataTransfer = new DataTransfer();
|
|
117
|
+
fire(from, "dragstart", dataTransfer);
|
|
118
|
+
fire(to, "dragenter", dataTransfer);
|
|
119
|
+
fire(to, "dragover", dataTransfer);
|
|
120
|
+
fire(to, "drop", dataTransfer);
|
|
121
|
+
fire(from, "dragend", dataTransfer);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function fire(target: HTMLElement, type: string, dataTransfer: DataTransfer): void {
|
|
125
|
+
const event = new Event(type, { bubbles: true, cancelable: true }) as Event & {
|
|
126
|
+
dataTransfer: DataTransfer;
|
|
127
|
+
};
|
|
128
|
+
event.dataTransfer = dataTransfer;
|
|
129
|
+
target.dispatchEvent(event);
|
|
130
|
+
}
|
|
@@ -3,6 +3,7 @@ import { ATTACHMENT_STATUS } from "../constants.js";
|
|
|
3
3
|
import type { AttachmentRef } from "../core/attachment.js";
|
|
4
4
|
import type { UploadHandler } from "../core/upload_attachment.js";
|
|
5
5
|
import { formatBytes, iconFor } from "./attachment_chips.js";
|
|
6
|
+
import { DEFAULT_UI_STRINGS, type UiStrings } from "./ui_strings.js";
|
|
6
7
|
|
|
7
8
|
/** Status of a pending tray chip. */
|
|
8
9
|
type AttachmentStatus = (typeof ATTACHMENT_STATUS)[keyof typeof ATTACHMENT_STATUS];
|
|
@@ -17,6 +18,8 @@ export interface AttachmentTrayConfig {
|
|
|
17
18
|
readonly accept: string;
|
|
18
19
|
/** Fired when the set of attachments changes (add / settle / remove). */
|
|
19
20
|
readonly onChange?: () => void;
|
|
21
|
+
/** Localized strings; defaults to the English {@link DEFAULT_UI_STRINGS}. */
|
|
22
|
+
readonly strings?: UiStrings;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
/** One pending file in the tray, from pick to ready/error. */
|
|
@@ -44,12 +47,15 @@ export class AttachmentTray {
|
|
|
44
47
|
readonly element: HTMLDivElement;
|
|
45
48
|
|
|
46
49
|
readonly #config: AttachmentTrayConfig;
|
|
50
|
+
readonly #strings: UiStrings;
|
|
47
51
|
#items: TrayItem[] = [];
|
|
48
52
|
|
|
49
53
|
constructor(config: AttachmentTrayConfig) {
|
|
50
54
|
this.#config = config;
|
|
55
|
+
this.#strings = config.strings ?? DEFAULT_UI_STRINGS;
|
|
51
56
|
this.element = document.createElement("div");
|
|
52
57
|
this.element.className = "attachment-tray";
|
|
58
|
+
this.element.setAttribute("part", "attachment-tray");
|
|
53
59
|
this.element.hidden = true;
|
|
54
60
|
}
|
|
55
61
|
|
|
@@ -113,10 +119,10 @@ export class AttachmentTray {
|
|
|
113
119
|
/** The size/type rejection reason for a file, or `null` when accepted. */
|
|
114
120
|
#reject(file: File): string | null {
|
|
115
121
|
if (this.#config.maxBytes > 0 && file.size > this.#config.maxBytes) {
|
|
116
|
-
return
|
|
122
|
+
return this.#strings.tooLarge.replace("{size}", formatBytes(this.#config.maxBytes));
|
|
117
123
|
}
|
|
118
124
|
if (!accepts(this.#config.accept, file)) {
|
|
119
|
-
return
|
|
125
|
+
return this.#strings.fileTypeNotAllowed;
|
|
120
126
|
}
|
|
121
127
|
return null;
|
|
122
128
|
}
|
|
@@ -137,7 +143,7 @@ export class AttachmentTray {
|
|
|
137
143
|
})
|
|
138
144
|
.catch((error: unknown) => {
|
|
139
145
|
item.status = ATTACHMENT_STATUS.ERROR;
|
|
140
|
-
item.error = error instanceof Error ? error.message :
|
|
146
|
+
item.error = error instanceof Error ? error.message : this.#strings.uploadFailed;
|
|
141
147
|
})
|
|
142
148
|
.finally(() => {
|
|
143
149
|
this.#render();
|
|
@@ -194,8 +200,8 @@ export class AttachmentTray {
|
|
|
194
200
|
const retry = document.createElement("button");
|
|
195
201
|
retry.type = "button";
|
|
196
202
|
retry.className = "attachment-chip-retry";
|
|
197
|
-
retry.title =
|
|
198
|
-
retry.setAttribute("aria-label",
|
|
203
|
+
retry.title = this.#strings.retry;
|
|
204
|
+
retry.setAttribute("aria-label", this.#strings.retryUpload);
|
|
199
205
|
retry.textContent = "↻";
|
|
200
206
|
retry.addEventListener("click", () => this.#upload(item));
|
|
201
207
|
chip.appendChild(retry);
|
|
@@ -204,8 +210,8 @@ export class AttachmentTray {
|
|
|
204
210
|
const remove = document.createElement("button");
|
|
205
211
|
remove.type = "button";
|
|
206
212
|
remove.className = "attachment-chip-remove";
|
|
207
|
-
remove.title =
|
|
208
|
-
remove.setAttribute("aria-label",
|
|
213
|
+
remove.title = this.#strings.remove;
|
|
214
|
+
remove.setAttribute("aria-label", this.#strings.removeAttachment);
|
|
209
215
|
remove.textContent = "✕";
|
|
210
216
|
remove.addEventListener("click", () => this.#remove(item));
|
|
211
217
|
chip.appendChild(remove);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { DEFAULT_UI_STRINGS, type UiStrings } from "./ui_strings.js";
|
|
2
|
+
|
|
1
3
|
/** What the inline confirmation card displays. */
|
|
2
4
|
export interface ConfirmationRequest {
|
|
3
5
|
toolName: string;
|
|
4
6
|
args: Record<string, unknown>;
|
|
5
7
|
/**
|
|
6
8
|
* Human-readable prompt (from the tool's `x-confirm` metadata), e.g.
|
|
7
|
-
* "Activate this project?". Falls back to
|
|
9
|
+
* "Activate this project?". Falls back to the generic `confirmRun` template.
|
|
8
10
|
*/
|
|
9
11
|
message?: string;
|
|
10
12
|
}
|
|
@@ -14,6 +16,7 @@ function actionButton(modifier: string, label: string): HTMLButtonElement {
|
|
|
14
16
|
const button = document.createElement("button");
|
|
15
17
|
button.type = "button";
|
|
16
18
|
button.className = `confirm-btn confirm-btn--${modifier}`;
|
|
19
|
+
button.setAttribute("part", `confirm-button confirm-${modifier}`);
|
|
17
20
|
button.textContent = label;
|
|
18
21
|
return button;
|
|
19
22
|
}
|
|
@@ -26,6 +29,8 @@ export interface ConfirmationOptions {
|
|
|
26
29
|
* pending confirmation when the user cancels the whole run.
|
|
27
30
|
*/
|
|
28
31
|
signal?: AbortSignal;
|
|
32
|
+
/** Localized strings; defaults to the English {@link DEFAULT_UI_STRINGS}. */
|
|
33
|
+
strings?: UiStrings;
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
/**
|
|
@@ -43,26 +48,31 @@ export function requestConfirmation(
|
|
|
43
48
|
request: ConfirmationRequest,
|
|
44
49
|
options: ConfirmationOptions = {},
|
|
45
50
|
): Promise<boolean> {
|
|
51
|
+
const strings = options.strings ?? DEFAULT_UI_STRINGS;
|
|
46
52
|
return new Promise<boolean>((resolve) => {
|
|
47
53
|
const card = document.createElement("div");
|
|
48
54
|
card.className = "confirm";
|
|
55
|
+
card.setAttribute("part", "confirm");
|
|
49
56
|
card.setAttribute("data-tool-name", request.toolName);
|
|
50
57
|
card.setAttribute("role", "group");
|
|
51
|
-
card.setAttribute("aria-label",
|
|
58
|
+
card.setAttribute("aria-label", strings.confirmAction);
|
|
52
59
|
|
|
53
60
|
const body = document.createElement("div");
|
|
54
61
|
body.className = "confirm-body";
|
|
55
|
-
body.
|
|
62
|
+
body.setAttribute("part", "confirm-body");
|
|
63
|
+
body.textContent = request.message ?? strings.confirmRun.replace("{tool}", request.toolName);
|
|
56
64
|
|
|
57
65
|
const args = document.createElement("pre");
|
|
58
66
|
args.className = "confirm-args";
|
|
67
|
+
args.setAttribute("part", "confirm-args");
|
|
59
68
|
args.textContent = JSON.stringify(request.args, null, 2);
|
|
60
69
|
|
|
61
70
|
const actions = document.createElement("div");
|
|
62
71
|
actions.className = "confirm-actions";
|
|
72
|
+
actions.setAttribute("part", "confirm-actions");
|
|
63
73
|
|
|
64
|
-
const cancel = actionButton("cancel",
|
|
65
|
-
const confirm = actionButton("confirm",
|
|
74
|
+
const cancel = actionButton("cancel", strings.cancel);
|
|
75
|
+
const confirm = actionButton("confirm", strings.confirm);
|
|
66
76
|
|
|
67
77
|
let settled = false;
|
|
68
78
|
const close = (accepted: boolean): void => {
|
package/src/ui/relative_time.ts
CHANGED
|
@@ -1,28 +1,35 @@
|
|
|
1
|
+
import { DEFAULT_UI_STRINGS, type UiStrings } from "./ui_strings.js";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* A compact relative timestamp for a thread row — e.g. `"just now"`, `"5m ago"`,
|
|
3
5
|
* `"3h ago"`, `"2d ago"`, `"4w ago"`.
|
|
4
6
|
*
|
|
5
7
|
* `now` is injectable so callers (and tests) can pin the reference point; it
|
|
6
8
|
* defaults to the current time. A timestamp in the future (clock skew) reads as
|
|
7
|
-
* `"just now"`.
|
|
8
|
-
*
|
|
9
|
+
* `"just now"`. The unit words come from {@link UiStrings} (the `{n}` token is
|
|
10
|
+
* filled in here) so a localized host translates them; the bucketing stays
|
|
11
|
+
* integer-rounded and locale-neutral.
|
|
9
12
|
*/
|
|
10
|
-
export function relativeTime(
|
|
13
|
+
export function relativeTime(
|
|
14
|
+
timestamp: number,
|
|
15
|
+
now: number = Date.now(),
|
|
16
|
+
strings: UiStrings = DEFAULT_UI_STRINGS,
|
|
17
|
+
): string {
|
|
11
18
|
const seconds = Math.round((now - timestamp) / 1000);
|
|
12
19
|
if (seconds < 60) {
|
|
13
|
-
return
|
|
20
|
+
return strings.justNow;
|
|
14
21
|
}
|
|
15
22
|
const minutes = Math.round(seconds / 60);
|
|
16
23
|
if (minutes < 60) {
|
|
17
|
-
return
|
|
24
|
+
return strings.minutesAgo.replace("{n}", String(minutes));
|
|
18
25
|
}
|
|
19
26
|
const hours = Math.round(minutes / 60);
|
|
20
27
|
if (hours < 24) {
|
|
21
|
-
return
|
|
28
|
+
return strings.hoursAgo.replace("{n}", String(hours));
|
|
22
29
|
}
|
|
23
30
|
const days = Math.round(hours / 24);
|
|
24
31
|
if (days < 7) {
|
|
25
|
-
return
|
|
32
|
+
return strings.daysAgo.replace("{n}", String(days));
|
|
26
33
|
}
|
|
27
|
-
return
|
|
34
|
+
return strings.weeksAgo.replace("{n}", String(Math.round(days / 7)));
|
|
28
35
|
}
|
package/src/ui/styles.ts
CHANGED
|
@@ -24,6 +24,17 @@ export const STYLES = `
|
|
|
24
24
|
--ag-ui-danger: #b91c1c;
|
|
25
25
|
--ag-ui-muted: #6b7280;
|
|
26
26
|
|
|
27
|
+
/* Tool-call status icon glyphs (override to re-theme) + spinner speed.
|
|
28
|
+
The pending state is the animated ring; the settled states use these. */
|
|
29
|
+
--ag-ui-tool-icon-done: "✓";
|
|
30
|
+
--ag-ui-tool-icon-error: "✕";
|
|
31
|
+
--ag-ui-tool-icon-declined: "⊘";
|
|
32
|
+
--ag-ui-tool-spin-duration: 0.7s;
|
|
33
|
+
|
|
34
|
+
/* Answer well (opt-in via data-answer-well) — boxes a whole assistant turn. */
|
|
35
|
+
--ag-ui-well-bg: transparent;
|
|
36
|
+
--ag-ui-well-border: var(--ag-ui-border);
|
|
37
|
+
|
|
27
38
|
/* Surface — set --ag-ui-shadow: none for a flush, embedded panel. */
|
|
28
39
|
--ag-ui-shadow: 0 12px 32px rgba(20, 20, 50, 0.18);
|
|
29
40
|
--ag-ui-font: inherit;
|
|
@@ -46,6 +57,8 @@ export const STYLES = `
|
|
|
46
57
|
--ag-ui-inset: auto 24px 24px auto;
|
|
47
58
|
--ag-ui-max-width: calc(100vw - 48px);
|
|
48
59
|
--ag-ui-max-height: calc(100vh - 48px);
|
|
60
|
+
/* Reading-column width for placement="page" (full-bleed, centred content). */
|
|
61
|
+
--ag-ui-content-max-width: 820px;
|
|
49
62
|
|
|
50
63
|
position: var(--ag-ui-position);
|
|
51
64
|
inset: var(--ag-ui-inset);
|
|
@@ -133,6 +146,99 @@ export const STYLES = `
|
|
|
133
146
|
--ag-ui-radius: 0;
|
|
134
147
|
}
|
|
135
148
|
|
|
149
|
+
/* Page (PAGE-1): full-bleed background with a centred reading column. Unlike
|
|
150
|
+
"full" (edge-to-edge, left-aligned messages) the content sits in a column
|
|
151
|
+
capped at --ag-ui-content-max-width. The column is produced by symmetric auto
|
|
152
|
+
padding on the scroll area + composer (no per-row wrapper), so user pills
|
|
153
|
+
still right-align and the assistant well spans the column. */
|
|
154
|
+
:host([placement="page"]) {
|
|
155
|
+
--ag-ui-inset: 0;
|
|
156
|
+
--ag-ui-width: 100vw;
|
|
157
|
+
--ag-ui-height: 100vh;
|
|
158
|
+
--ag-ui-max-width: 100vw;
|
|
159
|
+
--ag-ui-max-height: 100vh;
|
|
160
|
+
--ag-ui-radius: 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
:host([placement="page"]) .messages {
|
|
164
|
+
padding-inline: max(var(--ag-ui-pad), calc((100% - var(--ag-ui-content-max-width)) / 2));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
:host([placement="page"]) .input-row {
|
|
168
|
+
padding-inline: max(12px, calc((100% - var(--ag-ui-content-max-width)) / 2));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* In the reading column the assistant well uses the full width; the user
|
|
172
|
+
message stays a right-aligned pill (its default align-self + max-width). */
|
|
173
|
+
:host([placement="page"]) .message--assistant {
|
|
174
|
+
max-width: 100%;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* Sidebar (CUST-3): a full-height docked panel that slides open/closed and
|
|
178
|
+
collapses to a slim icon rail (not the floating launcher). Docked right by
|
|
179
|
+
default; data-side="left" docks it left. Overlay by default — set
|
|
180
|
+
--ag-ui-position: static (and place this element in your own layout) for a
|
|
181
|
+
host-managed push instead. */
|
|
182
|
+
:host([placement="sidebar"]) {
|
|
183
|
+
--ag-ui-inset: 0 0 0 auto;
|
|
184
|
+
--ag-ui-width: 420px;
|
|
185
|
+
--ag-ui-height: 100vh;
|
|
186
|
+
--ag-ui-max-height: 100vh;
|
|
187
|
+
--ag-ui-radius: 0;
|
|
188
|
+
--ag-ui-rail-width: 52px;
|
|
189
|
+
transition: width 0.28s ease;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
:host([placement="sidebar"][data-side="left"]) {
|
|
193
|
+
--ag-ui-inset: 0 auto 0 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
:host([placement="sidebar"]) .chat {
|
|
197
|
+
transition: transform 0.28s ease;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Collapsed sidebar: shrink the host to the rail width, hide the panel, and
|
|
201
|
+
reveal the rail. Higher specificity than the generic collapse rules, so it
|
|
202
|
+
wins regardless of source order. */
|
|
203
|
+
:host([placement="sidebar"][collapsed]) {
|
|
204
|
+
width: var(--ag-ui-rail-width);
|
|
205
|
+
height: 100vh;
|
|
206
|
+
max-height: 100vh;
|
|
207
|
+
bottom: 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
:host([placement="sidebar"][collapsed]) .chat {
|
|
211
|
+
display: none;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* The rail is a sibling of the panel (so it survives the panel being hidden);
|
|
215
|
+
shown only for a collapsed sidebar. */
|
|
216
|
+
.rail {
|
|
217
|
+
display: none;
|
|
218
|
+
border: none;
|
|
219
|
+
font: inherit;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
:host([placement="sidebar"][collapsed]) .rail {
|
|
223
|
+
display: flex;
|
|
224
|
+
position: absolute;
|
|
225
|
+
inset: 0;
|
|
226
|
+
align-items: flex-start;
|
|
227
|
+
justify-content: center;
|
|
228
|
+
padding-top: 16px;
|
|
229
|
+
border: 1px solid var(--ag-ui-border);
|
|
230
|
+
background: var(--ag-ui-header-bg);
|
|
231
|
+
color: var(--ag-ui-header-fg);
|
|
232
|
+
cursor: pointer;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
@media (prefers-reduced-motion: reduce) {
|
|
236
|
+
:host([placement="sidebar"]),
|
|
237
|
+
:host([placement="sidebar"]) .chat {
|
|
238
|
+
transition: none;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
136
242
|
/* Embedded: drop the floating chrome and the high z-index stacking context so
|
|
137
243
|
the widget lives in the host's own layout (fixes overlay/z-index clashes). */
|
|
138
244
|
:host([placement="embedded"]) {
|
|
@@ -170,12 +276,33 @@ export const STYLES = `
|
|
|
170
276
|
}
|
|
171
277
|
|
|
172
278
|
.header-title {
|
|
279
|
+
flex: 1;
|
|
280
|
+
min-width: 0;
|
|
173
281
|
font-weight: 600;
|
|
174
282
|
overflow: hidden;
|
|
175
283
|
text-overflow: ellipsis;
|
|
176
284
|
white-space: nowrap;
|
|
177
285
|
}
|
|
178
286
|
|
|
287
|
+
/* Header / launcher icon holder (CUST-2): a slot, with a data-icon-url <img>
|
|
288
|
+
fallback, sized via --ag-ui-icon-size. */
|
|
289
|
+
.icon-holder {
|
|
290
|
+
display: inline-flex;
|
|
291
|
+
align-items: center;
|
|
292
|
+
justify-content: center;
|
|
293
|
+
flex: none;
|
|
294
|
+
width: var(--ag-ui-icon-size, 22px);
|
|
295
|
+
height: var(--ag-ui-icon-size, 22px);
|
|
296
|
+
line-height: 1;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.icon-img {
|
|
300
|
+
width: 100%;
|
|
301
|
+
height: 100%;
|
|
302
|
+
object-fit: contain;
|
|
303
|
+
border-radius: var(--ag-ui-icon-radius, 4px);
|
|
304
|
+
}
|
|
305
|
+
|
|
179
306
|
.header-controls {
|
|
180
307
|
display: flex;
|
|
181
308
|
gap: 2px;
|
|
@@ -231,6 +358,39 @@ export const STYLES = `
|
|
|
231
358
|
gap: var(--ag-ui-space);
|
|
232
359
|
}
|
|
233
360
|
|
|
361
|
+
/* Empty-state region (CUST-1 slot): centred while it's the only thing in the
|
|
362
|
+
list, hidden as soon as a message, card, or pending indicator renders. */
|
|
363
|
+
.empty {
|
|
364
|
+
margin: auto;
|
|
365
|
+
text-align: center;
|
|
366
|
+
color: var(--ag-ui-muted);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.empty[hidden] {
|
|
370
|
+
display: none;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/* ── Answer group / well (WELL-1) ─────────────────────────────────────────
|
|
374
|
+
One .answer per assistant turn wraps the streamed text, its tool cards,
|
|
375
|
+
and the pending indicator so a whole answer reads (and can be boxed) as one
|
|
376
|
+
unit. A flex column on the message-list gap, stretched to the list width so
|
|
377
|
+
its children keep their own left/right alignment. data-answer-well opts into
|
|
378
|
+
the bordered "well"; without it the layout is today's flat stack. */
|
|
379
|
+
.answer {
|
|
380
|
+
display: flex;
|
|
381
|
+
flex-direction: column;
|
|
382
|
+
gap: var(--ag-ui-space);
|
|
383
|
+
align-self: stretch;
|
|
384
|
+
min-width: 0;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
:host([data-answer-well]) .answer {
|
|
388
|
+
padding: var(--ag-ui-pad);
|
|
389
|
+
background: var(--ag-ui-well-bg);
|
|
390
|
+
border: 1px solid var(--ag-ui-well-border);
|
|
391
|
+
border-radius: var(--ag-ui-radius);
|
|
392
|
+
}
|
|
393
|
+
|
|
234
394
|
.message {
|
|
235
395
|
max-width: 80%;
|
|
236
396
|
padding: var(--ag-ui-msg-pad);
|
|
@@ -403,10 +563,71 @@ export const STYLES = `
|
|
|
403
563
|
}
|
|
404
564
|
|
|
405
565
|
.tool-call-name {
|
|
566
|
+
flex: 1;
|
|
567
|
+
min-width: 0;
|
|
406
568
|
font-weight: 600;
|
|
407
569
|
word-break: break-word;
|
|
408
570
|
}
|
|
409
571
|
|
|
572
|
+
/* Leading status icon (CARD-1). Empty in the DOM — the glyph/spinner is drawn
|
|
573
|
+
here from the card's data-status, so it stays themeable. */
|
|
574
|
+
.tool-call-icon {
|
|
575
|
+
flex: none;
|
|
576
|
+
box-sizing: border-box;
|
|
577
|
+
display: inline-flex;
|
|
578
|
+
align-items: center;
|
|
579
|
+
justify-content: center;
|
|
580
|
+
width: 14px;
|
|
581
|
+
height: 14px;
|
|
582
|
+
font-size: 12px;
|
|
583
|
+
line-height: 1;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/* Pending: a real spinning ring. Speed is tunable; reduced motion stops it. */
|
|
587
|
+
.tool-call[data-status="pending"] .tool-call-icon {
|
|
588
|
+
border: 2px solid var(--ag-ui-muted);
|
|
589
|
+
border-top-color: transparent;
|
|
590
|
+
border-radius: 50%;
|
|
591
|
+
animation: ag-ui-tool-spin var(--ag-ui-tool-spin-duration) linear infinite;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
@keyframes ag-ui-tool-spin {
|
|
595
|
+
to { transform: rotate(360deg); }
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/* Settled: a themeable glyph coloured by outcome. */
|
|
599
|
+
.tool-call[data-status="done"] .tool-call-icon::before {
|
|
600
|
+
content: var(--ag-ui-tool-icon-done);
|
|
601
|
+
color: var(--ag-ui-success);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.tool-call[data-status="error"] .tool-call-icon::before {
|
|
605
|
+
content: var(--ag-ui-tool-icon-error);
|
|
606
|
+
color: var(--ag-ui-danger);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.tool-call[data-status="declined"] .tool-call-icon::before {
|
|
610
|
+
content: var(--ag-ui-tool-icon-declined);
|
|
611
|
+
color: var(--ag-ui-muted);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
@media (prefers-reduced-motion: reduce) {
|
|
615
|
+
.tool-call[data-status="pending"] .tool-call-icon {
|
|
616
|
+
animation: none;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/* Inline display mode (CARD-1): the lightest card — drop the box chrome so the
|
|
621
|
+
status row reads as one line of the answer; the result toggle still expands
|
|
622
|
+
below it. */
|
|
623
|
+
.tool-call[data-display="inline"] {
|
|
624
|
+
max-width: 100%;
|
|
625
|
+
background: transparent;
|
|
626
|
+
border: none;
|
|
627
|
+
padding: 2px 0;
|
|
628
|
+
gap: 2px;
|
|
629
|
+
}
|
|
630
|
+
|
|
410
631
|
.tool-call-status {
|
|
411
632
|
flex: none;
|
|
412
633
|
padding: 1px 8px;
|