@ciridae/ui 1.0.1 → 1.0.3
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/dist/app/auth.d.ts +10 -0
- package/dist/app/auth.js +60 -0
- package/dist/app/error-boundary.d.ts +18 -0
- package/dist/app/error-boundary.js +45 -0
- package/dist/app/feedback.d.ts +12 -0
- package/dist/app/feedback.js +128 -0
- package/dist/app/index.d.ts +8 -0
- package/dist/app/index.js +8 -0
- package/dist/app/request-errors.d.ts +63 -0
- package/dist/app/request-errors.js +172 -0
- package/dist/app/sentry.d.ts +18 -0
- package/dist/app/sentry.js +52 -0
- package/dist/app/telemetry.d.ts +7 -0
- package/dist/app/telemetry.js +28 -0
- package/dist/app/trail.d.ts +9 -0
- package/dist/app/trail.js +18 -0
- package/dist/chat/chat-query-keys.d.ts +3 -3
- package/dist/chat/contract.d.ts +15 -15
- package/dist/chat/conversation-items-query-options.d.ts +114 -114
- package/dist/chat/conversation-source.d.ts +75 -0
- package/dist/chat/conversation-source.js +1 -0
- package/dist/chat/message-text.d.ts +1 -1
- package/dist/chat/message-text.js +114 -46
- package/dist/chat/use-conversation-list.d.ts +6 -6
- package/dist/components/activity-feed.d.ts +2 -2
- package/dist/components/agent-work.d.ts +3 -3
- package/dist/components/app-frame.d.ts +79 -0
- package/dist/components/app-frame.js +92 -0
- package/dist/components/app-navigation.d.ts +3 -3
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/brand-icons.d.ts +3 -3
- package/dist/components/bulk-action-bar.d.ts +1 -1
- package/dist/components/button.d.ts +4 -4
- package/dist/components/calendar.d.ts +1 -1
- package/dist/components/centered-page.d.ts +1 -1
- package/dist/components/chart.d.ts +22 -2
- package/dist/components/chart.js +239 -44
- package/dist/components/chat-client-source.d.ts +16 -0
- package/dist/components/chat-client-source.js +256 -0
- package/dist/components/chat-page.d.ts +9 -8
- package/dist/components/chat-page.js +7 -11
- package/dist/components/chat-sheet-controller.d.ts +10 -14
- package/dist/components/chat-sheet-controller.js +2 -3
- package/dist/components/chat-sheet.d.ts +10 -11
- package/dist/components/chat-sheet.js +135 -273
- package/dist/components/chat.d.ts +22 -8
- package/dist/components/chat.js +128 -33
- package/dist/components/checkbox-item.d.ts +1 -1
- package/dist/components/checkbox-list.d.ts +1 -1
- package/dist/components/chip.d.ts +1 -1
- package/dist/components/collapsible.d.ts +1 -1
- package/dist/components/color-theme.d.ts +1 -1
- package/dist/components/column-settings.d.ts +1 -1
- package/dist/components/command-menu.d.ts +1 -1
- package/dist/components/command-palette.d.ts +3 -3
- package/dist/components/command-palette.js +1 -2
- package/dist/components/copyable-id.d.ts +22 -5
- package/dist/components/copyable-id.js +46 -6
- package/dist/components/data-display.d.ts +4 -4
- package/dist/components/data-grid.d.ts +10 -10
- package/dist/components/date-range-picker.d.ts +1 -1
- package/dist/components/date-time.d.ts +1 -1
- package/dist/components/dialog-height-context.d.ts +1 -1
- package/dist/components/document-viewer.d.ts +2 -2
- package/dist/components/drawer.d.ts +6 -6
- package/dist/components/dropzone.d.ts +4 -4
- package/dist/components/editable-field.d.ts +2 -2
- package/dist/components/email-input.d.ts +1 -1
- package/dist/components/environment-frame.d.ts +1 -1
- package/dist/components/expandable-text.d.ts +1 -1
- package/dist/components/feedback-screenshot.d.ts +1 -1
- package/dist/components/feedback-sheet.d.ts +18 -3
- package/dist/components/feedback-sheet.js +216 -55
- package/dist/components/field-context.d.ts +1 -1
- package/dist/components/filter-bar.d.ts +3 -3
- package/dist/components/filter-bar.js +4 -1
- package/dist/components/form-controls.d.ts +4 -4
- package/dist/components/form-layout.d.ts +7 -7
- package/dist/components/hotkeys.d.ts +2 -2
- package/dist/components/icon.d.ts +2 -2
- package/dist/components/input.d.ts +4 -4
- package/dist/components/input.js +3 -2
- package/dist/components/keyboard-shortcuts.d.ts +2 -2
- package/dist/components/keycap.d.ts +2 -2
- package/dist/components/layout.d.ts +15 -15
- package/dist/components/lightbox.d.ts +9 -3
- package/dist/components/lightbox.js +11 -8
- package/dist/components/link.d.ts +1 -1
- package/dist/components/locale.d.ts +32 -2
- package/dist/components/locale.js +25 -1
- package/dist/components/log-list.d.ts +3 -3
- package/dist/components/mention-input.d.ts +2 -2
- package/dist/components/menu.d.ts +2 -2
- package/dist/components/meter.d.ts +1 -1
- package/dist/components/navigation.d.ts +2 -2
- package/dist/components/notification-tray.d.ts +1 -1
- package/dist/components/overlays.d.ts +4 -4
- package/dist/components/page-frame.d.ts +4 -4
- package/dist/components/panel.d.ts +4 -4
- package/dist/components/profile-header.d.ts +1 -1
- package/dist/components/progress.d.ts +1 -1
- package/dist/components/project-avatar.d.ts +1 -1
- package/dist/components/radio-card.d.ts +2 -2
- package/dist/components/record.d.ts +4 -4
- package/dist/components/resolved-file-url.d.ts +19 -0
- package/dist/components/resolved-file-url.js +49 -0
- package/dist/components/scroll-area.d.ts +1 -1
- package/dist/components/secret-dialog.d.ts +1 -1
- package/dist/components/segmented-bar.d.ts +1 -1
- package/dist/components/select.d.ts +4 -4
- package/dist/components/settings.d.ts +4 -4
- package/dist/components/shell-skeleton.d.ts +1 -1
- package/dist/components/shell.d.ts +19 -19
- package/dist/components/shell.js +3 -1
- package/dist/components/side-panel.d.ts +3 -3
- package/dist/components/sidebar.d.ts +6 -6
- package/dist/components/site-banner.d.ts +2 -2
- package/dist/components/skeleton.d.ts +1 -1
- package/dist/components/skeletons.d.ts +5 -5
- package/dist/components/stages.d.ts +5 -5
- package/dist/components/stat-card.d.ts +3 -3
- package/dist/components/status-banner.d.ts +1 -1
- package/dist/components/status.d.ts +2 -2
- package/dist/components/steps.d.ts +1 -1
- package/dist/components/switch.d.ts +1 -1
- package/dist/components/tabs.d.ts +8 -8
- package/dist/components/theme-customizer.d.ts +2 -2
- package/dist/components/toast.d.ts +1 -1
- package/dist/components/use-new-version-available.d.ts +6 -0
- package/dist/components/use-new-version-available.js +29 -0
- package/dist/components/user-chip.d.ts +3 -3
- package/dist/components/wizard.d.ts +2 -2
- package/dist/feedback/contract.d.ts +136 -136
- package/dist/feedback/feedback-controller.d.ts +6 -6
- package/dist/feedback/generated/zod.gen.d.ts +28 -28
- package/dist/feedback/operator-contract.d.ts +11 -11
- package/dist/feedback/operator-controller.d.ts +32 -13
- package/dist/feedback/operator-controller.js +44 -11
- package/dist/styled-system/styles.css +203 -59
- package/dist/theme/tokens.js +17 -8
- package/package.json +32 -3
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ComponentProps } from "react";
|
|
2
2
|
/** One line on the chart: a label and its `[unixSeconds, value]` samples. */
|
|
3
3
|
export type TimeSeries = {
|
|
4
4
|
label: string;
|
|
5
5
|
points: readonly (readonly [number, number])[];
|
|
6
6
|
};
|
|
7
|
+
/** One bar or line point per category, such as a month or a team, for a series. */
|
|
8
|
+
export type CategorySeries = {
|
|
9
|
+
label: string;
|
|
10
|
+
/** One value per category, in the categories' order; `null` leaves it out. */
|
|
11
|
+
values: readonly (number | null)[];
|
|
12
|
+
};
|
|
7
13
|
/**
|
|
8
14
|
* Lines over a fixed time window. Samples from different series are joined on their timestamp, so
|
|
9
15
|
* a series missing a sample leaves a gap rather than a line drawn through it.
|
|
@@ -19,4 +25,18 @@ export declare function TimeSeriesChart({ series, start, end, formatValue, forma
|
|
|
19
25
|
height?: number;
|
|
20
26
|
/** Disable fractional axis ticks for discrete counts such as users. */
|
|
21
27
|
allowDecimals?: boolean;
|
|
22
|
-
}): import("
|
|
28
|
+
}): import("react").JSX.Element;
|
|
29
|
+
/**
|
|
30
|
+
* Values by category as bars, side by side per series, or as lines, such as counts per month.
|
|
31
|
+
* Each series takes the next accent; a legend names them once there is more than one.
|
|
32
|
+
*/
|
|
33
|
+
export declare function CategoryChart({ categories, series, variant, formatValue, height, allowDecimals, className, ...props }: ComponentProps<"div"> & {
|
|
34
|
+
categories: readonly string[];
|
|
35
|
+
series: readonly CategorySeries[];
|
|
36
|
+
/** `line` joins each series' values across the categories instead. */
|
|
37
|
+
variant?: "bar" | "line";
|
|
38
|
+
formatValue?: (value: number) => string;
|
|
39
|
+
height?: number;
|
|
40
|
+
/** Disable fractional axis ticks for discrete counts such as users. */
|
|
41
|
+
allowDecimals?: boolean;
|
|
42
|
+
}): import("react").JSX.Element;
|
package/dist/components/chart.js
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { barY, defineChart, lineY } from "@tanstack/charts";
|
|
3
|
+
import { crosshair } from "@tanstack/charts/crosshair";
|
|
4
|
+
import { group } from "@tanstack/charts/group";
|
|
5
|
+
import { Chart } from "@tanstack/charts/react/tooltip";
|
|
6
|
+
import { scaleBand } from "@tanstack/charts/scales/band";
|
|
7
|
+
import { scaleLinear } from "@tanstack/charts/scales/linear";
|
|
8
|
+
import { scaleOrdinal } from "@tanstack/charts/scales/ordinal";
|
|
9
|
+
import { tooltip as chartTooltip } from "@tanstack/charts/tooltip";
|
|
10
|
+
import { useMemo } from "react";
|
|
3
11
|
import { css, cx } from "#primitives/css";
|
|
4
12
|
import { useCiridaeLocale } from "./locale.js";
|
|
5
13
|
const seriesColorCount = 5;
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
// Series colors are custom properties so recharts' SVG attributes can use theme tokens.
|
|
14
|
+
// Series colors are custom properties so the chart's SVG paint follows the theme's tokens, and
|
|
15
|
+
// the axes and tooltip read the kit's muted text, grid and card colors.
|
|
9
16
|
const frame = css({
|
|
10
17
|
display: "grid",
|
|
11
18
|
gap: "layout.controlGap",
|
|
12
19
|
minWidth: "0",
|
|
20
|
+
color: "fg.muted",
|
|
21
|
+
fontFamily: "numeric",
|
|
22
|
+
fontSize: "caption",
|
|
13
23
|
"--series-0": "token(colors.accent.blue)",
|
|
14
24
|
"--series-1": "token(colors.accent.green)",
|
|
15
25
|
"--series-2": "token(colors.accent.yellow)",
|
|
@@ -17,14 +27,11 @@ const frame = css({
|
|
|
17
27
|
"--series-4": "token(colors.accent.red)",
|
|
18
28
|
"--chart-grid": "token(colors.border.subtle)",
|
|
19
29
|
"--chart-muted": "token(colors.fg.muted)",
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
fontFamily: "numeric",
|
|
23
|
-
fontSize: "caption",
|
|
24
|
-
},
|
|
25
|
-
"& .recharts-surface": { overflow: "visible" },
|
|
30
|
+
"--chart-cursor": "token(colors.bg.hover)",
|
|
31
|
+
"& .ts-chart-host": { overflow: "visible" },
|
|
26
32
|
});
|
|
27
|
-
|
|
33
|
+
// The tooltip's own React body replaces the chart's default chrome, so it draws the card itself.
|
|
34
|
+
const tooltipBody = css({
|
|
28
35
|
display: "grid",
|
|
29
36
|
gap: "control.trackInset",
|
|
30
37
|
padding: "panel.compact",
|
|
@@ -32,6 +39,8 @@ const tooltip = css({
|
|
|
32
39
|
borderRadius: "action",
|
|
33
40
|
background: "bg.card",
|
|
34
41
|
boxShadow: "card",
|
|
42
|
+
color: "fg.primary",
|
|
43
|
+
fontFamily: "sans",
|
|
35
44
|
fontSize: "caption",
|
|
36
45
|
});
|
|
37
46
|
const tooltipRow = css({
|
|
@@ -50,6 +59,7 @@ const legend = css({
|
|
|
50
59
|
flexWrap: "wrap",
|
|
51
60
|
gap: "layout.controlGap",
|
|
52
61
|
color: "fg.muted",
|
|
62
|
+
fontFamily: "sans",
|
|
53
63
|
fontSize: "caption",
|
|
54
64
|
});
|
|
55
65
|
const legendItem = css({
|
|
@@ -72,45 +82,230 @@ const swatch = css({
|
|
|
72
82
|
borderRadius: "round",
|
|
73
83
|
});
|
|
74
84
|
const seriesColor = (index) => `var(--series-${index % seriesColorCount})`;
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
const chartTheme = {
|
|
86
|
+
foreground: "var(--chart-muted)",
|
|
87
|
+
muted: "var(--chart-muted)",
|
|
88
|
+
grid: "var(--chart-grid)",
|
|
89
|
+
background: "transparent",
|
|
90
|
+
palette: Array.from({ length: seriesColorCount }, (_, index) => seriesColor(index)),
|
|
91
|
+
};
|
|
92
|
+
const gridLine = { stroke: "var(--chart-grid)", strokeOpacity: 1 };
|
|
93
|
+
/** The value axis' domain: from zero, or the lowest value below it, to the highest. */
|
|
94
|
+
function valueDomain(rows) {
|
|
95
|
+
const values = rows.flatMap((row) => (row.y === null ? [] : [row.y]));
|
|
96
|
+
return [Math.min(0, ...values), Math.max(0, ...values, 1)];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Without decimals, asks a short scale for no more ticks than whole numbers it spans, so 0 to 2
|
|
100
|
+
* reads 0, 1, 2 rather than halves.
|
|
101
|
+
*/
|
|
102
|
+
function valueTickCount(domain, allowDecimals) {
|
|
103
|
+
if (allowDecimals)
|
|
104
|
+
return undefined;
|
|
105
|
+
const span = Math.ceil(domain[1] - domain[0]);
|
|
106
|
+
return span < 5 ? Math.max(span, 1) : undefined;
|
|
107
|
+
}
|
|
77
108
|
/**
|
|
78
109
|
* Lines over a fixed time window. Samples from different series are joined on their timestamp, so
|
|
79
110
|
* a series missing a sample leaves a gap rather than a line drawn through it.
|
|
80
111
|
*/
|
|
81
112
|
export function TimeSeriesChart({ series, start, end, formatValue, formatTimestamp, height = 220, allowDecimals = true, className, ...props }) {
|
|
82
|
-
const rows = chartRows(series);
|
|
83
113
|
const { formatDateTime, formatTime } = useCiridaeLocale();
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
114
|
+
const definition = useMemo(() => {
|
|
115
|
+
const formatHeading = (unixSeconds) => formatTimestamp
|
|
116
|
+
? formatTimestamp(unixSeconds)
|
|
117
|
+
: formatDateTime(unixSeconds * 1000, {
|
|
118
|
+
month: "short",
|
|
119
|
+
day: "numeric",
|
|
120
|
+
hour: "2-digit",
|
|
121
|
+
minute: "2-digit",
|
|
122
|
+
second: "2-digit",
|
|
123
|
+
});
|
|
124
|
+
const rows = timeSeriesRows(series);
|
|
125
|
+
const domain = valueDomain(rows);
|
|
126
|
+
const spansDays = end - start > 24 * 60 * 60;
|
|
127
|
+
const formatTick = formatTimestamp ??
|
|
128
|
+
((unixSeconds) => spansDays
|
|
129
|
+
? formatDateTime(unixSeconds * 1000, {
|
|
130
|
+
month: "short",
|
|
131
|
+
day: "numeric",
|
|
132
|
+
hour: "2-digit",
|
|
133
|
+
minute: "2-digit",
|
|
134
|
+
})
|
|
135
|
+
: formatTime(unixSeconds * 1000, { hour: "2-digit", minute: "2-digit" }));
|
|
136
|
+
return defineChart({
|
|
137
|
+
marks: [
|
|
138
|
+
lineY(rows, {
|
|
139
|
+
key: "key",
|
|
140
|
+
x: "x",
|
|
141
|
+
y: "y",
|
|
142
|
+
z: "series",
|
|
143
|
+
color: "series",
|
|
144
|
+
strokeWidth: 1.5,
|
|
145
|
+
}),
|
|
146
|
+
crosshair({
|
|
147
|
+
y: false,
|
|
148
|
+
stroke: "var(--chart-muted)",
|
|
149
|
+
strokeOpacity: 1,
|
|
150
|
+
strokeWidth: 1,
|
|
151
|
+
strokeDasharray: "3 3",
|
|
152
|
+
}),
|
|
153
|
+
],
|
|
154
|
+
scales: {
|
|
155
|
+
x: {
|
|
156
|
+
scale: scaleLinear().domain([start, end]),
|
|
157
|
+
axis: {
|
|
158
|
+
line: gridLine,
|
|
159
|
+
ticks: { size: 0, spacing: 96, format: formatTick },
|
|
160
|
+
tickLabels: { thin: true },
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
y: {
|
|
164
|
+
scale: scaleLinear().domain(domain),
|
|
165
|
+
nice: true,
|
|
166
|
+
grid: gridLine,
|
|
167
|
+
axis: {
|
|
168
|
+
line: false,
|
|
169
|
+
ticks: {
|
|
170
|
+
size: 0,
|
|
171
|
+
count: valueTickCount(domain, allowDecimals),
|
|
172
|
+
format: formatValue,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
color: { scale: seriesColorScale(series.length) },
|
|
178
|
+
theme: chartTheme,
|
|
179
|
+
focus: "group-x",
|
|
180
|
+
maxFocusDistance: Number.POSITIVE_INFINITY,
|
|
181
|
+
tooltip: seriesTooltip(series, formatValue, formatHeading),
|
|
182
|
+
});
|
|
183
|
+
}, [allowDecimals, end, formatDateTime, formatTime, formatTimestamp, formatValue, series, start]);
|
|
184
|
+
return (_jsxs("div", { ...props, className: cx(frame, className), "data-ciridae-ui": "", children: [_jsx(Chart, { ariaLabel: series.map((item) => item.label).join(", "), definition: definition, height: height, renderTooltipBody: ({ points, content }) => (_jsx(SeriesTooltip, { content: content, points: points, series: series, formatValue: formatValue })) }), _jsx(SeriesLegend, { series: series })] }));
|
|
105
185
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
186
|
+
/**
|
|
187
|
+
* Values by category as bars, side by side per series, or as lines, such as counts per month.
|
|
188
|
+
* Each series takes the next accent; a legend names them once there is more than one.
|
|
189
|
+
*/
|
|
190
|
+
export function CategoryChart({ categories, series, variant = "bar", formatValue = String, height = 176, allowDecimals = true, className, ...props }) {
|
|
191
|
+
const definition = useMemo(() => {
|
|
192
|
+
const rows = series.flatMap((item, seriesIndex) => categories.map((category, categoryIndex) => ({
|
|
193
|
+
key: `${seriesIndex}:${categoryIndex}`,
|
|
194
|
+
x: category,
|
|
195
|
+
y: item.values[categoryIndex] ?? null,
|
|
196
|
+
series: seriesIndex,
|
|
197
|
+
})));
|
|
198
|
+
const domain = valueDomain(rows);
|
|
199
|
+
const mark = variant === "bar"
|
|
200
|
+
? barY(rows.filter((row) => row.y !== null), {
|
|
201
|
+
key: "key",
|
|
202
|
+
x: "x",
|
|
203
|
+
y: (row) => row.y ?? 0,
|
|
204
|
+
z: "series",
|
|
205
|
+
color: "series",
|
|
206
|
+
layout: group({ padding: 0.1 }),
|
|
207
|
+
radius: { end: 2 },
|
|
208
|
+
})
|
|
209
|
+
: lineY(rows, {
|
|
210
|
+
key: "key",
|
|
211
|
+
x: "x",
|
|
212
|
+
y: "y",
|
|
213
|
+
z: "series",
|
|
214
|
+
color: "series",
|
|
215
|
+
strokeWidth: 1.5,
|
|
216
|
+
});
|
|
217
|
+
return defineChart({
|
|
218
|
+
marks: [
|
|
219
|
+
mark,
|
|
220
|
+
crosshair(variant === "bar"
|
|
221
|
+
? { x: { band: { fill: "var(--chart-cursor)", fillOpacity: 1 } }, y: false }
|
|
222
|
+
: {
|
|
223
|
+
y: false,
|
|
224
|
+
stroke: "var(--chart-muted)",
|
|
225
|
+
strokeOpacity: 1,
|
|
226
|
+
strokeWidth: 1,
|
|
227
|
+
strokeDasharray: "3 3",
|
|
228
|
+
}),
|
|
229
|
+
],
|
|
230
|
+
scales: {
|
|
231
|
+
x: {
|
|
232
|
+
scale: scaleBand()
|
|
233
|
+
.domain(categories)
|
|
234
|
+
.padding(variant === "bar" ? 0.2 : 0),
|
|
235
|
+
axis: { line: gridLine, ticks: { size: 0 }, tickLabels: { thin: true } },
|
|
236
|
+
},
|
|
237
|
+
y: {
|
|
238
|
+
scale: scaleLinear().domain(domain),
|
|
239
|
+
nice: true,
|
|
240
|
+
grid: gridLine,
|
|
241
|
+
axis: {
|
|
242
|
+
line: false,
|
|
243
|
+
ticks: {
|
|
244
|
+
size: 0,
|
|
245
|
+
count: valueTickCount(domain, allowDecimals),
|
|
246
|
+
format: formatValue,
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
color: { scale: seriesColorScale(series.length) },
|
|
252
|
+
theme: chartTheme,
|
|
253
|
+
focus: "group-x",
|
|
254
|
+
maxFocusDistance: Number.POSITIVE_INFINITY,
|
|
255
|
+
// The cursor band marks the hovered category; a ring on one bar would single it out.
|
|
256
|
+
focusRing: variant === "line",
|
|
257
|
+
tooltip: seriesTooltip(series, formatValue, String),
|
|
258
|
+
});
|
|
259
|
+
}, [allowDecimals, categories, formatValue, series, variant]);
|
|
260
|
+
return (_jsxs("div", { ...props, className: cx(frame, className), "data-ciridae-ui": "", children: [_jsx(Chart, { ariaLabel: series.map((item) => item.label).join(", "), definition: definition, height: height, renderTooltipBody: ({ points, content }) => (_jsx(SeriesTooltip, { content: content, points: points, series: series, formatValue: formatValue })) }), _jsx(SeriesLegend, { series: series })] }));
|
|
261
|
+
}
|
|
262
|
+
/** Each series index to its accent, in order. */
|
|
263
|
+
function seriesColorScale(count) {
|
|
264
|
+
return scaleOrdinal()
|
|
265
|
+
.domain(Array.from({ length: count }, (_, index) => index))
|
|
266
|
+
.range(Array.from({ length: count }, (_, index) => seriesColor(index)));
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* The tooltip's plain text, which names the focused position for assistive tech: the heading,
|
|
270
|
+
* then each series' label and value, one per line.
|
|
271
|
+
*/
|
|
272
|
+
function seriesTooltip(series, formatValue, formatHeading) {
|
|
273
|
+
return {
|
|
274
|
+
use: chartTooltip,
|
|
275
|
+
formatGroup: (points) => [
|
|
276
|
+
points[0] === undefined ? "" : formatHeading(points[0].xValue),
|
|
277
|
+
...points.flatMap(({ datum }) => datum.y === null ? [] : [`${series[datum.series]?.label}: ${formatValue(datum.y)}`]),
|
|
278
|
+
].join("\n"),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
/** The hovered position: its heading, then each series' value beside its swatch. */
|
|
282
|
+
function SeriesTooltip({ content, points, series, formatValue, }) {
|
|
283
|
+
const heading = typeof content === "string" ? content.split("\n", 1)[0] : undefined;
|
|
284
|
+
return (_jsxs("div", { className: tooltipBody, children: [_jsx("span", { children: heading }), points.flatMap(({ datum }) => datum.y === null
|
|
285
|
+
? []
|
|
286
|
+
: [
|
|
287
|
+
_jsxs("span", { className: tooltipRow, children: [_jsx("span", { className: swatch, style: { background: seriesColor(datum.series) } }), _jsx("span", { children: series[datum.series]?.label }), _jsx("span", { "data-value": "", children: formatValue(datum.y) })] }, datum.series),
|
|
288
|
+
])] }));
|
|
289
|
+
}
|
|
290
|
+
/** Names each series by its color, once there is more than one. */
|
|
291
|
+
function SeriesLegend({ series }) {
|
|
292
|
+
if (series.length < 2)
|
|
293
|
+
return null;
|
|
294
|
+
return (_jsx("div", { className: legend, children: series.map((item, index) => (_jsxs("span", { className: legendItem, title: item.label, children: [_jsx("span", { className: swatch, style: { background: seriesColor(index) } }), _jsx("span", { children: item.label })] }, index))) }));
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* One row per series per timestamp any series sampled, in time order; a series without a sample
|
|
298
|
+
* at that time gets a null, which the line draws as a gap.
|
|
299
|
+
*/
|
|
300
|
+
function timeSeriesRows(series) {
|
|
301
|
+
const times = [...new Set(series.flatMap((item) => item.points.map(([time]) => time)))].sort((left, right) => left - right);
|
|
302
|
+
return series.flatMap((item, seriesIndex) => {
|
|
303
|
+
const values = new Map(item.points.map(([time, value]) => [time, value]));
|
|
304
|
+
return times.map((time) => ({
|
|
305
|
+
key: `${seriesIndex}:${time}`,
|
|
306
|
+
x: time,
|
|
307
|
+
y: values.get(time) ?? null,
|
|
308
|
+
series: seriesIndex,
|
|
309
|
+
}));
|
|
114
310
|
});
|
|
115
|
-
return [...rowsByTime.values()].sort((left, right) => left.time - right.time);
|
|
116
311
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ChatClient } from "../chat/chat-client.js";
|
|
2
|
+
import type { Conversation, ConversationId } from "../chat/contract.js";
|
|
3
|
+
import type { ChatConversationSource, ChatConversationSummary } from "../chat/conversation-source.js";
|
|
4
|
+
export type UseChatClientSourceOptions = {
|
|
5
|
+
client: ChatClient;
|
|
6
|
+
/** Null before the first message; the conversation is created with it and reported. */
|
|
7
|
+
conversationId: ConversationId | null;
|
|
8
|
+
onConversationCreated: (conversation: Conversation) => void;
|
|
9
|
+
};
|
|
10
|
+
/** One conversation of the lib's chat client, polled while its response runs. */
|
|
11
|
+
export declare function useChatClientSource({ client, conversationId, onConversationCreated, }: UseChatClientSourceOptions): ChatConversationSource;
|
|
12
|
+
/** The chat client's conversations, newest first, as `ChatHistory` lists them. */
|
|
13
|
+
export declare function useChatClientConversations(client: ChatClient): {
|
|
14
|
+
conversations: readonly ChatConversationSummary[];
|
|
15
|
+
loading: boolean;
|
|
16
|
+
};
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* The lib's own chat client as a `ChatConversationSource` and a `ChatHistory` list: the
|
|
4
|
+
* conversation's items, the messages on their way, the response's state, and the approvals and
|
|
5
|
+
* questions the agent waits on as cards in its flow. Needs a `QueryClientProvider` above it.
|
|
6
|
+
*/
|
|
7
|
+
import { Fragment, useMemo, useRef, useState } from "react";
|
|
8
|
+
import { css } from "#primitives/css";
|
|
9
|
+
import { describeApprovalRequest } from "../chat/describe-tool-call.js";
|
|
10
|
+
import { useChatConversation } from "../chat/use-chat-conversation.js";
|
|
11
|
+
import { useConversationList } from "../chat/use-conversation-list.js";
|
|
12
|
+
import { Button } from "./button.js";
|
|
13
|
+
import { useCiridaeLocale } from "./locale.js";
|
|
14
|
+
/* A card in the assistant's flow: an approval or a question, with its answers as flat rows. */
|
|
15
|
+
const card = css({
|
|
16
|
+
display: "grid",
|
|
17
|
+
gap: "control.iconGap",
|
|
18
|
+
width: "100%",
|
|
19
|
+
maxWidth: "chat.bubble",
|
|
20
|
+
paddingBlock: "control.block",
|
|
21
|
+
paddingInline: "control.compactInline",
|
|
22
|
+
border: "1px solid token(colors.border.control)",
|
|
23
|
+
borderRadius: "field",
|
|
24
|
+
background: "bg.card",
|
|
25
|
+
fontSize: "caption",
|
|
26
|
+
lineHeight: "log",
|
|
27
|
+
});
|
|
28
|
+
const cardLabel = css({ color: "fg.muted", fontSize: "code", lineHeight: "compact" });
|
|
29
|
+
const argumentList = css({
|
|
30
|
+
display: "grid",
|
|
31
|
+
gridTemplateColumns: "auto minmax(0, 1fr)",
|
|
32
|
+
columnGap: "layout.controlGap",
|
|
33
|
+
margin: "none",
|
|
34
|
+
fontSize: "code",
|
|
35
|
+
lineHeight: "compact",
|
|
36
|
+
"& > dt": { color: "fg.muted", whiteSpace: "nowrap" },
|
|
37
|
+
"& > dd": { margin: "none", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
38
|
+
});
|
|
39
|
+
const cardActions = css({ display: "flex", justifyContent: "flex-end", gap: "layout.controlGap" });
|
|
40
|
+
// Option rows run to the card's edges, split by its hairlines.
|
|
41
|
+
const optionList = css({
|
|
42
|
+
marginInline: "calc(token(spacing.control.compactInline) * -1)",
|
|
43
|
+
marginBlockEnd: "calc(token(spacing.control.block) * -1)",
|
|
44
|
+
"& > button": {
|
|
45
|
+
display: "block",
|
|
46
|
+
width: "100%",
|
|
47
|
+
paddingBlock: "control.compactBlock",
|
|
48
|
+
paddingInline: "control.compactInline",
|
|
49
|
+
border: "0",
|
|
50
|
+
borderTop: "1px solid token(colors.border.control)",
|
|
51
|
+
background: "transparent",
|
|
52
|
+
color: "fg.primary",
|
|
53
|
+
font: "inherit",
|
|
54
|
+
textAlign: "start",
|
|
55
|
+
cursor: "pointer",
|
|
56
|
+
_hover: { background: "bg.hover" },
|
|
57
|
+
_focusVisible: { focusRing: "inset" },
|
|
58
|
+
"&:last-child": { borderBottomLeftRadius: "field", borderBottomRightRadius: "field" },
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
const optionDetail = css({ display: "block", color: "fg.muted", fontSize: "code" });
|
|
62
|
+
/** How many of an approval's arguments show before the rest become "+N". */
|
|
63
|
+
const maximumArgumentRows = 3;
|
|
64
|
+
/** One conversation of the lib's chat client, polled while its response runs. */
|
|
65
|
+
export function useChatClientSource({ client, conversationId, onConversationCreated, }) {
|
|
66
|
+
const { labels } = useCiridaeLocale();
|
|
67
|
+
const state = useChatConversation({ client, conversationId, onConversationCreated });
|
|
68
|
+
const [sendError, setSendError] = useState(null);
|
|
69
|
+
// The uploads the composer holds by id, so a message sends the service's own records.
|
|
70
|
+
const uploadsRef = useRef(new Map());
|
|
71
|
+
const send = (content, attachments) => {
|
|
72
|
+
setSendError(null);
|
|
73
|
+
// The message shows as sending at once, and as not sent with its retry if it fails.
|
|
74
|
+
state.sendMessage(content, attachments).catch((error) => {
|
|
75
|
+
setSendError(error instanceof Error ? error.message : String(error));
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
const toChatAttachment = (attachment) => ({
|
|
79
|
+
id: attachment.id,
|
|
80
|
+
name: attachment.name,
|
|
81
|
+
mediaType: attachment.media_type,
|
|
82
|
+
url: client.getAttachmentUrl(attachment),
|
|
83
|
+
});
|
|
84
|
+
const { response, items, pendingMessages, isResponseActive } = state;
|
|
85
|
+
const status = response?.status === "failed"
|
|
86
|
+
? "failed"
|
|
87
|
+
: response?.status === "cancelled"
|
|
88
|
+
? "stopped"
|
|
89
|
+
: isResponseActive && response?.status !== "requires_action"
|
|
90
|
+
? "running"
|
|
91
|
+
: "idle";
|
|
92
|
+
const failedTurnMessages = response?.status === "failed"
|
|
93
|
+
? items.filter((item) => item.type === "message" && item.role === "user" && item.response_id === response.id)
|
|
94
|
+
: [];
|
|
95
|
+
const lastUserMessage = failedTurnMessages[failedTurnMessages.length - 1];
|
|
96
|
+
const conversationItems = [];
|
|
97
|
+
for (const item of items) {
|
|
98
|
+
const converted = toConversationItem(item, {
|
|
99
|
+
waitingOnUser: response?.id === item.response_id && response.status === "requires_action",
|
|
100
|
+
toChatAttachment,
|
|
101
|
+
answered: labels.chat.answered,
|
|
102
|
+
decided: (decision) => (decision === "allow" ? labels.chat.allowed : labels.chat.denied),
|
|
103
|
+
decideApproval: state.decideApproval,
|
|
104
|
+
answerQuestion: state.answerQuestion,
|
|
105
|
+
});
|
|
106
|
+
if (converted)
|
|
107
|
+
conversationItems.push(converted);
|
|
108
|
+
}
|
|
109
|
+
for (const message of pendingMessages) {
|
|
110
|
+
// A posted message the items already include would show twice until it is discarded.
|
|
111
|
+
if (message.state === "posted" && items.some((item) => isSameMessage(item, message)))
|
|
112
|
+
continue;
|
|
113
|
+
conversationItems.push({
|
|
114
|
+
kind: "message",
|
|
115
|
+
id: message.localId,
|
|
116
|
+
role: "user",
|
|
117
|
+
at: message.sentAt,
|
|
118
|
+
text: message.content,
|
|
119
|
+
attachments: message.attachments.map(toChatAttachment),
|
|
120
|
+
sending: message.state === "sending",
|
|
121
|
+
failure: message.state === "failed"
|
|
122
|
+
? {
|
|
123
|
+
message: sendError ?? labels.chat.notSent,
|
|
124
|
+
retry: () => {
|
|
125
|
+
state.discardPendingMessage(message.localId);
|
|
126
|
+
send(message.content, message.attachments);
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
: undefined,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
items: conversationItems,
|
|
134
|
+
status,
|
|
135
|
+
loading: state.isLoading,
|
|
136
|
+
failureMessage: response?.error?.message,
|
|
137
|
+
retry: lastUserMessage
|
|
138
|
+
? () => send(lastUserMessage.content, lastUserMessage.attachments)
|
|
139
|
+
: undefined,
|
|
140
|
+
send: async ({ text, attachments }) => send(text, attachments.flatMap((attachment) => uploadsRef.current.get(attachment.id) ?? [])),
|
|
141
|
+
upload: async (file) => {
|
|
142
|
+
const attachment = await client.uploadAttachment(file);
|
|
143
|
+
uploadsRef.current.set(attachment.id, attachment);
|
|
144
|
+
return toChatAttachment(attachment);
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/** The chat client's conversations, newest first, as `ChatHistory` lists them. */
|
|
149
|
+
export function useChatClientConversations(client) {
|
|
150
|
+
const list = useConversationList(client);
|
|
151
|
+
const conversations = useMemo(() => (list.data?.pages ?? []).flatMap((page) => page.data.map((conversation) => ({
|
|
152
|
+
id: conversation.id,
|
|
153
|
+
title: conversation.title,
|
|
154
|
+
preview: conversation.preview,
|
|
155
|
+
updatedAt: Date.parse(conversation.updated_at),
|
|
156
|
+
running: conversation.latest_response?.status === "queued" ||
|
|
157
|
+
conversation.latest_response?.status === "in_progress",
|
|
158
|
+
}))), [list.data]);
|
|
159
|
+
return { conversations, loading: list.isPending };
|
|
160
|
+
}
|
|
161
|
+
/** An item as the thread draws it; what the agent runs on its way to a reply stays behind. */
|
|
162
|
+
function toConversationItem(item, { waitingOnUser, toChatAttachment, answered, decided, decideApproval, answerQuestion, }) {
|
|
163
|
+
const at = Date.parse(item.created_at);
|
|
164
|
+
switch (item.type) {
|
|
165
|
+
case "message":
|
|
166
|
+
return {
|
|
167
|
+
kind: "message",
|
|
168
|
+
id: item.id,
|
|
169
|
+
at,
|
|
170
|
+
role: item.role,
|
|
171
|
+
text: item.content,
|
|
172
|
+
attachments: item.attachments.map(toChatAttachment),
|
|
173
|
+
};
|
|
174
|
+
case "approval_request":
|
|
175
|
+
if (item.decision !== null) {
|
|
176
|
+
return {
|
|
177
|
+
kind: "line",
|
|
178
|
+
id: item.id,
|
|
179
|
+
at,
|
|
180
|
+
text: `${describeApprovalRequest(item.tool)} · ${decided(item.decision)}`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return waitingOnUser
|
|
184
|
+
? {
|
|
185
|
+
kind: "card",
|
|
186
|
+
id: item.id,
|
|
187
|
+
at,
|
|
188
|
+
content: _jsx(ApprovalCard, { item: item, onDecide: decideApproval }),
|
|
189
|
+
}
|
|
190
|
+
: null;
|
|
191
|
+
case "question":
|
|
192
|
+
if (item.answers !== null) {
|
|
193
|
+
return {
|
|
194
|
+
kind: "line",
|
|
195
|
+
id: item.id,
|
|
196
|
+
at,
|
|
197
|
+
text: answered(item.answers.flat().filter(Boolean).join(", ")),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return waitingOnUser
|
|
201
|
+
? {
|
|
202
|
+
kind: "card",
|
|
203
|
+
id: item.id,
|
|
204
|
+
at,
|
|
205
|
+
content: _jsx(QuestionCard, { item: item, onAnswer: answerQuestion }),
|
|
206
|
+
}
|
|
207
|
+
: null;
|
|
208
|
+
case "function_call":
|
|
209
|
+
case "function_call_output":
|
|
210
|
+
case "reasoning":
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/** A tool call waiting for the user: what it would do, its arguments, and Deny or Allow. */
|
|
215
|
+
function ApprovalCard({ item, onDecide, }) {
|
|
216
|
+
const { labels } = useCiridaeLocale();
|
|
217
|
+
const rows = listArgumentRows(item.arguments);
|
|
218
|
+
return (_jsxs("div", { className: card, children: [_jsx("span", { children: describeApprovalRequest(item.tool) }), rows.length > 0 && (_jsxs("dl", { className: argumentList, children: [rows.slice(0, maximumArgumentRows).map(([name, value]) => (_jsxs(Fragment, { children: [_jsx("dt", { children: name }), _jsx("dd", { children: value })] }, name))), rows.length > maximumArgumentRows && (_jsxs("dd", { className: cardLabel, children: ["+", rows.length - maximumArgumentRows] }))] })), _jsxs("div", { className: cardActions, children: [_jsx(Button, { variant: "secondary", onClick: () => void onDecide(item, "deny"), children: labels.chat.deny }), _jsx(Button, { onClick: () => void onDecide(item, "allow"), children: labels.chat.allow })] })] }));
|
|
219
|
+
}
|
|
220
|
+
/** The assistant's questions, one option row per answer; picking one answers that question. */
|
|
221
|
+
function QuestionCard({ item, onAnswer, }) {
|
|
222
|
+
const [picked, setPicked] = useState([]);
|
|
223
|
+
const questionIndex = picked.length;
|
|
224
|
+
const question = item.questions[questionIndex];
|
|
225
|
+
if (!question)
|
|
226
|
+
return null;
|
|
227
|
+
const pick = (label) => {
|
|
228
|
+
const answers = [...picked, label];
|
|
229
|
+
if (answers.length === item.questions.length) {
|
|
230
|
+
void onAnswer(item, answers.map((answer) => [answer]));
|
|
231
|
+
}
|
|
232
|
+
setPicked(answers);
|
|
233
|
+
};
|
|
234
|
+
return (_jsxs("div", { className: card, children: [_jsxs("span", { className: cardLabel, children: [question.header, item.questions.length > 1 && ` · ${questionIndex + 1} of ${item.questions.length}`] }), _jsx("span", { children: question.question }), _jsx("div", { className: optionList, children: question.options.map((option) => (_jsxs("button", { type: "button", onClick: () => pick(option.label), children: [option.label, option.description && _jsx("span", { className: optionDetail, children: option.description })] }, option.label))) })] }));
|
|
235
|
+
}
|
|
236
|
+
function isSameMessage(item, message) {
|
|
237
|
+
return (item.type === "message" &&
|
|
238
|
+
item.role === "user" &&
|
|
239
|
+
item.content === message.content &&
|
|
240
|
+
Date.parse(item.created_at) >= message.sentAt - 60000);
|
|
241
|
+
}
|
|
242
|
+
/** A call's JSON arguments as name and value pairs, values shortened to one line. */
|
|
243
|
+
function listArgumentRows(argumentsJson) {
|
|
244
|
+
try {
|
|
245
|
+
const parsed = JSON.parse(argumentsJson);
|
|
246
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
247
|
+
return [];
|
|
248
|
+
return Object.entries(parsed).map(([name, value]) => [
|
|
249
|
+
name.replace(/[_-]+/g, " "),
|
|
250
|
+
typeof value === "string" ? value : JSON.stringify(value),
|
|
251
|
+
]);
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
}
|