@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.
- 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 +89 -0
- package/dist/chat/conversation-source.js +1 -0
- package/dist/chat/describe-tool-call.d.ts +3 -3
- package/dist/chat/describe-tool-call.js +3 -3
- package/dist/chat/message-text.d.ts +1 -1
- package/dist/chat/message-text.js +116 -48
- package/dist/chat/use-conversation-list.d.ts +6 -6
- package/dist/ciridae.css +16 -4
- package/dist/components/activity-feed.d.ts +3 -2
- package/dist/components/activity-feed.js +1 -0
- package/dist/components/agent-work.d.ts +3 -3
- package/dist/components/app-frame.d.ts +91 -0
- package/dist/components/app-frame.js +134 -0
- package/dist/components/app-navigation.d.ts +30 -3
- package/dist/components/app-navigation.js +105 -14
- package/dist/components/badge.d.ts +30 -5
- package/dist/components/badge.js +134 -15
- package/dist/components/brand-icons.d.ts +3 -3
- package/dist/components/bulk-action-bar.d.ts +1 -1
- package/dist/components/bulk-action-bar.js +3 -3
- package/dist/components/button.d.ts +12 -5
- package/dist/components/button.js +77 -44
- package/dist/components/calendar.d.ts +1 -1
- package/dist/components/calendar.js +44 -17
- package/dist/components/centered-page.d.ts +1 -1
- package/dist/components/centered-page.js +2 -2
- package/dist/components/chart.d.ts +22 -2
- package/dist/components/chart.js +251 -51
- package/dist/components/chat-client-source.d.ts +16 -0
- package/dist/components/chat-client-source.js +297 -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 +11 -14
- package/dist/components/chat-sheet-controller.js +3 -3
- package/dist/components/chat-sheet.d.ts +18 -11
- package/dist/components/chat-sheet.js +188 -289
- package/dist/components/chat.d.ts +109 -19
- package/dist/components/chat.js +520 -124
- package/dist/components/checkbox-item.d.ts +6 -3
- package/dist/components/checkbox-item.js +9 -5
- package/dist/components/checkbox-list.d.ts +1 -1
- package/dist/components/chip.d.ts +6 -1
- package/dist/components/chip.js +27 -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/column-settings.js +1 -1
- package/dist/components/command-menu.d.ts +5 -1
- package/dist/components/command-menu.js +98 -32
- package/dist/components/command-palette.d.ts +7 -3
- package/dist/components/command-palette.js +20 -11
- package/dist/components/copyable-id.d.ts +22 -5
- package/dist/components/copyable-id.js +46 -6
- package/dist/components/data-display.d.ts +39 -8
- package/dist/components/data-display.js +138 -19
- package/dist/components/data-grid.d.ts +23 -11
- package/dist/components/data-grid.js +61 -26
- package/dist/components/date-range-picker.d.ts +1 -1
- package/dist/components/date-range-picker.js +9 -20
- 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 +11 -6
- package/dist/components/drawer.js +91 -54
- package/dist/components/dropzone.d.ts +4 -4
- package/dist/components/editable-field.d.ts +12 -3
- package/dist/components/editable-field.js +57 -27
- package/dist/components/email-input.d.ts +1 -1
- package/dist/components/email-input.js +6 -5
- package/dist/components/environment-frame.d.ts +2 -2
- package/dist/components/environment-frame.js +37 -6
- 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 +43 -29
- package/dist/components/form-controls.d.ts +12 -3
- package/dist/components/form-controls.js +46 -22
- package/dist/components/form-layout.d.ts +14 -10
- package/dist/components/form-layout.js +68 -23
- package/dist/components/glass-menu.d.ts +3 -0
- package/dist/components/glass-menu.js +63 -0
- package/dist/components/gradient-edge.d.ts +9 -0
- package/dist/components/gradient-edge.js +31 -0
- package/dist/components/hotkeys.d.ts +2 -2
- package/dist/components/icon.d.ts +1 -1
- package/dist/components/input.d.ts +7 -5
- package/dist/components/input.js +29 -17
- package/dist/components/keyboard-shortcuts.d.ts +2 -2
- package/dist/components/keycap.d.ts +10 -6
- package/dist/components/keycap.js +57 -19
- package/dist/components/layout.d.ts +13 -13
- package/dist/components/lightbox.d.ts +9 -3
- package/dist/components/lightbox.js +13 -9
- package/dist/components/link.d.ts +1 -1
- package/dist/components/locale.d.ts +42 -2
- package/dist/components/locale.js +43 -3
- package/dist/components/log-entry.d.ts +40 -0
- package/dist/components/log-entry.js +146 -0
- package/dist/components/log-list.d.ts +3 -3
- package/dist/components/mention-input.d.ts +2 -2
- package/dist/components/mention-input.js +4 -3
- package/dist/components/menu.d.ts +7 -3
- package/dist/components/menu.js +34 -37
- package/dist/components/meter.d.ts +10 -2
- package/dist/components/meter.js +64 -6
- package/dist/components/navigation.d.ts +21 -4
- package/dist/components/navigation.js +84 -19
- package/dist/components/notification-tray.d.ts +1 -1
- package/dist/components/notification-tray.js +5 -4
- package/dist/components/overlays.d.ts +6 -4
- package/dist/components/overlays.js +69 -40
- package/dist/components/page-frame.d.ts +4 -4
- package/dist/components/page-frame.js +1 -1
- package/dist/components/panel.d.ts +4 -4
- package/dist/components/panel.js +18 -12
- 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/radio-card.js +10 -9
- package/dist/components/record.d.ts +29 -5
- package/dist/components/record.js +147 -18
- 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 +4 -2
- package/dist/components/segmented-bar.js +16 -6
- package/dist/components/select.d.ts +9 -6
- package/dist/components/select.js +103 -127
- package/dist/components/settings.d.ts +4 -4
- package/dist/components/shell-skeleton.d.ts +1 -1
- package/dist/components/shell-skeleton.js +1 -1
- package/dist/components/shell.d.ts +21 -21
- package/dist/components/shell.js +43 -43
- package/dist/components/side-panel.d.ts +3 -3
- package/dist/components/sidebar.d.ts +13 -8
- package/dist/components/sidebar.js +55 -27
- package/dist/components/single-line-text.d.ts +2 -0
- package/dist/components/single-line-text.js +16 -0
- package/dist/components/site-banner.d.ts +5 -2
- package/dist/components/site-banner.js +17 -6
- package/dist/components/skeleton.d.ts +8 -7
- package/dist/components/skeleton.js +28 -16
- package/dist/components/skeletons.d.ts +17 -5
- package/dist/components/skeletons.js +71 -8
- package/dist/components/stages.d.ts +83 -11
- package/dist/components/stages.js +512 -156
- package/dist/components/stat-card.d.ts +12 -4
- package/dist/components/stat-card.js +92 -37
- package/dist/components/status-banner.d.ts +1 -1
- package/dist/components/status-banner.js +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/tabs.js +26 -13
- package/dist/components/theme-customizer.d.ts +2 -2
- package/dist/components/theme-toggle.d.ts +4 -0
- package/dist/components/theme-toggle.js +67 -0
- package/dist/components/toast.d.ts +10 -3
- package/dist/components/toast.js +78 -29
- 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/preset.js +289 -33
- package/dist/styled-system/styles.css +3294 -1079
- package/dist/styled-system/tokens/index.mjs +598 -22
- package/dist/styled-system/tokens/tokens.d.ts +11 -11
- package/dist/styled-system/types/prop-type.d.ts +2 -2
- package/dist/styles.css +16 -4
- package/dist/theme/color.d.ts +3 -0
- package/dist/theme/color.js +5 -0
- package/dist/theme/derive.d.ts +9 -2
- package/dist/theme/derive.js +72 -12
- package/dist/theme/palettes.d.ts +49 -6
- package/dist/theme/palettes.js +84 -15
- package/dist/theme/tokens.d.ts +3 -3
- package/dist/theme/tokens.js +107 -32
- package/package.json +40 -3
package/dist/components/chart.js
CHANGED
|
@@ -1,37 +1,51 @@
|
|
|
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",
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
"
|
|
21
|
-
|
|
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
|
-
|
|
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.
|
|
34
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
+
}
|