@adecore/agents-react 0.22.0 → 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/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/locales/en/agent-chat.json +9 -3
- package/dist/locales/nl/agent-chat.json +9 -3
- package/package.json +9 -4
- 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/locales/en/agent-chat.json +9 -3
- package/src/locales/nl/agent-chat.json +9 -3
|
@@ -2,7 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { useId, useLayoutEffect, useRef, useState } from 'react';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
|
-
import { Lock, Radio, TriangleAlert
|
|
5
|
+
import { Info, Lock, Radio, TriangleAlert } from 'lucide-react';
|
|
6
6
|
import { Icon, Skeleton, Spinner, Tooltip, useTickingText } from '@adecore/ui';
|
|
7
7
|
import { formatAgo, formatMoment } from '@adecore/ui/format';
|
|
8
8
|
import { uiBlockHead, uiReasonText } from './node-text.js';
|
|
@@ -36,7 +36,7 @@ function LivePart({ live }) {
|
|
|
36
36
|
return hint === null ? body : _jsx(Tooltip, { label: hint, children: body });
|
|
37
37
|
}
|
|
38
38
|
/*
|
|
39
|
-
* The line under a block, only when it has something to say: live, answered,
|
|
39
|
+
* The line under a block, only when it has something to say: live, answered, noted or drawn as
|
|
40
40
|
* text, in that order. A hairline of the soft border runs the full width of the card over it.
|
|
41
41
|
*/
|
|
42
42
|
function UiBlockFooter({ live = null, answered = null, fixes = NO_FIXES, shownAsText = null }) {
|
|
@@ -46,7 +46,7 @@ function UiBlockFooter({ live = null, answered = null, fixes = NO_FIXES, shownAs
|
|
|
46
46
|
if (live === null && answered === null && fixes.length === 0 && shownAsText === null) {
|
|
47
47
|
return null;
|
|
48
48
|
}
|
|
49
|
-
return (_jsxs("div", { className: "-mx-2.25 mt-2.25 -mb-2.25 flex flex-col gap-1.5 border-t border-border-soft px-4.25 py-2 text-xs text-text-faint", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1", children: [live !== null && _jsx(LivePart, { live: live }), answered !== null && (_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { children: t('blocks.answered', { label: answered.label }) }), _jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx("time", { dateTime: new Date(answered.at).toISOString(), children: formatMoment(answered.at) })] })), fixes.length > 0 && (_jsxs("button", { type: "button", "aria-expanded": fixesOpen, "aria-controls": listId, className: "flex items-center gap-1.5 rounded-sm hover:text-text-muted", onClick: () => setFixesOpen(!fixesOpen), children: [_jsx(Icon, { icon:
|
|
49
|
+
return (_jsxs("div", { className: "-mx-2.25 mt-2.25 -mb-2.25 flex flex-col gap-1.5 border-t border-border-soft px-4.25 py-2 text-xs text-text-faint", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1", children: [live !== null && _jsx(LivePart, { live: live }), answered !== null && (_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { children: t('blocks.answered', { label: answered.label }) }), _jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx("time", { dateTime: new Date(answered.at).toISOString(), children: formatMoment(answered.at) })] })), fixes.length > 0 && (_jsxs("button", { type: "button", "aria-expanded": fixesOpen, "aria-controls": listId, className: "flex items-center gap-1.5 rounded-sm hover:text-text-muted", onClick: () => setFixesOpen(!fixesOpen), children: [_jsx(Icon, { icon: Info, size: 12 }), t('blocks.notes', { count: fixes.length })] })), shownAsText !== null && (_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { children: t('blocks.shownAsText') }), _jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx("span", { children: shownAsText })] }))] }), fixes.length > 0 && (_jsx("ul", { id: listId, hidden: !fixesOpen, className: "flex flex-col gap-0.5 text-text-muted", children: fixes.map((fix, index) => (_jsxs("li", { className: "flex gap-2", children: [_jsx("span", { className: "shrink-0 font-mono", children: fix.code }), _jsx("span", { className: "min-w-0", children: fix.message })] }, index))) }))] }));
|
|
50
50
|
}
|
|
51
51
|
const RELEASE_MS = 200;
|
|
52
52
|
/*
|
|
@@ -7,5 +7,5 @@ import { Markdown } from '../Markdown.js';
|
|
|
7
7
|
*/
|
|
8
8
|
export function UiFallbackPart({ fallback, problem }) {
|
|
9
9
|
const { t } = useTranslation('agent-chat');
|
|
10
|
-
return (_jsxs("div", { className: "flex flex-col gap-1 rounded-md bg-surface-hover p-2", children: [_jsx("span", { className: "text-xs text-text-faint", children: problem.kind === 'unknown' ? t('blocks.unknownComponent', { component: problem.component }) : t('blocks.renderFailed') }), fallback !== '' && _jsx(Markdown, { text: fallback, fileLinks: false })] }));
|
|
10
|
+
return (_jsxs("div", { className: "flex flex-col gap-1 rounded-md bg-surface-hover p-2", children: [_jsx("span", { className: "text-xs text-text-faint", children: problem.kind === 'unknown' ? t('blocks.unknownComponent', { component: problem.component }) : t('blocks.renderFailed') }), fallback !== '' && (_jsx("div", { className: "min-w-0 wrap-anywhere", children: _jsx(Markdown, { text: fallback, fileLinks: false }) }))] }));
|
|
11
11
|
}
|
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
import type { UiProps } from '@adecore/intelligent-ui';
|
|
2
2
|
export declare const UI_CHART_SERIES = 6;
|
|
3
3
|
export declare const UI_CHART_ROWS = 60;
|
|
4
|
+
type ChartKind = UiProps<'Chart'>['kind'];
|
|
4
5
|
export interface UiChartSeries {
|
|
5
6
|
key: string;
|
|
6
7
|
values: (number | null)[];
|
|
7
8
|
}
|
|
8
|
-
export interface
|
|
9
|
+
export interface UiChartValues {
|
|
9
10
|
labels: string[];
|
|
10
11
|
series: UiChartSeries[];
|
|
12
|
+
}
|
|
13
|
+
export interface UiChartData extends UiChartValues {
|
|
14
|
+
kind: ChartKind;
|
|
11
15
|
min: number;
|
|
12
16
|
max: number;
|
|
13
17
|
truncated: boolean;
|
|
18
|
+
total: number;
|
|
19
|
+
omitted: string[];
|
|
14
20
|
}
|
|
15
|
-
export declare function
|
|
21
|
+
export declare function uiChartValues(data: UiProps<'Chart'>['data']): UiChartValues;
|
|
22
|
+
export declare function uiChartRange(min: number, max: number): {
|
|
23
|
+
low: number;
|
|
24
|
+
high: number;
|
|
25
|
+
} | null;
|
|
26
|
+
export declare function uiChartData(data: UiProps<'Chart'>['data'], kind: ChartKind): UiChartData | null;
|
|
27
|
+
export declare const UI_CHART_CHARACTER = 7.5;
|
|
28
|
+
export declare function uiChartCharacter(rootFontSize: number): number;
|
|
29
|
+
export declare function uiChartTextWidth(text: string, character?: number): number;
|
|
30
|
+
export declare function uiChartText(text: string, room: number, character?: number): string;
|
|
31
|
+
export declare function uiChartAxis(ticks: readonly string[], width: number, character?: number): {
|
|
32
|
+
axisWidth: number;
|
|
33
|
+
tickTexts: string[];
|
|
34
|
+
};
|
|
16
35
|
export declare function niceCeiling(value: number): number;
|
|
36
|
+
export {};
|
|
@@ -2,38 +2,102 @@
|
|
|
2
2
|
export const UI_CHART_SERIES = 6;
|
|
3
3
|
/* Categories a chart draws; more would not fit a column of a thread. */
|
|
4
4
|
export const UI_CHART_ROWS = 60;
|
|
5
|
-
|
|
6
|
-
* The series a chart can draw: every key besides `label` with a finite number in some row, the
|
|
7
|
-
* first six of them. A value that is missing or no finite number is a gap. Null when there is no
|
|
8
|
-
* row or no series, which draws the fallback.
|
|
9
|
-
*/
|
|
10
|
-
export function uiChartData(data, kind) {
|
|
11
|
-
const rows = data.slice(0, UI_CHART_ROWS);
|
|
5
|
+
function numericKeys(rows) {
|
|
12
6
|
const keys = [];
|
|
13
7
|
for (const row of rows) {
|
|
14
8
|
for (const [key, value] of Object.entries(row)) {
|
|
15
|
-
if (key !== 'label' && !keys.includes(key) &&
|
|
9
|
+
if (key !== 'label' && !keys.includes(key) && typeof value === 'number' && Number.isFinite(value)) {
|
|
16
10
|
keys.push(key);
|
|
17
11
|
}
|
|
18
12
|
}
|
|
19
13
|
}
|
|
14
|
+
return keys;
|
|
15
|
+
}
|
|
16
|
+
function valuesOf(rows, keys) {
|
|
17
|
+
return {
|
|
18
|
+
labels: rows.map((row) => (typeof row.label === 'string' || typeof row.label === 'number' ? String(row.label) : '')),
|
|
19
|
+
series: keys.map((key) => ({
|
|
20
|
+
key,
|
|
21
|
+
values: rows.map((row) => {
|
|
22
|
+
const value = row[key];
|
|
23
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
24
|
+
})
|
|
25
|
+
}))
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/* All of a chart's numbers, for the table a person can open beside a drawing that left some out. */
|
|
29
|
+
export function uiChartValues(data) {
|
|
30
|
+
return valuesOf(data, numericKeys(data));
|
|
31
|
+
}
|
|
32
|
+
/*
|
|
33
|
+
* The rounded ends of a value axis, or null when no drawing can scale to them: the span has to be
|
|
34
|
+
* finite and above zero, and so does one over it, which rules out a subnormal span as well.
|
|
35
|
+
*/
|
|
36
|
+
export function uiChartRange(min, max) {
|
|
37
|
+
const low = min < 0 ? -niceCeiling(-min) : 0;
|
|
38
|
+
const high = max > 0 ? niceCeiling(max) : low === 0 ? 1 : 0;
|
|
39
|
+
const span = high - low;
|
|
40
|
+
return span > 0 && Number.isFinite(span) && Number.isFinite(1 / span) ? { low, high } : null;
|
|
41
|
+
}
|
|
42
|
+
/*
|
|
43
|
+
* The series a chart can draw: every key besides `label` with a finite number in some row, the
|
|
44
|
+
* first six of them. A value that is missing or no finite number is a gap. Null when there is no
|
|
45
|
+
* row or no series, or when the numbers are too large to scale, which draws the fallback.
|
|
46
|
+
*/
|
|
47
|
+
export function uiChartData(data, kind) {
|
|
48
|
+
const rows = data.slice(0, UI_CHART_ROWS);
|
|
49
|
+
const keys = numericKeys(rows);
|
|
20
50
|
if (rows.length === 0 || keys.length === 0) {
|
|
21
51
|
return null;
|
|
22
52
|
}
|
|
23
|
-
const series = keys.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
})
|
|
29
|
-
}));
|
|
30
|
-
const labels = rows.map((row) => (typeof row.label === 'string' || typeof row.label === 'number' ? String(row.label) : ''));
|
|
31
|
-
const extents = kind === 'stacked'
|
|
32
|
-
? labels.map((_, index) => series.reduce((sum, { values }) => sum + Math.max(0, values[index] ?? 0), 0))
|
|
53
|
+
const { labels, series } = valuesOf(rows, keys.slice(0, UI_CHART_SERIES));
|
|
54
|
+
const negative = series.some(({ values }) => values.some((value) => value !== null && value < 0));
|
|
55
|
+
const drawn = kind === 'stacked' && negative ? 'bar' : kind;
|
|
56
|
+
const extents = drawn === 'stacked'
|
|
57
|
+
? labels.map((_, index) => series.reduce((sum, { values }) => sum + (values[index] ?? 0), 0))
|
|
33
58
|
: series.flatMap(({ values }) => values.filter((value) => value !== null));
|
|
34
59
|
const min = Math.min(0, ...extents);
|
|
35
60
|
const max = Math.max(0, ...extents);
|
|
36
|
-
|
|
61
|
+
if (uiChartRange(min, max) === null) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
labels,
|
|
66
|
+
series,
|
|
67
|
+
kind: drawn,
|
|
68
|
+
min,
|
|
69
|
+
max: max === min ? min + 1 : max,
|
|
70
|
+
truncated: data.length > rows.length,
|
|
71
|
+
total: data.length,
|
|
72
|
+
omitted: numericKeys(data).filter((key) => !series.some((drawnSeries) => drawnSeries.key === key))
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/* 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. */
|
|
76
|
+
export const UI_CHART_CHARACTER = 7.5;
|
|
77
|
+
/* The narrowest a value axis is, and the gap between a tick label and the plot. */
|
|
78
|
+
const AXIS_WIDTH = 44;
|
|
79
|
+
const AXIS_GAP = 8;
|
|
80
|
+
/* The character width for a root font size, since chart type is set in rem and follows a person's larger text. */
|
|
81
|
+
export function uiChartCharacter(rootFontSize) {
|
|
82
|
+
return Number.isFinite(rootFontSize) && rootFontSize > 0 ? (UI_CHART_CHARACTER * rootFontSize) / 16 : UI_CHART_CHARACTER;
|
|
83
|
+
}
|
|
84
|
+
export function uiChartTextWidth(text, character = UI_CHART_CHARACTER) {
|
|
85
|
+
return Math.ceil(text.length * character);
|
|
86
|
+
}
|
|
87
|
+
/* Text cut to the room it has, so a label never reaches past its place; the whole of it is in the values. */
|
|
88
|
+
export function uiChartText(text, room, character = UI_CHART_CHARACTER) {
|
|
89
|
+
const fit = Math.floor(room / character);
|
|
90
|
+
return text.length <= fit ? text : `${text.slice(0, Math.max(1, fit - 1))}…`;
|
|
91
|
+
}
|
|
92
|
+
/* A value axis as wide as its longest tick label, at most two fifths of the chart; a label longer than that is cut. */
|
|
93
|
+
export function uiChartAxis(ticks, width, character = UI_CHART_CHARACTER) {
|
|
94
|
+
const scale = character / UI_CHART_CHARACTER;
|
|
95
|
+
const room = Math.max(AXIS_WIDTH * scale, Math.floor(width * 0.4));
|
|
96
|
+
const tickTexts = ticks.map((tick) => uiChartText(tick, room - AXIS_GAP, character));
|
|
97
|
+
return {
|
|
98
|
+
axisWidth: Math.round(Math.min(room, Math.max(AXIS_WIDTH * scale, ...tickTexts.map((text) => uiChartTextWidth(text, character) + AXIS_GAP)))),
|
|
99
|
+
tickTexts
|
|
100
|
+
};
|
|
37
101
|
}
|
|
38
102
|
/* A round number at or above `value`, for the top of an axis: 1, 2, 2.5 or 5 times a power of ten. */
|
|
39
103
|
export function niceCeiling(value) {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { UiProps } from '@adecore/intelligent-ui';
|
|
2
2
|
import type { UiRendererProps } from '../render-context.js';
|
|
3
|
-
export declare function ChartRenderer({ node }: UiRendererProps<UiProps<'Chart'>>): import("react").JSX.Element;
|
|
3
|
+
export declare function ChartRenderer({ node, context }: UiRendererProps<UiProps<'Chart'>>): import("react").JSX.Element;
|
|
@@ -2,17 +2,20 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { useMemo, useState } from 'react';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
|
-
import { SegmentBar, useMeasuredWidth } from '@adecore/ui';
|
|
6
|
-
import { formatRounded } from '@adecore/ui/format';
|
|
7
|
-
import {
|
|
5
|
+
import { Button, SegmentBar, Tooltip, useMeasuredWidth } from '@adecore/ui';
|
|
6
|
+
import { formatNumber, formatRounded } from '@adecore/ui/format';
|
|
7
|
+
import { useBlockLocal } from '../block-local.js';
|
|
8
|
+
import { UI_CHART_CHARACTER, UI_CHART_ROWS, uiChartCharacter, uiChartAxis, uiChartData, uiChartRange, uiChartText, uiChartTextWidth, uiChartValues } from '../chart-data.js';
|
|
8
9
|
import { uiWithUnit } from '../node-text.js';
|
|
10
|
+
import { useTruncated } from '../use-truncated.js';
|
|
9
11
|
import { UiFallbackPart } from '../UiFallbackPart.js';
|
|
10
12
|
const HEIGHT = 160;
|
|
11
13
|
const HBAR_ROW = 24;
|
|
12
14
|
const STACKED_ROW = 28;
|
|
13
|
-
const AXIS_WIDTH = 44;
|
|
14
15
|
const PLOT_TOP = 8;
|
|
15
16
|
const LABEL_ROOM = 20;
|
|
17
|
+
/* A unit longer than this stands once above the plot instead of beside every tick. */
|
|
18
|
+
const UNIT_ON_TICKS = 5;
|
|
16
19
|
/* The narrowest band a category label is written under; narrower bands skip labels. */
|
|
17
20
|
const LABEL_BAND = 56;
|
|
18
21
|
/* Points get a dot up to this many categories; more would crowd the line. */
|
|
@@ -24,56 +27,84 @@ function seriesColor(index) {
|
|
|
24
27
|
function valueText(value, unit) {
|
|
25
28
|
return value === null ? '' : uiWithUnit(formatRounded(value, 2), unit);
|
|
26
29
|
}
|
|
30
|
+
function unitApart(unit) {
|
|
31
|
+
return unit !== undefined && unit.length > UNIT_ON_TICKS;
|
|
32
|
+
}
|
|
33
|
+
/* Where a value axis stands and how much room its category labels get, from the labels it writes. */
|
|
34
|
+
function verticalLayout(chart, unit, width, character) {
|
|
35
|
+
// uiChartData only hands over data whose range is finite.
|
|
36
|
+
const { low, high } = uiChartRange(chart.min, chart.max);
|
|
37
|
+
const ticks = low === 0 ? [0, high / 2, high] : [low, 0, high];
|
|
38
|
+
const tickUnit = unitApart(unit) ? undefined : unit;
|
|
39
|
+
const { axisWidth, tickTexts } = uiChartAxis(ticks.map((tick) => valueText(tick, tickUnit)), width, character);
|
|
40
|
+
const band = Math.max(0, width - axisWidth) / chart.labels.length;
|
|
41
|
+
const labelEvery = Math.max(1, Math.ceil(LABEL_BAND / Math.max(1, band)));
|
|
42
|
+
return { low, high, ticks, tickTexts, axisWidth, band, labelEvery, labelRoom: Math.min(band * labelEvery, Math.max(0, width - axisWidth)) };
|
|
43
|
+
}
|
|
44
|
+
/* 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. */
|
|
45
|
+
function labelsCut(chart, unit, width, character) {
|
|
46
|
+
if (width <= 0 || chart.kind === 'hbar' || chart.kind === 'stacked') {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const { labelRoom } = verticalLayout(chart, unit, width, character);
|
|
50
|
+
return chart.labels.some((label) => uiChartTextWidth(label, character) > labelRoom);
|
|
51
|
+
}
|
|
52
|
+
/* 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. */
|
|
53
|
+
function useChartCharacter() {
|
|
54
|
+
const [character] = useState(() => typeof document === 'undefined' ? UI_CHART_CHARACTER : uiChartCharacter(Number.parseFloat(getComputedStyle(document.documentElement).fontSize)));
|
|
55
|
+
return character;
|
|
56
|
+
}
|
|
27
57
|
/* The height a chart will take, which its place holds while the node streams. */
|
|
28
58
|
function chartHeight(kind, rows) {
|
|
29
59
|
const count = Math.max(1, Math.min(rows, UI_CHART_ROWS));
|
|
30
60
|
return kind === 'hbar' ? count * HBAR_ROW : kind === 'stacked' ? count * STACKED_ROW : HEIGHT;
|
|
31
61
|
}
|
|
32
|
-
/* One sentence with the main point, which a screen reader hears in place of the drawing. */
|
|
33
|
-
function chartSummary(
|
|
62
|
+
/* One sentence with the main point over all of the data, drawn or not, which a screen reader hears in place of the drawing. */
|
|
63
|
+
function chartSummary(values, unit, t) {
|
|
34
64
|
let top = null;
|
|
35
|
-
for (const series of
|
|
65
|
+
for (const series of values.series) {
|
|
36
66
|
for (let i = 0; i < series.values.length; i++) {
|
|
37
67
|
const value = series.values[i];
|
|
38
68
|
if (value !== null && value !== undefined && (top === null || value > top.value)) {
|
|
39
|
-
top = { label:
|
|
69
|
+
top = { label: values.labels[i] ?? '', series: series.key, value };
|
|
40
70
|
}
|
|
41
71
|
}
|
|
42
72
|
}
|
|
43
73
|
if (top === null) {
|
|
44
74
|
return t('blocks.chart.values');
|
|
45
75
|
}
|
|
46
|
-
return
|
|
47
|
-
? t('blocks.chart.summarySeries', { count:
|
|
48
|
-
: t('blocks.chart.summary', { count:
|
|
76
|
+
return values.series.length > 1
|
|
77
|
+
? t('blocks.chart.summarySeries', { count: values.labels.length, series: top.series, label: top.label, value: valueText(top.value, unit) })
|
|
78
|
+
: t('blocks.chart.summary', { count: values.labels.length, label: top.label, value: valueText(top.value, unit) });
|
|
49
79
|
}
|
|
50
80
|
function Legend({ chart }) {
|
|
51
81
|
return (_jsx("div", { "aria-hidden": true, className: "flex flex-wrap gap-x-3 gap-y-1 text-xs text-text-muted", children: chart.series.map((series, index) => (_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "size-2 rounded-sm", style: { backgroundColor: seriesColor(index) } }), series.key] }, series.key))) }));
|
|
52
82
|
}
|
|
53
|
-
/*
|
|
54
|
-
function ValuesTable({
|
|
83
|
+
/* Every number of the data, for a screen reader, and on screen once a person opens it. */
|
|
84
|
+
function ValuesTable({ values, unit, shown }) {
|
|
55
85
|
const { t } = useTranslation('agent-chat');
|
|
56
|
-
|
|
86
|
+
const table = (_jsxs("table", { className: shown ? 'w-full border-collapse text-xs tabular-nums' : undefined, children: [_jsx("caption", { className: shown ? 'sr-only' : undefined, children: t('blocks.chart.values') }), _jsx("thead", { className: shown ? 'sticky top-0 z-1 bg-surface-hover' : undefined, children: _jsxs("tr", { children: [_jsx("td", {}), values.series.map((series) => (_jsx("th", { scope: "col", className: shown ? 'h-8 px-2 text-right font-medium whitespace-nowrap text-text-muted' : undefined, children: series.key }, series.key)))] }) }), _jsx("tbody", { children: values.labels.map((label, index) => (_jsxs("tr", { className: shown ? 'border-t border-border-soft' : undefined, children: [_jsx("th", { scope: "row", className: shown ? 'h-8 px-2 text-left font-normal wrap-anywhere text-text-muted' : undefined, children: label }), values.series.map((series) => (_jsx("td", { className: shown ? 'h-8 px-2 text-right whitespace-nowrap text-text' : undefined, children: valueText(series.values[index] ?? null, unit) }, series.key)))] }, index))) })] }));
|
|
87
|
+
// A table grows to its widest cell whatever width it is given, so it is hidden inside a block that clips.
|
|
88
|
+
return _jsx("div", { className: shown ? 'max-h-80 w-full overflow-auto rounded-md border border-border-soft' : 'sr-only', children: table });
|
|
57
89
|
}
|
|
58
90
|
/* The values of one category beside the pointer. */
|
|
59
91
|
function Readout({ chart, index, left, unit }) {
|
|
60
|
-
return (_jsxs("div", { "aria-hidden": true, 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
|
|
92
|
+
return (_jsxs("div", { "aria-hidden": true, 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", style: { left }, children: [_jsx("span", { className: "font-medium wrap-anywhere text-text", children: chart.labels[index] }), chart.series.map((series, at) => (_jsxs("span", { className: "flex items-center gap-1.5 text-text-muted tabular-nums", children: [_jsx("span", { className: "size-2 rounded-sm", style: { backgroundColor: seriesColor(at) } }), chart.series.length > 1 && _jsx("span", { children: series.key }), _jsx("span", { className: "text-text", children: valueText(series.values[index] ?? null, unit) })] }, series.key)))] }));
|
|
61
93
|
}
|
|
62
94
|
/* Bars and lines over a value axis, in SVG at the measured width. */
|
|
63
|
-
function VerticalChart({ chart, kind, unit, width }) {
|
|
95
|
+
function VerticalChart({ chart, kind, unit, width, character }) {
|
|
64
96
|
const [hover, setHover] = useState(null);
|
|
65
|
-
const low
|
|
66
|
-
const
|
|
67
|
-
const plotWidth = Math.max(0, width - AXIS_WIDTH);
|
|
68
|
-
const plotHeight = HEIGHT - PLOT_TOP - LABEL_ROOM;
|
|
69
|
-
const band = plotWidth / chart.labels.length;
|
|
97
|
+
const { low, high, ticks, tickTexts, axisWidth, band, labelEvery, labelRoom } = verticalLayout(chart, unit, width, character);
|
|
98
|
+
const plotHeight = HEIGHT - PLOT_TOP - Math.round((LABEL_ROOM * character) / UI_CHART_CHARACTER);
|
|
70
99
|
const y = (value) => Math.round(PLOT_TOP + ((high - value) / (high - low)) * plotHeight);
|
|
71
100
|
const baseline = y(0);
|
|
72
|
-
const
|
|
73
|
-
const labelEvery = Math.max(1, Math.ceil(LABEL_BAND / Math.max(1, band)));
|
|
74
|
-
const center = (index) => Math.round(AXIS_WIDTH + band * index + band / 2);
|
|
101
|
+
const center = (index) => Math.round(axisWidth + band * index + band / 2);
|
|
75
102
|
const gap = band * 0.2;
|
|
76
103
|
const barWidth = Math.max(1, (band - gap) / chart.series.length);
|
|
104
|
+
const labelAt = (index, text) => {
|
|
105
|
+
const half = uiChartTextWidth(text, character) / 2;
|
|
106
|
+
return Math.round(Math.min(Math.max(center(index), axisWidth + half), width - half));
|
|
107
|
+
};
|
|
77
108
|
const segments = (values) => values
|
|
78
109
|
.map((value, index) => (value === null ? null : `${center(index)} ${y(value)}`))
|
|
79
110
|
.reduce((paths, point, index, all) => {
|
|
@@ -83,34 +114,67 @@ function VerticalChart({ chart, kind, unit, width }) {
|
|
|
83
114
|
return paths;
|
|
84
115
|
}, [])
|
|
85
116
|
.join(' ');
|
|
86
|
-
return (_jsxs(_Fragment, { children: [_jsxs("svg", { width: width, height: HEIGHT, "aria-hidden": true, className: "block overflow-visible", onPointerLeave: () => setHover(null), children: [hover !== null && (_jsx("rect", { x: Math.round(
|
|
87
|
-
|
|
117
|
+
return (_jsxs(_Fragment, { children: [_jsxs("svg", { width: width, height: HEIGHT, "aria-hidden": true, className: "block overflow-visible", onPointerLeave: () => setHover(null), children: [hover !== null && (_jsx("rect", { x: Math.round(axisWidth + band * hover), y: PLOT_TOP, width: Math.round(band), height: plotHeight, className: "fill-surface-hover" })), ticks.map((tick, index) => (_jsxs("g", { children: [_jsx("line", { x1: axisWidth, x2: width, y1: y(tick), y2: y(tick), className: tick === 0 ? 'stroke-border' : 'stroke-border-soft', shapeRendering: "crispEdges" }), _jsx("text", { x: axisWidth - 6, y: y(tick), dy: "0.32em", textAnchor: "end", className: "fill-text-faint text-xs tabular-nums", children: tickTexts[index] })] }, tick))), chart.labels.map((label, index) => {
|
|
118
|
+
if (index % labelEvery !== 0) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const text = uiChartText(label, labelRoom, character);
|
|
122
|
+
return (_jsx("text", { x: labelAt(index, text), y: HEIGHT - 4, textAnchor: "middle", className: "fill-text-faint text-xs", children: text }, index));
|
|
123
|
+
}), kind === 'bar' ? (_jsx("g", { className: clsx(GROW, 'starting:scale-y-0'), style: { transformOrigin: `0px ${baseline}px` }, children: chart.series.map((series, at) => series.values.map((value, index) => value === null ? null : (_jsx("rect", { x: Math.round(axisWidth + band * index + gap / 2 + barWidth * at), y: Math.min(y(value), baseline), width: Math.max(1, Math.round(barWidth) - 1), height: Math.abs(y(value) - baseline), rx: 2, style: { fill: seriesColor(at) } }, `${series.key}:${index}`)))) })) : (_jsx("g", { className: clsx(GROW, 'starting:opacity-0'), children: chart.series.map((series, at) => (_jsxs("g", { style: { color: seriesColor(at) }, children: [_jsx("path", { d: segments(series.values), fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinejoin: "round", strokeLinecap: "round" }), chart.labels.length <= DOTS_UP_TO &&
|
|
124
|
+
series.values.map((value, index) => value === null ? null : _jsx("circle", { cx: center(index), cy: y(value), r: 3, fill: "currentColor" }, index))] }, series.key))) })), chart.labels.map((_, index) => (_jsx("rect", { x: Math.round(axisWidth + band * index), y: 0, width: Math.max(1, Math.round(band)), height: HEIGHT, fill: "transparent", onPointerEnter: () => setHover(index) }, index)))] }), hover !== null && _jsx(Readout, { chart: chart, index: hover, unit: unit, left: Math.min(Math.max(center(hover), 64), Math.max(64, width - 64)) })] }));
|
|
88
125
|
}
|
|
89
|
-
/* A row of 24px per category, its label beside the bars. */
|
|
126
|
+
/* A row of 24px per category, its label beside the bars, which grow from zero to either side. */
|
|
90
127
|
function HorizontalChart({ chart, unit }) {
|
|
91
|
-
|
|
92
|
-
const high =
|
|
128
|
+
const valueUnit = unitApart(unit) ? undefined : unit;
|
|
129
|
+
const { low, high } = uiChartRange(chart.min, chart.max);
|
|
130
|
+
const span = high - low;
|
|
131
|
+
const zero = (-low / span) * 100;
|
|
93
132
|
const single = chart.series.length === 1;
|
|
94
|
-
return (_jsx("div", { "aria-hidden": true, className: "flex flex-col", children: chart.labels.map((label, index) => (_jsxs("div", { className: "flex h-6 items-center gap-2 text-xs", children: [_jsx("span", { className: "w-2/5 max-w-36 shrink-0 truncate text-text-muted", children: label }),
|
|
133
|
+
return (_jsx("div", { "aria-hidden": true, className: "flex flex-col", children: chart.labels.map((label, index) => (_jsxs("div", { className: "flex h-6 items-center gap-2 text-xs", children: [_jsx("span", { className: "w-2/5 max-w-36 shrink-0 truncate text-text-muted", children: label }), _jsxs("span", { className: "relative flex min-w-0 grow flex-col justify-center gap-px", children: [low < 0 && _jsx("span", { className: "absolute inset-y-0 w-px bg-border", style: { left: `${zero}%` } }), chart.series.map((series, at) => {
|
|
134
|
+
const value = series.values[index] ?? 0;
|
|
135
|
+
return (_jsx("span", { className: clsx('relative block', single ? 'h-3' : 'h-1'), children: _jsx("span", { className: clsx(GROW, 'absolute inset-y-0 rounded-sm starting:scale-x-0', value < 0 ? 'origin-right' : 'origin-left'), style: {
|
|
136
|
+
left: `${((Math.min(value, 0) - low) / span) * 100}%`,
|
|
137
|
+
width: `${(Math.abs(value) / span) * 100}%`,
|
|
138
|
+
backgroundColor: seriesColor(at)
|
|
139
|
+
} }) }, series.key));
|
|
140
|
+
})] }), single && (_jsx("span", { className: "max-w-1/3 min-w-12 shrink-0 truncate text-right text-text-faint tabular-nums", children: valueText(chart.series[0].values[index] ?? null, valueUnit) }))] }, index))) }));
|
|
95
141
|
}
|
|
96
142
|
/* A bar per category split into its series, on one scale so the rows compare. */
|
|
97
143
|
function StackedChart({ chart, unit }) {
|
|
98
144
|
return (_jsx("div", { className: "flex flex-col gap-2", children: chart.labels.map((label, index) => (_jsxs("div", { className: "flex items-center gap-2 text-xs", children: [chart.labels.length > 1 && _jsx("span", { className: "w-2/5 max-w-36 shrink-0 truncate text-text-muted", children: label }), _jsx(SegmentBar, { size: "sm", className: "grow", label: label, range: [0, chart.max], parts: chart.series.map((series, at) => ({
|
|
99
|
-
value:
|
|
145
|
+
value: series.values[index] ?? 0,
|
|
100
146
|
color: seriesColor(at),
|
|
101
147
|
label: `${series.key} ${valueText(series.values[index] ?? 0, unit)}`
|
|
102
148
|
})) })] }, index))) }));
|
|
103
149
|
}
|
|
104
|
-
|
|
150
|
+
/* What the drawing leaves out or draws otherwise, said in words, with every value a press away. */
|
|
151
|
+
function ChartNotes({ chart, requested, cut, values, unit, context, nodeId }) {
|
|
152
|
+
const { t } = useTranslation('agent-chat');
|
|
153
|
+
const [shown, setShown] = useBlockLocal(context, `${nodeId}:values`, () => false);
|
|
154
|
+
const notes = [
|
|
155
|
+
...(chart.truncated ? [t('blocks.chart.rowsLeftOut', { shown: formatNumber(chart.labels.length), total: formatNumber(chart.total) })] : []),
|
|
156
|
+
...(chart.omitted.length > 0 ? [t('blocks.chart.seriesLeftOut', { count: chart.omitted.length, series: chart.omitted.join(', ') })] : []),
|
|
157
|
+
...(requested === 'stacked' && chart.kind !== 'stacked' ? [t('blocks.chart.sideBySide')] : [])
|
|
158
|
+
];
|
|
159
|
+
const partial = chart.truncated || chart.omitted.length > 0 || cut;
|
|
160
|
+
const [measure, truncated] = useTruncated();
|
|
161
|
+
const toggle = shown ? t('blocks.chart.hideValues') : t('blocks.chart.showValues');
|
|
162
|
+
return (_jsxs(_Fragment, { children: [notes.length > 0 && (_jsx("div", { className: "flex flex-col gap-0.5 text-xs text-text-muted", children: notes.map((note) => (_jsx("p", { children: note }, note))) })), partial && (_jsx(Tooltip, { label: toggle, disabled: !truncated, children: _jsx(Button, { size: "xs", variant: "secondary", className: "max-w-full self-start", "aria-expanded": shown, onClick: () => setShown(!shown), children: _jsx("span", { ref: measure, className: "min-w-0 truncate", children: toggle }) }) })), _jsx(ValuesTable, { values: values, unit: unit, shown: partial && shown })] }));
|
|
163
|
+
}
|
|
164
|
+
function DrawnChart({ chart, requested, data, unit, context, nodeId }) {
|
|
105
165
|
const { t } = useTranslation('agent-chat');
|
|
106
166
|
const [measure, width] = useMeasuredWidth();
|
|
107
|
-
|
|
167
|
+
const character = useChartCharacter();
|
|
168
|
+
const [measureRows, rowsCut] = useTruncated(true);
|
|
169
|
+
const values = useMemo(() => uiChartValues(data), [data]);
|
|
170
|
+
const { kind } = chart;
|
|
171
|
+
return (_jsxs("figure", { className: "flex flex-col gap-2 px-2", children: [chart.series.length > 1 && _jsx(Legend, { chart: chart }), kind !== 'stacked' && unitApart(unit) && _jsx("p", { className: "text-xs text-text-faint", children: unit }), _jsx("div", { ref: measure, role: "img", "aria-label": chartSummary(values, unit, t), className: "relative", children: kind === 'hbar' || kind === 'stacked' ? (_jsx("div", { ref: measureRows, children: kind === 'hbar' ? _jsx(HorizontalChart, { chart: chart, unit: unit }) : _jsx(StackedChart, { chart: chart, unit: unit }) })) : (_jsx("div", { style: { height: HEIGHT }, children: width > 0 && _jsx(VerticalChart, { chart: chart, kind: kind, unit: unit, width: width, character: character }) })) }), _jsx(ChartNotes, { chart: chart, requested: requested, cut: rowsCut || labelsCut(chart, unit, width, character), values: values, unit: unit, context: context, nodeId: nodeId })] }));
|
|
108
172
|
}
|
|
109
173
|
/*
|
|
110
174
|
* Draws only once its node closed; until then a quiet surface of the height it will take holds its
|
|
111
175
|
* place. It grows in once, and not at all for a person who asked for less motion.
|
|
112
176
|
*/
|
|
113
|
-
export function ChartRenderer({ node }) {
|
|
177
|
+
export function ChartRenderer({ node, context }) {
|
|
114
178
|
const { kind, data, unit } = node.props;
|
|
115
179
|
const chart = useMemo(() => (node.complete ? uiChartData(data, kind) : null), [node.complete, data, kind]);
|
|
116
180
|
if (!node.complete) {
|
|
@@ -119,5 +183,5 @@ export function ChartRenderer({ node }) {
|
|
|
119
183
|
if (chart === null) {
|
|
120
184
|
return _jsx(UiFallbackPart, { fallback: node.fallback, problem: { kind: 'failed' } });
|
|
121
185
|
}
|
|
122
|
-
return _jsx(DrawnChart, { chart: chart,
|
|
186
|
+
return _jsx(DrawnChart, { chart: chart, requested: kind, data: data, unit: unit, context: context, nodeId: node.id });
|
|
123
187
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type UiProps } from '@adecore/intelligent-ui';
|
|
2
2
|
import type { UiRendererProps } from '../render-context.js';
|
|
3
3
|
export declare function ChoicesRenderer({ children, context }: UiRendererProps<UiProps<'Choices'>>): import("react").JSX.Element;
|
|
4
4
|
export declare function ChoiceRenderer({ node, children, context }: UiRendererProps<UiProps<'Choice'>>): import("react").JSX.Element;
|
|
@@ -3,7 +3,9 @@ import { useId, useState } from 'react';
|
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import { ArrowRight, Check } from 'lucide-react';
|
|
6
|
+
import { uiChoiceLabel } from '@adecore/intelligent-ui';
|
|
6
7
|
import { DisabledReason, Icon, Spinner, Tooltip } from '@adecore/ui';
|
|
8
|
+
import { UiFallbackPart } from '../UiFallbackPart.js';
|
|
7
9
|
import { useLater } from '../use-later.js';
|
|
8
10
|
/* A context this long may run past its two lines, so the row offers all of it in a tooltip. */
|
|
9
11
|
const LONG_CONTEXT = 120;
|
|
@@ -17,7 +19,8 @@ export function ChoicesRenderer({ children, context }) {
|
|
|
17
19
|
/*
|
|
18
20
|
* A row that shows what it sends: the label, and under it the context the agent receives. It stays
|
|
19
21
|
* closed while the reply streams, once the block is answered and without a host to send it, and
|
|
20
|
-
* remains focusable then so a screen reader hears why.
|
|
22
|
+
* remains focusable then so a screen reader hears why. A finished Choice the host would refuse for
|
|
23
|
+
* its label is no row at all, but its text.
|
|
21
24
|
*/
|
|
22
25
|
export function ChoiceRenderer({ node, children, context }) {
|
|
23
26
|
const { t } = useTranslation('agent-chat');
|
|
@@ -30,6 +33,9 @@ export function ChoiceRenderer({ node, children, context }) {
|
|
|
30
33
|
const failed = answer === null && context.failedChoiceId === node.id;
|
|
31
34
|
const closed = waiting || unavailable || answer !== null;
|
|
32
35
|
const slow = useLater(chosen && answer.state === 'sending', SPINNER_AFTER_MS);
|
|
36
|
+
if (!waiting && uiChoiceLabel(node) === null) {
|
|
37
|
+
return _jsx(UiFallbackPart, { fallback: node.fallback, problem: { kind: 'failed' } });
|
|
38
|
+
}
|
|
33
39
|
const reason = waiting
|
|
34
40
|
? t('blocks.choice.waiting')
|
|
35
41
|
: answer === null && unavailable
|
|
@@ -40,6 +46,6 @@ export function ChoiceRenderer({ node, children, context }) {
|
|
|
40
46
|
if (!closed) {
|
|
41
47
|
onChoose?.(node.id);
|
|
42
48
|
}
|
|
43
|
-
}, children: [_jsx(Icon, { icon: ArrowRight, size: 14, className: clsx('mt-1 shrink-0', primary === true ? 'text-accent' : 'text-text-faint') }), _jsxs("span", { className: "flex min-w-0 grow flex-col", children: [_jsx("span", { className: "text-sm font-semibold text-text", children: children }), described && (_jsx("span", { id: descriptionId, className: "line-clamp-2 text-xs text-text-muted", children: detail }))] }), chosen && (_jsxs("span", { className: "mt-0.5 flex shrink-0 items-center gap-1 text-xs text-accent", children: [slow ? _jsx(Spinner, { size: 12 }) : _jsx(Icon, { icon: Check, size: 12 }), answer.state === 'queued' ? t('blocks.choice.queued') : t('blocks.choice.sent')] }))] }));
|
|
49
|
+
}, children: [_jsx(Icon, { icon: ArrowRight, size: 14, className: clsx('mt-1 shrink-0', primary === true ? 'text-accent' : 'text-text-faint') }), _jsxs("span", { className: "flex min-w-0 grow flex-col", children: [_jsx("span", { className: "text-sm font-semibold wrap-anywhere text-text", children: children }), described && (_jsx("span", { id: descriptionId, className: "line-clamp-2 text-xs wrap-anywhere text-text-muted", children: detail }))] }), chosen && (_jsxs("span", { className: "mt-0.5 flex shrink-0 items-center gap-1 text-xs text-accent", children: [slow ? _jsx(Spinner, { size: 12 }) : _jsx(Icon, { icon: Check, size: 12 }), answer.state === 'queued' ? t('blocks.choice.queued') : t('blocks.choice.sent')] }))] }));
|
|
44
50
|
return (_jsxs("div", { className: "flex flex-col gap-1", children: [reason !== null ? (_jsx(DisabledReason, { reason: reason, children: row })) : described && detail.length > LONG_CONTEXT ? (_jsx(Tooltip, { label: detail, children: row })) : (row), failed && (_jsx("p", { role: "alert", className: "px-2 text-xs text-status-error", children: t('blocks.choice.failed') }))] }));
|
|
45
51
|
}
|
|
@@ -2,13 +2,14 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { createContext, useContext, useId } from 'react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
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.js';
|
|
8
8
|
import { CodeBlock } from '../../CodeBlock.js';
|
|
9
9
|
import { CodeStreamingContext } from '../../code-streaming.js';
|
|
10
10
|
import { ImageThumb } from '../../ImageView.js';
|
|
11
11
|
import { uiChildrenOf, uiNodeLabel, uiNodeText, uiUrlDomain } from '../node-text.js';
|
|
12
|
+
import { useTruncated } from '../use-truncated.js';
|
|
12
13
|
// The number of every source by its node id, which the list counts in its own order.
|
|
13
14
|
const SourceNumbers = createContext(new Map());
|
|
14
15
|
/* Plain text in the same measure until the node closes, so highlighting never makes it jump. */
|
|
@@ -54,6 +55,7 @@ export function SourceRenderer({ node, context }) {
|
|
|
54
55
|
const number = useContext(SourceNumbers).get(node.id);
|
|
55
56
|
const { title, url } = node.props;
|
|
56
57
|
const { openUrl } = context;
|
|
57
|
-
const
|
|
58
|
-
|
|
58
|
+
const [measureDomain, domainCut] = useTruncated();
|
|
59
|
+
const face = (_jsxs(_Fragment, { children: [_jsx("span", { className: "w-5 shrink-0 self-start text-right text-text-faint tabular-nums", children: number }), _jsx("span", { className: "min-w-0 grow wrap-anywhere text-text", children: title }), _jsx("span", { ref: measureDomain, className: "max-w-1/2 min-w-0 shrink-0 self-start truncate text-text-faint", children: uiUrlDomain(url) })] }));
|
|
60
|
+
return (_jsx("li", { "data-ui-source": node.id, tabIndex: -1, className: "chat-ui-source scroll-my-2 rounded-md", children: _jsx(Tooltip, { label: _jsxs(_Fragment, { children: [_jsx("span", { className: "block", children: title }), _jsx("span", { className: "block text-text-muted", children: url })] }), children: openUrl === undefined ? (_jsx("span", { tabIndex: domainCut ? 0 : undefined, className: "flex min-h-8 items-start gap-2 rounded-md px-2 py-2 text-xs", children: face })) : (_jsx("button", { type: "button", 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", onClick: () => openUrl(url), children: face })) }) }));
|
|
59
61
|
}
|
|
@@ -2,8 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { createContext, useContext } from 'react';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
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.js';
|
|
7
|
+
import { useTruncated } from '../use-truncated.js';
|
|
7
8
|
const ChecklistContext = createContext({ values: [], enabled: false, toggle: () => undefined });
|
|
8
9
|
/*
|
|
9
10
|
* The binding of an input, or null for a read-only node. An input works once its own node closed and
|
|
@@ -51,6 +52,8 @@ export function SliderRenderer({ node, context }) {
|
|
|
51
52
|
// The slider prints its unit right after the number, so the space a unit stands apart with comes from here.
|
|
52
53
|
unit: uiWithUnit('', unit), label: uiNodeLabel(node), disabled: binding === null, className: clsx('px-2', !node.complete && PENDING) }));
|
|
53
54
|
}
|
|
55
|
+
/* A label longer than this reads as a sentence, which wraps badly beside another; the options then stand in a column. */
|
|
56
|
+
const SIDE_BY_SIDE_LABEL = 24;
|
|
54
57
|
/* Left aligned, never the full width. Each Option is a segment by its place, since a value may be any scalar. */
|
|
55
58
|
export function SegmentedRenderer({ node, context }) {
|
|
56
59
|
const { t } = useTranslation('agent-chat');
|
|
@@ -58,15 +61,17 @@ export function SegmentedRenderer({ node, context }) {
|
|
|
58
61
|
const current = binding?.value ?? node.props.value;
|
|
59
62
|
const options = uiChildrenOf(node, 'Option').map((option, index) => ({ id: String(index), label: uiNodeLabel(option), value: option.props.value }));
|
|
60
63
|
const picked = options.find((option) => option.value === current)?.id ?? '';
|
|
61
|
-
return (_jsx("div", { className: clsx('px-2', !node.complete && PENDING), children: _jsx(Segmented, { value: picked, options: options, label: t('blocks.options'), disabled: binding === null, onValueChange: (id) => {
|
|
64
|
+
return (_jsx("div", { className: clsx('px-2', !node.complete && PENDING), children: _jsx(Segmented, { value: picked, options: options, label: t('blocks.options'), orientation: options.some((option) => option.label.length > SIDE_BY_SIDE_LABEL) ? 'vertical' : 'horizontal', disabled: binding === null, onValueChange: (id) => {
|
|
62
65
|
const option = options[Number(id)];
|
|
63
66
|
if (option !== undefined) {
|
|
64
67
|
binding?.onValueChange(option.value);
|
|
65
68
|
}
|
|
66
69
|
} }) }));
|
|
67
70
|
}
|
|
68
|
-
/* Runs its local action once its own node closed; like an input, it closes once the block is answered. */
|
|
71
|
+
/* 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. */
|
|
69
72
|
export function ButtonRenderer({ node, children, context }) {
|
|
70
73
|
const enabled = node.onAction !== undefined && node.complete && context.answer === null && node.props.disabled !== true;
|
|
71
|
-
|
|
74
|
+
const label = uiNodeLabel(node);
|
|
75
|
+
const [measure, truncated] = useTruncated();
|
|
76
|
+
return (_jsx("div", { className: clsx('px-2', !node.complete && PENDING), children: _jsx(Tooltip, { label: label, disabled: !truncated, children: _jsx(Button, { variant: "secondary", size: "sm", className: "max-w-full", disabled: !enabled, onClick: () => node.onAction?.(), children: _jsx("span", { ref: measure, className: "min-w-0 truncate", children: children }) }) }) }));
|
|
72
77
|
}
|
|
@@ -70,7 +70,7 @@ function LinkRow({ target, context, described, children }) {
|
|
|
70
70
|
if (!described) {
|
|
71
71
|
return _jsx(UiLinkChip, { target: target, context: context });
|
|
72
72
|
}
|
|
73
|
-
return (_jsxs("div", { className: "flex min-w-0 items-baseline gap-2 px-2 text-sm", children: [_jsx("span", { className: "
|
|
73
|
+
return (_jsxs("div", { className: "flex min-w-0 flex-wrap items-baseline gap-x-2 px-2 text-sm", children: [_jsx("span", { className: "max-w-full min-w-0", children: _jsx(UiLinkChip, { target: target, context: context }) }), _jsx("span", { className: "min-w-0 wrap-break-word text-text-muted", children: children })] }));
|
|
74
74
|
}
|
|
75
75
|
export function FileRenderer({ node, children, context }) {
|
|
76
76
|
const { path, line } = node.props;
|
|
@@ -25,12 +25,15 @@ const STEP_TONES = {
|
|
|
25
25
|
function isLive(context) {
|
|
26
26
|
return context.phase === 'streaming' || (context.live !== undefined && context.live !== null && context.live.state !== 'refused');
|
|
27
27
|
}
|
|
28
|
+
/* A badge longer than this is a sentence, which a tag cannot wrap; it stands under the title as text instead. */
|
|
29
|
+
const BADGE_AS_TAG = 32;
|
|
28
30
|
/* The head of the block when it is the first node, a subheading without an icon further down. */
|
|
29
31
|
export function SummaryRenderer({ node, children }) {
|
|
30
32
|
const headId = useContext(UiHeadContext);
|
|
31
33
|
const head = headId === undefined || headId === node.id;
|
|
32
34
|
const { tone, badge } = node.props;
|
|
33
|
-
|
|
35
|
+
const tag = badge !== undefined && badge !== '' && badge.length <= BADGE_AS_TAG;
|
|
36
|
+
return (_jsxs("div", { className: "flex items-start gap-2 px-2", children: [head && tone !== undefined && _jsx(Icon, { icon: UI_TONE_ICONS[tone], size: 16, className: clsx('mt-0.75 shrink-0', UI_TONE_TEXT[tone]) }), _jsxs("div", { className: "flex min-w-0 grow flex-wrap items-start gap-x-2 gap-y-1", children: [_jsx("div", { className: "max-w-full min-w-0 grow basis-48 text-sm font-semibold text-balance wrap-break-word text-text", children: children }), tag && (_jsx(Pill, { shape: "tag", tone: tone === undefined ? 'muted' : UI_TONE_PILL[tone], children: badge })), !tag && badge !== undefined && badge !== '' && _jsx("p", { className: "w-full text-xs wrap-anywhere text-text-muted", children: badge })] })] }));
|
|
34
37
|
}
|
|
35
38
|
export function CalloutRenderer({ node, children }) {
|
|
36
39
|
const { tone, title } = node.props;
|
|
@@ -57,5 +60,5 @@ export function StepRenderer({ node, children, context }) {
|
|
|
57
60
|
const { t } = useTranslation('agent-chat');
|
|
58
61
|
const { state, detail } = node.props;
|
|
59
62
|
const quiet = state === 'pending' || state === 'skipped';
|
|
60
|
-
return (_jsxs("li", { className: "flex min-h-7 items-center gap-2 text-sm", children: [_jsx("span", { className: clsx('grid size-4 shrink-0 place-items-center', STEP_TONES[state]), children: state === 'running' ? (isLive(context) ? (_jsx(Spinner, { size: 14 })) : (_jsx(Icon, { icon: CircleDot, size: 14 }))) : (_jsx(Icon, { icon: STEP_ICONS[state], size: 14 })) }), _jsx("span", { className: "sr-only", children: t(`blocks.step.${state}`) }), _jsx("span", { className: clsx('min-w-0 grow', quiet ? 'text-text-faint' : 'text-text'), children: children }), detail !== undefined && detail !== '' && _jsx("span", { className: "
|
|
63
|
+
return (_jsxs("li", { className: "flex min-h-7 items-center gap-2 text-sm", children: [_jsx("span", { className: clsx('grid size-4 shrink-0 place-items-center', STEP_TONES[state]), children: state === 'running' ? (isLive(context) ? (_jsx(Spinner, { size: 14 })) : (_jsx(Icon, { icon: CircleDot, size: 14 }))) : (_jsx(Icon, { icon: STEP_ICONS[state], size: 14 })) }), _jsx("span", { className: "sr-only", children: t(`blocks.step.${state}`) }), _jsxs("span", { className: "flex min-w-0 grow flex-wrap items-baseline justify-between gap-x-2", children: [_jsx("span", { className: clsx('max-w-full min-w-0 grow basis-48 wrap-break-word', quiet ? 'text-text-faint' : 'text-text'), children: children }), detail !== undefined && detail !== '' && _jsx("span", { className: "max-w-full text-xs wrap-anywhere text-text-faint tabular-nums", children: detail })] })] }));
|
|
61
64
|
}
|