@artooi/ag-ui-web-component 0.44.0 → 0.46.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 +182 -1
- package/README.md +245 -32
- package/dist/ag-ui-web-component.bundle.js +3696 -2798
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +4 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +28 -8
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/read_flag.d.ts +11 -0
- package/dist/core/read_flag.d.ts.map +1 -0
- package/dist/core/read_json_attribute.d.ts +15 -0
- package/dist/core/read_json_attribute.d.ts.map +1 -0
- package/dist/index.js +5084 -3481
- package/dist/index.js.map +4 -4
- package/dist/tools/tool_catalog.d.ts +5 -0
- package/dist/tools/tool_catalog.d.ts.map +1 -1
- package/dist/tools/tool_dispatch.d.ts.map +1 -1
- package/dist/ui/charts/chart_styles.d.ts +2 -0
- package/dist/ui/charts/chart_styles.d.ts.map +1 -0
- package/dist/ui/composer/attachment_styles.d.ts +2 -0
- package/dist/ui/composer/attachment_styles.d.ts.map +1 -0
- package/dist/ui/composer/build_composer.d.ts +30 -0
- package/dist/ui/composer/build_composer.d.ts.map +1 -0
- package/dist/ui/composer/composer_styles.d.ts +2 -0
- package/dist/ui/composer/composer_styles.d.ts.map +1 -0
- package/dist/ui/composer/skill_styles.d.ts +2 -0
- package/dist/ui/composer/skill_styles.d.ts.map +1 -0
- package/dist/ui/history/history_styles.d.ts +2 -0
- package/dist/ui/history/history_styles.d.ts.map +1 -0
- package/dist/ui/interrupts/approval_styles.d.ts +2 -0
- package/dist/ui/interrupts/approval_styles.d.ts.map +1 -0
- package/dist/ui/interrupts/confirmation_styles.d.ts +2 -0
- package/dist/ui/interrupts/confirmation_styles.d.ts.map +1 -0
- package/dist/ui/interrupts/question_styles.d.ts +2 -0
- package/dist/ui/interrupts/question_styles.d.ts.map +1 -0
- package/dist/ui/placement/collapse_styles.d.ts +2 -0
- package/dist/ui/placement/collapse_styles.d.ts.map +1 -0
- package/dist/ui/placement/embedded_styles.d.ts +2 -0
- package/dist/ui/placement/embedded_styles.d.ts.map +1 -0
- package/dist/ui/placement/launcher_styles.d.ts +2 -0
- package/dist/ui/placement/launcher_styles.d.ts.map +1 -0
- package/dist/ui/placement/page_styles.d.ts +2 -0
- package/dist/ui/placement/page_styles.d.ts.map +1 -0
- package/dist/ui/placement/panel_placement.d.ts.map +1 -1
- package/dist/ui/placement/preset_styles.d.ts +2 -0
- package/dist/ui/placement/preset_styles.d.ts.map +1 -0
- package/dist/ui/placement/resize_styles.d.ts +2 -0
- package/dist/ui/placement/resize_styles.d.ts.map +1 -0
- package/dist/ui/placement/sidebar_styles.d.ts +2 -0
- package/dist/ui/placement/sidebar_styles.d.ts.map +1 -0
- package/dist/ui/placement/small_viewport_styles.d.ts +2 -0
- package/dist/ui/placement/small_viewport_styles.d.ts.map +1 -0
- package/dist/ui/placement/surface_for.d.ts +74 -0
- package/dist/ui/placement/surface_for.d.ts.map +1 -0
- package/dist/ui/progress/run_notice_styles.d.ts +2 -0
- package/dist/ui/progress/run_notice_styles.d.ts.map +1 -0
- package/dist/ui/progress/thoughts_styles.d.ts +2 -0
- package/dist/ui/progress/thoughts_styles.d.ts.map +1 -0
- package/dist/ui/progress/tool_call_card.d.ts +8 -0
- package/dist/ui/progress/tool_call_card.d.ts.map +1 -1
- package/dist/ui/progress/tool_group_styles.d.ts +2 -0
- package/dist/ui/progress/tool_group_styles.d.ts.map +1 -0
- package/dist/ui/progress/tool_step_group.d.ts +47 -0
- package/dist/ui/progress/tool_step_group.d.ts.map +1 -0
- package/dist/ui/progress/tool_step_styles.d.ts +2 -0
- package/dist/ui/progress/tool_step_styles.d.ts.map +1 -0
- package/dist/ui/shell/adopt_styles.d.ts +9 -4
- package/dist/ui/shell/adopt_styles.d.ts.map +1 -1
- package/dist/ui/shell/build_header.d.ts +40 -0
- package/dist/ui/shell/build_header.d.ts.map +1 -0
- package/dist/ui/shell/build_launcher.d.ts +20 -0
- package/dist/ui/shell/build_launcher.d.ts.map +1 -0
- package/dist/ui/shell/frame_styles.d.ts +2 -0
- package/dist/ui/shell/frame_styles.d.ts.map +1 -0
- package/dist/ui/shell/theme_styles.d.ts +2 -0
- package/dist/ui/shell/theme_styles.d.ts.map +1 -0
- package/dist/ui/shell/token_styles.d.ts +2 -0
- package/dist/ui/shell/token_styles.d.ts.map +1 -0
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/themes/classic_styles.d.ts +2 -0
- package/dist/ui/themes/classic_styles.d.ts.map +1 -0
- package/dist/ui/themes/classic_tokens.d.ts +2 -0
- package/dist/ui/themes/classic_tokens.d.ts.map +1 -0
- package/dist/ui/themes/code_tokens.d.ts +2 -0
- package/dist/ui/themes/code_tokens.d.ts.map +1 -0
- package/dist/ui/themes/ledger_styles.d.ts +2 -0
- package/dist/ui/themes/ledger_styles.d.ts.map +1 -0
- package/dist/ui/themes/ledger_tokens.d.ts +2 -0
- package/dist/ui/themes/ledger_tokens.d.ts.map +1 -0
- package/dist/ui/themes/modern_styles.d.ts +2 -0
- package/dist/ui/themes/modern_styles.d.ts.map +1 -0
- package/dist/ui/themes/quiet_styles.d.ts +2 -0
- package/dist/ui/themes/quiet_styles.d.ts.map +1 -0
- package/dist/ui/themes/scheme_styles.d.ts +2 -0
- package/dist/ui/themes/scheme_styles.d.ts.map +1 -0
- package/dist/ui/themes/soft_styles.d.ts +2 -0
- package/dist/ui/themes/soft_styles.d.ts.map +1 -0
- package/dist/ui/themes/soft_tokens.d.ts +2 -0
- package/dist/ui/themes/soft_tokens.d.ts.map +1 -0
- package/dist/ui/themes/theme_hosts.d.ts +15 -0
- package/dist/ui/themes/theme_hosts.d.ts.map +1 -0
- package/dist/ui/transcript/attachment_chip_styles.d.ts +2 -0
- package/dist/ui/transcript/attachment_chip_styles.d.ts.map +1 -0
- package/dist/ui/transcript/build_transcript.d.ts +21 -0
- package/dist/ui/transcript/build_transcript.d.ts.map +1 -0
- package/dist/ui/transcript/greeting_styles.d.ts +2 -0
- package/dist/ui/transcript/greeting_styles.d.ts.map +1 -0
- package/dist/ui/transcript/groups_tool_steps.d.ts +13 -0
- package/dist/ui/transcript/groups_tool_steps.d.ts.map +1 -0
- package/dist/ui/transcript/message_action_styles.d.ts +2 -0
- package/dist/ui/transcript/message_action_styles.d.ts.map +1 -0
- package/dist/ui/transcript/message_styles.d.ts +2 -0
- package/dist/ui/transcript/message_styles.d.ts.map +1 -0
- package/dist/ui/transcript/transcript.d.ts +16 -1
- package/dist/ui/transcript/transcript.d.ts.map +1 -1
- package/dist/ui/transcript/transcript_styles.d.ts +2 -0
- package/dist/ui/transcript/transcript_styles.d.ts.map +1 -0
- package/dist/ui/ui_strings.d.ts +18 -0
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/constants.ts +4 -1
- package/src/core/ag_ui_chat.ts +164 -299
- package/src/core/read_flag.ts +13 -0
- package/src/core/read_json_attribute.ts +30 -0
- package/src/tools/tool_catalog.ts +23 -10
- package/src/tools/tool_dispatch.ts +26 -11
- package/src/ui/charts/chart_block.ts +9 -9
- package/src/ui/charts/chart_styles.ts +63 -0
- package/src/ui/composer/attachment_styles.ts +71 -0
- package/src/ui/composer/build_composer.ts +112 -0
- package/src/ui/composer/composer_styles.ts +174 -0
- package/src/ui/composer/skill_styles.ts +120 -0
- package/src/ui/history/history_styles.ts +484 -0
- package/src/ui/interrupts/approval_styles.ts +52 -0
- package/src/ui/interrupts/confirmation_styles.ts +114 -0
- package/src/ui/interrupts/question_styles.ts +73 -0
- package/src/ui/placement/collapse_styles.ts +137 -0
- package/src/ui/placement/embedded_styles.ts +14 -0
- package/src/ui/placement/launcher_styles.ts +189 -0
- package/src/ui/placement/page_styles.ts +47 -0
- package/src/ui/placement/panel_placement.ts +3 -12
- package/src/ui/placement/preset_styles.ts +33 -0
- package/src/ui/placement/resize_styles.ts +186 -0
- package/src/ui/placement/sidebar_styles.ts +88 -0
- package/src/ui/placement/small_viewport_styles.ts +56 -0
- package/src/ui/placement/surface_for.ts +135 -0
- package/src/ui/progress/run_notice_styles.ts +52 -0
- package/src/ui/progress/thoughts_styles.ts +65 -0
- package/src/ui/progress/tool_call_card.ts +11 -0
- package/src/ui/progress/tool_group_styles.ts +232 -0
- package/src/ui/progress/tool_step_group.ts +269 -0
- package/src/ui/progress/tool_step_styles.ts +472 -0
- package/src/ui/shell/adopt_styles.ts +11 -5
- package/src/ui/shell/build_header.ts +108 -0
- package/src/ui/shell/build_launcher.ts +50 -0
- package/src/ui/shell/frame_styles.ts +119 -0
- package/src/ui/shell/theme_styles.ts +11 -0
- package/src/ui/shell/token_styles.ts +406 -0
- package/src/ui/styles.ts +104 -3774
- package/src/ui/themes/classic_styles.ts +32 -0
- package/src/ui/themes/classic_tokens.ts +50 -0
- package/src/ui/themes/code_tokens.ts +21 -0
- package/src/ui/themes/ledger_styles.ts +168 -0
- package/src/ui/themes/ledger_tokens.ts +32 -0
- package/src/ui/themes/modern_styles.ts +80 -0
- package/src/ui/themes/quiet_styles.ts +115 -0
- package/src/ui/themes/scheme_styles.ts +17 -0
- package/src/ui/themes/soft_styles.ts +148 -0
- package/src/ui/themes/soft_tokens.ts +28 -0
- package/src/ui/themes/theme_hosts.ts +14 -0
- package/src/ui/transcript/attachment_chip_styles.ts +116 -0
- package/src/ui/transcript/build_transcript.ts +67 -0
- package/src/ui/transcript/greeting_styles.ts +171 -0
- package/src/ui/transcript/groups_tool_steps.ts +16 -0
- package/src/ui/transcript/message_action_styles.ts +155 -0
- package/src/ui/transcript/message_styles.ts +262 -0
- package/src/ui/transcript/transcript.ts +109 -3
- package/src/ui/transcript/transcript_styles.ts +140 -0
- package/src/ui/ui_strings.ts +31 -0
- package/src/version.ts +1 -1
- package/dist/ui/placement/is_collapsible_placement.d.ts +0 -18
- package/dist/ui/placement/is_collapsible_placement.d.ts.map +0 -1
- package/dist/ui/placement/is_draggable_placement.d.ts +0 -9
- package/dist/ui/placement/is_draggable_placement.d.ts.map +0 -1
- package/src/ui/placement/is_collapsible_placement.ts +0 -19
- package/src/ui/placement/is_draggable_placement.ts +0 -23
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read an opt-in flag attribute on `element` the way HTML reads a boolean
|
|
3
|
+
* attribute.
|
|
4
|
+
*
|
|
5
|
+
* Present means on: bare (`data-prompt-chips`), empty (`=""`), or any value
|
|
6
|
+
* except the literal `"false"`. Comparing against `"true"` instead would make
|
|
7
|
+
* the bare spelling every native boolean attribute uses silently disable the
|
|
8
|
+
* feature it names. `="false"` still turns it off.
|
|
9
|
+
*/
|
|
10
|
+
export function readFlag(element: Element, name: string): boolean {
|
|
11
|
+
const value = element.getAttribute(name);
|
|
12
|
+
return value !== null && value !== "false";
|
|
13
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a JSON-valued attribute on `element`, saying so when it will not parse.
|
|
3
|
+
*
|
|
4
|
+
* `null` for an absent attribute, and `null` again for one that is not JSON --
|
|
5
|
+
* but not quietly the second time. Quoting JSON inside an HTML attribute is
|
|
6
|
+
* fiddly, and the result of getting it wrong is indistinguishable from the
|
|
7
|
+
* feature being switched off: no chips appear, or the strings stay English,
|
|
8
|
+
* with nothing anywhere saying why. That is the same failure `data-paste-attach`
|
|
9
|
+
* already reports for a value it cannot read.
|
|
10
|
+
*
|
|
11
|
+
* Console only. A page author's typo is not the reader's business, nothing the
|
|
12
|
+
* reader did was refused, and the degraded widget is still perfectly usable.
|
|
13
|
+
*/
|
|
14
|
+
export function readJsonAttribute(element: Element, name: string): unknown {
|
|
15
|
+
const raw = element.getAttribute(name);
|
|
16
|
+
if (raw === null) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
} catch {
|
|
22
|
+
console.warn(
|
|
23
|
+
`<ag-ui-chat>: ${name} is not valid JSON, so it was ignored entirely and ` +
|
|
24
|
+
"the built-in default is being used. Check the quoting -- JSON inside " +
|
|
25
|
+
"an HTML attribute needs single quotes around the attribute value, or " +
|
|
26
|
+
"its own double quotes escaped.",
|
|
27
|
+
);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -36,6 +36,11 @@ export interface ToolCatalogHost {
|
|
|
36
36
|
readonly askUserRenderer: () => QuestionRenderer | null;
|
|
37
37
|
/** The decision a run is suspended on, which a Stop abandons. */
|
|
38
38
|
readonly decision: PendingDecision;
|
|
39
|
+
/**
|
|
40
|
+
* Mark the card of ``callId`` as asking the user, or no longer. The call id is
|
|
41
|
+
* absent for a handler run outside a tool call, which has no card to mark.
|
|
42
|
+
*/
|
|
43
|
+
readonly setAsking: (callId: string | undefined, asking: boolean) => void;
|
|
39
44
|
/** The current answer group, opening one if none is open. */
|
|
40
45
|
readonly ensureGroup: () => HTMLDivElement;
|
|
41
46
|
/** The resolved string table. */
|
|
@@ -293,17 +298,25 @@ export class ToolCatalog {
|
|
|
293
298
|
// A host-supplied renderer takes full control of the UI.
|
|
294
299
|
const renderer = this.#host.askUserRenderer();
|
|
295
300
|
let answer: string;
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
301
|
+
// The card says it is asking for as long as a question is open on it, an
|
|
302
|
+
// answer, a Stop and a renderer that failed alike, so the step group's row
|
|
303
|
+
// does not read as running while the run waits on a person.
|
|
304
|
+
this.#host.setAsking(callId, true);
|
|
305
|
+
try {
|
|
306
|
+
if (renderer === null) {
|
|
307
|
+
answer = await builtIn();
|
|
308
|
+
} else {
|
|
309
|
+
// Awaited here rather than inside a helper, so an answering renderer
|
|
310
|
+
// takes exactly as many turns to be heard as it always did.
|
|
311
|
+
try {
|
|
312
|
+
// Called on the element, as `this.askUserRenderer(...)` always was.
|
|
313
|
+
answer = await renderer.call(this.#host.element, request, { signal });
|
|
314
|
+
} catch (error) {
|
|
315
|
+
answer = await this.#afterRendererFailed(error, signal, callId, builtIn);
|
|
316
|
+
}
|
|
306
317
|
}
|
|
318
|
+
} finally {
|
|
319
|
+
this.#host.setAsking(callId, false);
|
|
307
320
|
}
|
|
308
321
|
this.#host.decision.close();
|
|
309
322
|
this.#host.updateEmptyState();
|
|
@@ -166,6 +166,7 @@ export class ToolDispatch {
|
|
|
166
166
|
// transcript places itself against its own card, and by the time it runs the
|
|
167
167
|
// card is no longer reachable by id.
|
|
168
168
|
this.#host.transcript.setCardElement(call.id, card.element);
|
|
169
|
+
this.#host.transcript.holdRunning(call.id, card);
|
|
169
170
|
// Scoped out of this round's catalog ⇒ not a frontend tool of ours, for
|
|
170
171
|
// this round. A host that offers `delete_record` only on the page where
|
|
171
172
|
// deleting makes sense has said something about *this* run, and a call
|
|
@@ -254,17 +255,26 @@ export class ToolDispatch {
|
|
|
254
255
|
// to the message list made it a sibling *after* the group, so anything
|
|
255
256
|
// that streamed afterwards rendered above it and the prompt drifted to
|
|
256
257
|
// the foot of the turn no matter when it was asked.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
258
|
+
//
|
|
259
|
+
// The card says it is asking for as long as the prompt is open, however it
|
|
260
|
+
// ends, so a step group's row reads "waiting for you" rather than running.
|
|
261
|
+
card.setAsking(true);
|
|
262
|
+
let accepted: boolean;
|
|
263
|
+
try {
|
|
264
|
+
const decision = requestConfirmation(this.#host.transcript.ensureGroup(), request, {
|
|
265
|
+
signal,
|
|
266
|
+
strings: this.#host.strings(),
|
|
267
|
+
// Offered only where it can be honoured -- see `#confirmationRule`.
|
|
268
|
+
...(rule === "destructive"
|
|
269
|
+
? { onAlwaysAllow: () => this.#sessionApproved.add(call.name) }
|
|
270
|
+
: {}),
|
|
271
|
+
});
|
|
272
|
+
this.#host.transcript.updateEmptyState();
|
|
273
|
+
this.#host.transcript.follow();
|
|
274
|
+
accepted = await decision;
|
|
275
|
+
} finally {
|
|
276
|
+
card.setAsking(false);
|
|
277
|
+
}
|
|
268
278
|
this.#host.decision.close();
|
|
269
279
|
// A handover while the card was open declines it, as any Stop does, but
|
|
270
280
|
// the transcript the decline would be drawn into is the next principal's
|
|
@@ -458,6 +468,11 @@ export class ToolDispatch {
|
|
|
458
468
|
request.args = card.args;
|
|
459
469
|
}
|
|
460
470
|
card?.mark(TOOL_CALL_STATUS.DEFERRED);
|
|
471
|
+
// Before either renderer runs: the built-in card focuses its approve
|
|
472
|
+
// button as it is drawn, and a folded step group has no box to focus in.
|
|
473
|
+
if (card !== undefined) {
|
|
474
|
+
this.#host.transcript.revealStep(card);
|
|
475
|
+
}
|
|
461
476
|
// The built-in card renders into the gated call's own card, falling back
|
|
462
477
|
// to the answer group when the interrupt names no call we hold one for.
|
|
463
478
|
const builtIn = (): Promise<boolean> =>
|
|
@@ -96,16 +96,16 @@ const WIDTH_STEP = 8;
|
|
|
96
96
|
/** A rough advance width per character for the 10px axis font. */
|
|
97
97
|
const AXIS_CHAR_WIDTH = 5.6;
|
|
98
98
|
|
|
99
|
-
// Read from the
|
|
100
|
-
//
|
|
101
|
-
//
|
|
99
|
+
// Read from the theme's palette rather than a fixed ramp. The private alias
|
|
100
|
+
// reads the host's --ag-ui-chart-N first (see token_styles), so a host's
|
|
101
|
+
// colour wins over any theme's, and a theme can still supply its own.
|
|
102
102
|
const SERIES_COLORS: readonly string[] = [
|
|
103
|
-
"var(--
|
|
104
|
-
"var(--
|
|
105
|
-
"var(--
|
|
106
|
-
"var(--
|
|
107
|
-
"var(--
|
|
108
|
-
"var(--
|
|
103
|
+
"var(--_chart-1)",
|
|
104
|
+
"var(--_chart-2)",
|
|
105
|
+
"var(--_chart-3)",
|
|
106
|
+
"var(--_chart-4)",
|
|
107
|
+
"var(--_chart-5)",
|
|
108
|
+
"var(--_chart-6)",
|
|
109
109
|
];
|
|
110
110
|
|
|
111
111
|
/** The colour for series `index`, wrapping when there are more series than colours. */
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Charts drawn in the transcript: the block, its title and legend.
|
|
2
|
+
export const CHART_STYLES: string = `
|
|
3
|
+
/* Charts.
|
|
4
|
+
*
|
|
5
|
+
* The SVG scales to the column and carries no colours of its own beyond the
|
|
6
|
+
* series palette, so a host restyles it the same way it restyles everything
|
|
7
|
+
* else. Series colours are custom properties with fallbacks rather than fixed
|
|
8
|
+
* values, and the axis furniture inherits currentColor at low opacity so it
|
|
9
|
+
* reads correctly in either theme without a second palette.
|
|
10
|
+
*/
|
|
11
|
+
/* A chart is sized like a message, not like a panel: it takes the width it
|
|
12
|
+
needs and stops, rather than stretching into whatever room the transcript
|
|
13
|
+
has. A message bubble caps at 80% for the same reason -- widening the panel
|
|
14
|
+
should not resize what is already in it. The cap is a token so a host with a
|
|
15
|
+
wide panel and a real reason can raise it; the default is the width this
|
|
16
|
+
renderer has always drawn at. Below the cap the block still fills its column,
|
|
17
|
+
which is what a narrow panel needs. */
|
|
18
|
+
.chart-block {
|
|
19
|
+
align-self: flex-start;
|
|
20
|
+
width: 100%;
|
|
21
|
+
max-width: var(--_chart-max-width);
|
|
22
|
+
margin: 6px 0;
|
|
23
|
+
color: var(--_fg);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* The drawing fills the block's width and carries its own height in the
|
|
27
|
+
viewBox, so it is laid out rather than magnified: block display keeps the
|
|
28
|
+
inline baseline gap from adding a stripe under every chart. */
|
|
29
|
+
.chart-block svg {
|
|
30
|
+
display: block;
|
|
31
|
+
width: 100%;
|
|
32
|
+
height: auto;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.chart-title {
|
|
36
|
+
margin-bottom: 2px;
|
|
37
|
+
font-size: 0.85em;
|
|
38
|
+
font-weight: 600;
|
|
39
|
+
opacity: 0.85;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.chart-legend {
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-wrap: wrap;
|
|
45
|
+
gap: 4px 12px;
|
|
46
|
+
margin-top: 4px;
|
|
47
|
+
font-size: 0.78em;
|
|
48
|
+
opacity: 0.75;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.chart-legend-item {
|
|
52
|
+
display: inline-flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
gap: 5px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.chart-legend-swatch {
|
|
58
|
+
width: 9px;
|
|
59
|
+
height: 9px;
|
|
60
|
+
border-radius: 2px;
|
|
61
|
+
flex: 0 0 auto;
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// The composer's attachment picker, voice button and attachment tray.
|
|
2
|
+
export const ATTACHMENT_STYLES: string = `
|
|
3
|
+
/* ── File attachments ───────────────────────────────────────────────────── */
|
|
4
|
+
/* The picker button sits in the composer's tool row; hidden until
|
|
5
|
+
data-attachments-url. */
|
|
6
|
+
.attach-input {
|
|
7
|
+
display: none;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* The mic button's mount point; filled only once ComposerVoice.wire mounts
|
|
11
|
+
the control, which it skips unless transcription is configured. */
|
|
12
|
+
.voice-slot {
|
|
13
|
+
display: contents;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* Recording: a red tint + a gentle pulse so it's clearly "live". */
|
|
17
|
+
.voice-btn[data-state="recording"] {
|
|
18
|
+
background: var(--_danger);
|
|
19
|
+
color: var(--_on-danger);
|
|
20
|
+
animation: ag-ui-voice-pulse 1.2s ease-in-out infinite;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.voice-btn[data-state="recording"]:hover {
|
|
24
|
+
background: var(--_danger);
|
|
25
|
+
color: var(--_on-danger);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@keyframes ag-ui-voice-pulse {
|
|
29
|
+
0%, 100% { opacity: 1; }
|
|
30
|
+
50% { opacity: 0.6; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@media (prefers-reduced-motion: reduce) {
|
|
34
|
+
.voice-btn[data-state="recording"] {
|
|
35
|
+
animation: none;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Pending-attachments tray, above the input row; collapses (hidden) when empty. */
|
|
40
|
+
.attachment-slot {
|
|
41
|
+
display: contents;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* The padding is the only separation the tray gets: its slot is display:
|
|
45
|
+
contents, so the tray is a direct child of the .chat column, and that column
|
|
46
|
+
sets no gap. The bottom value keeps a chip off the composer's top edge.
|
|
47
|
+
The inline value here is only the default: the page placement overrides
|
|
48
|
+
padding-inline further down to compute its reading-column gutter, and wins
|
|
49
|
+
on specificity whichever way this declaration is written. Read the two
|
|
50
|
+
together before changing either. */
|
|
51
|
+
.attachment-tray {
|
|
52
|
+
display: flex;
|
|
53
|
+
flex-wrap: wrap;
|
|
54
|
+
gap: 6px;
|
|
55
|
+
padding: 8px 12px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* The tray sets the hidden property while empty, and an author display beats
|
|
59
|
+
the UA stylesheet's rule for it, so without this the empty tray keeps
|
|
60
|
+
laying out and its padding is permanent dead space above the composer. */
|
|
61
|
+
.attachment-tray[hidden] {
|
|
62
|
+
display: none;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.attachment-chips {
|
|
66
|
+
display: flex;
|
|
67
|
+
flex-wrap: wrap;
|
|
68
|
+
gap: 6px;
|
|
69
|
+
margin-top: 6px;
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { ICON_ATTACH, ICON_SEND, ICON_STOP } from "../../constants.js";
|
|
2
|
+
import { glyphSlot } from "../shell/glyph_slot.js";
|
|
3
|
+
import type { UiStrings } from "../ui_strings.js";
|
|
4
|
+
|
|
5
|
+
/** The composer's parts, owned by the element, and what they call back into. */
|
|
6
|
+
export interface ComposerOptions {
|
|
7
|
+
readonly input: HTMLTextAreaElement;
|
|
8
|
+
readonly send: HTMLButtonElement;
|
|
9
|
+
readonly attachButton: HTMLButtonElement;
|
|
10
|
+
readonly fileInput: HTMLInputElement;
|
|
11
|
+
readonly voiceSlot: HTMLElement;
|
|
12
|
+
/** The line saying what the composer still needs; sits above the input. */
|
|
13
|
+
readonly hint: HTMLElement;
|
|
14
|
+
/** The queued-message row; sits above the input beside the tray. */
|
|
15
|
+
readonly queuedRow: HTMLElement;
|
|
16
|
+
/** Where the attachment tray mounts; sits above the input. */
|
|
17
|
+
readonly attachSlot: HTMLElement;
|
|
18
|
+
readonly strings: UiStrings;
|
|
19
|
+
readonly signal: AbortSignal;
|
|
20
|
+
readonly onKeydown: (event: KeyboardEvent) => void;
|
|
21
|
+
readonly onInput: () => void;
|
|
22
|
+
/** Send while idle, Stop while a run is in flight: the element decides. */
|
|
23
|
+
readonly onSend: () => void;
|
|
24
|
+
readonly onFilesPicked: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build the composer row -- the field, and the tool row under it with the
|
|
29
|
+
* attach, voice and send controls -- and ready the composer surfaces the
|
|
30
|
+
* element mounts above it: the hint, the queued row and the attachment slot.
|
|
31
|
+
* Returns the row; the element places it and the surfaces.
|
|
32
|
+
*/
|
|
33
|
+
export function buildComposer(options: ComposerOptions): HTMLElement {
|
|
34
|
+
const { input, send, attachButton, fileInput, strings, signal } = options;
|
|
35
|
+
|
|
36
|
+
const queued = options.queuedRow;
|
|
37
|
+
queued.className = "queued";
|
|
38
|
+
queued.setAttribute("part", "queued");
|
|
39
|
+
queued.setAttribute("role", "group");
|
|
40
|
+
queued.setAttribute("aria-label", strings.queued);
|
|
41
|
+
queued.hidden = true;
|
|
42
|
+
|
|
43
|
+
const inputRow = document.createElement("div");
|
|
44
|
+
inputRow.className = "input-row";
|
|
45
|
+
inputRow.setAttribute("part", "composer");
|
|
46
|
+
|
|
47
|
+
// One bordered surface holds the field and the tool row under it, so the
|
|
48
|
+
// icon buttons stop competing with the field for weight.
|
|
49
|
+
const composer = document.createElement("div");
|
|
50
|
+
composer.className = "composer";
|
|
51
|
+
composer.setAttribute("part", "composer-surface");
|
|
52
|
+
|
|
53
|
+
const tools = document.createElement("div");
|
|
54
|
+
tools.className = "composer-tools";
|
|
55
|
+
tools.setAttribute("part", "composer-tools");
|
|
56
|
+
|
|
57
|
+
input.className = "input";
|
|
58
|
+
input.setAttribute("part", "input");
|
|
59
|
+
input.setAttribute("aria-label", strings.message);
|
|
60
|
+
input.rows = 1;
|
|
61
|
+
input.placeholder = strings.inputPlaceholder;
|
|
62
|
+
input.addEventListener("keydown", options.onKeydown, { signal });
|
|
63
|
+
input.addEventListener("input", options.onInput, { signal });
|
|
64
|
+
|
|
65
|
+
// Icon-only, with both glyphs mounted at once and CSS showing the one the
|
|
66
|
+
// state calls for — swapping a single glyph would leave a host that slotted
|
|
67
|
+
// its own Send mark holding a stop icon mid-run.
|
|
68
|
+
send.className = "send";
|
|
69
|
+
send.type = "button";
|
|
70
|
+
send.setAttribute("part", "send");
|
|
71
|
+
send.replaceChildren(
|
|
72
|
+
glyphSlot("icon-send", "send-send", ICON_SEND),
|
|
73
|
+
glyphSlot("icon-stop", "send-stop", ICON_STOP),
|
|
74
|
+
);
|
|
75
|
+
send.title = strings.send;
|
|
76
|
+
send.setAttribute("aria-label", strings.send);
|
|
77
|
+
send.dataset["state"] = "idle";
|
|
78
|
+
send.addEventListener("click", options.onSend, { signal });
|
|
79
|
+
|
|
80
|
+
const hint = options.hint;
|
|
81
|
+
hint.className = "skill-hint";
|
|
82
|
+
hint.setAttribute("part", "skill-hint");
|
|
83
|
+
hint.hidden = true;
|
|
84
|
+
|
|
85
|
+
// File-upload affordance: a paperclip button (hidden until
|
|
86
|
+
// `data-attachments-url` is wired) opening a hidden multi-file input.
|
|
87
|
+
// Drag-and-drop covers the whole shell (wired by ComposerAttachments).
|
|
88
|
+
attachButton.className = "attach-btn";
|
|
89
|
+
attachButton.type = "button";
|
|
90
|
+
attachButton.setAttribute("part", "attach-button");
|
|
91
|
+
attachButton.replaceChildren(glyphSlot("icon-attach", "attach-glyph", ICON_ATTACH));
|
|
92
|
+
attachButton.title = strings.attachFiles;
|
|
93
|
+
attachButton.setAttribute("aria-label", strings.attachFiles);
|
|
94
|
+
attachButton.hidden = true;
|
|
95
|
+
attachButton.addEventListener("click", () => fileInput.click(), { signal });
|
|
96
|
+
|
|
97
|
+
fileInput.className = "attach-input";
|
|
98
|
+
fileInput.type = "file";
|
|
99
|
+
fileInput.multiple = true;
|
|
100
|
+
fileInput.hidden = true;
|
|
101
|
+
fileInput.addEventListener("change", options.onFilesPicked, { signal });
|
|
102
|
+
|
|
103
|
+
options.attachSlot.className = "attachment-slot";
|
|
104
|
+
|
|
105
|
+
// Mic button mount point (kept empty until ComposerVoice mounts the control).
|
|
106
|
+
options.voiceSlot.className = "voice-slot";
|
|
107
|
+
|
|
108
|
+
tools.append(attachButton, options.voiceSlot, send);
|
|
109
|
+
composer.append(input, tools);
|
|
110
|
+
inputRow.append(composer, fileInput);
|
|
111
|
+
return inputRow;
|
|
112
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// The composer: field, tool row, icon buttons, send and stop, and the glyph
|
|
2
|
+
// class every icon button and icon holder paints with.
|
|
3
|
+
export const COMPOSER_STYLES: string = `
|
|
4
|
+
/* ── Composer ───────────────────────────────────────────────────────────────
|
|
5
|
+
One surface owns the border, the background and the focus ring; the field
|
|
6
|
+
and its tool row sit inside it, rather than being siblings stretched to the
|
|
7
|
+
textarea's height. */
|
|
8
|
+
.input-row {
|
|
9
|
+
display: flex;
|
|
10
|
+
padding: 12px;
|
|
11
|
+
border-top: 1px solid var(--_border);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.composer {
|
|
15
|
+
flex: 1;
|
|
16
|
+
min-width: 0;
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
gap: 4px;
|
|
20
|
+
padding: 6px 6px 6px 10px;
|
|
21
|
+
background: var(--_input-bg);
|
|
22
|
+
border: 1px solid var(--_border);
|
|
23
|
+
border-radius: var(--_composer-radius);
|
|
24
|
+
transition: border-color var(--_motion) var(--_ease);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.composer:focus-within {
|
|
28
|
+
border-color: var(--_accent);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* The field grows with its content (sized by #autoGrow) up to the ceiling,
|
|
32
|
+
then scrolls. border-box keeps that measurement stable: with content-box the
|
|
33
|
+
padding would be added to every scrollHeight read and the field would creep
|
|
34
|
+
taller on each keystroke. */
|
|
35
|
+
.input {
|
|
36
|
+
box-sizing: border-box;
|
|
37
|
+
resize: none;
|
|
38
|
+
max-height: var(--_composer-max-height);
|
|
39
|
+
overflow-y: auto;
|
|
40
|
+
/* Scrolling past the end of this must not scroll the page behind it. */
|
|
41
|
+
overscroll-behavior: contain;
|
|
42
|
+
padding: 6px 4px 2px;
|
|
43
|
+
background: transparent;
|
|
44
|
+
border: none;
|
|
45
|
+
font: inherit;
|
|
46
|
+
color: inherit;
|
|
47
|
+
outline: none;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.composer-tools {
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
gap: 2px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Icon buttons: quiet at rest, so the field is what the eye lands on. */
|
|
57
|
+
.attach-btn,
|
|
58
|
+
.voice-btn {
|
|
59
|
+
display: inline-flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
justify-content: center;
|
|
62
|
+
width: var(--_tool-btn-size);
|
|
63
|
+
height: var(--_tool-btn-size);
|
|
64
|
+
padding: 0;
|
|
65
|
+
border: none;
|
|
66
|
+
border-radius: 8px;
|
|
67
|
+
background: transparent;
|
|
68
|
+
color: var(--_muted);
|
|
69
|
+
font: inherit;
|
|
70
|
+
line-height: 1;
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
transition:
|
|
73
|
+
background-color var(--_motion) var(--_ease),
|
|
74
|
+
color var(--_motion) var(--_ease);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.attach-btn:hover,
|
|
78
|
+
.voice-btn:hover {
|
|
79
|
+
background: var(--_hover);
|
|
80
|
+
color: var(--_fg);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.attach-btn:disabled,
|
|
84
|
+
.voice-btn:disabled {
|
|
85
|
+
cursor: default;
|
|
86
|
+
opacity: 0.6;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* The same trap the attachment tray carries a note about, two rules along: an
|
|
90
|
+
author display beats the UA stylesheet's rule for the hidden property, so a
|
|
91
|
+
button the element has explicitly hidden keeps laying out and painting. The
|
|
92
|
+
clip is hidden until a host supplies an upload handler or an attachments URL,
|
|
93
|
+
and without this it is a visible control that cannot do anything.
|
|
94
|
+
|
|
95
|
+
The mic needs no such rule, and the asymmetry is worth knowing before adding
|
|
96
|
+
one: it is not hidden when unconfigured, it is never built. The voice wiring
|
|
97
|
+
returns before constructing the button, leaving only an empty voice slot that
|
|
98
|
+
is display: contents. A hidden-state rule for the mic would match nothing. */
|
|
99
|
+
.attach-btn[hidden] {
|
|
100
|
+
display: none;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* Send closes the row on the right: a circle, the only filled control in the
|
|
104
|
+
composer, so "the thing that acts" reads at a glance. */
|
|
105
|
+
.send {
|
|
106
|
+
margin-left: auto;
|
|
107
|
+
display: inline-flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
justify-content: center;
|
|
110
|
+
flex: none;
|
|
111
|
+
width: var(--_send-size);
|
|
112
|
+
height: var(--_send-size);
|
|
113
|
+
padding: 0;
|
|
114
|
+
border: none;
|
|
115
|
+
border-radius: 50%;
|
|
116
|
+
background: var(--_accent);
|
|
117
|
+
color: var(--_on-accent);
|
|
118
|
+
font: inherit;
|
|
119
|
+
line-height: 1;
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
transition:
|
|
122
|
+
background-color var(--_motion) var(--_ease),
|
|
123
|
+
transform var(--_motion) var(--_ease-pop);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.send:hover {
|
|
127
|
+
transform: scale(1.08);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.send:active {
|
|
131
|
+
transform: scale(0.92);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.send:disabled {
|
|
135
|
+
opacity: 0.5;
|
|
136
|
+
cursor: default;
|
|
137
|
+
transform: none;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* The composer button doubles as the Stop control while a run is in flight —
|
|
141
|
+
same circle, different glyph, so nothing moves when a run starts. */
|
|
142
|
+
.send[data-state="running"] {
|
|
143
|
+
background: var(--_muted);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.send[data-state="idle"] .send-stop,
|
|
147
|
+
.send[data-state="running"] .send-send {
|
|
148
|
+
display: none;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* Glyphs — one class, painted from the button's own colour so every state
|
|
152
|
+
(hover, recording, running) carries the icon with it. */
|
|
153
|
+
.glyph {
|
|
154
|
+
width: var(--_glyph-size);
|
|
155
|
+
height: var(--_glyph-size);
|
|
156
|
+
fill: none;
|
|
157
|
+
stroke: currentColor;
|
|
158
|
+
stroke-width: var(--_glyph-stroke);
|
|
159
|
+
stroke-linecap: round;
|
|
160
|
+
stroke-linejoin: round;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.glyph--solid {
|
|
164
|
+
fill: currentColor;
|
|
165
|
+
stroke: none;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* In an icon holder (header brand, launcher) the glyph takes the holder's size
|
|
169
|
+
rather than the composer's. */
|
|
170
|
+
.icon-holder .glyph {
|
|
171
|
+
width: 100%;
|
|
172
|
+
height: 100%;
|
|
173
|
+
}
|
|
174
|
+
`;
|