@adecore/agents-react 0.22.0 → 0.23.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 (113) hide show
  1. package/dist/chat/ui/EditDiff.d.ts +3 -1
  2. package/dist/chat/ui/EditDiff.js +25 -18
  3. package/dist/chat/ui/UnifiedDiff.d.ts +2 -11
  4. package/dist/chat/ui/UnifiedDiff.js +31 -58
  5. package/dist/chat/ui/diff/ApprovalReview.d.ts +5 -0
  6. package/dist/chat/ui/diff/ApprovalReview.js +29 -0
  7. package/dist/chat/ui/diff/DiffFallbackBoundary.d.ts +17 -0
  8. package/dist/chat/ui/diff/DiffFallbackBoundary.js +17 -0
  9. package/dist/chat/ui/diff/DiffOmitted.d.ts +3 -0
  10. package/dist/chat/ui/diff/DiffOmitted.js +8 -0
  11. package/dist/chat/ui/diff/DiffStack.d.ts +9 -0
  12. package/dist/chat/ui/diff/DiffStack.js +15 -0
  13. package/dist/chat/ui/diff/DiffUnavailable.d.ts +3 -0
  14. package/dist/chat/ui/diff/DiffUnavailable.js +8 -0
  15. package/dist/chat/ui/diff/EditorEditDiff.d.ts +23 -0
  16. package/dist/chat/ui/diff/EditorEditDiff.js +67 -0
  17. package/dist/chat/ui/diff/EditorPatchDiff.d.ts +16 -0
  18. package/dist/chat/ui/diff/EditorPatchDiff.js +20 -0
  19. package/dist/chat/ui/diff/LegacyDiffPool.d.ts +4 -0
  20. package/dist/chat/ui/diff/LegacyDiffPool.js +12 -0
  21. package/dist/chat/ui/diff/LegacyEditDiff.d.ts +5 -0
  22. package/dist/chat/ui/diff/LegacyEditDiff.js +23 -0
  23. package/dist/chat/ui/diff/LegacyUnifiedDiff.d.ts +2 -0
  24. package/dist/chat/ui/diff/LegacyUnifiedDiff.js +67 -0
  25. package/dist/chat/ui/diff/RawDiff.d.ts +6 -0
  26. package/dist/chat/ui/diff/RawDiff.js +7 -0
  27. package/dist/chat/ui/diff/ReviewDiff.d.ts +5 -0
  28. package/dist/chat/ui/diff/ReviewDiff.js +10 -0
  29. package/dist/chat/ui/diff/diff-props.d.ts +12 -0
  30. package/dist/chat/ui/diff/diff-props.js +1 -0
  31. package/dist/chat/ui/diff/diff-source-budget.d.ts +10 -0
  32. package/dist/chat/ui/diff/diff-source-budget.js +47 -0
  33. package/dist/chat/ui/diff/editor-edit-model.d.ts +8 -0
  34. package/dist/chat/ui/diff/editor-edit-model.js +19 -0
  35. package/dist/chat/ui/diff/review-plan.d.ts +28 -0
  36. package/dist/chat/ui/diff/review-plan.js +48 -0
  37. package/dist/chat/ui/diff/review-settle.d.ts +5 -0
  38. package/dist/chat/ui/diff/review-settle.js +12 -0
  39. package/dist/chat/ui/diff/syntax-runtime.d.ts +18 -0
  40. package/dist/chat/ui/diff/syntax-runtime.js +184 -0
  41. package/dist/chat/ui/diff/use-review-stack.d.ts +8 -0
  42. package/dist/chat/ui/diff/use-review-stack.js +30 -0
  43. package/dist/chat/ui/diff-syntax.d.ts +19 -0
  44. package/dist/chat/ui/diff-syntax.js +63 -0
  45. package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +3 -3
  46. package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +1 -1
  47. package/dist/chat/ui/intelligent-ui/chart-data.d.ts +22 -2
  48. package/dist/chat/ui/intelligent-ui/chart-data.js +83 -19
  49. package/dist/chat/ui/intelligent-ui/renderers/chart.d.ts +1 -1
  50. package/dist/chat/ui/intelligent-ui/renderers/chart.js +99 -35
  51. package/dist/chat/ui/intelligent-ui/renderers/choices.d.ts +1 -1
  52. package/dist/chat/ui/intelligent-ui/renderers/choices.js +8 -2
  53. package/dist/chat/ui/intelligent-ui/renderers/content.js +5 -3
  54. package/dist/chat/ui/intelligent-ui/renderers/inputs.js +9 -4
  55. package/dist/chat/ui/intelligent-ui/renderers/links.js +1 -1
  56. package/dist/chat/ui/intelligent-ui/renderers/text.js +5 -2
  57. package/dist/chat/ui/intelligent-ui/table-data.js +4 -4
  58. package/dist/chat/ui/intelligent-ui/use-truncated.d.ts +1 -0
  59. package/dist/chat/ui/intelligent-ui/use-truncated.js +28 -0
  60. package/dist/chat/ui/rows/WorkRows.js +16 -5
  61. package/dist/chat/ui/use-diff-source-tokens.d.ts +4 -0
  62. package/dist/chat/ui/use-diff-source-tokens.js +40 -0
  63. package/dist/host.d.ts +6 -1
  64. package/dist/lazy.d.ts +3 -1
  65. package/dist/lazy.js +5 -3
  66. package/dist/locales/en/agent-chat.json +12 -3
  67. package/dist/locales/en/agent-prompts.json +1 -0
  68. package/dist/locales/nl/agent-chat.json +12 -3
  69. package/dist/locales/nl/agent-prompts.json +1 -0
  70. package/dist/prompts/ui/ApprovalBody.js +7 -5
  71. package/package.json +23 -5
  72. package/src/chat/ui/EditDiff.tsx +37 -22
  73. package/src/chat/ui/UnifiedDiff.tsx +55 -88
  74. package/src/chat/ui/diff/ApprovalReview.tsx +53 -0
  75. package/src/chat/ui/diff/DiffFallbackBoundary.tsx +32 -0
  76. package/src/chat/ui/diff/DiffOmitted.tsx +13 -0
  77. package/src/chat/ui/diff/DiffStack.tsx +45 -0
  78. package/src/chat/ui/diff/DiffUnavailable.tsx +13 -0
  79. package/src/chat/ui/diff/EditorEditDiff.tsx +145 -0
  80. package/src/chat/ui/diff/EditorPatchDiff.tsx +60 -0
  81. package/src/chat/ui/diff/LegacyDiffPool.tsx +13 -0
  82. package/src/chat/ui/diff/LegacyEditDiff.tsx +33 -0
  83. package/src/chat/ui/diff/LegacyUnifiedDiff.tsx +93 -0
  84. package/src/chat/ui/diff/RawDiff.tsx +19 -0
  85. package/src/chat/ui/diff/ReviewDiff.tsx +22 -0
  86. package/src/chat/ui/diff/diff-props.ts +16 -0
  87. package/src/chat/ui/diff/diff-source-budget.ts +56 -0
  88. package/src/chat/ui/diff/editor-edit-model.ts +21 -0
  89. package/src/chat/ui/diff/review-plan.ts +69 -0
  90. package/src/chat/ui/diff/review-settle.ts +14 -0
  91. package/src/chat/ui/diff/syntax-runtime.ts +222 -0
  92. package/src/chat/ui/diff/use-review-stack.ts +38 -0
  93. package/src/chat/ui/diff-syntax.ts +82 -0
  94. package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +5 -5
  95. package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +6 -1
  96. package/src/chat/ui/intelligent-ui/chart-data.ts +106 -20
  97. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +237 -72
  98. package/src/chat/ui/intelligent-ui/renderers/choices.tsx +9 -4
  99. package/src/chat/ui/intelligent-ui/renderers/content.tsx +31 -15
  100. package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +16 -5
  101. package/src/chat/ui/intelligent-ui/renderers/links.tsx +3 -3
  102. package/src/chat/ui/intelligent-ui/renderers/text.tsx +18 -8
  103. package/src/chat/ui/intelligent-ui/table-data.ts +4 -4
  104. package/src/chat/ui/intelligent-ui/use-truncated.ts +33 -0
  105. package/src/chat/ui/rows/WorkRows.tsx +42 -33
  106. package/src/chat/ui/use-diff-source-tokens.ts +43 -0
  107. package/src/host.ts +10 -1
  108. package/src/lazy.tsx +6 -3
  109. package/src/locales/en/agent-chat.json +12 -3
  110. package/src/locales/en/agent-prompts.json +1 -0
  111. package/src/locales/nl/agent-chat.json +12 -3
  112. package/src/locales/nl/agent-prompts.json +1 -0
  113. package/src/prompts/ui/ApprovalBody.tsx +8 -14
@@ -3,11 +3,25 @@ import type { TFunction } from 'i18next';
3
3
  import clsx from 'clsx';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import type { UiProps } from '@adecore/intelligent-ui';
6
- import { SegmentBar, useMeasuredWidth } from '@adecore/ui';
7
- import { formatRounded } from '@adecore/ui/format';
8
- import { niceCeiling, UI_CHART_ROWS, uiChartData, type UiChartData } from '../chart-data';
6
+ import { Button, SegmentBar, Tooltip, useMeasuredWidth } from '@adecore/ui';
7
+ import { formatNumber, formatRounded } from '@adecore/ui/format';
8
+ import { useBlockLocal } from '../block-local';
9
+ import {
10
+ UI_CHART_CHARACTER,
11
+ UI_CHART_ROWS,
12
+ uiChartCharacter,
13
+ uiChartAxis,
14
+ uiChartData,
15
+ uiChartRange,
16
+ uiChartText,
17
+ uiChartTextWidth,
18
+ uiChartValues,
19
+ type UiChartData,
20
+ type UiChartValues
21
+ } from '../chart-data';
9
22
  import { uiWithUnit } from '../node-text';
10
- import type { UiRendererProps } from '../render-context';
23
+ import { useTruncated } from '../use-truncated';
24
+ import type { UiRenderContext, UiRendererProps } from '../render-context';
11
25
  import { UiFallbackPart } from '../UiFallbackPart';
12
26
 
13
27
  type ChartKind = UiProps<'Chart'>['kind'];
@@ -15,9 +29,10 @@ type ChartKind = UiProps<'Chart'>['kind'];
15
29
  const HEIGHT = 160;
16
30
  const HBAR_ROW = 24;
17
31
  const STACKED_ROW = 28;
18
- const AXIS_WIDTH = 44;
19
32
  const PLOT_TOP = 8;
20
33
  const LABEL_ROOM = 20;
34
+ /* A unit longer than this stands once above the plot instead of beside every tick. */
35
+ const UNIT_ON_TICKS = 5;
21
36
  /* The narrowest band a category label is written under; narrower bands skip labels. */
22
37
  const LABEL_BAND = 56;
23
38
  /* Points get a dot up to this many categories; more would crowd the line. */
@@ -32,29 +47,66 @@ function valueText(value: number | null, unit: string | undefined): string {
32
47
  return value === null ? '' : uiWithUnit(formatRounded(value, 2), unit);
33
48
  }
34
49
 
50
+ function unitApart(unit: string | undefined): boolean {
51
+ return unit !== undefined && unit.length > UNIT_ON_TICKS;
52
+ }
53
+
54
+ /* Where a value axis stands and how much room its category labels get, from the labels it writes. */
55
+ function verticalLayout(chart: UiChartData, unit: string | undefined, width: number, character: number) {
56
+ // uiChartData only hands over data whose range is finite.
57
+ const { low, high } = uiChartRange(chart.min, chart.max)!;
58
+ const ticks = low === 0 ? [0, high / 2, high] : [low, 0, high];
59
+ const tickUnit = unitApart(unit) ? undefined : unit;
60
+ const { axisWidth, tickTexts } = uiChartAxis(
61
+ ticks.map((tick) => valueText(tick, tickUnit)),
62
+ width,
63
+ character
64
+ );
65
+ const band = Math.max(0, width - axisWidth) / chart.labels.length;
66
+ const labelEvery = Math.max(1, Math.ceil(LABEL_BAND / Math.max(1, band)));
67
+ return { low, high, ticks, tickTexts, axisWidth, band, labelEvery, labelRoom: Math.min(band * labelEvery, Math.max(0, width - axisWidth)) };
68
+ }
69
+
70
+ /* Whether a drawing in SVG at this width cuts a category label short, which then offers the values to read it whole; HTML rows are measured where they stand. */
71
+ function labelsCut(chart: UiChartData, unit: string | undefined, width: number, character: number): boolean {
72
+ if (width <= 0 || chart.kind === 'hbar' || chart.kind === 'stacked') {
73
+ return false;
74
+ }
75
+ const { labelRoom } = verticalLayout(chart, unit, width, character);
76
+ return chart.labels.some((label) => uiChartTextWidth(label, character) > labelRoom);
77
+ }
78
+
79
+ /* The character width of chart type, which follows the root font size; a drawing depends on a measured width, so only a page reads it. */
80
+ function useChartCharacter(): number {
81
+ const [character] = useState(() =>
82
+ typeof document === 'undefined' ? UI_CHART_CHARACTER : uiChartCharacter(Number.parseFloat(getComputedStyle(document.documentElement).fontSize))
83
+ );
84
+ return character;
85
+ }
86
+
35
87
  /* The height a chart will take, which its place holds while the node streams. */
36
88
  function chartHeight(kind: ChartKind, rows: number): number {
37
89
  const count = Math.max(1, Math.min(rows, UI_CHART_ROWS));
38
90
  return kind === 'hbar' ? count * HBAR_ROW : kind === 'stacked' ? count * STACKED_ROW : HEIGHT;
39
91
  }
40
92
 
41
- /* One sentence with the main point, which a screen reader hears in place of the drawing. */
42
- function chartSummary(chart: UiChartData, unit: string | undefined, t: TFunction<'agent-chat'>): string {
93
+ /* One sentence with the main point over all of the data, drawn or not, which a screen reader hears in place of the drawing. */
94
+ function chartSummary(values: UiChartValues, unit: string | undefined, t: TFunction<'agent-chat'>): string {
43
95
  let top: { label: string; series: string; value: number } | null = null;
44
- for (const series of chart.series) {
96
+ for (const series of values.series) {
45
97
  for (let i = 0; i < series.values.length; i++) {
46
98
  const value = series.values[i];
47
99
  if (value !== null && value !== undefined && (top === null || value > top.value)) {
48
- top = { label: chart.labels[i] ?? '', series: series.key, value };
100
+ top = { label: values.labels[i] ?? '', series: series.key, value };
49
101
  }
50
102
  }
51
103
  }
52
104
  if (top === null) {
53
105
  return t('blocks.chart.values');
54
106
  }
55
- return chart.series.length > 1
56
- ? t('blocks.chart.summarySeries', { count: chart.labels.length, series: top.series, label: top.label, value: valueText(top.value, unit) })
57
- : t('blocks.chart.summary', { count: chart.labels.length, label: top.label, value: valueText(top.value, unit) });
107
+ return values.series.length > 1
108
+ ? t('blocks.chart.summarySeries', { count: values.labels.length, series: top.series, label: top.label, value: valueText(top.value, unit) })
109
+ : t('blocks.chart.summary', { count: values.labels.length, label: top.label, value: valueText(top.value, unit) });
58
110
  }
59
111
 
60
112
  function Legend({ chart }: { chart: UiChartData }) {
@@ -70,34 +122,40 @@ function Legend({ chart }: { chart: UiChartData }) {
70
122
  );
71
123
  }
72
124
 
73
- /* The numbers behind the drawing, for a screen reader. */
74
- function ValuesTable({ chart, unit }: { chart: UiChartData; unit: string | undefined }) {
125
+ /* Every number of the data, for a screen reader, and on screen once a person opens it. */
126
+ function ValuesTable({ values, unit, shown }: { values: UiChartValues; unit: string | undefined; shown: boolean }) {
75
127
  const { t } = useTranslation('agent-chat');
76
- return (
77
- <table className="sr-only">
78
- <caption>{t('blocks.chart.values')}</caption>
79
- <thead>
128
+ const table = (
129
+ <table className={shown ? 'w-full border-collapse text-xs tabular-nums' : undefined}>
130
+ <caption className={shown ? 'sr-only' : undefined}>{t('blocks.chart.values')}</caption>
131
+ <thead className={shown ? 'sticky top-0 z-1 bg-surface-hover' : undefined}>
80
132
  <tr>
81
133
  <td />
82
- {chart.series.map((series) => (
83
- <th key={series.key} scope="col">
134
+ {values.series.map((series) => (
135
+ <th key={series.key} scope="col" className={shown ? 'h-8 px-2 text-right font-medium whitespace-nowrap text-text-muted' : undefined}>
84
136
  {series.key}
85
137
  </th>
86
138
  ))}
87
139
  </tr>
88
140
  </thead>
89
141
  <tbody>
90
- {chart.labels.map((label, index) => (
91
- <tr key={index}>
92
- <th scope="row">{label}</th>
93
- {chart.series.map((series) => (
94
- <td key={series.key}>{valueText(series.values[index] ?? null, unit)}</td>
142
+ {values.labels.map((label, index) => (
143
+ <tr key={index} className={shown ? 'border-t border-border-soft' : undefined}>
144
+ <th scope="row" className={shown ? 'h-8 px-2 text-left font-normal wrap-anywhere text-text-muted' : undefined}>
145
+ {label}
146
+ </th>
147
+ {values.series.map((series) => (
148
+ <td key={series.key} className={shown ? 'h-8 px-2 text-right whitespace-nowrap text-text' : undefined}>
149
+ {valueText(series.values[index] ?? null, unit)}
150
+ </td>
95
151
  ))}
96
152
  </tr>
97
153
  ))}
98
154
  </tbody>
99
155
  </table>
100
156
  );
157
+ // A table grows to its widest cell whatever width it is given, so it is hidden inside a block that clips.
158
+ return <div className={shown ? 'max-h-80 w-full overflow-auto rounded-md border border-border-soft' : 'sr-only'}>{table}</div>;
101
159
  }
102
160
 
103
161
  /* The values of one category beside the pointer. */
@@ -105,10 +163,10 @@ function Readout({ chart, index, left, unit }: { chart: UiChartData; index: numb
105
163
  return (
106
164
  <div
107
165
  aria-hidden
108
- className="pointer-events-none absolute top-0 z-1 flex -translate-x-1/2 flex-col gap-0.5 rounded-md bg-surface-raised px-2 py-1 text-xs whitespace-nowrap shadow-raised"
166
+ className="pointer-events-none absolute top-0 z-1 flex w-max max-w-56 -translate-x-1/2 flex-col gap-0.5 rounded-md bg-surface-raised px-2 py-1 text-xs shadow-raised"
109
167
  style={{ left }}
110
168
  >
111
- <span className="font-medium text-text">{chart.labels[index]}</span>
169
+ <span className="font-medium wrap-anywhere text-text">{chart.labels[index]}</span>
112
170
  {chart.series.map((series, at) => (
113
171
  <span key={series.key} className="flex items-center gap-1.5 text-text-muted tabular-nums">
114
172
  <span className="size-2 rounded-sm" style={{ backgroundColor: seriesColor(at) }} />
@@ -121,20 +179,31 @@ function Readout({ chart, index, left, unit }: { chart: UiChartData; index: numb
121
179
  }
122
180
 
123
181
  /* Bars and lines over a value axis, in SVG at the measured width. */
124
- function VerticalChart({ chart, kind, unit, width }: { chart: UiChartData; kind: 'bar' | 'line'; unit: string | undefined; width: number }) {
182
+ function VerticalChart({
183
+ chart,
184
+ kind,
185
+ unit,
186
+ width,
187
+ character
188
+ }: {
189
+ chart: UiChartData;
190
+ kind: 'bar' | 'line';
191
+ unit: string | undefined;
192
+ width: number;
193
+ character: number;
194
+ }) {
125
195
  const [hover, setHover] = useState<number | null>(null);
126
- const low = chart.min < 0 ? -niceCeiling(-chart.min) : 0;
127
- const high = chart.max > 0 ? niceCeiling(chart.max) : low === 0 ? 1 : 0;
128
- const plotWidth = Math.max(0, width - AXIS_WIDTH);
129
- const plotHeight = HEIGHT - PLOT_TOP - LABEL_ROOM;
130
- const band = plotWidth / chart.labels.length;
196
+ const { low, high, ticks, tickTexts, axisWidth, band, labelEvery, labelRoom } = verticalLayout(chart, unit, width, character);
197
+ const plotHeight = HEIGHT - PLOT_TOP - Math.round((LABEL_ROOM * character) / UI_CHART_CHARACTER);
131
198
  const y = (value: number): number => Math.round(PLOT_TOP + ((high - value) / (high - low)) * plotHeight);
132
199
  const baseline = y(0);
133
- const ticks = low === 0 ? [0, high / 2, high] : [low, 0, high];
134
- const labelEvery = Math.max(1, Math.ceil(LABEL_BAND / Math.max(1, band)));
135
- const center = (index: number): number => Math.round(AXIS_WIDTH + band * index + band / 2);
200
+ const center = (index: number): number => Math.round(axisWidth + band * index + band / 2);
136
201
  const gap = band * 0.2;
137
202
  const barWidth = Math.max(1, (band - gap) / chart.series.length);
203
+ const labelAt = (index: number, text: string): number => {
204
+ const half = uiChartTextWidth(text, character) / 2;
205
+ return Math.round(Math.min(Math.max(center(index), axisWidth + half), width - half));
206
+ };
138
207
 
139
208
  const segments = (values: (number | null)[]): string =>
140
209
  values
@@ -151,30 +220,34 @@ function VerticalChart({ chart, kind, unit, width }: { chart: UiChartData; kind:
151
220
  <>
152
221
  <svg width={width} height={HEIGHT} aria-hidden className="block overflow-visible" onPointerLeave={() => setHover(null)}>
153
222
  {hover !== null && (
154
- <rect x={Math.round(AXIS_WIDTH + band * hover)} y={PLOT_TOP} width={Math.round(band)} height={plotHeight} className="fill-surface-hover" />
223
+ <rect x={Math.round(axisWidth + band * hover)} y={PLOT_TOP} width={Math.round(band)} height={plotHeight} className="fill-surface-hover" />
155
224
  )}
156
- {ticks.map((tick) => (
225
+ {ticks.map((tick, index) => (
157
226
  <g key={tick}>
158
227
  <line
159
- x1={AXIS_WIDTH}
228
+ x1={axisWidth}
160
229
  x2={width}
161
230
  y1={y(tick)}
162
231
  y2={y(tick)}
163
232
  className={tick === 0 ? 'stroke-border' : 'stroke-border-soft'}
164
233
  shapeRendering="crispEdges"
165
234
  />
166
- <text x={AXIS_WIDTH - 6} y={y(tick)} dy="0.32em" textAnchor="end" className="fill-text-faint text-xs tabular-nums">
167
- {valueText(tick, unit)}
235
+ <text x={axisWidth - 6} y={y(tick)} dy="0.32em" textAnchor="end" className="fill-text-faint text-xs tabular-nums">
236
+ {tickTexts[index]}
168
237
  </text>
169
238
  </g>
170
239
  ))}
171
- {chart.labels.map((label, index) =>
172
- index % labelEvery === 0 ? (
173
- <text key={index} x={center(index)} y={HEIGHT - 4} textAnchor="middle" className="fill-text-faint text-xs">
174
- {label}
240
+ {chart.labels.map((label, index) => {
241
+ if (index % labelEvery !== 0) {
242
+ return null;
243
+ }
244
+ const text = uiChartText(label, labelRoom, character);
245
+ return (
246
+ <text key={index} x={labelAt(index, text)} y={HEIGHT - 4} textAnchor="middle" className="fill-text-faint text-xs">
247
+ {text}
175
248
  </text>
176
- ) : null
177
- )}
249
+ );
250
+ })}
178
251
  {kind === 'bar' ? (
179
252
  <g className={clsx(GROW, 'starting:scale-y-0')} style={{ transformOrigin: `0px ${baseline}px` }}>
180
253
  {chart.series.map((series, at) =>
@@ -182,7 +255,7 @@ function VerticalChart({ chart, kind, unit, width }: { chart: UiChartData; kind:
182
255
  value === null ? null : (
183
256
  <rect
184
257
  key={`${series.key}:${index}`}
185
- x={Math.round(AXIS_WIDTH + band * index + gap / 2 + barWidth * at)}
258
+ x={Math.round(axisWidth + band * index + gap / 2 + barWidth * at)}
186
259
  y={Math.min(y(value), baseline)}
187
260
  width={Math.max(1, Math.round(barWidth) - 1)}
188
261
  height={Math.abs(y(value) - baseline)}
@@ -216,7 +289,7 @@ function VerticalChart({ chart, kind, unit, width }: { chart: UiChartData; kind:
216
289
  {chart.labels.map((_, index) => (
217
290
  <rect
218
291
  key={index}
219
- x={Math.round(AXIS_WIDTH + band * index)}
292
+ x={Math.round(axisWidth + band * index)}
220
293
  y={0}
221
294
  width={Math.max(1, Math.round(band))}
222
295
  height={HEIGHT}
@@ -230,27 +303,40 @@ function VerticalChart({ chart, kind, unit, width }: { chart: UiChartData; kind:
230
303
  );
231
304
  }
232
305
 
233
- /* A row of 24px per category, its label beside the bars. */
306
+ /* A row of 24px per category, its label beside the bars, which grow from zero to either side. */
234
307
  function HorizontalChart({ chart, unit }: { chart: UiChartData; unit: string | undefined }) {
235
- // Bars grow right from zero only, so a chart of only negative values has no length to share and keeps a unit scale.
236
- const high = niceCeiling(chart.max) || 1;
308
+ const valueUnit = unitApart(unit) ? undefined : unit;
309
+ const { low, high } = uiChartRange(chart.min, chart.max)!;
310
+ const span = high - low;
311
+ const zero = (-low / span) * 100;
237
312
  const single = chart.series.length === 1;
238
313
  return (
239
314
  <div aria-hidden className="flex flex-col">
240
315
  {chart.labels.map((label, index) => (
241
316
  <div key={index} className="flex h-6 items-center gap-2 text-xs">
242
317
  <span className="w-2/5 max-w-36 shrink-0 truncate text-text-muted">{label}</span>
243
- <span className="flex min-w-0 grow flex-col justify-center gap-px">
244
- {chart.series.map((series, at) => (
245
- <span
246
- key={series.key}
247
- className={clsx(GROW, 'block origin-left rounded-sm starting:scale-x-0', single ? 'h-3' : 'h-1')}
248
- style={{ width: `${(Math.max(0, series.values[index] ?? 0) / high) * 100}%`, backgroundColor: seriesColor(at) }}
249
- />
250
- ))}
318
+ <span className="relative flex min-w-0 grow flex-col justify-center gap-px">
319
+ {low < 0 && <span className="absolute inset-y-0 w-px bg-border" style={{ left: `${zero}%` }} />}
320
+ {chart.series.map((series, at) => {
321
+ const value = series.values[index] ?? 0;
322
+ return (
323
+ <span key={series.key} className={clsx('relative block', single ? 'h-3' : 'h-1')}>
324
+ <span
325
+ className={clsx(GROW, 'absolute inset-y-0 rounded-sm starting:scale-x-0', value < 0 ? 'origin-right' : 'origin-left')}
326
+ style={{
327
+ left: `${((Math.min(value, 0) - low) / span) * 100}%`,
328
+ width: `${(Math.abs(value) / span) * 100}%`,
329
+ backgroundColor: seriesColor(at)
330
+ }}
331
+ />
332
+ </span>
333
+ );
334
+ })}
251
335
  </span>
252
336
  {single && (
253
- <span className="w-16 shrink-0 text-right text-text-faint tabular-nums">{valueText(chart.series[0]!.values[index] ?? null, unit)}</span>
337
+ <span className="max-w-1/3 min-w-12 shrink-0 truncate text-right text-text-faint tabular-nums">
338
+ {valueText(chart.series[0]!.values[index] ?? null, valueUnit)}
339
+ </span>
254
340
  )}
255
341
  </div>
256
342
  ))}
@@ -271,7 +357,7 @@ function StackedChart({ chart, unit }: { chart: UiChartData; unit: string | unde
271
357
  label={label}
272
358
  range={[0, chart.max]}
273
359
  parts={chart.series.map((series, at) => ({
274
- value: Math.max(0, series.values[index] ?? 0),
360
+ value: series.values[index] ?? 0,
275
361
  color: seriesColor(at),
276
362
  label: `${series.key} ${valueText(series.values[index] ?? 0, unit)}`
277
363
  }))}
@@ -282,22 +368,101 @@ function StackedChart({ chart, unit }: { chart: UiChartData; unit: string | unde
282
368
  );
283
369
  }
284
370
 
285
- function DrawnChart({ chart, kind, unit }: { chart: UiChartData; kind: ChartKind; unit: string | undefined }) {
371
+ /* What the drawing leaves out or draws otherwise, said in words, with every value a press away. */
372
+ function ChartNotes({
373
+ chart,
374
+ requested,
375
+ cut,
376
+ values,
377
+ unit,
378
+ context,
379
+ nodeId
380
+ }: {
381
+ chart: UiChartData;
382
+ requested: ChartKind;
383
+ /* Whether a category label is cut short at this width. */
384
+ cut: boolean;
385
+ values: UiChartValues;
386
+ unit: string | undefined;
387
+ context: UiRenderContext;
388
+ nodeId: string;
389
+ }) {
390
+ const { t } = useTranslation('agent-chat');
391
+ const [shown, setShown] = useBlockLocal(context, `${nodeId}:values`, () => false);
392
+ const notes = [
393
+ ...(chart.truncated ? [t('blocks.chart.rowsLeftOut', { shown: formatNumber(chart.labels.length), total: formatNumber(chart.total) })] : []),
394
+ ...(chart.omitted.length > 0 ? [t('blocks.chart.seriesLeftOut', { count: chart.omitted.length, series: chart.omitted.join(', ') })] : []),
395
+ ...(requested === 'stacked' && chart.kind !== 'stacked' ? [t('blocks.chart.sideBySide')] : [])
396
+ ];
397
+ const partial = chart.truncated || chart.omitted.length > 0 || cut;
398
+ const [measure, truncated] = useTruncated<HTMLSpanElement>();
399
+ const toggle = shown ? t('blocks.chart.hideValues') : t('blocks.chart.showValues');
400
+ return (
401
+ <>
402
+ {notes.length > 0 && (
403
+ <div className="flex flex-col gap-0.5 text-xs text-text-muted">
404
+ {notes.map((note) => (
405
+ <p key={note}>{note}</p>
406
+ ))}
407
+ </div>
408
+ )}
409
+ {partial && (
410
+ <Tooltip label={toggle} disabled={!truncated}>
411
+ <Button size="xs" variant="secondary" className="max-w-full self-start" aria-expanded={shown} onClick={() => setShown(!shown)}>
412
+ <span ref={measure} className="min-w-0 truncate">
413
+ {toggle}
414
+ </span>
415
+ </Button>
416
+ </Tooltip>
417
+ )}
418
+ <ValuesTable values={values} unit={unit} shown={partial && shown} />
419
+ </>
420
+ );
421
+ }
422
+
423
+ function DrawnChart({
424
+ chart,
425
+ requested,
426
+ data,
427
+ unit,
428
+ context,
429
+ nodeId
430
+ }: {
431
+ chart: UiChartData;
432
+ requested: ChartKind;
433
+ data: UiProps<'Chart'>['data'];
434
+ unit: string | undefined;
435
+ context: UiRenderContext;
436
+ nodeId: string;
437
+ }) {
286
438
  const { t } = useTranslation('agent-chat');
287
439
  const [measure, width] = useMeasuredWidth();
440
+ const character = useChartCharacter();
441
+ const [measureRows, rowsCut] = useTruncated<HTMLDivElement>(true);
442
+ const values = useMemo(() => uiChartValues(data), [data]);
443
+ const { kind } = chart;
288
444
  return (
289
445
  <figure className="flex flex-col gap-2 px-2">
290
446
  {chart.series.length > 1 && <Legend chart={chart} />}
291
- <div ref={measure} role="img" aria-label={chartSummary(chart, unit, t)} className="relative">
292
- {kind === 'hbar' ? (
293
- <HorizontalChart chart={chart} unit={unit} />
294
- ) : kind === 'stacked' ? (
295
- <StackedChart chart={chart} unit={unit} />
447
+ {kind !== 'stacked' && unitApart(unit) && <p className="text-xs text-text-faint">{unit}</p>}
448
+ <div ref={measure} role="img" aria-label={chartSummary(values, unit, t)} className="relative">
449
+ {kind === 'hbar' || kind === 'stacked' ? (
450
+ <div ref={measureRows}>{kind === 'hbar' ? <HorizontalChart chart={chart} unit={unit} /> : <StackedChart chart={chart} unit={unit} />}</div>
296
451
  ) : (
297
- <div style={{ height: HEIGHT }}>{width > 0 && <VerticalChart chart={chart} kind={kind} unit={unit} width={width} />}</div>
452
+ <div style={{ height: HEIGHT }}>
453
+ {width > 0 && <VerticalChart chart={chart} kind={kind} unit={unit} width={width} character={character} />}
454
+ </div>
298
455
  )}
299
456
  </div>
300
- <ValuesTable chart={chart} unit={unit} />
457
+ <ChartNotes
458
+ chart={chart}
459
+ requested={requested}
460
+ cut={rowsCut || labelsCut(chart, unit, width, character)}
461
+ values={values}
462
+ unit={unit}
463
+ context={context}
464
+ nodeId={nodeId}
465
+ />
301
466
  </figure>
302
467
  );
303
468
  }
@@ -306,7 +471,7 @@ function DrawnChart({ chart, kind, unit }: { chart: UiChartData; kind: ChartKind
306
471
  * Draws only once its node closed; until then a quiet surface of the height it will take holds its
307
472
  * place. It grows in once, and not at all for a person who asked for less motion.
308
473
  */
309
- export function ChartRenderer({ node }: UiRendererProps<UiProps<'Chart'>>) {
474
+ export function ChartRenderer({ node, context }: UiRendererProps<UiProps<'Chart'>>) {
310
475
  const { kind, data, unit } = node.props;
311
476
  const chart = useMemo(() => (node.complete ? uiChartData(data, kind) : null), [node.complete, data, kind]);
312
477
  if (!node.complete) {
@@ -315,5 +480,5 @@ export function ChartRenderer({ node }: UiRendererProps<UiProps<'Chart'>>) {
315
480
  if (chart === null) {
316
481
  return <UiFallbackPart fallback={node.fallback} problem={{ kind: 'failed' }} />;
317
482
  }
318
- return <DrawnChart chart={chart} kind={kind} unit={unit} />;
483
+ return <DrawnChart chart={chart} requested={kind} data={data} unit={unit} context={context} nodeId={node.id} />;
319
484
  }
@@ -2,9 +2,10 @@ import { useId, useState } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { ArrowRight, Check } from 'lucide-react';
5
- import type { UiProps } from '@adecore/intelligent-ui';
5
+ import { uiChoiceLabel, type UiProps } from '@adecore/intelligent-ui';
6
6
  import { DisabledReason, Icon, Spinner, Tooltip } from '@adecore/ui';
7
7
  import type { UiRendererProps } from '../render-context';
8
+ import { UiFallbackPart } from '../UiFallbackPart';
8
9
  import { useLater } from '../use-later';
9
10
 
10
11
  /* A context this long may run past its two lines, so the row offers all of it in a tooltip. */
@@ -28,7 +29,8 @@ export function ChoicesRenderer({ children, context }: UiRendererProps<UiProps<'
28
29
  /*
29
30
  * A row that shows what it sends: the label, and under it the context the agent receives. It stays
30
31
  * closed while the reply streams, once the block is answered and without a host to send it, and
31
- * remains focusable then so a screen reader hears why.
32
+ * remains focusable then so a screen reader hears why. A finished Choice the host would refuse for
33
+ * its label is no row at all, but its text.
32
34
  */
33
35
  export function ChoiceRenderer({ node, children, context }: UiRendererProps<UiProps<'Choice'>>) {
34
36
  const { t } = useTranslation('agent-chat');
@@ -41,6 +43,9 @@ export function ChoiceRenderer({ node, children, context }: UiRendererProps<UiPr
41
43
  const failed = answer === null && context.failedChoiceId === node.id;
42
44
  const closed = waiting || unavailable || answer !== null;
43
45
  const slow = useLater(chosen && answer.state === 'sending', SPINNER_AFTER_MS);
46
+ if (!waiting && uiChoiceLabel(node) === null) {
47
+ return <UiFallbackPart fallback={node.fallback} problem={{ kind: 'failed' }} />;
48
+ }
44
49
  const reason = waiting
45
50
  ? t('blocks.choice.waiting')
46
51
  : answer === null && unavailable
@@ -67,9 +72,9 @@ export function ChoiceRenderer({ node, children, context }: UiRendererProps<UiPr
67
72
  >
68
73
  <Icon icon={ArrowRight} size={14} className={clsx('mt-1 shrink-0', primary === true ? 'text-accent' : 'text-text-faint')} />
69
74
  <span className="flex min-w-0 grow flex-col">
70
- <span className="text-sm font-semibold text-text">{children}</span>
75
+ <span className="text-sm font-semibold wrap-anywhere text-text">{children}</span>
71
76
  {described && (
72
- <span id={descriptionId} className="line-clamp-2 text-xs text-text-muted">
77
+ <span id={descriptionId} className="line-clamp-2 text-xs wrap-anywhere text-text-muted">
73
78
  {detail}
74
79
  </span>
75
80
  )}
@@ -2,7 +2,7 @@ import clsx from 'clsx';
2
2
  import { createContext, useContext, useId } from 'react';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import type { UiProps } from '@adecore/intelligent-ui';
5
- import { PreviewCard } from '@adecore/ui';
5
+ import { PreviewCard, Tooltip } from '@adecore/ui';
6
6
  import { formatNumber } from '@adecore/ui/format';
7
7
  import { chatHost } from '../../../../host';
8
8
  import { CodeBlock } from '../../CodeBlock';
@@ -10,6 +10,7 @@ import { CodeStreamingContext } from '../../code-streaming';
10
10
  import { ImageThumb } from '../../ImageView';
11
11
  import { uiChildrenOf, uiNodeLabel, uiNodeText, uiUrlDomain } from '../node-text';
12
12
  import type { UiRendererProps } from '../render-context';
13
+ import { useTruncated } from '../use-truncated';
13
14
 
14
15
  // The number of every source by its node id, which the list counts in its own order.
15
16
  const SourceNumbers = createContext<ReadonlyMap<string, number>>(new Map());
@@ -115,26 +116,41 @@ export function SourceRenderer({ node, context }: UiRendererProps<UiProps<'Sourc
115
116
  const number = useContext(SourceNumbers).get(node.id);
116
117
  const { title, url } = node.props;
117
118
  const { openUrl } = context;
119
+ const [measureDomain, domainCut] = useTruncated<HTMLSpanElement>();
118
120
  const face = (
119
121
  <>
120
- <span className="w-5 shrink-0 text-right text-text-faint tabular-nums">{number}</span>
121
- <span className="min-w-0 truncate text-text">{title}</span>
122
- <span className="shrink-0 text-text-faint">{uiUrlDomain(url)}</span>
122
+ <span className="w-5 shrink-0 self-start text-right text-text-faint tabular-nums">{number}</span>
123
+ <span className="min-w-0 grow wrap-anywhere text-text">{title}</span>
124
+ <span ref={measureDomain} className="max-w-1/2 min-w-0 shrink-0 self-start truncate text-text-faint">
125
+ {uiUrlDomain(url)}
126
+ </span>
123
127
  </>
124
128
  );
125
129
  return (
126
130
  <li data-ui-source={node.id} tabIndex={-1} className="chat-ui-source scroll-my-2 rounded-md">
127
- {openUrl === undefined ? (
128
- <span className="flex h-8 items-center gap-2 px-2 text-xs">{face}</span>
129
- ) : (
130
- <button
131
- type="button"
132
- className="flex h-8 w-full items-center gap-2 rounded-md px-2 text-left text-xs hover:bg-surface-hover"
133
- onClick={() => openUrl(url)}
134
- >
135
- {face}
136
- </button>
137
- )}
131
+ {/* The title wraps and the domain may be cut short, so the tooltip says both whole; a row that opens nothing takes focus only while it cuts. */}
132
+ <Tooltip
133
+ label={
134
+ <>
135
+ <span className="block">{title}</span>
136
+ <span className="block text-text-muted">{url}</span>
137
+ </>
138
+ }
139
+ >
140
+ {openUrl === undefined ? (
141
+ <span tabIndex={domainCut ? 0 : undefined} className="flex min-h-8 items-start gap-2 rounded-md px-2 py-2 text-xs">
142
+ {face}
143
+ </span>
144
+ ) : (
145
+ <button
146
+ type="button"
147
+ className="flex min-h-8 w-full items-start gap-2 rounded-md px-2 py-2 text-left text-xs hover:bg-surface-hover"
148
+ onClick={() => openUrl(url)}
149
+ >
150
+ {face}
151
+ </button>
152
+ )}
153
+ </Tooltip>
138
154
  </li>
139
155
  );
140
156
  }
@@ -2,9 +2,10 @@ import { createContext, useContext, type MouseEvent, type ReactNode } from 'reac
2
2
  import clsx from 'clsx';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import type { UiBinding, UiProps, UiViewNode } from '@adecore/intelligent-ui';
5
- import { Button, Checkbox, Segmented, Slider, Switch } from '@adecore/ui';
5
+ import { Button, Checkbox, Segmented, Slider, Switch, Tooltip } from '@adecore/ui';
6
6
  import { uiChildrenOf, uiNodeLabel, uiWithUnit } from '../node-text';
7
7
  import type { UiRenderContext, UiRendererProps } from '../render-context';
8
+ import { useTruncated } from '../use-truncated';
8
9
 
9
10
  type Scalar = UiProps<'Item'>['value'];
10
11
 
@@ -106,6 +107,9 @@ export function SliderRenderer({ node, context }: UiRendererProps<UiProps<'Slide
106
107
  );
107
108
  }
108
109
 
110
+ /* A label longer than this reads as a sentence, which wraps badly beside another; the options then stand in a column. */
111
+ const SIDE_BY_SIDE_LABEL = 24;
112
+
109
113
  /* Left aligned, never the full width. Each Option is a segment by its place, since a value may be any scalar. */
110
114
  export function SegmentedRenderer({ node, context }: UiRendererProps<UiProps<'Segmented'>>) {
111
115
  const { t } = useTranslation('agent-chat');
@@ -119,6 +123,7 @@ export function SegmentedRenderer({ node, context }: UiRendererProps<UiProps<'Se
119
123
  value={picked}
120
124
  options={options}
121
125
  label={t('blocks.options')}
126
+ orientation={options.some((option) => option.label.length > SIDE_BY_SIDE_LABEL) ? 'vertical' : 'horizontal'}
122
127
  disabled={binding === null}
123
128
  onValueChange={(id) => {
124
129
  const option = options[Number(id)];
@@ -131,14 +136,20 @@ export function SegmentedRenderer({ node, context }: UiRendererProps<UiProps<'Se
131
136
  );
132
137
  }
133
138
 
134
- /* Runs its local action once its own node closed; like an input, it closes once the block is answered. */
139
+ /* Runs its local action once its own node closed; like an input, it closes once the block is answered. A label cut to the button's one line reads whole in its tooltip. */
135
140
  export function ButtonRenderer({ node, children, context }: UiRendererProps<UiProps<'Button'>>) {
136
141
  const enabled = node.onAction !== undefined && node.complete && context.answer === null && node.props.disabled !== true;
142
+ const label = uiNodeLabel(node);
143
+ const [measure, truncated] = useTruncated<HTMLSpanElement>();
137
144
  return (
138
145
  <div className={clsx('px-2', !node.complete && PENDING)}>
139
- <Button variant="secondary" size="sm" disabled={!enabled} onClick={() => node.onAction?.()}>
140
- {children}
141
- </Button>
146
+ <Tooltip label={label} disabled={!truncated}>
147
+ <Button variant="secondary" size="sm" className="max-w-full" disabled={!enabled} onClick={() => node.onAction?.()}>
148
+ <span ref={measure} className="min-w-0 truncate">
149
+ {children}
150
+ </span>
151
+ </Button>
152
+ </Tooltip>
142
153
  </div>
143
154
  );
144
155
  }