@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.
Files changed (128) hide show
  1. package/dist/app/agentkit/react/chat.d.ts.map +1 -1
  2. package/dist/app/agentkit/react/chat.js +2 -2
  3. package/dist/app/agentkit/react/chat.js.map +1 -1
  4. package/dist/app/agentkit/react/components.d.ts +4 -1
  5. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  6. package/dist/app/agentkit/react/components.js +69 -15
  7. package/dist/app/agentkit/react/components.js.map +1 -1
  8. package/dist/app/agentkit/react/context.d.ts +7 -1
  9. package/dist/app/agentkit/react/context.d.ts.map +1 -1
  10. package/dist/app/agentkit/react/context.js +5 -1
  11. package/dist/app/agentkit/react/context.js.map +1 -1
  12. package/dist/app/agentkit/react/index.d.ts +1 -0
  13. package/dist/app/agentkit/react/index.d.ts.map +1 -1
  14. package/dist/app/agentkit/react/index.js +1 -0
  15. package/dist/app/agentkit/react/index.js.map +1 -1
  16. package/dist/app/agentkit/react/root.d.ts +4 -1
  17. package/dist/app/agentkit/react/root.d.ts.map +1 -1
  18. package/dist/app/agentkit/react/root.js +2 -2
  19. package/dist/app/agentkit/react/root.js.map +1 -1
  20. package/dist/app/agentkit/react/styles.css +43 -6
  21. package/dist/app/agentkit/react/transcript-layout.d.ts +51 -0
  22. package/dist/app/agentkit/react/transcript-layout.d.ts.map +1 -0
  23. package/dist/app/agentkit/react/transcript-layout.js +34 -0
  24. package/dist/app/agentkit/react/transcript-layout.js.map +1 -0
  25. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  26. package/dist/app/chat/AgentKitAssistantChat.js +3 -3
  27. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  28. package/dist/app/chat/AgentPanel.d.ts +2 -0
  29. package/dist/app/chat/AgentPanel.d.ts.map +1 -1
  30. package/dist/app/chat/AgentPanel.js +6 -2
  31. package/dist/app/chat/AgentPanel.js.map +1 -1
  32. package/dist/app/chat/chat/index.d.ts +2 -0
  33. package/dist/app/chat/chat/index.d.ts.map +1 -1
  34. package/dist/app/chat/chat/index.js.map +1 -1
  35. package/dist/app/chat/chat/surface-types.d.ts +7 -0
  36. package/dist/app/chat/chat/surface-types.d.ts.map +1 -1
  37. package/dist/app/chat/chat/surface-types.js.map +1 -1
  38. package/dist/app/chat/chat/tool-call-display.d.ts +7 -2
  39. package/dist/app/chat/chat/tool-call-display.d.ts.map +1 -1
  40. package/dist/app/chat/chat/tool-call-display.js +10 -9
  41. package/dist/app/chat/chat/tool-call-display.js.map +1 -1
  42. package/dist/app/chat/chat/widgets/DataChartRenderer.d.ts.map +1 -1
  43. package/dist/app/chat/chat/widgets/DataChartRenderer.js +196 -26
  44. package/dist/app/chat/chat/widgets/DataChartRenderer.js.map +1 -1
  45. package/dist/app/chat/chat/widgets/DataChartWidget.d.ts.map +1 -1
  46. package/dist/app/chat/chat/widgets/DataChartWidget.js +5 -1
  47. package/dist/app/chat/chat/widgets/DataChartWidget.js.map +1 -1
  48. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.d.ts +2 -2
  49. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.d.ts.map +1 -1
  50. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.js +6 -4
  51. package/dist/app/chat/chat/widgets/DataWidgetDisplayControls.js.map +1 -1
  52. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  53. package/dist/app/i18n/catalogs/ar-SA.js +1 -0
  54. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  55. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  56. package/dist/app/i18n/catalogs/de-DE.js +1 -0
  57. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  58. package/dist/app/i18n/catalogs/en-US.d.ts +1 -0
  59. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  60. package/dist/app/i18n/catalogs/en-US.js +1 -0
  61. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  62. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  63. package/dist/app/i18n/catalogs/es-ES.js +1 -0
  64. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  65. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  66. package/dist/app/i18n/catalogs/fr-FR.js +1 -0
  67. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  68. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  69. package/dist/app/i18n/catalogs/hi-IN.js +1 -0
  70. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  71. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  72. package/dist/app/i18n/catalogs/ja-JP.js +1 -0
  73. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  74. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  75. package/dist/app/i18n/catalogs/ko-KR.js +1 -0
  76. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  77. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  78. package/dist/app/i18n/catalogs/pt-BR.js +1 -0
  79. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  80. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  81. package/dist/app/i18n/catalogs/zh-CN.js +1 -0
  82. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  83. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  84. package/dist/app/i18n/catalogs/zh-TW.js +1 -0
  85. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  86. package/dist/composer/ComposerContextBanner.d.ts +3 -1
  87. package/dist/composer/ComposerContextBanner.d.ts.map +1 -1
  88. package/dist/composer/ComposerContextBanner.js +2 -2
  89. package/dist/composer/ComposerContextBanner.js.map +1 -1
  90. package/package.json +3 -3
  91. package/src/app/agentkit/react/chat.tsx +2 -0
  92. package/src/app/agentkit/react/components.interaction.spec.tsx +107 -0
  93. package/src/app/agentkit/react/components.spec.tsx +28 -1
  94. package/src/app/agentkit/react/components.tsx +153 -17
  95. package/src/app/agentkit/react/context.tsx +12 -0
  96. package/src/app/agentkit/react/index.ts +1 -0
  97. package/src/app/agentkit/react/lifecycle.spec.tsx +186 -16
  98. package/src/app/agentkit/react/root.tsx +7 -0
  99. package/src/app/agentkit/react/styles.css +43 -6
  100. package/src/app/agentkit/react/transcript-layout.spec.ts +72 -0
  101. package/src/app/agentkit/react/transcript-layout.ts +97 -0
  102. package/src/app/auth/AuthPage.attribution.spec.tsx +6 -0
  103. package/src/app/chat/AgentKitAssistantChat.spec.tsx +39 -0
  104. package/src/app/chat/AgentKitAssistantChat.tsx +5 -0
  105. package/src/app/chat/AgentPanel.page-header-share.spec.tsx +101 -0
  106. package/src/app/chat/AgentPanel.tsx +14 -2
  107. package/src/app/chat/chat/index.ts +2 -0
  108. package/src/app/chat/chat/surface-types.ts +10 -0
  109. package/src/app/chat/chat/tool-call-display.spec.tsx +83 -0
  110. package/src/app/chat/chat/tool-call-display.tsx +40 -8
  111. package/src/app/chat/chat/widgets/DataChartRenderer.spec.tsx +632 -0
  112. package/src/app/chat/chat/widgets/DataChartRenderer.tsx +308 -51
  113. package/src/app/chat/chat/widgets/DataChartWidget.spec.tsx +126 -23
  114. package/src/app/chat/chat/widgets/DataChartWidget.tsx +10 -0
  115. package/src/app/chat/chat/widgets/DataWidgetDisplayControls.tsx +26 -6
  116. package/src/app/i18n/catalogs/ar-SA.ts +1 -0
  117. package/src/app/i18n/catalogs/de-DE.ts +2 -0
  118. package/src/app/i18n/catalogs/en-US.ts +2 -0
  119. package/src/app/i18n/catalogs/es-ES.ts +2 -0
  120. package/src/app/i18n/catalogs/fr-FR.ts +2 -0
  121. package/src/app/i18n/catalogs/hi-IN.ts +1 -0
  122. package/src/app/i18n/catalogs/ja-JP.ts +2 -0
  123. package/src/app/i18n/catalogs/ko-KR.ts +1 -0
  124. package/src/app/i18n/catalogs/pt-BR.ts +2 -0
  125. package/src/app/i18n/catalogs/zh-CN.ts +1 -0
  126. package/src/app/i18n/catalogs/zh-TW.ts +1 -0
  127. package/src/composer/ComposerContextBanner.spec.tsx +31 -5
  128. 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
- formatTick(value),
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
- formatTick(value),
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
- categoryMargin +
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(MARGIN.right, horizontalPositiveValueWidth + 13),
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 gridTicks = Array.from({ length: 4 }, (_, index) => {
155
- const value = min + ((max - min) * index) / 3;
156
- return {
157
- value,
158
- x: xForValue(value),
159
- y: yForValue(value),
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
- <text
185
- x={tick.x}
186
- y={HEIGHT - margin.bottom + 14}
187
- fill="hsl(var(--muted-foreground))"
188
- fontSize={labelFontSize}
189
- textAnchor="middle"
190
- >
191
- {formatTick(tick.value)}
192
- </text>
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
- <text
205
- x={margin.left - 8}
206
- y={tick.y + 4}
207
- fill="hsl(var(--muted-foreground))"
208
- fontSize={labelFontSize}
209
- textAnchor="end"
210
- >
211
- {formatTick(tick.value)}
212
- </text>
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={14}
505
+ x={Y_AXIS_TITLE_X}
267
506
  y={margin.top + plotHeight / 2}
268
- transform={`rotate(-90 14 ${margin.top + plotHeight / 2})`}
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 = slotSize * 0.72;
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(2, barSize - 2)
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
- {formatTick(value)}
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 contentHeight = chart.contentHeight;
453
- const contentPadding = chart.contentPadding ?? 0;
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 = LABEL_FONT_FAMILIES[chart.labelFontFamily ?? "sans"];
456
- const labelFontSize = chart.labelFontSize ?? 11;
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={chart.title ?? t("agentChat.widget.dataChart")}
734
+ aria-label={renderedChart.title ?? t("agentChat.widget.dataChart")}
478
735
  >
479
- <ChartBody chart={chart} labelFontSize={labelFontSize} />
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 { DataWidgetAction } from "./data-widget-types.js";
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(chartActions?: DataWidgetAction[]) {
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: [{ day: "Mon", count: 2 }],
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
  {