@agent-native/toolkit 0.214.0 → 0.216.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/agentkit/react/chat.d.ts.map +1 -1
- package/dist/app/agentkit/react/chat.js +2 -2
- package/dist/app/agentkit/react/chat.js.map +1 -1
- package/dist/app/agentkit/react/components.d.ts +4 -1
- package/dist/app/agentkit/react/components.d.ts.map +1 -1
- package/dist/app/agentkit/react/components.js +69 -15
- package/dist/app/agentkit/react/components.js.map +1 -1
- package/dist/app/agentkit/react/context.d.ts +7 -1
- package/dist/app/agentkit/react/context.d.ts.map +1 -1
- package/dist/app/agentkit/react/context.js +5 -1
- package/dist/app/agentkit/react/context.js.map +1 -1
- package/dist/app/agentkit/react/index.d.ts +1 -0
- package/dist/app/agentkit/react/index.d.ts.map +1 -1
- package/dist/app/agentkit/react/index.js +1 -0
- package/dist/app/agentkit/react/index.js.map +1 -1
- package/dist/app/agentkit/react/root.d.ts +4 -1
- package/dist/app/agentkit/react/root.d.ts.map +1 -1
- package/dist/app/agentkit/react/root.js +2 -2
- package/dist/app/agentkit/react/root.js.map +1 -1
- package/dist/app/agentkit/react/styles.css +43 -6
- package/dist/app/agentkit/react/transcript-layout.d.ts +51 -0
- package/dist/app/agentkit/react/transcript-layout.d.ts.map +1 -0
- package/dist/app/agentkit/react/transcript-layout.js +34 -0
- package/dist/app/agentkit/react/transcript-layout.js.map +1 -0
- package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.js +3 -3
- package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
- package/dist/app/chat/AgentPanel.d.ts +2 -0
- package/dist/app/chat/AgentPanel.d.ts.map +1 -1
- package/dist/app/chat/AgentPanel.js +6 -2
- package/dist/app/chat/AgentPanel.js.map +1 -1
- package/dist/app/chat/chat/index.d.ts +2 -0
- package/dist/app/chat/chat/index.d.ts.map +1 -1
- package/dist/app/chat/chat/index.js.map +1 -1
- package/dist/app/chat/chat/surface-types.d.ts +7 -0
- package/dist/app/chat/chat/surface-types.d.ts.map +1 -1
- package/dist/app/chat/chat/surface-types.js.map +1 -1
- package/dist/app/chat/chat/tool-call-display.d.ts +7 -2
- package/dist/app/chat/chat/tool-call-display.d.ts.map +1 -1
- package/dist/app/chat/chat/tool-call-display.js +10 -9
- package/dist/app/chat/chat/tool-call-display.js.map +1 -1
- package/dist/app/chat/chat/widgets/DataChartRenderer.d.ts.map +1 -1
- package/dist/app/chat/chat/widgets/DataChartRenderer.js +196 -26
- package/dist/app/chat/chat/widgets/DataChartRenderer.js.map +1 -1
- package/dist/app/chat/chat/widgets/DataChartWidget.d.ts.map +1 -1
- package/dist/app/chat/chat/widgets/DataChartWidget.js +5 -1
- package/dist/app/chat/chat/widgets/DataChartWidget.js.map +1 -1
- package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.d.ts +2 -2
- package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.d.ts.map +1 -1
- package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.js +6 -4
- package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.js.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.js +1 -0
- package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.js +1 -0
- package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
- package/dist/app/i18n/catalogs/en-US.d.ts +1 -0
- package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/en-US.js +1 -0
- package/dist/app/i18n/catalogs/en-US.js.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.js +1 -0
- package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.js +1 -0
- package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.js +1 -0
- package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.js +1 -0
- package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.js +1 -0
- package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.js +1 -0
- package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.js +1 -0
- package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.js +1 -0
- package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
- package/dist/composer/ComposerContextBanner.d.ts +3 -1
- package/dist/composer/ComposerContextBanner.d.ts.map +1 -1
- package/dist/composer/ComposerContextBanner.js +2 -2
- package/dist/composer/ComposerContextBanner.js.map +1 -1
- package/package.json +3 -3
- package/src/app/agentkit/react/chat.tsx +2 -0
- package/src/app/agentkit/react/components.interaction.spec.tsx +107 -0
- package/src/app/agentkit/react/components.spec.tsx +28 -1
- package/src/app/agentkit/react/components.tsx +153 -17
- package/src/app/agentkit/react/context.tsx +12 -0
- package/src/app/agentkit/react/index.ts +1 -0
- package/src/app/agentkit/react/lifecycle.spec.tsx +186 -16
- package/src/app/agentkit/react/root.tsx +7 -0
- package/src/app/agentkit/react/styles.css +43 -6
- package/src/app/agentkit/react/transcript-layout.spec.ts +72 -0
- package/src/app/agentkit/react/transcript-layout.ts +97 -0
- package/src/app/auth/AuthPage.attribution.spec.tsx +6 -0
- package/src/app/chat/AgentKitAssistantChat.spec.tsx +39 -0
- package/src/app/chat/AgentKitAssistantChat.tsx +5 -0
- package/src/app/chat/AgentPanel.page-header-share.spec.tsx +101 -0
- package/src/app/chat/AgentPanel.tsx +14 -2
- package/src/app/chat/chat/index.ts +2 -0
- package/src/app/chat/chat/surface-types.ts +10 -0
- package/src/app/chat/chat/tool-call-display.spec.tsx +83 -0
- package/src/app/chat/chat/tool-call-display.tsx +40 -8
- package/src/app/chat/chat/widgets/DataChartRenderer.spec.tsx +632 -0
- package/src/app/chat/chat/widgets/DataChartRenderer.tsx +308 -51
- package/src/app/chat/chat/widgets/DataChartWidget.spec.tsx +126 -23
- package/src/app/chat/chat/widgets/DataChartWidget.tsx +10 -0
- package/src/app/chat/chat/widgets/DataWidgetDisplayControls.tsx +26 -6
- package/src/app/i18n/catalogs/ar-SA.ts +1 -0
- package/src/app/i18n/catalogs/de-DE.ts +2 -0
- package/src/app/i18n/catalogs/en-US.ts +2 -0
- package/src/app/i18n/catalogs/es-ES.ts +2 -0
- package/src/app/i18n/catalogs/fr-FR.ts +2 -0
- package/src/app/i18n/catalogs/hi-IN.ts +1 -0
- package/src/app/i18n/catalogs/ja-JP.ts +2 -0
- package/src/app/i18n/catalogs/ko-KR.ts +1 -0
- package/src/app/i18n/catalogs/pt-BR.ts +2 -0
- package/src/app/i18n/catalogs/zh-CN.ts +1 -0
- package/src/app/i18n/catalogs/zh-TW.ts +1 -0
- package/src/composer/ComposerContextBanner.spec.tsx +31 -5
- package/src/composer/ComposerContextBanner.tsx +7 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useT } from "@agent-native/core/client/i18n";
|
|
2
|
+
import { DATA_WIDGET_MAX_CHART_POINTS } from "@agent-native/core/data-widgets";
|
|
2
3
|
|
|
3
4
|
import type { DataChartWidget as DataChartWidgetData } from "./data-widget-types.js";
|
|
4
5
|
|
|
@@ -12,7 +13,11 @@ const DEFAULT_COLORS = [
|
|
|
12
13
|
const WIDTH = 640;
|
|
13
14
|
const HEIGHT = 360;
|
|
14
15
|
const MARGIN = { top: 14, right: 18, bottom: 34, left: 46 };
|
|
16
|
+
const Y_AXIS_TITLE_X = 14;
|
|
17
|
+
const VALUE_AXIS_TICK_LABEL_INSET = 8;
|
|
18
|
+
const VALUE_AXIS_TITLE_TICK_GAP = 8;
|
|
15
19
|
const MAX_LABEL_LENGTH = 16;
|
|
20
|
+
const MIN_PLOT_WIDTH = 32;
|
|
16
21
|
const LABEL_FONT_FAMILIES = {
|
|
17
22
|
sans: "var(--font-sans, ui-sans-serif, system-ui, sans-serif)",
|
|
18
23
|
serif: "ui-serif, Georgia, Cambria, serif",
|
|
@@ -38,6 +43,24 @@ function formatTick(value: number): string {
|
|
|
38
43
|
return value.toFixed(1);
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
function formatAxisTick(value: number, interval: number): string {
|
|
47
|
+
const [mantissa, exponent = "0"] = interval
|
|
48
|
+
.toString()
|
|
49
|
+
.toLowerCase()
|
|
50
|
+
.split("e");
|
|
51
|
+
const decimalPlaces = Math.max(
|
|
52
|
+
0,
|
|
53
|
+
(mantissa.split(".")[1]?.length ?? 0) - Number(exponent),
|
|
54
|
+
);
|
|
55
|
+
if (Math.abs(value) >= 1000) {
|
|
56
|
+
return value.toLocaleString(undefined, {
|
|
57
|
+
minimumFractionDigits: decimalPlaces,
|
|
58
|
+
maximumFractionDigits: decimalPlaces,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return value.toFixed(decimalPlaces);
|
|
62
|
+
}
|
|
63
|
+
|
|
41
64
|
function labelFor(value: unknown): string {
|
|
42
65
|
const characters = Array.from(value == null ? "" : String(value));
|
|
43
66
|
return characters.length > MAX_LABEL_LENGTH
|
|
@@ -52,6 +75,53 @@ function estimateTextWidth(text: string, fontSize: number): number {
|
|
|
52
75
|
}, 0);
|
|
53
76
|
}
|
|
54
77
|
|
|
78
|
+
function formatCompactTick(
|
|
79
|
+
value: number,
|
|
80
|
+
fontSize: number,
|
|
81
|
+
maxWidth: number,
|
|
82
|
+
): string {
|
|
83
|
+
for (let fractionDigits = 15; fractionDigits >= 0; fractionDigits -= 1) {
|
|
84
|
+
const [mantissa = "0", exponent = "0"] = value
|
|
85
|
+
.toExponential(fractionDigits)
|
|
86
|
+
.split("e");
|
|
87
|
+
const label = `${mantissa.replace(/\.?0+$/, "")}e${exponent}`;
|
|
88
|
+
if (estimateTextWidth(label, fontSize) <= maxWidth) return label;
|
|
89
|
+
}
|
|
90
|
+
return value.toExponential(0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function maximumHorizontalLabelWidth(
|
|
94
|
+
categoryMargin: number,
|
|
95
|
+
includeValueLabels: boolean,
|
|
96
|
+
): number {
|
|
97
|
+
let lower = 0;
|
|
98
|
+
let upper = WIDTH;
|
|
99
|
+
while (lower < upper) {
|
|
100
|
+
const candidate = Math.ceil((lower + upper) / 2);
|
|
101
|
+
const axisInset = Math.ceil(candidate / 2) + 2;
|
|
102
|
+
const valueLabelInset = includeValueLabels ? candidate + 13 : 0;
|
|
103
|
+
const leftMargin = Math.max(categoryMargin, axisInset) + valueLabelInset;
|
|
104
|
+
const rightMargin = Math.max(MARGIN.right, axisInset, valueLabelInset);
|
|
105
|
+
if (WIDTH - leftMargin - rightMargin >= MIN_PLOT_WIDTH) {
|
|
106
|
+
lower = candidate;
|
|
107
|
+
} else {
|
|
108
|
+
upper = candidate - 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return lower;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function formatHorizontalValueLabel(
|
|
115
|
+
value: number,
|
|
116
|
+
fontSize: number,
|
|
117
|
+
maxWidth: number,
|
|
118
|
+
): string {
|
|
119
|
+
const label = formatTick(value);
|
|
120
|
+
return estimateTextWidth(label, fontSize) > maxWidth
|
|
121
|
+
? formatCompactTick(value, fontSize, maxWidth)
|
|
122
|
+
: label;
|
|
123
|
+
}
|
|
124
|
+
|
|
55
125
|
function rangeFor(chart: DataChartWidgetData) {
|
|
56
126
|
const values = chart.data.flatMap((row) =>
|
|
57
127
|
chart.series
|
|
@@ -81,8 +151,48 @@ function ChartBody({
|
|
|
81
151
|
chart: DataChartWidgetData;
|
|
82
152
|
labelFontSize: number;
|
|
83
153
|
}) {
|
|
154
|
+
const categoryGapRatio = chart.horizontalCategoryGapRatio;
|
|
155
|
+
if (
|
|
156
|
+
categoryGapRatio !== undefined &&
|
|
157
|
+
(!Number.isFinite(categoryGapRatio) ||
|
|
158
|
+
categoryGapRatio < 0 ||
|
|
159
|
+
categoryGapRatio > 0.9)
|
|
160
|
+
) {
|
|
161
|
+
throw new Error("Horizontal category gap ratio must be between 0 and 0.9");
|
|
162
|
+
}
|
|
84
163
|
const { min, max } = rangeFor(chart);
|
|
85
164
|
const isHorizontalBar = chart.type === "bar" && chart.layout === "horizontal";
|
|
165
|
+
const tickInterval = chart.valueAxisTickInterval;
|
|
166
|
+
const gridTickValues =
|
|
167
|
+
tickInterval === undefined
|
|
168
|
+
? Array.from({ length: 4 }, (_, index) => min + ((max - min) * index) / 3)
|
|
169
|
+
: (() => {
|
|
170
|
+
if (!Number.isFinite(tickInterval) || tickInterval < 1e-12) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
"Chart value-axis interval must be finite and positive",
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
if (!Number.isFinite(max - min)) {
|
|
176
|
+
throw new Error("Chart value-axis range must be finite");
|
|
177
|
+
}
|
|
178
|
+
const firstTick = Math.ceil(min / tickInterval - 1e-9);
|
|
179
|
+
const lastTick = Math.floor(max / tickInterval + 1e-9);
|
|
180
|
+
const tickCount = lastTick - firstTick + 1;
|
|
181
|
+
if (
|
|
182
|
+
!Number.isSafeInteger(firstTick) ||
|
|
183
|
+
!Number.isSafeInteger(lastTick) ||
|
|
184
|
+
!Number.isSafeInteger(tickCount) ||
|
|
185
|
+
tickCount <= 0 ||
|
|
186
|
+
tickCount > 100
|
|
187
|
+
) {
|
|
188
|
+
throw new Error("Chart value-axis interval exceeds the tick limit");
|
|
189
|
+
}
|
|
190
|
+
return Array.from(
|
|
191
|
+
{ length: tickCount },
|
|
192
|
+
(_, index) => (firstTick + index) * tickInterval,
|
|
193
|
+
);
|
|
194
|
+
})();
|
|
195
|
+
const axisTitleFontSize = chart.axisTitleFontSize ?? 11;
|
|
86
196
|
const horizontalCategoryWidth = isHorizontalBar
|
|
87
197
|
? Math.max(
|
|
88
198
|
0,
|
|
@@ -91,6 +201,45 @@ function ChartBody({
|
|
|
91
201
|
),
|
|
92
202
|
)
|
|
93
203
|
: 0;
|
|
204
|
+
const categoryMargin =
|
|
205
|
+
Math.max(MARGIN.left, horizontalCategoryWidth + 14) +
|
|
206
|
+
(chart.yAxisTitle ? 20 : 0);
|
|
207
|
+
const categoryLabelX = categoryMargin - 8;
|
|
208
|
+
const horizontalTextWidthLimit = isHorizontalBar
|
|
209
|
+
? maximumHorizontalLabelWidth(
|
|
210
|
+
categoryMargin,
|
|
211
|
+
Boolean(chart.showValueLabels),
|
|
212
|
+
)
|
|
213
|
+
: 0;
|
|
214
|
+
const verticalValueTickMargin = isHorizontalBar
|
|
215
|
+
? 0
|
|
216
|
+
: chart.yAxisTitle
|
|
217
|
+
? Y_AXIS_TITLE_X +
|
|
218
|
+
axisTitleFontSize / 2 +
|
|
219
|
+
VALUE_AXIS_TITLE_TICK_GAP +
|
|
220
|
+
VALUE_AXIS_TICK_LABEL_INSET
|
|
221
|
+
: VALUE_AXIS_TICK_LABEL_INSET * 2;
|
|
222
|
+
const valueTickLabels = gridTickValues.map((value) => {
|
|
223
|
+
const label =
|
|
224
|
+
tickInterval === undefined
|
|
225
|
+
? formatTick(value)
|
|
226
|
+
: formatAxisTick(value, tickInterval);
|
|
227
|
+
const maximumWidth = isHorizontalBar
|
|
228
|
+
? horizontalTextWidthLimit
|
|
229
|
+
: WIDTH - MARGIN.right - MIN_PLOT_WIDTH - verticalValueTickMargin;
|
|
230
|
+
return estimateTextWidth(label, labelFontSize) > maximumWidth
|
|
231
|
+
? formatCompactTick(value, labelFontSize, maximumWidth)
|
|
232
|
+
: label;
|
|
233
|
+
});
|
|
234
|
+
const valueTickLabelWidth = Math.max(
|
|
235
|
+
0,
|
|
236
|
+
...valueTickLabels.map((label) => estimateTextWidth(label, labelFontSize)),
|
|
237
|
+
);
|
|
238
|
+
const customValueTickLabelWidth = isHorizontalBar ? 0 : valueTickLabelWidth;
|
|
239
|
+
const horizontalValueTickLabelInset = isHorizontalBar
|
|
240
|
+
? Math.ceil(valueTickLabelWidth / 2) + 2
|
|
241
|
+
: 0;
|
|
242
|
+
const customValueTickWidth = isHorizontalBar ? 0 : customValueTickLabelWidth;
|
|
94
243
|
const horizontalPositiveValueWidth =
|
|
95
244
|
isHorizontalBar && chart.showValueLabels
|
|
96
245
|
? Math.max(
|
|
@@ -101,7 +250,11 @@ function ChartBody({
|
|
|
101
250
|
.filter((value): value is number => value !== null && value >= 0)
|
|
102
251
|
.map((value) =>
|
|
103
252
|
estimateTextWidth(
|
|
104
|
-
|
|
253
|
+
formatHorizontalValueLabel(
|
|
254
|
+
value,
|
|
255
|
+
chart.valueLabelFontSize ?? 11,
|
|
256
|
+
horizontalTextWidthLimit,
|
|
257
|
+
),
|
|
105
258
|
chart.valueLabelFontSize ?? 11,
|
|
106
259
|
),
|
|
107
260
|
),
|
|
@@ -118,25 +271,33 @@ function ChartBody({
|
|
|
118
271
|
.filter((value): value is number => value !== null && value < 0)
|
|
119
272
|
.map((value) =>
|
|
120
273
|
estimateTextWidth(
|
|
121
|
-
|
|
274
|
+
formatHorizontalValueLabel(
|
|
275
|
+
value,
|
|
276
|
+
chart.valueLabelFontSize ?? 11,
|
|
277
|
+
horizontalTextWidthLimit,
|
|
278
|
+
),
|
|
122
279
|
chart.valueLabelFontSize ?? 11,
|
|
123
280
|
),
|
|
124
281
|
),
|
|
125
282
|
),
|
|
126
283
|
)
|
|
127
284
|
: 0;
|
|
128
|
-
const categoryMargin =
|
|
129
|
-
Math.max(MARGIN.left, horizontalCategoryWidth + 14) +
|
|
130
|
-
(chart.yAxisTitle ? 20 : 0);
|
|
131
|
-
const categoryLabelX = categoryMargin - 8;
|
|
132
285
|
const margin = {
|
|
133
286
|
...MARGIN,
|
|
134
287
|
left:
|
|
135
|
-
|
|
288
|
+
Math.max(
|
|
289
|
+
categoryMargin,
|
|
290
|
+
customValueTickWidth + verticalValueTickMargin,
|
|
291
|
+
horizontalValueTickLabelInset,
|
|
292
|
+
) +
|
|
136
293
|
(horizontalNegativeValueWidth > 0
|
|
137
294
|
? horizontalNegativeValueWidth + 13
|
|
138
295
|
: 0),
|
|
139
|
-
right: Math.max(
|
|
296
|
+
right: Math.max(
|
|
297
|
+
MARGIN.right,
|
|
298
|
+
horizontalPositiveValueWidth + 13,
|
|
299
|
+
horizontalValueTickLabelInset,
|
|
300
|
+
),
|
|
140
301
|
bottom: MARGIN.bottom + (chart.xAxisTitle ? 16 : 0),
|
|
141
302
|
};
|
|
142
303
|
const plotWidth = WIDTH - margin.left - margin.right;
|
|
@@ -151,19 +312,93 @@ function ChartBody({
|
|
|
151
312
|
: margin.left + (index / (chart.data.length - 1)) * plotWidth;
|
|
152
313
|
const yForRow = (index: number) =>
|
|
153
314
|
margin.top + ((index + 0.5) * plotHeight) / Math.max(1, chart.data.length);
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
315
|
+
const axisLabelMinSpacing = isHorizontalBar
|
|
316
|
+
? valueTickLabelWidth + 8
|
|
317
|
+
: labelFontSize + 4;
|
|
318
|
+
const visibleAxisLabelCount = Math.max(
|
|
319
|
+
1,
|
|
320
|
+
Math.floor(
|
|
321
|
+
(isHorizontalBar ? plotWidth : plotHeight) / axisLabelMinSpacing,
|
|
322
|
+
) + 1,
|
|
323
|
+
);
|
|
324
|
+
const axisLabelStride = Math.max(
|
|
325
|
+
1,
|
|
326
|
+
Math.ceil(
|
|
327
|
+
(gridTickValues.length - 1) / Math.max(1, visibleAxisLabelCount - 1),
|
|
328
|
+
),
|
|
329
|
+
);
|
|
330
|
+
const axisLabelIndexes = new Set(
|
|
331
|
+
gridTickValues.flatMap((_, index) =>
|
|
332
|
+
index % axisLabelStride === 0 &&
|
|
333
|
+
(visibleAxisLabelCount > 1 || index === 0)
|
|
334
|
+
? [index]
|
|
335
|
+
: [],
|
|
336
|
+
),
|
|
337
|
+
);
|
|
338
|
+
const finalAxisLabelIndex = gridTickValues.length - 1;
|
|
339
|
+
if (
|
|
340
|
+
visibleAxisLabelCount > 1 &&
|
|
341
|
+
finalAxisLabelIndex >= 0 &&
|
|
342
|
+
!axisLabelIndexes.has(finalAxisLabelIndex)
|
|
343
|
+
) {
|
|
344
|
+
if (isHorizontalBar) {
|
|
345
|
+
axisLabelIndexes.add(finalAxisLabelIndex);
|
|
346
|
+
} else {
|
|
347
|
+
const precedingAxisLabelIndex =
|
|
348
|
+
Math.floor(finalAxisLabelIndex / axisLabelStride) * axisLabelStride;
|
|
349
|
+
const tickSpacing = plotHeight / Math.max(1, finalAxisLabelIndex);
|
|
350
|
+
const finalLabelSpacing =
|
|
351
|
+
(finalAxisLabelIndex - precedingAxisLabelIndex) * tickSpacing;
|
|
352
|
+
if (finalLabelSpacing < axisLabelMinSpacing) {
|
|
353
|
+
axisLabelIndexes.delete(precedingAxisLabelIndex);
|
|
354
|
+
}
|
|
355
|
+
axisLabelIndexes.add(finalAxisLabelIndex);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const axisLabelPosition = (index: number) =>
|
|
359
|
+
isHorizontalBar
|
|
360
|
+
? xForValue(gridTickValues[index]!)
|
|
361
|
+
: yForValue(gridTickValues[index]!);
|
|
362
|
+
while (true) {
|
|
363
|
+
const selectedAxisLabelIndexes = [...axisLabelIndexes].sort(
|
|
364
|
+
(first, second) => first - second,
|
|
365
|
+
);
|
|
366
|
+
const crowdedPair = selectedAxisLabelIndexes.findIndex(
|
|
367
|
+
(currentIndex, position) => {
|
|
368
|
+
if (position === 0) return false;
|
|
369
|
+
const precedingIndex = selectedAxisLabelIndexes[position - 1]!;
|
|
370
|
+
const centerSpacing = Math.abs(
|
|
371
|
+
axisLabelPosition(currentIndex) - axisLabelPosition(precedingIndex),
|
|
372
|
+
);
|
|
373
|
+
const requiredSpacing = isHorizontalBar
|
|
374
|
+
? (estimateTextWidth(valueTickLabels[currentIndex]!, labelFontSize) +
|
|
375
|
+
estimateTextWidth(
|
|
376
|
+
valueTickLabels[precedingIndex]!,
|
|
377
|
+
labelFontSize,
|
|
378
|
+
)) /
|
|
379
|
+
2 +
|
|
380
|
+
8
|
|
381
|
+
: labelFontSize + 4;
|
|
382
|
+
return centerSpacing < requiredSpacing;
|
|
383
|
+
},
|
|
384
|
+
);
|
|
385
|
+
if (crowdedPair < 0) break;
|
|
386
|
+
const precedingIndex = selectedAxisLabelIndexes[crowdedPair - 1]!;
|
|
387
|
+
const currentIndex = selectedAxisLabelIndexes[crowdedPair]!;
|
|
388
|
+
axisLabelIndexes.delete(
|
|
389
|
+
currentIndex === finalAxisLabelIndex ? precedingIndex : currentIndex,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
const gridTicks = gridTickValues.map((value, index) => ({
|
|
393
|
+
value,
|
|
394
|
+
index,
|
|
395
|
+
x: xForValue(value),
|
|
396
|
+
y: yForValue(value),
|
|
397
|
+
}));
|
|
162
398
|
const xTickEvery = Math.max(1, Math.ceil(chart.data.length / 6));
|
|
163
399
|
const zero = Math.max(min, Math.min(max, 0));
|
|
164
400
|
const zeroX = xForValue(zero);
|
|
165
401
|
const zeroY = yForValue(zero);
|
|
166
|
-
const axisTitleFontSize = chart.axisTitleFontSize ?? 11;
|
|
167
402
|
const axisTitleFontWeight = chart.axisTitleFontWeight ?? 500;
|
|
168
403
|
const valueLabelFontSize = chart.valueLabelFontSize ?? 11;
|
|
169
404
|
const valueLabelFontWeight = chart.valueLabelFontWeight ?? 400;
|
|
@@ -178,18 +413,20 @@ function ChartBody({
|
|
|
178
413
|
x2={tick.x}
|
|
179
414
|
y1={margin.top}
|
|
180
415
|
y2={HEIGHT - margin.bottom}
|
|
181
|
-
stroke="hsl(var(--border))"
|
|
182
|
-
strokeDasharray="3 3"
|
|
416
|
+
stroke="hsl(var(--chart-grid, var(--border)))"
|
|
417
|
+
strokeDasharray={tickInterval === undefined ? "3 3" : undefined}
|
|
183
418
|
/>
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
419
|
+
{axisLabelIndexes.has(tick.index) ? (
|
|
420
|
+
<text
|
|
421
|
+
x={tick.x}
|
|
422
|
+
y={HEIGHT - margin.bottom + 14}
|
|
423
|
+
fill="hsl(var(--muted-foreground))"
|
|
424
|
+
fontSize={labelFontSize}
|
|
425
|
+
textAnchor="middle"
|
|
426
|
+
>
|
|
427
|
+
{valueTickLabels[tick.index]}
|
|
428
|
+
</text>
|
|
429
|
+
) : null}
|
|
193
430
|
</g>
|
|
194
431
|
) : (
|
|
195
432
|
<g key={tick.y}>
|
|
@@ -198,18 +435,20 @@ function ChartBody({
|
|
|
198
435
|
x2={WIDTH - margin.right}
|
|
199
436
|
y1={tick.y}
|
|
200
437
|
y2={tick.y}
|
|
201
|
-
stroke="hsl(var(--border))"
|
|
202
|
-
strokeDasharray="3 3"
|
|
438
|
+
stroke="hsl(var(--chart-grid, var(--border)))"
|
|
439
|
+
strokeDasharray={tickInterval === undefined ? "3 3" : undefined}
|
|
203
440
|
/>
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
441
|
+
{axisLabelIndexes.has(tick.index) ? (
|
|
442
|
+
<text
|
|
443
|
+
x={margin.left - VALUE_AXIS_TICK_LABEL_INSET}
|
|
444
|
+
y={tick.y + 4}
|
|
445
|
+
fill="hsl(var(--muted-foreground))"
|
|
446
|
+
fontSize={labelFontSize}
|
|
447
|
+
textAnchor="end"
|
|
448
|
+
>
|
|
449
|
+
{valueTickLabels[tick.index]}
|
|
450
|
+
</text>
|
|
451
|
+
) : null}
|
|
213
452
|
</g>
|
|
214
453
|
),
|
|
215
454
|
)}
|
|
@@ -218,7 +457,7 @@ function ChartBody({
|
|
|
218
457
|
x2={isHorizontalBar ? zeroX : WIDTH - margin.right}
|
|
219
458
|
y1={isHorizontalBar ? margin.top : zeroY}
|
|
220
459
|
y2={isHorizontalBar ? HEIGHT - margin.bottom : zeroY}
|
|
221
|
-
stroke="hsl(var(--border))"
|
|
460
|
+
stroke="hsl(var(--chart-grid, var(--border)))"
|
|
222
461
|
/>
|
|
223
462
|
{chart.data.map((row, index) =>
|
|
224
463
|
isHorizontalBar ? (
|
|
@@ -263,9 +502,9 @@ function ChartBody({
|
|
|
263
502
|
) : null}
|
|
264
503
|
{chart.yAxisTitle ? (
|
|
265
504
|
<text
|
|
266
|
-
x={
|
|
505
|
+
x={Y_AXIS_TITLE_X}
|
|
267
506
|
y={margin.top + plotHeight / 2}
|
|
268
|
-
transform={`rotate(-90
|
|
507
|
+
transform={`rotate(-90 ${Y_AXIS_TITLE_X} ${margin.top + plotHeight / 2})`}
|
|
269
508
|
fill="hsl(var(--foreground))"
|
|
270
509
|
fontSize={axisTitleFontSize}
|
|
271
510
|
fontWeight={axisTitleFontWeight}
|
|
@@ -281,7 +520,11 @@ function ChartBody({
|
|
|
281
520
|
const slotSize =
|
|
282
521
|
(isHorizontalBar ? plotHeight : plotWidth) /
|
|
283
522
|
Math.max(1, chart.data.length);
|
|
284
|
-
const groupSize =
|
|
523
|
+
const groupSize =
|
|
524
|
+
slotSize *
|
|
525
|
+
(isHorizontalBar && categoryGapRatio !== undefined
|
|
526
|
+
? 1 - categoryGapRatio
|
|
527
|
+
: 0.72);
|
|
285
528
|
const barSize = groupSize / Math.max(1, chart.series.length);
|
|
286
529
|
return (
|
|
287
530
|
<>
|
|
@@ -311,7 +554,7 @@ function ChartBody({
|
|
|
311
554
|
? Math.max(2, Math.abs(valueX - zeroX))
|
|
312
555
|
: Math.max(2, barSize - 2);
|
|
313
556
|
const height = isHorizontalBar
|
|
314
|
-
? Math.max(
|
|
557
|
+
? Math.max(0, barSize - Math.min(2, barSize * 0.1))
|
|
315
558
|
: Math.max(1, Math.abs(zeroY - valueY));
|
|
316
559
|
return (
|
|
317
560
|
<g key={`${rowIndex}-${series.key}`}>
|
|
@@ -342,7 +585,13 @@ function ChartBody({
|
|
|
342
585
|
isHorizontalBar ? (value < 0 ? "end" : "start") : "middle"
|
|
343
586
|
}
|
|
344
587
|
>
|
|
345
|
-
{
|
|
588
|
+
{isHorizontalBar
|
|
589
|
+
? formatHorizontalValueLabel(
|
|
590
|
+
value,
|
|
591
|
+
valueLabelFontSize,
|
|
592
|
+
horizontalTextWidthLimit,
|
|
593
|
+
)
|
|
594
|
+
: formatTick(value)}
|
|
346
595
|
</text>
|
|
347
596
|
) : null}
|
|
348
597
|
</g>
|
|
@@ -449,11 +698,19 @@ function ChartBody({
|
|
|
449
698
|
|
|
450
699
|
export function DataChartRenderer({ chart }: { chart: DataChartWidgetData }) {
|
|
451
700
|
const t = useT();
|
|
452
|
-
const
|
|
453
|
-
|
|
701
|
+
const renderedChart =
|
|
702
|
+
chart.data.length > DATA_WIDGET_MAX_CHART_POINTS
|
|
703
|
+
? {
|
|
704
|
+
...chart,
|
|
705
|
+
data: chart.data.slice(0, DATA_WIDGET_MAX_CHART_POINTS),
|
|
706
|
+
}
|
|
707
|
+
: chart;
|
|
708
|
+
const contentHeight = renderedChart.contentHeight;
|
|
709
|
+
const contentPadding = renderedChart.contentPadding ?? 0;
|
|
454
710
|
const hasContentBox = contentHeight !== undefined || contentPadding > 0;
|
|
455
|
-
const labelFontFamily =
|
|
456
|
-
|
|
711
|
+
const labelFontFamily =
|
|
712
|
+
LABEL_FONT_FAMILIES[renderedChart.labelFontFamily ?? "sans"];
|
|
713
|
+
const labelFontSize = renderedChart.labelFontSize ?? 11;
|
|
457
714
|
return (
|
|
458
715
|
<div
|
|
459
716
|
className="box-border w-full min-w-0 overflow-hidden"
|
|
@@ -474,9 +731,9 @@ export function DataChartRenderer({ chart }: { chart: DataChartWidgetData }) {
|
|
|
474
731
|
preserveAspectRatio={contentHeight !== undefined ? "none" : undefined}
|
|
475
732
|
fontFamily={labelFontFamily}
|
|
476
733
|
role="img"
|
|
477
|
-
aria-label={
|
|
734
|
+
aria-label={renderedChart.title ?? t("agentChat.widget.dataChart")}
|
|
478
735
|
>
|
|
479
|
-
<ChartBody chart={
|
|
736
|
+
<ChartBody chart={renderedChart} labelFontSize={labelFontSize} />
|
|
480
737
|
</svg>
|
|
481
738
|
</div>
|
|
482
739
|
);
|
|
@@ -6,7 +6,11 @@ import { createRoot, type Root } from "react-dom/client";
|
|
|
6
6
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
7
|
|
|
8
8
|
import { createToolkitI18nCatalog } from "../../../i18n.js";
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
DataWidgetAction,
|
|
11
|
+
DataWidgetChartAction,
|
|
12
|
+
DataWidgetDisplayAction,
|
|
13
|
+
} from "./data-widget-types.js";
|
|
10
14
|
import { DataChartWidget } from "./DataChartWidget.js";
|
|
11
15
|
|
|
12
16
|
const mocks = vi.hoisted(() => ({
|
|
@@ -45,7 +49,32 @@ const toolkitI18nCatalog = createToolkitI18nCatalog({ messages: {} });
|
|
|
45
49
|
const initialBasePath = process.env.VITE_APP_BASE_PATH;
|
|
46
50
|
const roots: Root[] = [];
|
|
47
51
|
|
|
48
|
-
async function renderWidget(
|
|
52
|
+
async function renderWidget(
|
|
53
|
+
chartActions?: DataWidgetChartAction[],
|
|
54
|
+
displayActions: DataWidgetDisplayAction[] = [
|
|
55
|
+
{
|
|
56
|
+
id: "details",
|
|
57
|
+
type: "link",
|
|
58
|
+
label: "Open details",
|
|
59
|
+
href: "/reports/1/details",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "query",
|
|
63
|
+
type: "copy",
|
|
64
|
+
label: "Copy query",
|
|
65
|
+
text: "select count(*)",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "unsafe",
|
|
69
|
+
type: "link",
|
|
70
|
+
label: "Unsafe",
|
|
71
|
+
href: "javascript:alert(1)",
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
sampled = false,
|
|
75
|
+
chartDataCount = 1,
|
|
76
|
+
truncated = false,
|
|
77
|
+
) {
|
|
49
78
|
const container = document.createElement("div");
|
|
50
79
|
document.body.appendChild(container);
|
|
51
80
|
const root = createRoot(container);
|
|
@@ -63,7 +92,12 @@ async function renderWidget(chartActions?: DataWidgetAction[]) {
|
|
|
63
92
|
title: "Responses by day",
|
|
64
93
|
xKey: "day",
|
|
65
94
|
series: [{ key: "count", label: "Responses" }],
|
|
66
|
-
data:
|
|
95
|
+
data: Array.from({ length: chartDataCount }, (_, index) => ({
|
|
96
|
+
day: `Day ${index}`,
|
|
97
|
+
count: index,
|
|
98
|
+
})),
|
|
99
|
+
sampled,
|
|
100
|
+
truncated,
|
|
67
101
|
actions: chartActions,
|
|
68
102
|
}}
|
|
69
103
|
display={{
|
|
@@ -79,26 +113,7 @@ async function renderWidget(chartActions?: DataWidgetAction[]) {
|
|
|
79
113
|
},
|
|
80
114
|
{ id: "local", label: "Local data" },
|
|
81
115
|
],
|
|
82
|
-
actions:
|
|
83
|
-
{
|
|
84
|
-
id: "details",
|
|
85
|
-
type: "link",
|
|
86
|
-
label: "Open details",
|
|
87
|
-
href: "/reports/1/details",
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
id: "query",
|
|
91
|
-
type: "copy",
|
|
92
|
-
label: "Copy query",
|
|
93
|
-
text: "select count(*)",
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
id: "unsafe",
|
|
97
|
-
type: "link",
|
|
98
|
-
label: "Unsafe",
|
|
99
|
-
href: "javascript:alert(1)",
|
|
100
|
-
},
|
|
101
|
-
],
|
|
116
|
+
actions: displayActions,
|
|
102
117
|
}}
|
|
103
118
|
/>
|
|
104
119
|
</AgentNativeI18nProvider>,
|
|
@@ -171,6 +186,49 @@ describe("DataChartWidget display contract", () => {
|
|
|
171
186
|
expect(window.location.search).toBe("?tab=chart");
|
|
172
187
|
});
|
|
173
188
|
|
|
189
|
+
it("announces incomplete chart data and stays quiet for complete data", async () => {
|
|
190
|
+
const cappedContainer = await renderWidget(
|
|
191
|
+
undefined,
|
|
192
|
+
undefined,
|
|
193
|
+
false,
|
|
194
|
+
201,
|
|
195
|
+
);
|
|
196
|
+
const cappedDisclosure = cappedContainer.querySelector('[role="status"]');
|
|
197
|
+
|
|
198
|
+
expect(cappedDisclosure?.textContent).toBe(
|
|
199
|
+
"This chart shows only some of the available rows.",
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const sampledContainer = await renderWidget(undefined, undefined, true);
|
|
203
|
+
expect(sampledContainer.querySelector('[role="status"]')?.textContent).toBe(
|
|
204
|
+
"This chart shows only some of the available rows.",
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const sampledAndCappedContainer = await renderWidget(
|
|
208
|
+
undefined,
|
|
209
|
+
undefined,
|
|
210
|
+
true,
|
|
211
|
+
201,
|
|
212
|
+
);
|
|
213
|
+
expect(
|
|
214
|
+
sampledAndCappedContainer.querySelector('[role="status"]')?.textContent,
|
|
215
|
+
).toBe("This chart shows only some of the available rows.");
|
|
216
|
+
|
|
217
|
+
const clampedContainer = await renderWidget(
|
|
218
|
+
undefined,
|
|
219
|
+
undefined,
|
|
220
|
+
true,
|
|
221
|
+
200,
|
|
222
|
+
true,
|
|
223
|
+
);
|
|
224
|
+
expect(clampedContainer.querySelector('[role="status"]')?.textContent).toBe(
|
|
225
|
+
"This chart shows only some of the available rows.",
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const completeContainer = await renderWidget();
|
|
229
|
+
expect(completeContainer.querySelector('[role="status"]')).toBeNull();
|
|
230
|
+
});
|
|
231
|
+
|
|
174
232
|
it("opens overflow links and copies the requested text", async () => {
|
|
175
233
|
const container = await renderWidget();
|
|
176
234
|
const menu = await openActionsMenu(container);
|
|
@@ -240,6 +298,51 @@ describe("DataChartWidget display contract", () => {
|
|
|
240
298
|
).not.toBeNull();
|
|
241
299
|
});
|
|
242
300
|
|
|
301
|
+
it("renders an icon action directly with an accessible label", async () => {
|
|
302
|
+
const container = await renderWidget([
|
|
303
|
+
{
|
|
304
|
+
label: "Download image",
|
|
305
|
+
href: "/reports/1/image",
|
|
306
|
+
icon: "camera",
|
|
307
|
+
presentation: "icon",
|
|
308
|
+
},
|
|
309
|
+
]);
|
|
310
|
+
const directAction = container.querySelector<HTMLAnchorElement>(
|
|
311
|
+
'a[aria-label="Download image"]',
|
|
312
|
+
);
|
|
313
|
+
const menu = await openActionsMenu(container);
|
|
314
|
+
|
|
315
|
+
expect(directAction?.getAttribute("href")).toBe("/mounted/reports/1/image");
|
|
316
|
+
expect(directAction?.getAttribute("title")).toBe("Download image");
|
|
317
|
+
expect(directAction?.querySelector("svg")).not.toBeNull();
|
|
318
|
+
expect(menu?.textContent).not.toContain("Download image");
|
|
319
|
+
expect(menu?.textContent).toContain("Open details");
|
|
320
|
+
expect(menu?.textContent).toContain("Copy query");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("hides the overflow control when all actions are direct icons", async () => {
|
|
324
|
+
const container = await renderWidget(
|
|
325
|
+
[],
|
|
326
|
+
[
|
|
327
|
+
{
|
|
328
|
+
id: "direct-download",
|
|
329
|
+
type: "link",
|
|
330
|
+
label: "Download image",
|
|
331
|
+
href: "/reports/1/image",
|
|
332
|
+
icon: "camera",
|
|
333
|
+
presentation: "icon",
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
expect(
|
|
339
|
+
container.querySelector('a[aria-label="Download image"]'),
|
|
340
|
+
).not.toBeNull();
|
|
341
|
+
expect(
|
|
342
|
+
container.querySelector('button[aria-label="Chart actions"]'),
|
|
343
|
+
).toBeNull();
|
|
344
|
+
});
|
|
345
|
+
|
|
243
346
|
it("keeps chart actions as links when payload metadata includes a type", async () => {
|
|
244
347
|
const container = await renderWidget([
|
|
245
348
|
{
|