@adecore/agents-react 0.21.2 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/model-requirement.d.ts +6 -0
- package/dist/agents/model-requirement.js +26 -0
- package/dist/chat/chat-client.d.ts +2 -1
- package/dist/chat/chat-client.js +2 -0
- package/dist/chat/drafts.d.ts +2 -1
- package/dist/chat/drafts.js +15 -5
- package/dist/chat/logic/picker-entries.d.ts +10 -0
- package/dist/chat/logic/picker-entries.js +19 -0
- package/dist/chat/logic/timeline.d.ts +5 -0
- package/dist/chat/logic/timeline.js +10 -1
- package/dist/chat/prompt-hold.d.ts +8 -0
- package/dist/chat/prompt-hold.js +16 -0
- package/dist/chat/quote.d.ts +4 -0
- package/dist/chat/quote.js +19 -0
- package/dist/chat/stash.d.ts +8 -2
- package/dist/chat/stash.js +21 -5
- package/dist/chat/subagent-list.d.ts +7 -1
- package/dist/chat/subagent-list.js +13 -1
- package/dist/chat/timeline-scroll.d.ts +6 -2
- package/dist/chat/timeline-scroll.js +12 -7
- package/dist/chat/ui/ChatActivity.js +22 -8
- package/dist/chat/ui/Composer.js +103 -19
- package/dist/chat/ui/ComposerInput.d.ts +3 -1
- package/dist/chat/ui/ComposerInput.js +13 -4
- package/dist/chat/ui/Pickers.js +26 -18
- package/dist/chat/ui/PromptComposer.d.ts +8 -1
- package/dist/chat/ui/PromptComposer.js +18 -6
- package/dist/chat/ui/QuoteButton.js +2 -2
- package/dist/chat/ui/SubagentControls.d.ts +4 -2
- package/dist/chat/ui/SubagentControls.js +12 -8
- package/dist/chat/ui/SubagentInfo.d.ts +1 -1
- package/dist/chat/ui/SubagentInfo.js +9 -15
- package/dist/chat/ui/SubagentStopButton.js +2 -1
- package/dist/chat/ui/Timeline.js +24 -3
- package/dist/chat/ui/composer/menu-aria.d.ts +10 -0
- package/dist/chat/ui/composer/menu-aria.js +18 -0
- package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +3 -3
- package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +1 -1
- package/dist/chat/ui/intelligent-ui/chart-data.d.ts +22 -2
- package/dist/chat/ui/intelligent-ui/chart-data.js +83 -19
- package/dist/chat/ui/intelligent-ui/renderers/chart.d.ts +1 -1
- package/dist/chat/ui/intelligent-ui/renderers/chart.js +99 -35
- package/dist/chat/ui/intelligent-ui/renderers/choices.d.ts +1 -1
- package/dist/chat/ui/intelligent-ui/renderers/choices.js +8 -2
- package/dist/chat/ui/intelligent-ui/renderers/content.js +5 -3
- package/dist/chat/ui/intelligent-ui/renderers/inputs.js +9 -4
- package/dist/chat/ui/intelligent-ui/renderers/links.js +1 -1
- package/dist/chat/ui/intelligent-ui/renderers/text.js +5 -2
- package/dist/chat/ui/intelligent-ui/table-data.js +4 -4
- package/dist/chat/ui/intelligent-ui/use-truncated.d.ts +1 -0
- package/dist/chat/ui/intelligent-ui/use-truncated.js +28 -0
- package/dist/chat/ui/quote-selection.d.ts +5 -2
- package/dist/chat/ui/quote-selection.js +3 -2
- package/dist/chat/ui/rows/MessageRows.js +24 -5
- package/dist/chat/ui/rows/Rows.js +1 -1
- package/dist/chat/ui/rows/WorkRows.d.ts +3 -1
- package/dist/chat/ui/rows/WorkRows.js +4 -4
- package/dist/host.d.ts +1 -0
- package/dist/host.js +1 -0
- package/dist/locales/en/agent-chat.json +39 -10
- package/dist/locales/nl/agent-chat.json +39 -10
- package/dist/state/chats.d.ts +2 -1
- package/dist/state/chats.js +19 -1
- package/package.json +29 -4
- package/src/agents/model-requirement.ts +36 -0
- package/src/chat/chat-client.ts +5 -0
- package/src/chat/drafts.ts +27 -7
- package/src/chat/logic/picker-entries.ts +25 -0
- package/src/chat/logic/timeline.ts +17 -2
- package/src/chat/prompt-hold.ts +27 -0
- package/src/chat/quote.ts +20 -0
- package/src/chat/stash.ts +31 -5
- package/src/chat/subagent-list.ts +19 -5
- package/src/chat/timeline-scroll.ts +24 -10
- package/src/chat/ui/ChatActivity.tsx +35 -9
- package/src/chat/ui/Composer.tsx +197 -52
- package/src/chat/ui/ComposerInput.tsx +18 -3
- package/src/chat/ui/Pickers.tsx +51 -30
- package/src/chat/ui/PromptComposer.tsx +37 -8
- package/src/chat/ui/QuoteButton.tsx +3 -2
- package/src/chat/ui/SubagentControls.tsx +22 -19
- package/src/chat/ui/SubagentInfo.tsx +24 -42
- package/src/chat/ui/SubagentStopButton.tsx +2 -1
- package/src/chat/ui/Timeline.tsx +26 -4
- package/src/chat/ui/composer/menu-aria.ts +29 -0
- package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +5 -5
- package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +6 -1
- package/src/chat/ui/intelligent-ui/chart-data.ts +106 -20
- package/src/chat/ui/intelligent-ui/renderers/chart.tsx +237 -72
- package/src/chat/ui/intelligent-ui/renderers/choices.tsx +9 -4
- package/src/chat/ui/intelligent-ui/renderers/content.tsx +31 -15
- package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +16 -5
- package/src/chat/ui/intelligent-ui/renderers/links.tsx +3 -3
- package/src/chat/ui/intelligent-ui/renderers/text.tsx +18 -8
- package/src/chat/ui/intelligent-ui/table-data.ts +4 -4
- package/src/chat/ui/intelligent-ui/use-truncated.ts +33 -0
- package/src/chat/ui/quote-selection.ts +6 -4
- package/src/chat/ui/rows/MessageRows.tsx +44 -5
- package/src/chat/ui/rows/Rows.tsx +3 -1
- package/src/chat/ui/rows/WorkRows.tsx +15 -1
- package/src/host.ts +3 -0
- package/src/locales/en/agent-chat.json +39 -10
- package/src/locales/nl/agent-chat.json +39 -10
- package/src/state/chats.ts +31 -2
|
@@ -5,55 +5,141 @@ export const UI_CHART_SERIES = 6;
|
|
|
5
5
|
/* Categories a chart draws; more would not fit a column of a thread. */
|
|
6
6
|
export const UI_CHART_ROWS = 60;
|
|
7
7
|
|
|
8
|
+
type ChartRow = UiProps<'Chart'>['data'][number];
|
|
9
|
+
type ChartKind = UiProps<'Chart'>['kind'];
|
|
10
|
+
|
|
8
11
|
export interface UiChartSeries {
|
|
9
12
|
key: string;
|
|
10
13
|
/* One per label, null where a row has no number for this series. */
|
|
11
14
|
values: (number | null)[];
|
|
12
15
|
}
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
/* Every row and every numeric series of a chart's data, whatever a drawing can hold. */
|
|
18
|
+
export interface UiChartValues {
|
|
15
19
|
labels: string[];
|
|
16
20
|
series: UiChartSeries[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UiChartData extends UiChartValues {
|
|
24
|
+
/* What it is drawn as: stacked data with a negative value stands side by side as bars. */
|
|
25
|
+
kind: ChartKind;
|
|
17
26
|
/* The range the axis covers, from zero or below it, never empty. */
|
|
18
27
|
min: number;
|
|
19
28
|
max: number;
|
|
20
29
|
/* Whether rows past `UI_CHART_ROWS` were left out. */
|
|
21
30
|
truncated: boolean;
|
|
31
|
+
/* The rows of the data, drawn or not. */
|
|
32
|
+
total: number;
|
|
33
|
+
/* The numeric series past the first six, which only the values hold. */
|
|
34
|
+
omitted: string[];
|
|
22
35
|
}
|
|
23
36
|
|
|
24
|
-
|
|
25
|
-
* The series a chart can draw: every key besides `label` with a finite number in some row, the
|
|
26
|
-
* first six of them. A value that is missing or no finite number is a gap. Null when there is no
|
|
27
|
-
* row or no series, which draws the fallback.
|
|
28
|
-
*/
|
|
29
|
-
export function uiChartData(data: UiProps<'Chart'>['data'], kind: UiProps<'Chart'>['kind']): UiChartData | null {
|
|
30
|
-
const rows = data.slice(0, UI_CHART_ROWS);
|
|
37
|
+
function numericKeys(rows: readonly ChartRow[]): string[] {
|
|
31
38
|
const keys: string[] = [];
|
|
32
39
|
for (const row of rows) {
|
|
33
40
|
for (const [key, value] of Object.entries(row)) {
|
|
34
|
-
if (key !== 'label' && !keys.includes(key) &&
|
|
41
|
+
if (key !== 'label' && !keys.includes(key) && typeof value === 'number' && Number.isFinite(value)) {
|
|
35
42
|
keys.push(key);
|
|
36
43
|
}
|
|
37
44
|
}
|
|
38
45
|
}
|
|
46
|
+
return keys;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function valuesOf(rows: readonly ChartRow[], keys: readonly string[]): UiChartValues {
|
|
50
|
+
return {
|
|
51
|
+
labels: rows.map((row) => (typeof row.label === 'string' || typeof row.label === 'number' ? String(row.label) : '')),
|
|
52
|
+
series: keys.map((key) => ({
|
|
53
|
+
key,
|
|
54
|
+
values: rows.map((row) => {
|
|
55
|
+
const value = row[key];
|
|
56
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
57
|
+
})
|
|
58
|
+
}))
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* All of a chart's numbers, for the table a person can open beside a drawing that left some out. */
|
|
63
|
+
export function uiChartValues(data: UiProps<'Chart'>['data']): UiChartValues {
|
|
64
|
+
return valuesOf(data, numericKeys(data));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/*
|
|
68
|
+
* The rounded ends of a value axis, or null when no drawing can scale to them: the span has to be
|
|
69
|
+
* finite and above zero, and so does one over it, which rules out a subnormal span as well.
|
|
70
|
+
*/
|
|
71
|
+
export function uiChartRange(min: number, max: number): { low: number; high: number } | null {
|
|
72
|
+
const low = min < 0 ? -niceCeiling(-min) : 0;
|
|
73
|
+
const high = max > 0 ? niceCeiling(max) : low === 0 ? 1 : 0;
|
|
74
|
+
const span = high - low;
|
|
75
|
+
return span > 0 && Number.isFinite(span) && Number.isFinite(1 / span) ? { low, high } : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/*
|
|
79
|
+
* The series a chart can draw: every key besides `label` with a finite number in some row, the
|
|
80
|
+
* first six of them. A value that is missing or no finite number is a gap. Null when there is no
|
|
81
|
+
* row or no series, or when the numbers are too large to scale, which draws the fallback.
|
|
82
|
+
*/
|
|
83
|
+
export function uiChartData(data: UiProps<'Chart'>['data'], kind: ChartKind): UiChartData | null {
|
|
84
|
+
const rows = data.slice(0, UI_CHART_ROWS);
|
|
85
|
+
const keys = numericKeys(rows);
|
|
39
86
|
if (rows.length === 0 || keys.length === 0) {
|
|
40
87
|
return null;
|
|
41
88
|
}
|
|
42
|
-
const series = keys.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const value = row[key];
|
|
46
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
47
|
-
})
|
|
48
|
-
}));
|
|
49
|
-
const labels = rows.map((row) => (typeof row.label === 'string' || typeof row.label === 'number' ? String(row.label) : ''));
|
|
89
|
+
const { labels, series } = valuesOf(rows, keys.slice(0, UI_CHART_SERIES));
|
|
90
|
+
const negative = series.some(({ values }) => values.some((value) => value !== null && value < 0));
|
|
91
|
+
const drawn = kind === 'stacked' && negative ? 'bar' : kind;
|
|
50
92
|
const extents =
|
|
51
|
-
|
|
52
|
-
? labels.map((_, index) => series.reduce((sum, { values }) => sum +
|
|
93
|
+
drawn === 'stacked'
|
|
94
|
+
? labels.map((_, index) => series.reduce((sum, { values }) => sum + (values[index] ?? 0), 0))
|
|
53
95
|
: series.flatMap(({ values }) => values.filter((value) => value !== null));
|
|
54
96
|
const min = Math.min(0, ...extents);
|
|
55
97
|
const max = Math.max(0, ...extents);
|
|
56
|
-
|
|
98
|
+
if (uiChartRange(min, max) === null) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
labels,
|
|
103
|
+
series,
|
|
104
|
+
kind: drawn,
|
|
105
|
+
min,
|
|
106
|
+
max: max === min ? min + 1 : max,
|
|
107
|
+
truncated: data.length > rows.length,
|
|
108
|
+
total: data.length,
|
|
109
|
+
omitted: numericKeys(data).filter((key) => !series.some((drawnSeries) => drawnSeries.key === key))
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* The width of one character of a chart's type at a 16px root, an estimate since SVG text cannot wrap or be measured before it draws. */
|
|
114
|
+
export const UI_CHART_CHARACTER = 7.5;
|
|
115
|
+
/* The narrowest a value axis is, and the gap between a tick label and the plot. */
|
|
116
|
+
const AXIS_WIDTH = 44;
|
|
117
|
+
const AXIS_GAP = 8;
|
|
118
|
+
|
|
119
|
+
/* The character width for a root font size, since chart type is set in rem and follows a person's larger text. */
|
|
120
|
+
export function uiChartCharacter(rootFontSize: number): number {
|
|
121
|
+
return Number.isFinite(rootFontSize) && rootFontSize > 0 ? (UI_CHART_CHARACTER * rootFontSize) / 16 : UI_CHART_CHARACTER;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function uiChartTextWidth(text: string, character = UI_CHART_CHARACTER): number {
|
|
125
|
+
return Math.ceil(text.length * character);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Text cut to the room it has, so a label never reaches past its place; the whole of it is in the values. */
|
|
129
|
+
export function uiChartText(text: string, room: number, character = UI_CHART_CHARACTER): string {
|
|
130
|
+
const fit = Math.floor(room / character);
|
|
131
|
+
return text.length <= fit ? text : `${text.slice(0, Math.max(1, fit - 1))}…`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* A value axis as wide as its longest tick label, at most two fifths of the chart; a label longer than that is cut. */
|
|
135
|
+
export function uiChartAxis(ticks: readonly string[], width: number, character = UI_CHART_CHARACTER): { axisWidth: number; tickTexts: string[] } {
|
|
136
|
+
const scale = character / UI_CHART_CHARACTER;
|
|
137
|
+
const room = Math.max(AXIS_WIDTH * scale, Math.floor(width * 0.4));
|
|
138
|
+
const tickTexts = ticks.map((tick) => uiChartText(tick, room - AXIS_GAP, character));
|
|
139
|
+
return {
|
|
140
|
+
axisWidth: Math.round(Math.min(room, Math.max(AXIS_WIDTH * scale, ...tickTexts.map((text) => uiChartTextWidth(text, character) + AXIS_GAP)))),
|
|
141
|
+
tickTexts
|
|
142
|
+
};
|
|
57
143
|
}
|
|
58
144
|
|
|
59
145
|
/* A round number at or above `value`, for the top of an axis: 1, 2, 2.5 or 5 times a power of ten. */
|
|
@@ -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 {
|
|
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
|
|
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(
|
|
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
|
|
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:
|
|
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
|
|
56
|
-
? t('blocks.chart.summarySeries', { count:
|
|
57
|
-
: t('blocks.chart.summary', { count:
|
|
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
|
-
/*
|
|
74
|
-
function ValuesTable({
|
|
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
|
-
|
|
77
|
-
<table className=
|
|
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
|
-
{
|
|
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
|
-
{
|
|
91
|
-
<tr key={index}>
|
|
92
|
-
<th scope="row"
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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({
|
|
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
|
|
127
|
-
const
|
|
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
|
|
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(
|
|
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={
|
|
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={
|
|
167
|
-
{
|
|
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
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
)
|
|
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(
|
|
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(
|
|
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
|
-
|
|
236
|
-
const high =
|
|
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
|
-
{
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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 }}>
|
|
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
|
-
<
|
|
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}
|
|
483
|
+
return <DrawnChart chart={chart} requested={kind} data={data} unit={unit} context={context} nodeId={node.id} />;
|
|
319
484
|
}
|