@ciridae/ui 1.0.2 → 1.0.4

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.
Files changed (209) hide show
  1. package/dist/app/auth.d.ts +10 -0
  2. package/dist/app/auth.js +60 -0
  3. package/dist/app/error-boundary.d.ts +18 -0
  4. package/dist/app/error-boundary.js +45 -0
  5. package/dist/app/feedback.d.ts +12 -0
  6. package/dist/app/feedback.js +128 -0
  7. package/dist/app/index.d.ts +8 -0
  8. package/dist/app/index.js +8 -0
  9. package/dist/app/request-errors.d.ts +63 -0
  10. package/dist/app/request-errors.js +172 -0
  11. package/dist/app/sentry.d.ts +18 -0
  12. package/dist/app/sentry.js +52 -0
  13. package/dist/app/telemetry.d.ts +7 -0
  14. package/dist/app/telemetry.js +28 -0
  15. package/dist/app/trail.d.ts +9 -0
  16. package/dist/app/trail.js +18 -0
  17. package/dist/chat/chat-query-keys.d.ts +3 -3
  18. package/dist/chat/contract.d.ts +15 -15
  19. package/dist/chat/conversation-items-query-options.d.ts +114 -114
  20. package/dist/chat/conversation-source.d.ts +89 -0
  21. package/dist/chat/conversation-source.js +1 -0
  22. package/dist/chat/describe-tool-call.d.ts +3 -3
  23. package/dist/chat/describe-tool-call.js +3 -3
  24. package/dist/chat/message-text.d.ts +1 -1
  25. package/dist/chat/message-text.js +116 -48
  26. package/dist/chat/use-conversation-list.d.ts +6 -6
  27. package/dist/ciridae.css +16 -4
  28. package/dist/components/activity-feed.d.ts +3 -2
  29. package/dist/components/activity-feed.js +1 -0
  30. package/dist/components/agent-work.d.ts +3 -3
  31. package/dist/components/app-frame.d.ts +91 -0
  32. package/dist/components/app-frame.js +134 -0
  33. package/dist/components/app-navigation.d.ts +30 -3
  34. package/dist/components/app-navigation.js +105 -14
  35. package/dist/components/badge.d.ts +30 -5
  36. package/dist/components/badge.js +134 -15
  37. package/dist/components/brand-icons.d.ts +3 -3
  38. package/dist/components/bulk-action-bar.d.ts +1 -1
  39. package/dist/components/bulk-action-bar.js +3 -3
  40. package/dist/components/button.d.ts +12 -5
  41. package/dist/components/button.js +77 -44
  42. package/dist/components/calendar.d.ts +1 -1
  43. package/dist/components/calendar.js +44 -17
  44. package/dist/components/centered-page.d.ts +1 -1
  45. package/dist/components/centered-page.js +2 -2
  46. package/dist/components/chart.d.ts +22 -2
  47. package/dist/components/chart.js +251 -51
  48. package/dist/components/chat-client-source.d.ts +16 -0
  49. package/dist/components/chat-client-source.js +297 -0
  50. package/dist/components/chat-page.d.ts +9 -8
  51. package/dist/components/chat-page.js +7 -11
  52. package/dist/components/chat-sheet-controller.d.ts +11 -14
  53. package/dist/components/chat-sheet-controller.js +3 -3
  54. package/dist/components/chat-sheet.d.ts +18 -11
  55. package/dist/components/chat-sheet.js +188 -289
  56. package/dist/components/chat.d.ts +109 -19
  57. package/dist/components/chat.js +520 -124
  58. package/dist/components/checkbox-item.d.ts +6 -3
  59. package/dist/components/checkbox-item.js +9 -5
  60. package/dist/components/checkbox-list.d.ts +1 -1
  61. package/dist/components/chip.d.ts +6 -1
  62. package/dist/components/chip.js +27 -1
  63. package/dist/components/collapsible.d.ts +1 -1
  64. package/dist/components/color-theme.d.ts +1 -1
  65. package/dist/components/column-settings.d.ts +1 -1
  66. package/dist/components/column-settings.js +1 -1
  67. package/dist/components/command-menu.d.ts +5 -1
  68. package/dist/components/command-menu.js +98 -32
  69. package/dist/components/command-palette.d.ts +7 -3
  70. package/dist/components/command-palette.js +20 -11
  71. package/dist/components/copyable-id.d.ts +22 -5
  72. package/dist/components/copyable-id.js +46 -6
  73. package/dist/components/data-display.d.ts +39 -8
  74. package/dist/components/data-display.js +138 -19
  75. package/dist/components/data-grid.d.ts +23 -11
  76. package/dist/components/data-grid.js +61 -26
  77. package/dist/components/date-range-picker.d.ts +1 -1
  78. package/dist/components/date-range-picker.js +9 -20
  79. package/dist/components/date-time.d.ts +1 -1
  80. package/dist/components/dialog-height-context.d.ts +1 -1
  81. package/dist/components/document-viewer.d.ts +2 -2
  82. package/dist/components/drawer.d.ts +11 -6
  83. package/dist/components/drawer.js +91 -54
  84. package/dist/components/dropzone.d.ts +4 -4
  85. package/dist/components/editable-field.d.ts +12 -3
  86. package/dist/components/editable-field.js +57 -27
  87. package/dist/components/email-input.d.ts +1 -1
  88. package/dist/components/email-input.js +6 -5
  89. package/dist/components/environment-frame.d.ts +2 -2
  90. package/dist/components/environment-frame.js +37 -6
  91. package/dist/components/expandable-text.d.ts +1 -1
  92. package/dist/components/feedback-screenshot.d.ts +1 -1
  93. package/dist/components/feedback-sheet.d.ts +18 -3
  94. package/dist/components/feedback-sheet.js +216 -55
  95. package/dist/components/field-context.d.ts +1 -1
  96. package/dist/components/filter-bar.d.ts +3 -3
  97. package/dist/components/filter-bar.js +43 -29
  98. package/dist/components/form-controls.d.ts +12 -3
  99. package/dist/components/form-controls.js +46 -22
  100. package/dist/components/form-layout.d.ts +14 -10
  101. package/dist/components/form-layout.js +68 -23
  102. package/dist/components/glass-menu.d.ts +3 -0
  103. package/dist/components/glass-menu.js +63 -0
  104. package/dist/components/gradient-edge.d.ts +9 -0
  105. package/dist/components/gradient-edge.js +31 -0
  106. package/dist/components/hotkeys.d.ts +2 -2
  107. package/dist/components/icon.d.ts +1 -1
  108. package/dist/components/input.d.ts +7 -5
  109. package/dist/components/input.js +29 -17
  110. package/dist/components/keyboard-shortcuts.d.ts +2 -2
  111. package/dist/components/keycap.d.ts +10 -6
  112. package/dist/components/keycap.js +57 -19
  113. package/dist/components/layout.d.ts +13 -13
  114. package/dist/components/lightbox.d.ts +9 -3
  115. package/dist/components/lightbox.js +13 -9
  116. package/dist/components/link.d.ts +1 -1
  117. package/dist/components/locale.d.ts +42 -2
  118. package/dist/components/locale.js +43 -3
  119. package/dist/components/log-entry.d.ts +40 -0
  120. package/dist/components/log-entry.js +146 -0
  121. package/dist/components/log-list.d.ts +3 -3
  122. package/dist/components/mention-input.d.ts +2 -2
  123. package/dist/components/mention-input.js +4 -3
  124. package/dist/components/menu.d.ts +7 -3
  125. package/dist/components/menu.js +34 -37
  126. package/dist/components/meter.d.ts +10 -2
  127. package/dist/components/meter.js +64 -6
  128. package/dist/components/navigation.d.ts +21 -4
  129. package/dist/components/navigation.js +84 -19
  130. package/dist/components/notification-tray.d.ts +1 -1
  131. package/dist/components/notification-tray.js +5 -4
  132. package/dist/components/overlays.d.ts +6 -4
  133. package/dist/components/overlays.js +69 -40
  134. package/dist/components/page-frame.d.ts +4 -4
  135. package/dist/components/page-frame.js +1 -1
  136. package/dist/components/panel.d.ts +4 -4
  137. package/dist/components/panel.js +18 -12
  138. package/dist/components/profile-header.d.ts +1 -1
  139. package/dist/components/progress.d.ts +1 -1
  140. package/dist/components/project-avatar.d.ts +1 -1
  141. package/dist/components/radio-card.d.ts +2 -2
  142. package/dist/components/radio-card.js +10 -9
  143. package/dist/components/record.d.ts +29 -5
  144. package/dist/components/record.js +147 -18
  145. package/dist/components/resolved-file-url.d.ts +19 -0
  146. package/dist/components/resolved-file-url.js +49 -0
  147. package/dist/components/scroll-area.d.ts +1 -1
  148. package/dist/components/secret-dialog.d.ts +1 -1
  149. package/dist/components/segmented-bar.d.ts +4 -2
  150. package/dist/components/segmented-bar.js +16 -6
  151. package/dist/components/select.d.ts +9 -6
  152. package/dist/components/select.js +103 -127
  153. package/dist/components/settings.d.ts +4 -4
  154. package/dist/components/shell-skeleton.d.ts +1 -1
  155. package/dist/components/shell-skeleton.js +1 -1
  156. package/dist/components/shell.d.ts +21 -21
  157. package/dist/components/shell.js +43 -43
  158. package/dist/components/side-panel.d.ts +3 -3
  159. package/dist/components/sidebar.d.ts +13 -8
  160. package/dist/components/sidebar.js +55 -27
  161. package/dist/components/single-line-text.d.ts +2 -0
  162. package/dist/components/single-line-text.js +16 -0
  163. package/dist/components/site-banner.d.ts +5 -2
  164. package/dist/components/site-banner.js +17 -6
  165. package/dist/components/skeleton.d.ts +8 -7
  166. package/dist/components/skeleton.js +28 -16
  167. package/dist/components/skeletons.d.ts +17 -5
  168. package/dist/components/skeletons.js +71 -8
  169. package/dist/components/stages.d.ts +83 -11
  170. package/dist/components/stages.js +512 -156
  171. package/dist/components/stat-card.d.ts +12 -4
  172. package/dist/components/stat-card.js +92 -37
  173. package/dist/components/status-banner.d.ts +1 -1
  174. package/dist/components/status-banner.js +1 -1
  175. package/dist/components/status.d.ts +2 -2
  176. package/dist/components/steps.d.ts +1 -1
  177. package/dist/components/switch.d.ts +1 -1
  178. package/dist/components/tabs.d.ts +8 -8
  179. package/dist/components/tabs.js +26 -13
  180. package/dist/components/theme-customizer.d.ts +2 -2
  181. package/dist/components/theme-toggle.d.ts +4 -0
  182. package/dist/components/theme-toggle.js +67 -0
  183. package/dist/components/toast.d.ts +10 -3
  184. package/dist/components/toast.js +78 -29
  185. package/dist/components/use-new-version-available.d.ts +6 -0
  186. package/dist/components/use-new-version-available.js +29 -0
  187. package/dist/components/user-chip.d.ts +3 -3
  188. package/dist/components/wizard.d.ts +2 -2
  189. package/dist/feedback/contract.d.ts +136 -136
  190. package/dist/feedback/feedback-controller.d.ts +6 -6
  191. package/dist/feedback/generated/zod.gen.d.ts +28 -28
  192. package/dist/feedback/operator-contract.d.ts +11 -11
  193. package/dist/feedback/operator-controller.d.ts +32 -13
  194. package/dist/feedback/operator-controller.js +44 -11
  195. package/dist/preset.js +289 -33
  196. package/dist/styled-system/styles.css +3294 -1079
  197. package/dist/styled-system/tokens/index.mjs +598 -22
  198. package/dist/styled-system/tokens/tokens.d.ts +11 -11
  199. package/dist/styled-system/types/prop-type.d.ts +2 -2
  200. package/dist/styles.css +16 -4
  201. package/dist/theme/color.d.ts +3 -0
  202. package/dist/theme/color.js +5 -0
  203. package/dist/theme/derive.d.ts +9 -2
  204. package/dist/theme/derive.js +72 -12
  205. package/dist/theme/palettes.d.ts +49 -6
  206. package/dist/theme/palettes.js +84 -15
  207. package/dist/theme/tokens.d.ts +3 -3
  208. package/dist/theme/tokens.js +107 -32
  209. package/package.json +40 -3
@@ -1,37 +1,51 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts";
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
- // Recharts axes default to 500; keep series above them and below the hover cursor at 1100.
7
- const seriesLineZIndex = 600;
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",
13
- "--series-0": "token(colors.accent.blue)",
14
- "--series-1": "token(colors.accent.green)",
15
- "--series-2": "token(colors.accent.yellow)",
16
- "--series-3": "token(colors.accent.bronze)",
17
- "--series-4": "token(colors.accent.red)",
20
+ color: "fg.muted",
21
+ fontFamily: "numeric",
22
+ fontSize: "caption",
23
+ // The series that matters is the trend's blue, a bar the lighter bar blue; the rest take the
24
+ // chart's four categories in their fixed order.
25
+ "--series-0": "token(colors.chart.trend)",
26
+ "--series-1": "token(colors.chart.category1)",
27
+ "--series-2": "token(colors.chart.category2)",
28
+ "--series-3": "token(colors.chart.category3)",
29
+ "--series-4": "token(colors.chart.category4)",
30
+ "&[data-variant=bar]": { "--series-0": "token(colors.chart.bar)" },
18
31
  "--chart-grid": "token(colors.border.subtle)",
19
32
  "--chart-muted": "token(colors.fg.muted)",
20
- "& .recharts-cartesian-axis-tick-value": {
21
- fill: "fg.muted",
22
- fontFamily: "numeric",
23
- fontSize: "caption",
24
- },
25
- "& .recharts-surface": { overflow: "visible" },
33
+ "--chart-cursor": "token(colors.bg.hover)",
34
+ "& .ts-chart-host": { overflow: "visible" },
26
35
  });
27
- const tooltip = css({
36
+ // The tooltip's own React body replaces the chart's default chrome, so it draws the popover's
37
+ // glass itself.
38
+ const tooltipBody = css({
28
39
  display: "grid",
29
40
  gap: "control.trackInset",
30
41
  padding: "panel.compact",
31
42
  border: "1px solid token(colors.border.control)",
32
43
  borderRadius: "action",
33
- background: "bg.card",
34
- boxShadow: "card",
44
+ background: "bg.popover",
45
+ backdropFilter: "blur(token(blurs.popover))",
46
+ boxShadow: "soft",
47
+ color: "fg.primary",
48
+ fontFamily: "sans",
35
49
  fontSize: "caption",
36
50
  });
37
51
  const tooltipRow = css({
@@ -50,6 +64,7 @@ const legend = css({
50
64
  flexWrap: "wrap",
51
65
  gap: "layout.controlGap",
52
66
  color: "fg.muted",
67
+ fontFamily: "sans",
53
68
  fontSize: "caption",
54
69
  });
55
70
  const legendItem = css({
@@ -72,45 +87,230 @@ const swatch = css({
72
87
  borderRadius: "round",
73
88
  });
74
89
  const seriesColor = (index) => `var(--series-${index % seriesColorCount})`;
75
- // Positional keys: a label such as a pod name may hold dots, which recharts reads as a path.
76
- const seriesKey = (index) => `series${index}`;
90
+ const chartTheme = {
91
+ foreground: "var(--chart-muted)",
92
+ muted: "var(--chart-muted)",
93
+ grid: "var(--chart-grid)",
94
+ background: "transparent",
95
+ palette: Array.from({ length: seriesColorCount }, (_, index) => seriesColor(index)),
96
+ };
97
+ const gridLine = { stroke: "var(--chart-grid)", strokeOpacity: 1 };
98
+ /** The value axis' domain: from zero, or the lowest value below it, to the highest. */
99
+ function valueDomain(rows) {
100
+ const values = rows.flatMap((row) => (row.y === null ? [] : [row.y]));
101
+ return [Math.min(0, ...values), Math.max(0, ...values, 1)];
102
+ }
103
+ /**
104
+ * Without decimals, asks a short scale for no more ticks than whole numbers it spans, so 0 to 2
105
+ * reads 0, 1, 2 rather than halves.
106
+ */
107
+ function valueTickCount(domain, allowDecimals) {
108
+ if (allowDecimals)
109
+ return undefined;
110
+ const span = Math.ceil(domain[1] - domain[0]);
111
+ return span < 5 ? Math.max(span, 1) : undefined;
112
+ }
77
113
  /**
78
114
  * Lines over a fixed time window. Samples from different series are joined on their timestamp, so
79
115
  * a series missing a sample leaves a gap rather than a line drawn through it.
80
116
  */
81
117
  export function TimeSeriesChart({ series, start, end, formatValue, formatTimestamp, height = 220, allowDecimals = true, className, ...props }) {
82
- const rows = chartRows(series);
83
118
  const { formatDateTime, formatTime } = useCiridaeLocale();
84
- const spansDays = end - start > 24 * 60 * 60;
85
- const formatTick = (unixSeconds) => spansDays
86
- ? formatDateTime(unixSeconds * 1000, {
87
- month: "short",
88
- day: "numeric",
89
- hour: "2-digit",
90
- minute: "2-digit",
91
- })
92
- : formatTime(unixSeconds * 1000, { hour: "2-digit", minute: "2-digit" });
93
- return (_jsxs("div", { ...props, className: cx(frame, className), "data-ciridae-ui": "", children: [_jsx(ResponsiveContainer, { width: "100%", height: height, children: _jsxs(LineChart, { data: rows, margin: { top: 4, right: 4, bottom: 0, left: 0 }, children: [_jsx(CartesianGrid, { stroke: "var(--chart-grid)", vertical: false }), _jsx(XAxis, { dataKey: "time", type: "number", domain: [start, end], scale: "time", tickFormatter: formatTimestamp ?? formatTick, stroke: "var(--chart-grid)", tickLine: false, minTickGap: 48 }), _jsx(YAxis, { allowDecimals: allowDecimals, tickFormatter: formatValue, stroke: "var(--chart-grid)", tickLine: false, axisLine: false, width: "auto" }), _jsx(Tooltip, { cursor: { stroke: "var(--chart-muted)", strokeDasharray: "3 3" }, isAnimationActive: false, content: ({ active, label, payload }) => active && payload && payload.length > 0 ? (_jsxs("div", { className: tooltip, children: [_jsx("span", { children: formatTimestamp
94
- ? formatTimestamp(Number(label))
95
- : formatDateTime(Number(label) * 1000, {
96
- month: "short",
97
- day: "numeric",
98
- hour: "2-digit",
99
- minute: "2-digit",
100
- second: "2-digit",
101
- }) }), payload.map((item) => {
102
- const index = Number(String(item.dataKey).replace("series", ""));
103
- return (_jsxs("span", { className: tooltipRow, children: [_jsx("span", { className: swatch, style: { background: seriesColor(index) } }), _jsx("span", { children: series[index]?.label }), _jsx("span", { "data-value": "", children: formatValue(Number(item.value)) })] }, String(item.dataKey)));
104
- })] })) : null }), series.map((item, index) => (_jsx(Line, { dataKey: seriesKey(index), name: item.label, stroke: seriesColor(index), strokeWidth: 1.5, zIndex: seriesLineZIndex, dot: false, isAnimationActive: false }, item.label)))] }) }), series.length > 1 && (_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 })] }, item.label))) }))] }));
119
+ const definition = useMemo(() => {
120
+ const formatHeading = (unixSeconds) => formatTimestamp
121
+ ? formatTimestamp(unixSeconds)
122
+ : formatDateTime(unixSeconds * 1000, {
123
+ month: "short",
124
+ day: "numeric",
125
+ hour: "2-digit",
126
+ minute: "2-digit",
127
+ second: "2-digit",
128
+ });
129
+ const rows = timeSeriesRows(series);
130
+ const domain = valueDomain(rows);
131
+ const spansDays = end - start > 24 * 60 * 60;
132
+ const formatTick = formatTimestamp ??
133
+ ((unixSeconds) => spansDays
134
+ ? formatDateTime(unixSeconds * 1000, {
135
+ month: "short",
136
+ day: "numeric",
137
+ hour: "2-digit",
138
+ minute: "2-digit",
139
+ })
140
+ : formatTime(unixSeconds * 1000, { hour: "2-digit", minute: "2-digit" }));
141
+ return defineChart({
142
+ marks: [
143
+ lineY(rows, {
144
+ key: "key",
145
+ x: "x",
146
+ y: "y",
147
+ z: "series",
148
+ color: "series",
149
+ strokeWidth: 1.5,
150
+ }),
151
+ crosshair({
152
+ y: false,
153
+ stroke: "var(--chart-muted)",
154
+ strokeOpacity: 1,
155
+ strokeWidth: 1,
156
+ strokeDasharray: "3 3",
157
+ }),
158
+ ],
159
+ scales: {
160
+ x: {
161
+ scale: scaleLinear().domain([start, end]),
162
+ axis: {
163
+ line: gridLine,
164
+ ticks: { size: 0, spacing: 96, format: formatTick },
165
+ tickLabels: { thin: true },
166
+ },
167
+ },
168
+ y: {
169
+ scale: scaleLinear().domain(domain),
170
+ nice: true,
171
+ grid: gridLine,
172
+ axis: {
173
+ line: false,
174
+ ticks: {
175
+ size: 0,
176
+ count: valueTickCount(domain, allowDecimals),
177
+ format: formatValue,
178
+ },
179
+ },
180
+ },
181
+ },
182
+ color: { scale: seriesColorScale(series.length) },
183
+ theme: chartTheme,
184
+ focus: "group-x",
185
+ maxFocusDistance: Number.POSITIVE_INFINITY,
186
+ tooltip: seriesTooltip(series, formatValue, formatHeading),
187
+ });
188
+ }, [allowDecimals, end, formatDateTime, formatTime, formatTimestamp, formatValue, series, start]);
189
+ 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
190
  }
106
- function chartRows(series) {
107
- const rowsByTime = new Map();
108
- series.forEach((item, index) => {
109
- for (const [time, value] of item.points) {
110
- const row = rowsByTime.get(time) ?? { time };
111
- row[seriesKey(index)] = value;
112
- rowsByTime.set(time, row);
113
- }
191
+ /**
192
+ * Values by category as bars, side by side per series, or as lines, such as counts per month.
193
+ * Each series takes the next chart colour; a legend names them once there is more than one.
194
+ */
195
+ export function CategoryChart({ categories, series, variant = "bar", formatValue = String, height = 176, allowDecimals = true, className, ...props }) {
196
+ const definition = useMemo(() => {
197
+ const rows = series.flatMap((item, seriesIndex) => categories.map((category, categoryIndex) => ({
198
+ key: `${seriesIndex}:${categoryIndex}`,
199
+ x: category,
200
+ y: item.values[categoryIndex] ?? null,
201
+ series: seriesIndex,
202
+ })));
203
+ const domain = valueDomain(rows);
204
+ const mark = variant === "bar"
205
+ ? barY(rows.filter((row) => row.y !== null), {
206
+ key: "key",
207
+ x: "x",
208
+ y: (row) => row.y ?? 0,
209
+ z: "series",
210
+ color: "series",
211
+ layout: group({ padding: 0.1 }),
212
+ radius: { end: 2 },
213
+ })
214
+ : lineY(rows, {
215
+ key: "key",
216
+ x: "x",
217
+ y: "y",
218
+ z: "series",
219
+ color: "series",
220
+ strokeWidth: 1.5,
221
+ });
222
+ return defineChart({
223
+ marks: [
224
+ mark,
225
+ crosshair(variant === "bar"
226
+ ? { x: { band: { fill: "var(--chart-cursor)", fillOpacity: 1 } }, y: false }
227
+ : {
228
+ y: false,
229
+ stroke: "var(--chart-muted)",
230
+ strokeOpacity: 1,
231
+ strokeWidth: 1,
232
+ strokeDasharray: "3 3",
233
+ }),
234
+ ],
235
+ scales: {
236
+ x: {
237
+ scale: scaleBand()
238
+ .domain(categories)
239
+ .padding(variant === "bar" ? 0.2 : 0),
240
+ axis: { line: gridLine, ticks: { size: 0 }, tickLabels: { thin: true } },
241
+ },
242
+ y: {
243
+ scale: scaleLinear().domain(domain),
244
+ nice: true,
245
+ grid: gridLine,
246
+ axis: {
247
+ line: false,
248
+ ticks: {
249
+ size: 0,
250
+ count: valueTickCount(domain, allowDecimals),
251
+ format: formatValue,
252
+ },
253
+ },
254
+ },
255
+ },
256
+ color: { scale: seriesColorScale(series.length) },
257
+ theme: chartTheme,
258
+ focus: "group-x",
259
+ maxFocusDistance: Number.POSITIVE_INFINITY,
260
+ // The cursor band marks the hovered category; a ring on one bar would single it out.
261
+ focusRing: variant === "line",
262
+ tooltip: seriesTooltip(series, formatValue, String),
263
+ });
264
+ }, [allowDecimals, categories, formatValue, series, variant]);
265
+ return (_jsxs("div", { ...props, className: cx(frame, className), "data-ciridae-ui": "", "data-variant": variant, 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 })] }));
266
+ }
267
+ /** Each series index to its chart colour, in order. */
268
+ function seriesColorScale(count) {
269
+ return scaleOrdinal()
270
+ .domain(Array.from({ length: count }, (_, index) => index))
271
+ .range(Array.from({ length: count }, (_, index) => seriesColor(index)));
272
+ }
273
+ /**
274
+ * The tooltip's plain text, which names the focused position for assistive tech: the heading,
275
+ * then each series' label and value, one per line.
276
+ */
277
+ function seriesTooltip(series, formatValue, formatHeading) {
278
+ return {
279
+ use: chartTooltip,
280
+ formatGroup: (points) => [
281
+ points[0] === undefined ? "" : formatHeading(points[0].xValue),
282
+ ...points.flatMap(({ datum }) => datum.y === null ? [] : [`${series[datum.series]?.label}: ${formatValue(datum.y)}`]),
283
+ ].join("\n"),
284
+ };
285
+ }
286
+ /** The hovered position: its heading, then each series' value beside its swatch. */
287
+ function SeriesTooltip({ content, points, series, formatValue, }) {
288
+ const heading = typeof content === "string" ? content.split("\n", 1)[0] : undefined;
289
+ return (_jsxs("div", { className: tooltipBody, children: [_jsx("span", { children: heading }), points.flatMap(({ datum }) => datum.y === null
290
+ ? []
291
+ : [
292
+ _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),
293
+ ])] }));
294
+ }
295
+ /** Names each series by its color, once there is more than one. */
296
+ function SeriesLegend({ series }) {
297
+ if (series.length < 2)
298
+ return null;
299
+ 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))) }));
300
+ }
301
+ /**
302
+ * One row per series per timestamp any series sampled, in time order; a series without a sample
303
+ * at that time gets a null, which the line draws as a gap.
304
+ */
305
+ function timeSeriesRows(series) {
306
+ const times = [...new Set(series.flatMap((item) => item.points.map(([time]) => time)))].sort((left, right) => left - right);
307
+ return series.flatMap((item, seriesIndex) => {
308
+ const values = new Map(item.points.map(([time, value]) => [time, value]));
309
+ return times.map((time) => ({
310
+ key: `${seriesIndex}:${time}`,
311
+ x: time,
312
+ y: values.get(time) ?? null,
313
+ series: seriesIndex,
314
+ }));
114
315
  });
115
- return [...rowsByTime.values()].sort((left, right) => left.time - right.time);
116
316
  }
@@ -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,297 @@
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 { describeToolCall } 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
+ // How each tool call ended, by its call id; a call with none has not returned yet.
97
+ const callStatuses = new Map(items.flatMap((item) => item.type === "function_call_output" ? [[item.call_id, item.status]] : []));
98
+ const lastItem = items[items.length - 1];
99
+ const conversationItems = [];
100
+ for (const item of items) {
101
+ const converted = toConversationItem(item, {
102
+ waitingOnUser: response?.id === item.response_id && response.status === "requires_action",
103
+ inRunningTurn: status === "running" && response?.id === item.response_id,
104
+ newest: item === lastItem,
105
+ callStatuses,
106
+ toChatAttachment,
107
+ answered: labels.chat.answered,
108
+ decided: (decision) => (decision === "allow" ? labels.chat.allowed : labels.chat.denied),
109
+ decideApproval: state.decideApproval,
110
+ answerQuestion: state.answerQuestion,
111
+ });
112
+ if (converted)
113
+ conversationItems.push(converted);
114
+ }
115
+ for (const message of pendingMessages) {
116
+ // A posted message the items already include would show twice until it is discarded.
117
+ if (message.state === "posted" && items.some((item) => isSameMessage(item, message)))
118
+ continue;
119
+ conversationItems.push({
120
+ kind: "message",
121
+ id: message.localId,
122
+ role: "user",
123
+ at: message.sentAt,
124
+ text: message.content,
125
+ attachments: message.attachments.map(toChatAttachment),
126
+ sending: message.state === "sending",
127
+ failure: message.state === "failed"
128
+ ? {
129
+ message: sendError ?? labels.chat.notSent,
130
+ retry: () => {
131
+ state.discardPendingMessage(message.localId);
132
+ send(message.content, message.attachments);
133
+ },
134
+ }
135
+ : undefined,
136
+ });
137
+ }
138
+ return {
139
+ items: conversationItems,
140
+ status,
141
+ loading: state.isLoading,
142
+ failureMessage: response?.error?.message,
143
+ retry: lastUserMessage
144
+ ? () => send(lastUserMessage.content, lastUserMessage.attachments)
145
+ : undefined,
146
+ send: async ({ text, attachments }) => send(text, attachments.flatMap((attachment) => uploadsRef.current.get(attachment.id) ?? [])),
147
+ upload: async (file) => {
148
+ const attachment = await client.uploadAttachment(file);
149
+ uploadsRef.current.set(attachment.id, attachment);
150
+ return toChatAttachment(attachment);
151
+ },
152
+ };
153
+ }
154
+ /** The chat client's conversations, newest first, as `ChatHistory` lists them. */
155
+ export function useChatClientConversations(client) {
156
+ const list = useConversationList(client);
157
+ const conversations = useMemo(() => (list.data?.pages ?? []).flatMap((page) => page.data.map((conversation) => ({
158
+ id: conversation.id,
159
+ title: conversation.title,
160
+ preview: conversation.preview,
161
+ updatedAt: Date.parse(conversation.updated_at),
162
+ running: conversation.latest_response?.status === "queued" ||
163
+ conversation.latest_response?.status === "in_progress",
164
+ }))), [list.data]);
165
+ return { conversations, loading: list.isPending };
166
+ }
167
+ /**
168
+ * An item as the thread draws it. A tool call is a step named by its tool, with how it ended and
169
+ * neither its arguments nor its output; a thought is a step named by its first line.
170
+ */
171
+ function toConversationItem(item, { waitingOnUser, inRunningTurn, newest, callStatuses, toChatAttachment, answered, decided, decideApproval, answerQuestion, }) {
172
+ const at = Date.parse(item.created_at);
173
+ switch (item.type) {
174
+ case "message":
175
+ return {
176
+ kind: "message",
177
+ id: item.id,
178
+ at,
179
+ role: item.role,
180
+ text: item.content,
181
+ attachments: item.attachments.map(toChatAttachment),
182
+ };
183
+ case "approval_request":
184
+ if (item.decision !== null) {
185
+ return {
186
+ kind: "line",
187
+ id: item.id,
188
+ at,
189
+ text: `${describeToolCall(item.tool)} · ${decided(item.decision)}`,
190
+ };
191
+ }
192
+ return waitingOnUser
193
+ ? {
194
+ kind: "card",
195
+ id: item.id,
196
+ at,
197
+ content: _jsx(ApprovalCard, { item: item, onDecide: decideApproval }),
198
+ }
199
+ : null;
200
+ case "question":
201
+ if (item.answers !== null) {
202
+ return {
203
+ kind: "line",
204
+ id: item.id,
205
+ at,
206
+ text: answered(item.answers.flat().filter(Boolean).join(", ")),
207
+ };
208
+ }
209
+ return waitingOnUser
210
+ ? {
211
+ kind: "card",
212
+ id: item.id,
213
+ at,
214
+ content: _jsx(QuestionCard, { item: item, onAnswer: answerQuestion }),
215
+ }
216
+ : null;
217
+ case "function_call":
218
+ // Only the tool's name and how the call ended: its arguments can hold values, such as a
219
+ // credential or a customer's details, that anyone reading the thread should not see.
220
+ return {
221
+ kind: "step",
222
+ id: item.id,
223
+ at,
224
+ step: {
225
+ kind: "tool",
226
+ title: describeToolCall(item.name),
227
+ // A call that never returned from a finished response did not run to its end.
228
+ status: callStatuses.get(item.call_id) ?? (inRunningTurn ? "running" : "failed"),
229
+ },
230
+ };
231
+ case "function_call_output":
232
+ return null;
233
+ case "reasoning": {
234
+ const [title = "", ...rest] = item.content
235
+ .split("\n")
236
+ .map((line) => line.trim())
237
+ .filter(Boolean);
238
+ if (!title)
239
+ return null;
240
+ return {
241
+ kind: "step",
242
+ id: item.id,
243
+ at,
244
+ step: {
245
+ kind: "reasoning",
246
+ // A thought often opens with its gist in bold, as a heading.
247
+ title: title.replace(/^\*\*(.+)\*\*$/, "$1"),
248
+ detail: rest.join(" ") || undefined,
249
+ status: inRunningTurn && newest ? "running" : "completed",
250
+ },
251
+ };
252
+ }
253
+ }
254
+ }
255
+ /** A tool call waiting for the user: what it would do, its arguments, and Deny or Allow. */
256
+ function ApprovalCard({ item, onDecide, }) {
257
+ const { labels } = useCiridaeLocale();
258
+ const rows = listArgumentRows(item.arguments);
259
+ return (_jsxs("div", { className: card, children: [_jsx("span", { children: describeToolCall(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 })] })] }));
260
+ }
261
+ /** The assistant's questions, one option row per answer; picking one answers that question. */
262
+ function QuestionCard({ item, onAnswer, }) {
263
+ const [picked, setPicked] = useState([]);
264
+ const questionIndex = picked.length;
265
+ const question = item.questions[questionIndex];
266
+ if (!question)
267
+ return null;
268
+ const pick = (label) => {
269
+ const answers = [...picked, label];
270
+ if (answers.length === item.questions.length) {
271
+ void onAnswer(item, answers.map((answer) => [answer]));
272
+ }
273
+ setPicked(answers);
274
+ };
275
+ 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))) })] }));
276
+ }
277
+ function isSameMessage(item, message) {
278
+ return (item.type === "message" &&
279
+ item.role === "user" &&
280
+ item.content === message.content &&
281
+ Date.parse(item.created_at) >= message.sentAt - 60000);
282
+ }
283
+ /** A call's JSON arguments as name and value pairs, values shortened to one line. */
284
+ function listArgumentRows(argumentsJson) {
285
+ try {
286
+ const parsed = JSON.parse(argumentsJson);
287
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
288
+ return [];
289
+ return Object.entries(parsed).map(([name, value]) => [
290
+ name.replace(/[_-]+/g, " "),
291
+ typeof value === "string" ? value : JSON.stringify(value),
292
+ ]);
293
+ }
294
+ catch {
295
+ return [];
296
+ }
297
+ }