@adecore/agents-react 0.20.1 → 0.21.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 (116) hide show
  1. package/dist/chat/logic/generated-image.d.ts +27 -0
  2. package/dist/chat/logic/generated-image.js +32 -0
  3. package/dist/chat/logic/timeline-target.d.ts +13 -1
  4. package/dist/chat/logic/timeline-target.js +26 -0
  5. package/dist/chat/logic/timeline.js +16 -10
  6. package/dist/chat/subagent-conversation.d.ts +2 -1
  7. package/dist/chat/subagent-conversation.js +7 -1
  8. package/dist/chat/timeline-flash.d.ts +3 -1
  9. package/dist/chat/timeline-flash.js +2 -2
  10. package/dist/chat/ui/ImageView.d.ts +8 -2
  11. package/dist/chat/ui/ImageView.js +12 -6
  12. package/dist/chat/ui/SubagentTimeline.js +6 -2
  13. package/dist/chat/ui/Timeline.js +67 -7
  14. package/dist/chat/ui/intelligent-ui/UiBlockFooter.d.ts +15 -0
  15. package/dist/chat/ui/intelligent-ui/UiBlockFooter.js +50 -0
  16. package/dist/chat/ui/intelligent-ui/UiBlockFrame.d.ts +10 -0
  17. package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +61 -0
  18. package/dist/chat/ui/intelligent-ui/UiFallbackPart.d.ts +10 -0
  19. package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +11 -0
  20. package/dist/chat/ui/intelligent-ui/UiReply.d.ts +13 -0
  21. package/dist/chat/ui/intelligent-ui/UiReply.js +325 -0
  22. package/dist/chat/ui/intelligent-ui/block-local.d.ts +2 -0
  23. package/dist/chat/ui/intelligent-ui/block-local.js +13 -0
  24. package/dist/chat/ui/intelligent-ui/chart-data.d.ts +16 -0
  25. package/dist/chat/ui/intelligent-ui/chart-data.js +46 -0
  26. package/dist/chat/ui/intelligent-ui/node-text.d.ts +9 -0
  27. package/dist/chat/ui/intelligent-ui/node-text.js +52 -0
  28. package/dist/chat/ui/intelligent-ui/registry.d.ts +2 -0
  29. package/dist/chat/ui/intelligent-ui/registry.js +50 -0
  30. package/dist/chat/ui/intelligent-ui/render-context.d.ts +50 -0
  31. package/dist/chat/ui/intelligent-ui/render-context.js +3 -0
  32. package/dist/chat/ui/intelligent-ui/renderers/chart.d.ts +3 -0
  33. package/dist/chat/ui/intelligent-ui/renderers/chart.js +125 -0
  34. package/dist/chat/ui/intelligent-ui/renderers/choices.d.ts +4 -0
  35. package/dist/chat/ui/intelligent-ui/renderers/choices.js +41 -0
  36. package/dist/chat/ui/intelligent-ui/renderers/content.d.ts +14 -0
  37. package/dist/chat/ui/intelligent-ui/renderers/content.js +66 -0
  38. package/dist/chat/ui/intelligent-ui/renderers/data.d.ts +7 -0
  39. package/dist/chat/ui/intelligent-ui/renderers/data.js +87 -0
  40. package/dist/chat/ui/intelligent-ui/renderers/inputs.d.ts +7 -0
  41. package/dist/chat/ui/intelligent-ui/renderers/inputs.js +60 -0
  42. package/dist/chat/ui/intelligent-ui/renderers/links.d.ts +10 -0
  43. package/dist/chat/ui/intelligent-ui/renderers/links.js +86 -0
  44. package/dist/chat/ui/intelligent-ui/renderers/structure.d.ts +6 -0
  45. package/dist/chat/ui/intelligent-ui/renderers/structure.js +32 -0
  46. package/dist/chat/ui/intelligent-ui/renderers/text.d.ts +8 -0
  47. package/dist/chat/ui/intelligent-ui/renderers/text.js +61 -0
  48. package/dist/chat/ui/intelligent-ui/reply-parts.d.ts +9 -0
  49. package/dist/chat/ui/intelligent-ui/reply-parts.js +18 -0
  50. package/dist/chat/ui/intelligent-ui/table-data.d.ts +31 -0
  51. package/dist/chat/ui/intelligent-ui/table-data.js +79 -0
  52. package/dist/chat/ui/intelligent-ui/tones.d.ts +7 -0
  53. package/dist/chat/ui/intelligent-ui/tones.js +31 -0
  54. package/dist/chat/ui/intelligent-ui/use-later.d.ts +1 -0
  55. package/dist/chat/ui/intelligent-ui/use-later.js +16 -0
  56. package/dist/chat/ui/intelligent-ui/use-ui-links.d.ts +9 -0
  57. package/dist/chat/ui/intelligent-ui/use-ui-links.js +107 -0
  58. package/dist/chat/ui/intelligent-ui/use-ui-queries.d.ts +20 -0
  59. package/dist/chat/ui/intelligent-ui/use-ui-queries.js +259 -0
  60. package/dist/chat/ui/reply-context.d.ts +8 -0
  61. package/dist/chat/ui/reply-context.js +1 -0
  62. package/dist/chat/ui/rows/GeneratedImageRow.d.ts +5 -0
  63. package/dist/chat/ui/rows/GeneratedImageRow.js +121 -0
  64. package/dist/chat/ui/rows/MessageRows.js +21 -4
  65. package/dist/chat/ui/rows/Rows.js +4 -2
  66. package/dist/chat/ui/rows/SubagentRow.js +5 -0
  67. package/dist/chat/ui/rows/WorkRows.d.ts +4 -1
  68. package/dist/chat/ui/rows/WorkRows.js +4 -3
  69. package/dist/host.d.ts +11 -1
  70. package/dist/locales/en/agent-chat.json +82 -0
  71. package/dist/locales/nl/agent-chat.json +82 -0
  72. package/dist/state/chats.js +9 -0
  73. package/dist/theme.css +19 -0
  74. package/package.json +133 -7
  75. package/src/chat/logic/generated-image.ts +48 -0
  76. package/src/chat/logic/timeline-target.ts +41 -1
  77. package/src/chat/logic/timeline.ts +19 -10
  78. package/src/chat/subagent-conversation.ts +13 -2
  79. package/src/chat/timeline-flash.ts +5 -3
  80. package/src/chat/ui/ImageView.tsx +51 -8
  81. package/src/chat/ui/SubagentTimeline.tsx +24 -1
  82. package/src/chat/ui/Timeline.tsx +97 -21
  83. package/src/chat/ui/intelligent-ui/UiBlockFooter.tsx +139 -0
  84. package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +89 -0
  85. package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +21 -0
  86. package/src/chat/ui/intelligent-ui/UiReply.tsx +531 -0
  87. package/src/chat/ui/intelligent-ui/block-local.ts +16 -0
  88. package/src/chat/ui/intelligent-ui/chart-data.ts +67 -0
  89. package/src/chat/ui/intelligent-ui/node-text.ts +59 -0
  90. package/src/chat/ui/intelligent-ui/registry.ts +52 -0
  91. package/src/chat/ui/intelligent-ui/render-context.ts +78 -0
  92. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +321 -0
  93. package/src/chat/ui/intelligent-ui/renderers/choices.tsx +98 -0
  94. package/src/chat/ui/intelligent-ui/renderers/content.tsx +145 -0
  95. package/src/chat/ui/intelligent-ui/renderers/data.tsx +187 -0
  96. package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +121 -0
  97. package/src/chat/ui/intelligent-ui/renderers/links.tsx +157 -0
  98. package/src/chat/ui/intelligent-ui/renderers/structure.tsx +84 -0
  99. package/src/chat/ui/intelligent-ui/renderers/text.tsx +124 -0
  100. package/src/chat/ui/intelligent-ui/reply-parts.ts +22 -0
  101. package/src/chat/ui/intelligent-ui/table-data.ts +104 -0
  102. package/src/chat/ui/intelligent-ui/tones.ts +37 -0
  103. package/src/chat/ui/intelligent-ui/use-later.ts +17 -0
  104. package/src/chat/ui/intelligent-ui/use-ui-links.ts +123 -0
  105. package/src/chat/ui/intelligent-ui/use-ui-queries.ts +282 -0
  106. package/src/chat/ui/reply-context.ts +7 -0
  107. package/src/chat/ui/rows/GeneratedImageRow.tsx +244 -0
  108. package/src/chat/ui/rows/MessageRows.tsx +60 -4
  109. package/src/chat/ui/rows/Rows.tsx +4 -2
  110. package/src/chat/ui/rows/SubagentRow.tsx +5 -0
  111. package/src/chat/ui/rows/WorkRows.tsx +20 -5
  112. package/src/host.ts +27 -1
  113. package/src/locales/en/agent-chat.json +82 -0
  114. package/src/locales/nl/agent-chat.json +82 -0
  115. package/src/state/chats.ts +9 -0
  116. package/src/theme.css +19 -0
@@ -0,0 +1,52 @@
1
+ import type { UiRenderers } from './render-context';
2
+ import { ChartRenderer } from './renderers/chart';
3
+ import { ChoiceRenderer, ChoicesRenderer } from './renderers/choices';
4
+ import { CodeBlockRenderer, ImageRenderer, SourceRenderer, SourcesRenderer } from './renderers/content';
5
+ import { EntityListRenderer, EntryRenderer, StatRenderer, StatsRenderer, TableRenderer } from './renderers/data';
6
+ import { ChecklistRenderer, ItemRenderer, SegmentedRenderer, SliderRenderer, SwitchRenderer } from './renderers/inputs';
7
+ import { CommitRenderer, DiffRenderer, FileRenderer, NodeRenderer } from './renderers/links';
8
+ import { SectionRenderer, SectionsRenderer, TabRenderer, TabsRenderer } from './renderers/structure';
9
+ import { CalloutRenderer, ProgressRenderer, StepRenderer, StepsRenderer, SummaryRenderer, TagRenderer } from './renderers/text';
10
+
11
+ /*
12
+ * A renderer for every component of the catalog; a name missing here fails the typecheck. Null draws
13
+ * nothing for the node itself: the runtime resolves Show and Each before rendering, and Column and
14
+ * Option are metadata their parent reads from its node (a table its columns, Segmented its segments).
15
+ */
16
+ export const UI_RENDERERS: UiRenderers = {
17
+ Summary: SummaryRenderer,
18
+ Callout: CalloutRenderer,
19
+ Tag: TagRenderer,
20
+ Progress: ProgressRenderer,
21
+ Steps: StepsRenderer,
22
+ Step: StepRenderer,
23
+ Stats: StatsRenderer,
24
+ Stat: StatRenderer,
25
+ EntityList: EntityListRenderer,
26
+ Entry: EntryRenderer,
27
+ Table: TableRenderer,
28
+ Column: null,
29
+ Chart: ChartRenderer,
30
+ Tabs: TabsRenderer,
31
+ Tab: TabRenderer,
32
+ Sections: SectionsRenderer,
33
+ Section: SectionRenderer,
34
+ CodeBlock: CodeBlockRenderer,
35
+ Image: ImageRenderer,
36
+ Sources: SourcesRenderer,
37
+ Source: SourceRenderer,
38
+ File: FileRenderer,
39
+ Diff: DiffRenderer,
40
+ Commit: CommitRenderer,
41
+ Node: NodeRenderer,
42
+ Checklist: ChecklistRenderer,
43
+ Item: ItemRenderer,
44
+ Switch: SwitchRenderer,
45
+ Slider: SliderRenderer,
46
+ Segmented: SegmentedRenderer,
47
+ Option: null,
48
+ Show: null,
49
+ Each: null,
50
+ Choices: ChoicesRenderer,
51
+ Choice: ChoiceRenderer
52
+ };
@@ -0,0 +1,78 @@
1
+ import { createContext, type ReactNode } from 'react';
2
+ import type { UiComponentName, UiProps, UiViewNode } from '@adecore/intelligent-ui';
3
+
4
+ export type { UiLinkTarget } from '@adecore/intelligent-ui/links';
5
+ import type { UiLinkTarget } from '@adecore/intelligent-ui/links';
6
+
7
+ /* How the host lets a link be drawn: a chip it can open, or plain text it cannot. */
8
+ export interface UiLink {
9
+ state: 'chip' | 'plain';
10
+ /* What the host calls the target, such as the title of a node or the subject of a commit. */
11
+ label?: string;
12
+ /* Why a plain link is not a chip, in its tooltip. */
13
+ reason?: string;
14
+ }
15
+
16
+ /* The Choice that answered a block. `sending` already reads as sent, so a second click does nothing. */
17
+ export interface UiAnswer {
18
+ choiceId: string;
19
+ state: 'sending' | 'sent' | 'queued';
20
+ }
21
+
22
+ /* What the footer of a block that reads live data says about its last reading. */
23
+ export interface UiLiveStatus {
24
+ state: 'fresh' | 'reading' | 'failed' | 'refused';
25
+ /* What the block reads, by the names a person knows them by. */
26
+ sources: readonly string[];
27
+ /* The last reading that succeeded, in milliseconds since the epoch; null when nothing was read yet. */
28
+ readAt: number | null;
29
+ /* The source that failed or was refused. */
30
+ source?: string;
31
+ /* Why a reading failed, in its tooltip. */
32
+ reason?: string;
33
+ }
34
+
35
+ /* Live history is separate from the comparison the agent supplies in Stat.previous. */
36
+ export interface UiLiveValue {
37
+ previous?: unknown;
38
+ changed: boolean;
39
+ stale: boolean;
40
+ }
41
+
42
+ /*
43
+ * What every renderer of a block knows besides its node. The callbacks are the host's: left out, a link
44
+ * stays plain text, a source cannot be opened and a choice stays closed.
45
+ */
46
+ export interface UiRenderContext {
47
+ scopeId: string;
48
+ chatId: string;
49
+ itemId: string;
50
+ blockId: string;
51
+ phase: 'streaming' | 'final';
52
+ answer: UiAnswer | null;
53
+ /* The Choice whose last send failed, which opened the block again. */
54
+ failedChoiceId?: string | null;
55
+ live?: UiLiveStatus | null;
56
+ link?(target: UiLinkTarget): UiLink;
57
+ openLink?(target: UiLinkTarget): void;
58
+ openUrl?(url: string): void;
59
+ onChoose?(nodeId: string): void;
60
+ /* Table cells use the evaluated rows index and column key. */
61
+ liveValue?(nodeId: string, prop: string, path?: readonly [row: number, key: string]): UiLiveValue | undefined;
62
+ }
63
+
64
+ export interface UiRendererProps<Props> {
65
+ /* Valid and evaluated; its children are there as metadata, such as the columns of a table. */
66
+ node: UiViewNode<Props>;
67
+ /* The rendered children, in order, each inside its own error boundary. */
68
+ children: ReactNode;
69
+ context: UiRenderContext;
70
+ }
71
+
72
+ export type UiRenderer<Props> = (props: UiRendererProps<Props>) => ReactNode;
73
+
74
+ /* A renderer for every name in the catalog, or null where the runtime draws the node some other way. */
75
+ export type UiRenderers = { readonly [Name in UiComponentName]: UiRenderer<UiProps<Name>> | null };
76
+
77
+ /* The id of the Summary that heads the block. Undefined outside a frame, where every Summary heads. */
78
+ export const UiHeadContext = createContext<string | null | undefined>(undefined);
@@ -0,0 +1,321 @@
1
+ import { useMemo, useState } from 'react';
2
+ import type { TFunction } from 'i18next';
3
+ import clsx from 'clsx';
4
+ import { useTranslation } from 'react-i18next';
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';
9
+ import type { UiRendererProps } from '../render-context';
10
+ import { UiFallbackPart } from '../UiFallbackPart';
11
+
12
+ type ChartKind = UiProps<'Chart'>['kind'];
13
+
14
+ const HEIGHT = 160;
15
+ const HBAR_ROW = 24;
16
+ const STACKED_ROW = 28;
17
+ const AXIS_WIDTH = 44;
18
+ const PLOT_TOP = 8;
19
+ const LABEL_ROOM = 20;
20
+ /* The narrowest band a category label is written under; narrower bands skip labels. */
21
+ const LABEL_BAND = 56;
22
+ /* Points get a dot up to this many categories; more would crowd the line. */
23
+ const DOTS_UP_TO = 30;
24
+ const GROW = 'transition-[scale,opacity] duration-400 ease-out motion-reduce:transition-none';
25
+
26
+ function seriesColor(index: number): string {
27
+ return `var(--chart-${index + 1})`;
28
+ }
29
+
30
+ function valueText(value: number | null, unit: string | undefined): string {
31
+ if (value === null) {
32
+ return '';
33
+ }
34
+ const text = formatRounded(value, 2);
35
+ return unit === undefined || unit === '' ? text : unit === '%' ? `${text}%` : `${text} ${unit}`;
36
+ }
37
+
38
+ /* The height a chart will take, which its place holds while the node streams. */
39
+ function chartHeight(kind: ChartKind, rows: number): number {
40
+ const count = Math.max(1, Math.min(rows, UI_CHART_ROWS));
41
+ return kind === 'hbar' ? count * HBAR_ROW : kind === 'stacked' ? count * STACKED_ROW : HEIGHT;
42
+ }
43
+
44
+ /* One sentence with the main point, which a screen reader hears in place of the drawing. */
45
+ function chartSummary(chart: UiChartData, unit: string | undefined, t: TFunction<'agent-chat'>): string {
46
+ let top: { label: string; series: string; value: number } | null = null;
47
+ for (const series of chart.series) {
48
+ for (let i = 0; i < series.values.length; i++) {
49
+ const value = series.values[i];
50
+ if (value !== null && value !== undefined && (top === null || value > top.value)) {
51
+ top = { label: chart.labels[i] ?? '', series: series.key, value };
52
+ }
53
+ }
54
+ }
55
+ if (top === null) {
56
+ return t('blocks.chart.values');
57
+ }
58
+ return chart.series.length > 1
59
+ ? t('blocks.chart.summarySeries', { count: chart.labels.length, series: top.series, label: top.label, value: valueText(top.value, unit) })
60
+ : t('blocks.chart.summary', { count: chart.labels.length, label: top.label, value: valueText(top.value, unit) });
61
+ }
62
+
63
+ function Legend({ chart }: { chart: UiChartData }) {
64
+ return (
65
+ <div aria-hidden className="flex flex-wrap gap-x-3 gap-y-1 text-xs text-text-muted">
66
+ {chart.series.map((series, index) => (
67
+ <span key={series.key} className="flex items-center gap-1.5">
68
+ <span className="size-2 rounded-sm" style={{ backgroundColor: seriesColor(index) }} />
69
+ {series.key}
70
+ </span>
71
+ ))}
72
+ </div>
73
+ );
74
+ }
75
+
76
+ /* The numbers behind the drawing, for a screen reader. */
77
+ function ValuesTable({ chart, unit }: { chart: UiChartData; unit: string | undefined }) {
78
+ const { t } = useTranslation('agent-chat');
79
+ return (
80
+ <table className="sr-only">
81
+ <caption>{t('blocks.chart.values')}</caption>
82
+ <thead>
83
+ <tr>
84
+ <td />
85
+ {chart.series.map((series) => (
86
+ <th key={series.key} scope="col">
87
+ {series.key}
88
+ </th>
89
+ ))}
90
+ </tr>
91
+ </thead>
92
+ <tbody>
93
+ {chart.labels.map((label, index) => (
94
+ <tr key={index}>
95
+ <th scope="row">{label}</th>
96
+ {chart.series.map((series) => (
97
+ <td key={series.key}>{valueText(series.values[index] ?? null, unit)}</td>
98
+ ))}
99
+ </tr>
100
+ ))}
101
+ </tbody>
102
+ </table>
103
+ );
104
+ }
105
+
106
+ /* The values of one category beside the pointer. */
107
+ function Readout({ chart, index, left, unit }: { chart: UiChartData; index: number; left: number; unit: string | undefined }) {
108
+ return (
109
+ <div
110
+ aria-hidden
111
+ 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"
112
+ style={{ left }}
113
+ >
114
+ <span className="font-medium text-text">{chart.labels[index]}</span>
115
+ {chart.series.map((series, at) => (
116
+ <span key={series.key} className="flex items-center gap-1.5 text-text-muted tabular-nums">
117
+ <span className="size-2 rounded-sm" style={{ backgroundColor: seriesColor(at) }} />
118
+ {chart.series.length > 1 && <span>{series.key}</span>}
119
+ <span className="text-text">{valueText(series.values[index] ?? null, unit)}</span>
120
+ </span>
121
+ ))}
122
+ </div>
123
+ );
124
+ }
125
+
126
+ /* Bars and lines over a value axis, in SVG at the measured width. */
127
+ function VerticalChart({ chart, kind, unit, width }: { chart: UiChartData; kind: 'bar' | 'line'; unit: string | undefined; width: number }) {
128
+ const [hover, setHover] = useState<number | null>(null);
129
+ const low = chart.min < 0 ? -niceCeiling(-chart.min) : 0;
130
+ const high = chart.max > 0 ? niceCeiling(chart.max) : low === 0 ? 1 : 0;
131
+ const plotWidth = Math.max(0, width - AXIS_WIDTH);
132
+ const plotHeight = HEIGHT - PLOT_TOP - LABEL_ROOM;
133
+ const band = plotWidth / chart.labels.length;
134
+ const y = (value: number): number => Math.round(PLOT_TOP + ((high - value) / (high - low)) * plotHeight);
135
+ const baseline = y(0);
136
+ const ticks = low === 0 ? [0, high / 2, high] : [low, 0, high];
137
+ const labelEvery = Math.max(1, Math.ceil(LABEL_BAND / Math.max(1, band)));
138
+ const center = (index: number): number => Math.round(AXIS_WIDTH + band * index + band / 2);
139
+ const gap = band * 0.2;
140
+ const barWidth = Math.max(1, (band - gap) / chart.series.length);
141
+
142
+ const segments = (values: (number | null)[]): string =>
143
+ values
144
+ .map((value, index) => (value === null ? null : `${center(index)} ${y(value)}`))
145
+ .reduce<string[]>((paths, point, index, all) => {
146
+ if (point !== null) {
147
+ paths.push(`${index === 0 || all[index - 1] === null ? 'M' : 'L'}${point}`);
148
+ }
149
+ return paths;
150
+ }, [])
151
+ .join(' ');
152
+
153
+ return (
154
+ <>
155
+ <svg width={width} height={HEIGHT} aria-hidden className="block overflow-visible" onPointerLeave={() => setHover(null)}>
156
+ {hover !== null && (
157
+ <rect x={Math.round(AXIS_WIDTH + band * hover)} y={PLOT_TOP} width={Math.round(band)} height={plotHeight} className="fill-surface-hover" />
158
+ )}
159
+ {ticks.map((tick) => (
160
+ <g key={tick}>
161
+ <line
162
+ x1={AXIS_WIDTH}
163
+ x2={width}
164
+ y1={y(tick)}
165
+ y2={y(tick)}
166
+ className={tick === 0 ? 'stroke-border' : 'stroke-border-soft'}
167
+ shapeRendering="crispEdges"
168
+ />
169
+ <text x={AXIS_WIDTH - 6} y={y(tick)} dy="0.32em" textAnchor="end" className="fill-text-faint text-xs tabular-nums">
170
+ {valueText(tick, unit)}
171
+ </text>
172
+ </g>
173
+ ))}
174
+ {chart.labels.map((label, index) =>
175
+ index % labelEvery === 0 ? (
176
+ <text key={index} x={center(index)} y={HEIGHT - 4} textAnchor="middle" className="fill-text-faint text-xs">
177
+ {label}
178
+ </text>
179
+ ) : null
180
+ )}
181
+ {kind === 'bar' ? (
182
+ <g className={clsx(GROW, 'starting:scale-y-0')} style={{ transformOrigin: `0px ${baseline}px` }}>
183
+ {chart.series.map((series, at) =>
184
+ series.values.map((value, index) =>
185
+ value === null ? null : (
186
+ <rect
187
+ key={`${series.key}:${index}`}
188
+ x={Math.round(AXIS_WIDTH + band * index + gap / 2 + barWidth * at)}
189
+ y={Math.min(y(value), baseline)}
190
+ width={Math.max(1, Math.round(barWidth) - 1)}
191
+ height={Math.abs(y(value) - baseline)}
192
+ rx={2}
193
+ style={{ fill: seriesColor(at) }}
194
+ />
195
+ )
196
+ )
197
+ )}
198
+ </g>
199
+ ) : (
200
+ <g className={clsx(GROW, 'starting:opacity-0')}>
201
+ {chart.series.map((series, at) => (
202
+ <g key={series.key} style={{ color: seriesColor(at) }}>
203
+ <path
204
+ d={segments(series.values)}
205
+ fill="none"
206
+ stroke="currentColor"
207
+ strokeWidth={2}
208
+ strokeLinejoin="round"
209
+ strokeLinecap="round"
210
+ />
211
+ {chart.labels.length <= DOTS_UP_TO &&
212
+ series.values.map((value, index) =>
213
+ value === null ? null : <circle key={index} cx={center(index)} cy={y(value)} r={2.5} fill="currentColor" />
214
+ )}
215
+ </g>
216
+ ))}
217
+ </g>
218
+ )}
219
+ {chart.labels.map((_, index) => (
220
+ <rect
221
+ key={index}
222
+ x={Math.round(AXIS_WIDTH + band * index)}
223
+ y={0}
224
+ width={Math.max(1, Math.round(band))}
225
+ height={HEIGHT}
226
+ fill="transparent"
227
+ onPointerEnter={() => setHover(index)}
228
+ />
229
+ ))}
230
+ </svg>
231
+ {hover !== null && <Readout chart={chart} index={hover} unit={unit} left={Math.min(Math.max(center(hover), 64), Math.max(64, width - 64))} />}
232
+ </>
233
+ );
234
+ }
235
+
236
+ /* A row of 24px per category, its label beside the bars. */
237
+ function HorizontalChart({ chart, unit }: { chart: UiChartData; unit: string | undefined }) {
238
+ const high = niceCeiling(chart.max);
239
+ const single = chart.series.length === 1;
240
+ return (
241
+ <div aria-hidden className="flex flex-col">
242
+ {chart.labels.map((label, index) => (
243
+ <div key={index} className="flex h-6 items-center gap-2 text-xs">
244
+ <span className="w-2/5 max-w-36 shrink-0 truncate text-text-muted">{label}</span>
245
+ <span className="flex min-w-0 grow flex-col justify-center gap-px">
246
+ {chart.series.map((series, at) => (
247
+ <span
248
+ key={series.key}
249
+ className={clsx(GROW, 'block origin-left rounded-sm starting:scale-x-0', single ? 'h-3' : 'h-1')}
250
+ style={{ width: `${(Math.max(0, series.values[index] ?? 0) / high) * 100}%`, backgroundColor: seriesColor(at) }}
251
+ />
252
+ ))}
253
+ </span>
254
+ {single && (
255
+ <span className="w-16 shrink-0 text-right text-text-faint tabular-nums">{valueText(chart.series[0]!.values[index] ?? null, unit)}</span>
256
+ )}
257
+ </div>
258
+ ))}
259
+ </div>
260
+ );
261
+ }
262
+
263
+ /* A bar per category split into its series, on one scale so the rows compare. */
264
+ function StackedChart({ chart, unit }: { chart: UiChartData; unit: string | undefined }) {
265
+ return (
266
+ <div className="flex flex-col gap-2">
267
+ {chart.labels.map((label, index) => (
268
+ <div key={index} className="flex items-center gap-2 text-xs">
269
+ {chart.labels.length > 1 && <span className="w-2/5 max-w-36 shrink-0 truncate text-text-muted">{label}</span>}
270
+ <SegmentBar
271
+ size="sm"
272
+ className="grow"
273
+ label={label}
274
+ range={[0, chart.max]}
275
+ parts={chart.series.map((series, at) => ({
276
+ value: Math.max(0, series.values[index] ?? 0),
277
+ color: seriesColor(at),
278
+ label: `${series.key} ${valueText(series.values[index] ?? 0, unit)}`
279
+ }))}
280
+ />
281
+ </div>
282
+ ))}
283
+ </div>
284
+ );
285
+ }
286
+
287
+ function DrawnChart({ chart, kind, unit }: { chart: UiChartData; kind: ChartKind; unit: string | undefined }) {
288
+ const { t } = useTranslation('agent-chat');
289
+ const [measure, width] = useMeasuredWidth();
290
+ return (
291
+ <figure className="flex flex-col gap-2 px-2">
292
+ {chart.series.length > 1 && <Legend chart={chart} />}
293
+ <div ref={measure} role="img" aria-label={chartSummary(chart, unit, t)} className="relative">
294
+ {kind === 'hbar' ? (
295
+ <HorizontalChart chart={chart} unit={unit} />
296
+ ) : kind === 'stacked' ? (
297
+ <StackedChart chart={chart} unit={unit} />
298
+ ) : (
299
+ <div style={{ height: HEIGHT }}>{width > 0 && <VerticalChart chart={chart} kind={kind} unit={unit} width={width} />}</div>
300
+ )}
301
+ </div>
302
+ <ValuesTable chart={chart} unit={unit} />
303
+ </figure>
304
+ );
305
+ }
306
+
307
+ /*
308
+ * Draws only once its node closed; until then a quiet surface of the height it will take holds its
309
+ * place. It grows in once, and not at all for a person who asked for less motion.
310
+ */
311
+ export function ChartRenderer({ node }: UiRendererProps<UiProps<'Chart'>>) {
312
+ const { kind, data, unit } = node.props;
313
+ const chart = useMemo(() => (node.complete ? uiChartData(data, kind) : null), [node.complete, data, kind]);
314
+ if (!node.complete) {
315
+ return <div aria-hidden className="mx-2 rounded-md bg-surface-hover" style={{ height: chartHeight(kind, data.length) }} />;
316
+ }
317
+ if (chart === null) {
318
+ return <UiFallbackPart fallback={node.fallback} problem={{ kind: 'failed' }} />;
319
+ }
320
+ return <DrawnChart chart={chart} kind={kind} unit={unit} />;
321
+ }
@@ -0,0 +1,98 @@
1
+ import { useId, useState } from 'react';
2
+ import clsx from 'clsx';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { ArrowRight, Check } from 'lucide-react';
5
+ import type { UiProps } from '@adecore/intelligent-ui';
6
+ import { DisabledReason, Icon, Spinner, Tooltip } from '@adecore/ui';
7
+ import type { UiRendererProps } from '../render-context';
8
+ import { useLater } from '../use-later';
9
+
10
+ /* A context this long may run past its two lines, so the row offers all of it in a tooltip. */
11
+ const LONG_CONTEXT = 120;
12
+ const SPINNER_AFTER_MS = 300;
13
+
14
+ /* Says once, politely, that the choices opened when the reply was done; never on a block that was already done. */
15
+ export function ChoicesRenderer({ children, context }: UiRendererProps<UiProps<'Choices'>>) {
16
+ const { t } = useTranslation('agent-chat');
17
+ const [streamed] = useState(context.phase === 'streaming');
18
+ return (
19
+ <div role="group" aria-label={t('blocks.choices')} className="chat-ui-nodes flex flex-col gap-1.5">
20
+ {children}
21
+ <span className="sr-only" role="status">
22
+ {streamed && context.phase === 'final' ? t('blocks.choice.ready') : ''}
23
+ </span>
24
+ </div>
25
+ );
26
+ }
27
+
28
+ /*
29
+ * A row that shows what it sends: the label, and under it the context the agent receives. It stays
30
+ * 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
+ */
33
+ export function ChoiceRenderer({ node, children, context }: UiRendererProps<UiProps<'Choice'>>) {
34
+ const { t } = useTranslation('agent-chat');
35
+ const descriptionId = useId();
36
+ const { context: detail, primary, disabled } = node.props;
37
+ const { answer, onChoose } = context;
38
+ const chosen = answer !== null && answer.choiceId === node.id;
39
+ const waiting = context.phase === 'streaming';
40
+ const unavailable = disabled === true || onChoose === undefined;
41
+ const failed = answer === null && context.failedChoiceId === node.id;
42
+ const closed = waiting || unavailable || answer !== null;
43
+ const slow = useLater(chosen && answer.state === 'sending', SPINNER_AFTER_MS);
44
+ const reason = waiting ? t('blocks.choice.waiting') : answer === null && unavailable ? t('blocks.choice.unavailable') : null;
45
+ const described = detail !== undefined && detail !== '';
46
+
47
+ const row = (
48
+ <button
49
+ type="button"
50
+ aria-disabled={closed || undefined}
51
+ aria-describedby={described ? descriptionId : undefined}
52
+ className={clsx(
53
+ 'flex w-full items-start gap-2 rounded-md border px-2 py-1.5 text-left transition-[opacity,scale,background-color] duration-150 motion-reduce:transition-none',
54
+ chosen ? 'border-accent bg-accent-soft' : failed ? 'border-status-error bg-surface-hover' : 'border-border bg-surface-hover',
55
+ !closed && 'hover:bg-surface-active active:scale-99',
56
+ waiting ? 'opacity-60' : closed && !chosen && 'opacity-50'
57
+ )}
58
+ onClick={() => {
59
+ if (!closed) {
60
+ onChoose?.(node.id);
61
+ }
62
+ }}
63
+ >
64
+ <Icon icon={ArrowRight} size={14} className={clsx('mt-1 shrink-0', primary === true ? 'text-accent' : 'text-text-faint')} />
65
+ <span className="flex min-w-0 grow flex-col">
66
+ <span className="text-sm font-semibold text-text">{children}</span>
67
+ {described && (
68
+ <span id={descriptionId} className="line-clamp-2 text-xs text-text-muted">
69
+ {detail}
70
+ </span>
71
+ )}
72
+ </span>
73
+ {chosen && (
74
+ <span className="mt-0.5 flex shrink-0 items-center gap-1 text-xs text-accent">
75
+ {slow ? <Spinner size={12} /> : <Icon icon={Check} size={12} />}
76
+ {answer.state === 'queued' ? t('blocks.choice.queued') : t('blocks.choice.sent')}
77
+ </span>
78
+ )}
79
+ </button>
80
+ );
81
+
82
+ return (
83
+ <div className={clsx('flex flex-col gap-1', primary === true && 'order-first')}>
84
+ {reason !== null ? (
85
+ <DisabledReason reason={reason}>{row}</DisabledReason>
86
+ ) : described && detail.length > LONG_CONTEXT ? (
87
+ <Tooltip label={detail}>{row}</Tooltip>
88
+ ) : (
89
+ row
90
+ )}
91
+ {failed && (
92
+ <p role="alert" className="px-2 text-xs text-status-error">
93
+ {t('blocks.choice.failed')}
94
+ </p>
95
+ )}
96
+ </div>
97
+ );
98
+ }