@artooi/ag-ui-web-component 0.25.2 → 0.26.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 +42 -1
- package/README.md +69 -0
- package/dist/ag-ui-web-component.bundle.js +73 -29
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +10 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +19 -0
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +12 -1
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +712 -165
- package/dist/index.js.map +4 -4
- package/dist/tools/client_tool_registry.d.ts +24 -1
- package/dist/tools/client_tool_registry.d.ts.map +1 -1
- package/dist/ui/chart_block.d.ts +39 -0
- package/dist/ui/chart_block.d.ts.map +1 -0
- package/dist/ui/chart_spec_from.d.ts +12 -0
- package/dist/ui/chart_spec_from.d.ts.map +1 -0
- package/dist/ui/chart_tool.d.ts +15 -0
- package/dist/ui/chart_tool.d.ts.map +1 -0
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +11 -0
- package/src/core/ag_ui_chat.ts +145 -3
- package/src/core/agui_client.ts +51 -3
- package/src/index.ts +8 -0
- package/src/tools/client_tool_registry.ts +24 -1
- package/src/ui/chart_block.ts +349 -0
- package/src/ui/chart_spec_from.ts +106 -0
- package/src/ui/chart_tool.ts +64 -0
- package/src/ui/styles.ts +44 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow untrusted input into a {@link ChartSpec}.
|
|
3
|
+
*
|
|
4
|
+
* Both arrival routes are untrusted in the same way — a model writes one, a
|
|
5
|
+
* server the other — so neither is taken on shape. Kept out of the renderer
|
|
6
|
+
* because the renderer's job is drawing, and a value that reaches it has
|
|
7
|
+
* already been vouched for.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ChartKind, ChartSpec } from "./chart_block.js";
|
|
11
|
+
|
|
12
|
+
const KINDS: readonly ChartKind[] = ["bar", "line", "pie", "scatter", "stacked"];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Most points a spec may carry, across every series.
|
|
16
|
+
*
|
|
17
|
+
* Not a taste limit. `Math.max(0, ...values)` throws `RangeError` on a large
|
|
18
|
+
* enough spread, and the renderer runs inside the history replay, where a throw
|
|
19
|
+
* abandons the replay and takes every later turn of the transcript with it —
|
|
20
|
+
* permanently, on every reload. A chart nobody can read is a far smaller
|
|
21
|
+
* problem than a conversation that silently loses its tail.
|
|
22
|
+
*/
|
|
23
|
+
const MAX_POINTS = 20_000;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Largest magnitude a point may carry.
|
|
27
|
+
*
|
|
28
|
+
* `Number.isFinite` is not enough on its own: two finite extremes still give an
|
|
29
|
+
* infinite *range*, and `(value - min) / Infinity` is `NaN`, which reaches the
|
|
30
|
+
* DOM as `y="NaN"`. Bounding the values bounds the range.
|
|
31
|
+
*/
|
|
32
|
+
const MAX_MAGNITUDE = 1e15;
|
|
33
|
+
|
|
34
|
+
function asKind(value: unknown): ChartKind {
|
|
35
|
+
// Anything unrecognised falls back to `bar` rather than refusing the spec: an
|
|
36
|
+
// unknown kind is a caller reaching for a chart type we do not draw, and the
|
|
37
|
+
// data is still worth showing.
|
|
38
|
+
return KINDS.includes(value as ChartKind) ? (value as ChartKind) : "bar";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function asNumbers(value: unknown): number[] | null {
|
|
42
|
+
if (!Array.isArray(value)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const out: number[] = [];
|
|
46
|
+
for (const item of value) {
|
|
47
|
+
// `Number.isFinite` rather than `typeof === "number"`: JSON encoders render
|
|
48
|
+
// NaN and Infinity as nulls or strings depending on the encoder, and either
|
|
49
|
+
// would scale into a chart with no visible extent.
|
|
50
|
+
if (typeof item !== "number" || !Number.isFinite(item)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
if (Math.abs(item) > MAX_MAGNITUDE) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
out.push(item);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function asStrings(value: unknown): string[] | null {
|
|
62
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return value as string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A well-formed spec, or `null` for anything that cannot be drawn honestly. */
|
|
69
|
+
export function chartSpecFrom(value: unknown): ChartSpec | null {
|
|
70
|
+
if (typeof value !== "object" || value === null) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const raw = value as Record<string, unknown>;
|
|
74
|
+
const labels = asStrings(raw["labels"]);
|
|
75
|
+
if (labels === null || !Array.isArray(raw["series"])) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const series: { label: string; points: number[] }[] = [];
|
|
80
|
+
for (const entry of raw["series"]) {
|
|
81
|
+
if (typeof entry !== "object" || entry === null) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const item = entry as Record<string, unknown>;
|
|
85
|
+
const points = asNumbers(item["points"]);
|
|
86
|
+
// A series with a different number of points than there are labels would
|
|
87
|
+
// silently misalign every value after the gap. A chart that is subtly wrong
|
|
88
|
+
// still reads as authoritative, which is worse than no chart at all.
|
|
89
|
+
if (points === null || points.length !== labels.length) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
series.push({ label: typeof item["label"] === "string" ? item["label"] : "", points });
|
|
93
|
+
}
|
|
94
|
+
if (series.length === 0) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
if (series.length * labels.length > MAX_POINTS) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const kind = asKind(raw["kind"]);
|
|
102
|
+
const title = raw["title"];
|
|
103
|
+
// The key is omitted rather than set to `undefined`: `title` is genuinely
|
|
104
|
+
// optional and this tsconfig distinguishes absent from present-and-undefined.
|
|
105
|
+
return typeof title === "string" ? { kind, title, labels, series } : { kind, labels, series };
|
|
106
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in `render_chart` tool: the agent-called route to a chart.
|
|
3
|
+
*
|
|
4
|
+
* A thin thing on purpose. It carries no `handler` of its own beyond reporting
|
|
5
|
+
* back to the model, because everything it does is drawing — which lives in
|
|
6
|
+
* `render`, the half the component replays. A consumer wanting charts gets this
|
|
7
|
+
* without writing a renderer; a consumer wanting something else writes their own
|
|
8
|
+
* tool against the same seam.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ClientTool } from "../tools/client_tool_registry.js";
|
|
12
|
+
import { renderChart } from "./chart_block.js";
|
|
13
|
+
import { chartSpecFrom } from "./chart_spec_from.js";
|
|
14
|
+
|
|
15
|
+
/** The name the built-in chart tool registers under. */
|
|
16
|
+
export const CHART_TOOL_NAME = "render_chart";
|
|
17
|
+
|
|
18
|
+
function draw(args: Record<string, unknown>): HTMLDivElement | null {
|
|
19
|
+
const spec = chartSpecFrom(args);
|
|
20
|
+
return spec === null ? null : renderChart(spec);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const REJECTED =
|
|
24
|
+
"chart not rendered: expected labels (strings) and series, each with one finite number per label";
|
|
25
|
+
|
|
26
|
+
/** The built-in chart tool. */
|
|
27
|
+
export function createChartTool(): ClientTool {
|
|
28
|
+
return {
|
|
29
|
+
name: CHART_TOOL_NAME,
|
|
30
|
+
description:
|
|
31
|
+
"Show a chart in the conversation. Supply the data and the page draws it. " +
|
|
32
|
+
"Every series must have exactly one point per label.",
|
|
33
|
+
parameters: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {
|
|
36
|
+
kind: { type: "string", enum: ["bar", "line", "pie", "scatter", "stacked"] },
|
|
37
|
+
title: { type: "string" },
|
|
38
|
+
labels: { type: "array", items: { type: "string" } },
|
|
39
|
+
series: {
|
|
40
|
+
type: "array",
|
|
41
|
+
items: {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
label: { type: "string" },
|
|
45
|
+
points: { type: "array", items: { type: "number" } },
|
|
46
|
+
},
|
|
47
|
+
required: ["points"],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
required: ["labels", "series"],
|
|
52
|
+
"x-summary": "Draw a chart",
|
|
53
|
+
},
|
|
54
|
+
// Says what happened and nothing else; the drawing is `render`'s job. Told
|
|
55
|
+
// plainly when the arguments are unusable, because the model can fix that
|
|
56
|
+
// and retry — a silent no-op would leave it believing the chart is on screen.
|
|
57
|
+
// Answers on what was actually drawn, not on what validated. A spec can
|
|
58
|
+
// pass validation and still have nothing to show -- zero labels matches zero
|
|
59
|
+
// points -- and reporting success then would leave the model believing a
|
|
60
|
+
// chart is on screen.
|
|
61
|
+
handler: (args: Record<string, unknown>) => (draw(args) === null ? REJECTED : "chart rendered"),
|
|
62
|
+
render: draw,
|
|
63
|
+
};
|
|
64
|
+
}
|
package/src/ui/styles.ts
CHANGED
|
@@ -1528,6 +1528,50 @@ export const STYLES = `
|
|
|
1528
1528
|
margin-top: 6px;
|
|
1529
1529
|
}
|
|
1530
1530
|
|
|
1531
|
+
/* Charts.
|
|
1532
|
+
*
|
|
1533
|
+
* The SVG scales to the column and carries no colours of its own beyond the
|
|
1534
|
+
* series palette, so a host restyles it the same way it restyles everything
|
|
1535
|
+
* else. Series colours are custom properties with fallbacks rather than fixed
|
|
1536
|
+
* values, and the axis furniture inherits currentColor at low opacity so it
|
|
1537
|
+
* reads correctly in either theme without a second palette.
|
|
1538
|
+
*/
|
|
1539
|
+
.chart-block {
|
|
1540
|
+
align-self: stretch;
|
|
1541
|
+
max-width: 100%;
|
|
1542
|
+
margin: 6px 0;
|
|
1543
|
+
color: var(--_fg);
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
.chart-title {
|
|
1547
|
+
margin-bottom: 2px;
|
|
1548
|
+
font-size: 0.85em;
|
|
1549
|
+
font-weight: 600;
|
|
1550
|
+
opacity: 0.85;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
.chart-legend {
|
|
1554
|
+
display: flex;
|
|
1555
|
+
flex-wrap: wrap;
|
|
1556
|
+
gap: 4px 12px;
|
|
1557
|
+
margin-top: 4px;
|
|
1558
|
+
font-size: 0.78em;
|
|
1559
|
+
opacity: 0.75;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
.chart-legend-item {
|
|
1563
|
+
display: inline-flex;
|
|
1564
|
+
align-items: center;
|
|
1565
|
+
gap: 5px;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
.chart-legend-swatch {
|
|
1569
|
+
width: 9px;
|
|
1570
|
+
height: 9px;
|
|
1571
|
+
border-radius: 2px;
|
|
1572
|
+
flex: 0 0 auto;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1531
1575
|
.run-notice {
|
|
1532
1576
|
display: inline-flex;
|
|
1533
1577
|
align-items: center;
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION: string = "0.
|
|
1
|
+
export const VERSION: string = "0.26.0";
|