@adecore/agents-react 0.27.5 → 0.29.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.
- package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +19 -9
- package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +4 -2
- package/dist/chat/ui/intelligent-ui/UiReply.js +3 -3
- package/dist/chat/ui/intelligent-ui/registry.js +2 -1
- package/dist/chat/ui/intelligent-ui/renderers/chart.js +2 -2
- package/dist/chat/ui/intelligent-ui/renderers/choices.js +13 -8
- package/dist/chat/ui/intelligent-ui/renderers/content.d.ts +1 -0
- package/dist/chat/ui/intelligent-ui/renderers/content.js +38 -11
- package/dist/chat/ui/intelligent-ui/renderers/data.js +19 -6
- package/dist/chat/ui/intelligent-ui/renderers/inputs.js +10 -9
- package/dist/chat/ui/intelligent-ui/renderers/links.js +5 -3
- package/dist/chat/ui/intelligent-ui/renderers/map.js +7 -7
- package/dist/chat/ui/intelligent-ui/renderers/structure.js +5 -5
- package/dist/chat/ui/intelligent-ui/renderers/text.js +25 -14
- package/dist/chat/ui/intelligent-ui/tones.d.ts +2 -0
- package/dist/chat/ui/intelligent-ui/tones.js +16 -0
- package/dist/host.d.ts +10 -0
- package/dist/index.d.ts +1 -1
- package/dist/locales/en/agent-chat.d.ts +5 -0
- package/dist/locales/en/agent-chat.js +5 -0
- package/dist/locales/nl/agent-chat.d.ts +5 -0
- package/dist/locales/nl/agent-chat.js +5 -0
- package/dist/theme.css +38 -0
- package/package.json +8 -8
- package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +26 -13
- package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +7 -4
- package/src/chat/ui/intelligent-ui/UiReply.tsx +3 -3
- package/src/chat/ui/intelligent-ui/registry.ts +2 -1
- package/src/chat/ui/intelligent-ui/renderers/chart.tsx +2 -2
- package/src/chat/ui/intelligent-ui/renderers/choices.tsx +33 -19
- package/src/chat/ui/intelligent-ui/renderers/content.tsx +82 -19
- package/src/chat/ui/intelligent-ui/renderers/data.tsx +61 -19
- package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +21 -14
- package/src/chat/ui/intelligent-ui/renderers/links.tsx +7 -4
- package/src/chat/ui/intelligent-ui/renderers/map.tsx +15 -12
- package/src/chat/ui/intelligent-ui/renderers/structure.tsx +6 -6
- package/src/chat/ui/intelligent-ui/renderers/text.tsx +63 -42
- package/src/chat/ui/intelligent-ui/tones.ts +18 -0
- package/src/host.ts +19 -0
- package/src/index.ts +1 -0
- package/src/locales/en/agent-chat.ts +5 -0
- package/src/locales/nl/agent-chat.ts +5 -0
- package/src/theme.css +38 -0
|
@@ -18,14 +18,14 @@ const MapView = lazyNamed(() => import('./map-view.js'), 'MapView');
|
|
|
18
18
|
const LOADING_SPINNER_MS = 1000;
|
|
19
19
|
/* The number that ties a row to its marker. The list numbers itself for a screen reader, so the chip is only for the eye. */
|
|
20
20
|
function NumberChip({ number, selected }) {
|
|
21
|
-
return (_jsx("span", { "aria-hidden": true, className: clsx('mt-px flex size-5 shrink-0 items-center justify-center rounded-
|
|
21
|
+
return (_jsx("span", { "aria-hidden": true, className: clsx('mt-px flex size-5 shrink-0 items-center justify-center rounded-md text-2xs font-semibold tabular-nums', selected ? 'bg-accent text-accent-text' : 'bg-ui-inset text-text'), children: number }));
|
|
22
22
|
}
|
|
23
23
|
function PlaceRow({ listed, number, selected, onPick }) {
|
|
24
24
|
const { t } = useTranslation('agent-chat');
|
|
25
25
|
const { detail, address } = listed.node.props;
|
|
26
|
-
const body = (_jsxs(_Fragment, { children: [_jsx(NumberChip, { number: number, selected: selected }), _jsxs("span", { className: "flex min-w-0 grow flex-col gap-0.5", children: [_jsx("span", { className: "text-
|
|
27
|
-
const row = 'flex w-full min-h-
|
|
28
|
-
return (_jsx("li", { children: onPick === null ? (_jsx("div", { className: clsx(row, selected && '
|
|
26
|
+
const body = (_jsxs(_Fragment, { children: [_jsx(NumberChip, { number: number, selected: selected }), _jsxs("span", { className: "flex min-w-0 grow flex-col gap-0.5", children: [_jsx("span", { className: "text-xs font-medium wrap-break-word text-text", children: listed.name }), detail !== undefined && detail !== '' && _jsx("span", { className: "text-xs wrap-break-word text-text-muted", children: detail }), address !== undefined && address !== '' && _jsx("span", { className: "text-xs wrap-break-word text-text-faint", children: address }), listed.point === undefined && _jsx("span", { className: "text-xs text-text-faint", children: t('blocks.map.lookingUp') }), listed.point === null && _jsx("span", { className: "text-xs text-text-faint", children: t('blocks.map.notFound') })] })] }));
|
|
27
|
+
const row = 'flex h-full w-full min-h-12 items-start gap-2.5 rounded-xl bg-ui-tile px-3 py-2 text-left';
|
|
28
|
+
return (_jsx("li", { children: onPick === null ? (_jsx("div", { className: clsx(row, selected && 'ring-2 ring-accent ring-inset'), children: body })) : (_jsx("button", { type: "button", "aria-pressed": selected, className: clsx(row, 'hover:bg-surface-active', selected && 'ring-2 ring-accent ring-inset'), onClick: onPick, children: body })) }));
|
|
29
29
|
}
|
|
30
30
|
/* Why the map is not the host's: its own reason when the source has none, or what the view found out on the way. */
|
|
31
31
|
function noteFor(t, source, status) {
|
|
@@ -75,7 +75,7 @@ function MapFrame({ markers, source, selected, onPick }) {
|
|
|
75
75
|
else if (drawn) {
|
|
76
76
|
body = (_jsx(DiffFallbackBoundary, { fallback: quiet(t('blocks.map.unavailable')), resetKeys: [], children: _jsx(LazyBoundary, { children: _jsx(MapView, { markers: markers, source: source ?? { state: 'off', reason: 'unavailable' }, selected: selected, onPick: onPick, onStatus: setStatus }, attempt) }) }));
|
|
77
77
|
}
|
|
78
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: setElement, className: "relative h-50 overflow-hidden rounded-
|
|
78
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: setElement, className: "relative h-50 overflow-hidden rounded-xl bg-ui-tile @min-[420px]:h-60", children: [body, waiting && (_jsx("div", { className: "pointer-events-none absolute inset-0 grid place-items-center", children: _jsx(Spinner, { size: 16 }) }))] }), note !== null && _jsx("p", { className: "px-0.5 text-xs text-text-faint", children: note })] }));
|
|
79
79
|
}
|
|
80
80
|
/* Asks the host for its tiles in a component of its own, since a host that offers none has no hook to call. */
|
|
81
81
|
function HostedFrame({ maps, scopeId, ...frame }) {
|
|
@@ -106,8 +106,8 @@ export function MapRenderer({ node, children, context }) {
|
|
|
106
106
|
};
|
|
107
107
|
const onPick = binding !== null ? pick : null;
|
|
108
108
|
const frame = maps ? (_jsx(HostedFrame, { maps: maps, scopeId: context.scopeId, markers: markers, selected: selectedId, onPick: onPick })) : (_jsx(MapFrame, { markers: markers, source: null, selected: selectedId, onPick: onPick }));
|
|
109
|
-
return (_jsxs("div", { className: clsx('@container flex flex-col gap-1.5
|
|
109
|
+
return (_jsxs("div", { className: clsx('@container flex flex-col gap-1.5', !node.complete && PENDING), children: [frame, _jsx("ol", { "aria-label": t('blocks.map.label', { count: places.length }), className: "grid gap-1.5 @min-[420px]:grid-cols-2", children: places.map((listed, index) => {
|
|
110
110
|
const { value } = listed.node.props;
|
|
111
111
|
return (_jsx(PlaceRow, { listed: listed, number: index + 1, selected: value !== undefined && value === current, onPick: binding !== null && value !== undefined ? () => binding.onValueChange(value) : null }, listed.node.id));
|
|
112
|
-
}) }), node.omitted !== undefined && node.omitted > 0 &&
|
|
112
|
+
}) }), node.omitted !== undefined && node.omitted > 0 && _jsx("p", { className: "text-2xs text-text-faint", children: t('blocks.map.omitted', { count: node.omitted }) }), _jsx("div", { className: "chat-ui-nodes flex flex-col gap-1.5 empty:hidden", children: children })] }));
|
|
113
113
|
}
|
|
@@ -11,22 +11,22 @@ export function TabsRenderer({ node, children, context }) {
|
|
|
11
11
|
const tabs = uiChildrenOf(node, 'Tab');
|
|
12
12
|
const [picked, setPicked] = useBlockLocal(context, node.id, () => null);
|
|
13
13
|
const value = picked !== null && tabs.some((tab) => tab.id === picked) ? picked : (tabs[0]?.id ?? null);
|
|
14
|
-
return (_jsxs(Tabs.Root, { value: value, onValueChange: (next) => setPicked(String(next)), className: "flex flex-col gap-3", children: [_jsx(Tabs.List, { className: "
|
|
14
|
+
return (_jsxs(Tabs.Root, { value: value, onValueChange: (next) => setPicked(String(next)), className: "flex flex-col gap-3", children: [_jsx(Tabs.List, { variant: "pill", overflow: "scroll", className: "w-fit max-w-full rounded-xl bg-ui-tile p-0.75", children: tabs.map((tab) => (_jsx(Tabs.Tab, { value: tab.id, "data-ui-reveal": tab.id, children: tab.props.title }, tab.id))) }), children] }));
|
|
15
15
|
}
|
|
16
16
|
export function TabRenderer({ node, children }) {
|
|
17
17
|
return (_jsx(Tabs.Panel, { value: node.id, className: "chat-ui-nodes flex flex-col gap-3", children: children }));
|
|
18
18
|
}
|
|
19
19
|
export function SectionsRenderer({ node, children, context }) {
|
|
20
20
|
const [touched, setTouched] = useBlockLocal(context, node.id, () => false);
|
|
21
|
-
return (_jsx(SectionsContext, { value: { touched, touch: () => setTouched(true) }, children: _jsx("div", { className: "chat-ui-nodes flex flex-col
|
|
21
|
+
return (_jsx(SectionsContext, { value: { touched, touch: () => setTouched(true) }, children: _jsx("div", { className: "chat-ui-nodes flex flex-col gap-1.5", children: children }) }));
|
|
22
22
|
}
|
|
23
|
-
/* Arrives open until the person folds a section of the set themselves; the content
|
|
23
|
+
/* Arrives open until the person folds a section of the set themselves; the content keeps one margin on both sides. */
|
|
24
24
|
export function SectionRenderer({ node, children, context }) {
|
|
25
25
|
const fold = useContext(SectionsContext);
|
|
26
26
|
const [open, setOpen] = useBlockLocal(context, node.id, () => !fold.touched);
|
|
27
27
|
const contentId = useId();
|
|
28
|
-
return (_jsxs("section", { children: [_jsxs("button", { type: "button", "data-ui-reveal": node.id, "aria-expanded": open, "aria-controls": contentId, className: "flex h-
|
|
28
|
+
return (_jsxs("section", { className: "rounded-xl bg-ui-tile", children: [_jsxs("button", { type: "button", "data-ui-reveal": node.id, "aria-expanded": open, "aria-controls": contentId, className: "flex min-h-10 w-full items-center gap-2 rounded-xl px-3 text-left text-xs font-medium text-text hover:bg-text/4", onClick: () => {
|
|
29
29
|
fold.touch();
|
|
30
30
|
setOpen(!open);
|
|
31
|
-
}, children: [_jsx(Icon, { icon: ChevronRight, size: 14, className: clsx('shrink-0 text-text-faint transition-transform duration-150 motion-reduce:transition-none', open && 'rotate-90') }), _jsx("span", { className: "min-w-0 truncate", children: node.props.title })] }), _jsx("div", { id: contentId, hidden: !open, className: "chat-ui-nodes flex flex-col gap-3
|
|
31
|
+
}, children: [_jsx(Icon, { icon: ChevronRight, size: 14, className: clsx('shrink-0 text-text-faint transition-transform duration-150 motion-reduce:transition-none', open && 'rotate-90') }), _jsx("span", { className: "min-w-0 truncate", children: node.props.title })] }), _jsx("div", { id: contentId, hidden: !open, className: "chat-ui-section-body chat-ui-nodes flex flex-col gap-3 px-3 pb-3", children: children })] }));
|
|
32
32
|
}
|
|
@@ -8,7 +8,7 @@ import { Icon, Meter, Pill, Spinner } from '@adecore/ui';
|
|
|
8
8
|
import { formatNumber, formatPercent } from '@adecore/ui/format';
|
|
9
9
|
import { UiHeadContext } from '../render-context.js';
|
|
10
10
|
import { uiEventTime, uiNodeLabel } from '../node-text.js';
|
|
11
|
-
import { UI_TONE_ICONS, UI_TONE_PILL, UI_TONE_SURFACE, UI_TONE_TEXT } from '../tones.js';
|
|
11
|
+
import { UI_STEP_PILL, UI_TONE_FILL, UI_TONE_ICONS, UI_TONE_PILL, UI_TONE_SURFACE, UI_TONE_TEXT } from '../tones.js';
|
|
12
12
|
const STEP_ICONS = {
|
|
13
13
|
done: CircleCheck,
|
|
14
14
|
pending: Circle,
|
|
@@ -22,23 +22,32 @@ const STEP_TONES = {
|
|
|
22
22
|
failed: 'text-status-error',
|
|
23
23
|
skipped: 'text-text-faint'
|
|
24
24
|
};
|
|
25
|
+
/* A detail longer than this reads as a sentence, which a tag cannot wrap. */
|
|
26
|
+
const DETAIL_AS_TAG = 24;
|
|
25
27
|
/* Whether the block still changes: while it streams, or while it reads live data. Only then does anything spin. */
|
|
26
28
|
function isLive(context) {
|
|
27
29
|
return context.phase === 'streaming' || (context.live !== undefined && context.live !== null && context.live.state !== 'refused');
|
|
28
30
|
}
|
|
29
|
-
/* A
|
|
30
|
-
const
|
|
31
|
-
|
|
31
|
+
/* A Summary and a Callout share one row: the icon in a square, and beside it text that centers on the square while it is shorter and starts at its top once it is taller. */
|
|
32
|
+
const ICON_ROW = 'flex items-start gap-3 rounded-xl p-3';
|
|
33
|
+
const ICON_SQUARE = 'grid size-8.5 shrink-0 place-items-center rounded-lg';
|
|
34
|
+
const ICON_TEXT = 'flex min-h-8.5 min-w-0 grow flex-col justify-center gap-0.5';
|
|
35
|
+
/*
|
|
36
|
+
* A tile in its tone; the badge stands under the title in the tone's color. The head of the block
|
|
37
|
+
* carries its icon in a square of its own, a Summary further down is a subheading without one. The
|
|
38
|
+
* icon is the ground's ink in the dark theme, where its tones are too light for white.
|
|
39
|
+
*/
|
|
32
40
|
export function SummaryRenderer({ node, children }) {
|
|
33
41
|
const headId = useContext(UiHeadContext);
|
|
34
42
|
const head = headId === undefined || headId === node.id;
|
|
35
43
|
const { tone, badge } = node.props;
|
|
36
|
-
const
|
|
37
|
-
return (_jsxs("div", { className:
|
|
44
|
+
const shown = tone ?? 'neutral';
|
|
45
|
+
return (_jsxs("div", { className: clsx(ICON_ROW, tone === undefined ? 'bg-ui-tile' : UI_TONE_SURFACE[tone]), children: [head && (_jsx("span", { "aria-hidden": true, className: clsx(ICON_SQUARE, 'text-accent-text dark:text-surface', UI_TONE_FILL[shown]), children: _jsx(Icon, { icon: UI_TONE_ICONS[shown], size: 16 }) })), _jsxs("div", { className: ICON_TEXT, children: [_jsx("div", { className: "text-base font-semibold text-balance wrap-break-word text-text", children: children }), badge !== undefined && badge !== '' && (_jsx("p", { className: clsx('text-2xs font-medium wrap-anywhere', tone === undefined ? 'text-text-muted' : UI_TONE_TEXT[tone]), children: badge }))] })] }));
|
|
38
46
|
}
|
|
47
|
+
/* A quiet tile whatever its tone: only the square of its icon wears the tone. */
|
|
39
48
|
export function CalloutRenderer({ node, children }) {
|
|
40
49
|
const { tone, title } = node.props;
|
|
41
|
-
return (_jsxs("div", { className: clsx('
|
|
50
|
+
return (_jsxs("div", { className: clsx('chat-ui-flat bg-ui-tile text-xs', ICON_ROW), children: [_jsx("span", { "aria-hidden": true, className: clsx(ICON_SQUARE, UI_TONE_SURFACE[tone], UI_TONE_TEXT[tone]), children: _jsx(Icon, { icon: UI_TONE_ICONS[tone], size: 16 }) }), _jsxs("div", { className: ICON_TEXT, children: [title !== undefined && title !== '' && _jsx("div", { className: "font-semibold text-text", children: title }), _jsx("div", { className: "text-text-muted", children: children })] })] }));
|
|
42
51
|
}
|
|
43
52
|
export function TagRenderer({ node, children }) {
|
|
44
53
|
const { tone } = node.props;
|
|
@@ -51,22 +60,22 @@ export function ProgressRenderer({ node, children, context }) {
|
|
|
51
60
|
const limit = max ?? 100;
|
|
52
61
|
const reading = value === undefined ? null : limit === 100 ? formatPercent(value) : t('blocks.progress.of', { value: formatNumber(value), max: formatNumber(limit) });
|
|
53
62
|
const label = uiNodeLabel(node);
|
|
54
|
-
return (_jsxs("div", { className: "flex flex-col gap-
|
|
63
|
+
return (_jsxs("div", { className: "chat-ui-flat flex flex-col gap-2 rounded-xl bg-ui-tile p-3", children: [_jsxs("div", { className: "flex items-baseline justify-between gap-3 text-xs", children: [_jsx("span", { className: "min-w-0 text-text", children: children }), reading !== null ? (_jsx("span", { className: "shrink-0 text-text-muted tabular-nums", children: reading })) : (isLive(context) && _jsx(Spinner, { size: 12, className: "shrink-0 self-center text-text-faint" }))] }), _jsx(Meter, { value: value ?? null, max: limit, label: label === '' ? t('blocks.progress.label') : label, valueText: reading ?? undefined })] }));
|
|
55
64
|
}
|
|
56
65
|
export function StepsRenderer({ children }) {
|
|
57
|
-
return _jsx("ol", { className: "chat-ui-nodes flex flex-col
|
|
66
|
+
return _jsx("ol", { className: "chat-ui-nodes flex flex-col gap-1.5", children: children });
|
|
58
67
|
}
|
|
59
|
-
/* A running step spins only while the block is live; in a quiet block it is a still dot. */
|
|
68
|
+
/* A tile per step with its state in words at the end. A running step spins only while the block is live; in a quiet block it is a still dot. */
|
|
60
69
|
export function StepRenderer({ node, children, context }) {
|
|
61
70
|
const { t } = useTranslation('agent-chat');
|
|
62
71
|
const { state, detail } = node.props;
|
|
63
72
|
const quiet = state === 'pending' || state === 'skipped';
|
|
64
|
-
return (_jsxs("li", { className: "flex min-h-
|
|
73
|
+
return (_jsxs("li", { className: "flex min-h-10 items-start gap-3 rounded-xl bg-ui-tile px-3 py-2.5 text-xs", children: [_jsx("span", { className: clsx('mt-0.5 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 })) }), _jsxs("span", { className: "flex min-w-0 grow flex-wrap items-baseline gap-x-2", children: [_jsx("span", { className: clsx('max-w-full min-w-0 font-medium wrap-break-word', quiet ? 'text-text-muted' : 'text-text'), children: children }), detail !== undefined && detail !== '' && _jsx("span", { className: "max-w-full text-2xs wrap-anywhere text-text-muted tabular-nums", children: detail })] }), _jsx(Pill, { shape: "tag", size: "sm", tone: UI_STEP_PILL[state], className: "shrink-0", children: t(`blocks.step.${state}`) })] }));
|
|
65
74
|
}
|
|
66
75
|
export function TimelineRenderer({ children }) {
|
|
67
|
-
return _jsx("ol", { className: "chat-ui-nodes flex flex-col px-
|
|
76
|
+
return _jsx("ol", { className: "chat-ui-nodes chat-ui-flat flex flex-col rounded-xl bg-ui-tile px-3 py-1.5", children: children });
|
|
68
77
|
}
|
|
69
|
-
/*
|
|
78
|
+
/* A row of the tile: the time, a dot in the tone, the title, and a short detail as a tag at the end or a longer one under the title. */
|
|
70
79
|
export function EventRenderer({ node, children }) {
|
|
71
80
|
const { t } = useTranslation('agent-chat');
|
|
72
81
|
const { at, detail } = node.props;
|
|
@@ -74,5 +83,7 @@ export function EventRenderer({ node, children }) {
|
|
|
74
83
|
const time = uiEventTime(at);
|
|
75
84
|
// A neutral dot says nothing the title does not, and a detail is part of what the row says.
|
|
76
85
|
const label = [time, uiNodeLabel(node), tone === 'neutral' ? '' : t(`blocks.event.tone.${tone}`), detail ?? ''].filter((part) => part !== '').join(', ');
|
|
77
|
-
|
|
86
|
+
const described = detail !== undefined && detail !== '';
|
|
87
|
+
const tagged = described && detail.length <= DETAIL_AS_TAG;
|
|
88
|
+
return (_jsxs("li", { "aria-label": label, className: "flex min-h-9 items-start gap-3 py-2 text-xs", children: [parseUiEventTime(at) === null ? (_jsx("span", { className: "w-24 shrink-0 text-2xs leading-5 wrap-anywhere text-text-muted tabular-nums", children: time })) : (_jsx("time", { dateTime: at, className: "w-24 shrink-0 text-2xs leading-5 text-text-muted tabular-nums", children: time })), _jsx("span", { "aria-hidden": true, className: clsx('mt-1.5 size-2 shrink-0 rounded-full bg-current', tone === 'neutral' ? 'text-text-faint' : UI_TONE_TEXT[tone]) }), _jsxs("div", { className: "flex min-w-0 grow flex-col gap-0.5", children: [_jsx("div", { className: "wrap-break-word text-text", children: children }), described && !tagged && _jsx("div", { className: "text-2xs wrap-anywhere text-text-muted", children: detail })] }), tagged && (_jsx(Pill, { shape: "tag", size: "sm", tone: UI_TONE_PILL[tone], className: "shrink-0", children: detail }))] }));
|
|
78
89
|
}
|
|
@@ -5,3 +5,5 @@ export declare const UI_TONE_ICONS: Readonly<Record<UiTone, LucideIcon>>;
|
|
|
5
5
|
export declare const UI_TONE_TEXT: Readonly<Record<UiTone, string>>;
|
|
6
6
|
export declare const UI_TONE_SURFACE: Readonly<Record<UiTone, string>>;
|
|
7
7
|
export declare const UI_TONE_PILL: Readonly<Record<UiTone, NonNullable<PillProps['tone']>>>;
|
|
8
|
+
export declare const UI_TONE_FILL: Readonly<Record<UiTone, string>>;
|
|
9
|
+
export declare const UI_STEP_PILL: Readonly<Record<'done' | 'running' | 'pending' | 'failed' | 'skipped', NonNullable<PillProps['tone']>>>;
|
|
@@ -29,3 +29,19 @@ export const UI_TONE_PILL = {
|
|
|
29
29
|
warning: 'needsYou',
|
|
30
30
|
danger: 'error'
|
|
31
31
|
};
|
|
32
|
+
/* The tone at full strength, for the square that carries the icon of a Summary. */
|
|
33
|
+
export const UI_TONE_FILL = {
|
|
34
|
+
neutral: 'bg-text-muted',
|
|
35
|
+
info: 'bg-status-running',
|
|
36
|
+
success: 'bg-positive',
|
|
37
|
+
warning: 'bg-status-needs-you',
|
|
38
|
+
danger: 'bg-status-error'
|
|
39
|
+
};
|
|
40
|
+
/* The tag at the end of a step, in words, so a state never rests on its icon alone. */
|
|
41
|
+
export const UI_STEP_PILL = {
|
|
42
|
+
done: 'idle',
|
|
43
|
+
running: 'accent',
|
|
44
|
+
pending: 'muted',
|
|
45
|
+
failed: 'error',
|
|
46
|
+
skipped: 'muted'
|
|
47
|
+
};
|
package/dist/host.d.ts
CHANGED
|
@@ -162,6 +162,15 @@ export type UiMapSource = {
|
|
|
162
162
|
state: 'off';
|
|
163
163
|
reason: 'disabled' | 'signed-out' | 'unavailable';
|
|
164
164
|
};
|
|
165
|
+
export interface UiModelViewProps {
|
|
166
|
+
scopeId: string;
|
|
167
|
+
chatId: string;
|
|
168
|
+
attachmentId: string;
|
|
169
|
+
url: string;
|
|
170
|
+
label: string;
|
|
171
|
+
alt?: string;
|
|
172
|
+
caption: string;
|
|
173
|
+
}
|
|
165
174
|
export interface ChatHost {
|
|
166
175
|
intelligentUi?: {
|
|
167
176
|
subscribe?(scopeId: string, chatId: string, changed: () => void): () => void;
|
|
@@ -195,6 +204,7 @@ export interface ChatHost {
|
|
|
195
204
|
ReadImage: ComponentType<{
|
|
196
205
|
path: string;
|
|
197
206
|
}> | null;
|
|
207
|
+
ModelView?: ComponentType<UiModelViewProps>;
|
|
198
208
|
MarkdownImage?: ComponentType<{
|
|
199
209
|
path: string;
|
|
200
210
|
alt: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { type ChatActions, type FileRef, type ShellCodeBlockContext, type ChatPlace, type SubagentTask, type TimelineFindOptions, type TimelineFind, type ComposerDictationProps, type ComposerSlotProps, type SubagentSlotProps, type ThreadCard, type VisualHost, type ReplyAuthor, type ChatHost, type UiMapSource, type CommandLook, type CommandLookMatch, type CommandName, ChatHostProvider, useChatHost, type AccentChoice, type ChatToast, type TextareaProps, type ResourceUrl } from './host.js';
|
|
1
|
+
export { type ChatActions, type FileRef, type ShellCodeBlockContext, type ChatPlace, type SubagentTask, type TimelineFindOptions, type TimelineFind, type ComposerDictationProps, type ComposerSlotProps, type SubagentSlotProps, type ThreadCard, type VisualHost, type ReplyAuthor, type ChatHost, type UiMapSource, type UiModelViewProps, type CommandLook, type CommandLookMatch, type CommandName, ChatHostProvider, useChatHost, type AccentChoice, type ChatToast, type TextareaProps, type ResourceUrl } from './host.js';
|
|
2
2
|
export { type ChatScope, ChatScopeContext, useChatScope } from './scope.js';
|
|
3
3
|
export { type ChatRequestMap, type ChatEventMap, type ChatTransportStatus, type ChatTransport, ChatTransportError, errorCode, isConnectionError } from './transport.js';
|
|
4
4
|
export { portTransport, type PortTransport } from './port-transport.js';
|
|
@@ -621,6 +621,11 @@ export default {
|
|
|
621
621
|
label: 'Image',
|
|
622
622
|
unavailable: 'The image is not available'
|
|
623
623
|
},
|
|
624
|
+
model: {
|
|
625
|
+
label: '3D model',
|
|
626
|
+
unavailable: 'The model is not available',
|
|
627
|
+
crashed: 'This model failed to render'
|
|
628
|
+
},
|
|
624
629
|
sources: 'Sources',
|
|
625
630
|
citation: 'Source {{number}}: {{title}}',
|
|
626
631
|
outsideProject: 'Outside the project, shown as text',
|
|
@@ -621,6 +621,11 @@ export default {
|
|
|
621
621
|
label: 'Afbeelding',
|
|
622
622
|
unavailable: 'De afbeelding is niet beschikbaar'
|
|
623
623
|
},
|
|
624
|
+
model: {
|
|
625
|
+
label: '3D-model',
|
|
626
|
+
unavailable: 'Het model is niet beschikbaar',
|
|
627
|
+
crashed: 'Dit model kon niet worden getekend'
|
|
628
|
+
},
|
|
624
629
|
sources: 'Bronnen',
|
|
625
630
|
citation: 'Bron {{number}}: {{title}}',
|
|
626
631
|
outsideProject: 'Buiten het project, als tekst getoond',
|
package/dist/theme.css
CHANGED
|
@@ -30,6 +30,12 @@
|
|
|
30
30
|
/* The two grounds of the map a page draws without tiles. */
|
|
31
31
|
--map-land: #f2f1ed;
|
|
32
32
|
--map-water: #d3dde5;
|
|
33
|
+
/* The parts of an intelligent UI block, from the tokens an app's palette sets: a tile on the thread's
|
|
34
|
+
ground, a card inset in a tile, and the hatch of its band. The tile mixes the ink in so lightly that
|
|
35
|
+
muted text keeps 4.5 to 1 on it; the design's #f0f0f3 did not. */
|
|
36
|
+
--chat-ui-tile: color-mix(in srgb, var(--text) 4.5%, var(--surface));
|
|
37
|
+
--chat-ui-inset: var(--surface);
|
|
38
|
+
--chat-ui-hatch: var(--separator);
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
[data-theme="dark"] {
|
|
@@ -51,6 +57,9 @@
|
|
|
51
57
|
--chart-6: #008300;
|
|
52
58
|
--map-land: #2c3135;
|
|
53
59
|
--map-water: #1a2026;
|
|
60
|
+
--chat-ui-tile: color-mix(in srgb, var(--text) 3%, var(--surface));
|
|
61
|
+
--chat-ui-inset: var(--surface);
|
|
62
|
+
--chat-ui-hatch: var(--separator);
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
/* Output and diffs take the terminal's colors when the app loads `@adecore/terminal/terminal.css`,
|
|
@@ -82,6 +91,8 @@
|
|
|
82
91
|
--color-chat-added: var(--chat-added);
|
|
83
92
|
--color-chat-removed: var(--chat-removed);
|
|
84
93
|
--color-map-marker: var(--map-marker);
|
|
94
|
+
--color-ui-tile: var(--chat-ui-tile);
|
|
95
|
+
--color-ui-inset: var(--chat-ui-inset);
|
|
85
96
|
--color-map-marker-text: var(--map-marker-text);
|
|
86
97
|
}
|
|
87
98
|
|
|
@@ -155,6 +166,24 @@
|
|
|
155
166
|
@media (prefers-reduced-motion: reduce) {
|
|
156
167
|
.chat-ui-changed { animation: none; }
|
|
157
168
|
}
|
|
169
|
+
/*
|
|
170
|
+
* A block stands in a band across the whole thread, hatched above and below, with its tiles at the
|
|
171
|
+
* reading width. The thread's scroller is the container the band measures, so a block anywhere
|
|
172
|
+
* else keeps the width of its column.
|
|
173
|
+
*/
|
|
174
|
+
.chat-scroll { container-type: inline-size; }
|
|
175
|
+
.chat-ui-band { position: relative; padding-block: 14px; }
|
|
176
|
+
.chat-ui-band::before, .chat-ui-band::after {
|
|
177
|
+
content: "";
|
|
178
|
+
position: absolute;
|
|
179
|
+
inset-inline: 0;
|
|
180
|
+
height: 14px;
|
|
181
|
+
border-block: 1px solid var(--border);
|
|
182
|
+
background-image: repeating-linear-gradient(-45deg, var(--chat-ui-hatch) 0 1px, transparent 1px 7px);
|
|
183
|
+
}
|
|
184
|
+
.chat-ui-band::before { top: 0; }
|
|
185
|
+
.chat-ui-band::after { bottom: 0; }
|
|
186
|
+
.chat-scroll .chat-ui-band::before, .chat-scroll .chat-ui-band::after { inset-inline: calc(50% - 50cqw); }
|
|
158
187
|
/* A source row a citation revealed. The row only takes focus from code, since its own button takes a click. */
|
|
159
188
|
.chat-ui-source:focus { animation: chat-flash 1600ms ease-out both; }
|
|
160
189
|
/* A visual still on its way says so only once it takes long enough to be noticed. */
|
|
@@ -225,6 +254,15 @@
|
|
|
225
254
|
|
|
226
255
|
/* A code block is shiki's own `<pre>`, which brings a background of its own to override. */
|
|
227
256
|
.chat-code { position: relative; min-height: 38px; border-radius: 8px; border: 1px solid var(--border); background: var(--chat-code-bg); overflow: hidden; }
|
|
257
|
+
/* A code block in an intelligent UI block is a card inset in its tile. */
|
|
258
|
+
.chat-ui-code .chat-code { background: var(--chat-ui-inset); }
|
|
259
|
+
/* Inside a Section the section is the tile. A tile that only frames its parts goes flat, so they keep the
|
|
260
|
+
section's own margin, and a card among them takes the inset ground so it still reads as one. Unlayered, so it outweighs
|
|
261
|
+
the utilities it overrides. */
|
|
262
|
+
.chat-ui-section-body .bg-ui-tile { background-color: var(--chat-ui-inset); }
|
|
263
|
+
.chat-ui-section-body .chat-ui-flat { background-color: transparent; padding: 0; }
|
|
264
|
+
.chat-ui-section-body .chat-ui-flat .chat-ui-flat-inner { padding-inline: 0; }
|
|
265
|
+
.chat-ui-section-body .chat-ui-flat thead { background-color: var(--chat-ui-tile); }
|
|
228
266
|
.chat-code pre { margin: 0; padding: round(0.625rem, 1px) round(0.75rem, 1px); overflow: auto; user-select: text; background: transparent !important; font-family: var(--font-mono); font-size: var(--text-code); line-height: var(--text-code--line-height); }
|
|
229
267
|
.chat-code code { font-family: inherit; }
|
|
230
268
|
.chat-code-actions { position: absolute; top: 4px; right: 4px; border-radius: 6px; background: var(--surface-raised); color: var(--text-muted); box-shadow: 0 0 0 1px var(--border); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/agents-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "React agent chat threads, composers, approvals, provider accounts and usage views.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -67,11 +67,11 @@
|
|
|
67
67
|
"test:visuals": "bun --config=integration.bunfig.toml --conditions=source test ./src/chat/visual-viewport.integration.test.ts"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"@adecore/agent-contracts": "0.
|
|
71
|
-
"@adecore/editor": "0.
|
|
72
|
-
"@adecore/editor-react": "0.
|
|
73
|
-
"@adecore/intelligent-ui": "0.
|
|
74
|
-
"@adecore/merge": "0.
|
|
70
|
+
"@adecore/agent-contracts": "0.29.0",
|
|
71
|
+
"@adecore/editor": "0.29.0",
|
|
72
|
+
"@adecore/editor-react": "0.29.0",
|
|
73
|
+
"@adecore/intelligent-ui": "0.29.0",
|
|
74
|
+
"@adecore/merge": "0.29.0",
|
|
75
75
|
"@codemirror/commands": "^6.11.1",
|
|
76
76
|
"@codemirror/language": "^6.12.4",
|
|
77
77
|
"@codemirror/state": "^6.7.6",
|
|
@@ -94,14 +94,14 @@
|
|
|
94
94
|
"zustand": "^5.0.15"
|
|
95
95
|
},
|
|
96
96
|
"peerDependencies": {
|
|
97
|
-
"@adecore/ui": "^0.
|
|
97
|
+
"@adecore/ui": "^0.29.0",
|
|
98
98
|
"i18next": "^26.4.2",
|
|
99
99
|
"react": "^19.3.0",
|
|
100
100
|
"react-dom": "^19.3.0",
|
|
101
101
|
"react-i18next": "^17.0.15"
|
|
102
102
|
},
|
|
103
103
|
"devDependencies": {
|
|
104
|
-
"@adecore/ui": "0.
|
|
104
|
+
"@adecore/ui": "0.29.0",
|
|
105
105
|
"@types/bun": "^1.4.2",
|
|
106
106
|
"@types/react": "^19.3.0",
|
|
107
107
|
"@types/react-dom": "^19.3.0",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { useId, useLayoutEffect, useRef, useState, type ReactNode, type RefObject } from 'react';
|
|
1
|
+
import { useId, useLayoutEffect, useRef, useState, type KeyboardEvent, type ReactNode, type RefObject } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import type { UiViewNode } from '@adecore/intelligent-ui';
|
|
5
5
|
import { Info, Lock, Radio, TriangleAlert } from 'lucide-react';
|
|
6
|
-
import { Icon, Skeleton, Spinner, Tooltip, useTickingText } from '@adecore/ui';
|
|
6
|
+
import { Icon, KEY_SHORTCUTS, matchesShortcut, Skeleton, Spinner, Tooltip, useTickingText } from '@adecore/ui';
|
|
7
|
+
import { useChatHost } from '../../../host';
|
|
7
8
|
import { formatAgo, formatMoment } from '@adecore/ui/format';
|
|
8
9
|
import { uiBlockHead, uiReasonText } from './node-text';
|
|
9
10
|
import { UiHeadContext, type UiLiveStatus } from './render-context';
|
|
@@ -48,7 +49,7 @@ function LivePart({ live }: { live: UiLiveStatus }) {
|
|
|
48
49
|
) : live.state === 'refused' ? (
|
|
49
50
|
<Icon icon={Lock} size={12} />
|
|
50
51
|
) : (
|
|
51
|
-
<Icon icon={Radio} size={12} className="text-
|
|
52
|
+
<Icon icon={Radio} size={12} className="text-positive" />
|
|
52
53
|
);
|
|
53
54
|
const words =
|
|
54
55
|
live.state === 'failed'
|
|
@@ -81,10 +82,7 @@ function LivePart({ live }: { live: UiLiveStatus }) {
|
|
|
81
82
|
return hint === null ? body : <Tooltip label={hint}>{body}</Tooltip>;
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
/*
|
|
85
|
-
* The line under a block, only when it has something to say: live, answered, noted or drawn as
|
|
86
|
-
* text, in that order. A hairline of the soft border runs the full width of the card over it.
|
|
87
|
-
*/
|
|
85
|
+
/* The line under a block, only when it has something to say: live, answered, noted or drawn as text, in that order. */
|
|
88
86
|
function UiBlockFooter({ live = null, answered = null, fixes = NO_FIXES, shownAsText = null }: UiBlockFooterProps) {
|
|
89
87
|
const { t } = useTranslation('agent-chat');
|
|
90
88
|
const [fixesOpen, setFixesOpen] = useState(false);
|
|
@@ -93,7 +91,7 @@ function UiBlockFooter({ live = null, answered = null, fixes = NO_FIXES, shownAs
|
|
|
93
91
|
return null;
|
|
94
92
|
}
|
|
95
93
|
return (
|
|
96
|
-
<div className="
|
|
94
|
+
<div className="flex flex-col gap-1.5 text-2xs text-text-faint">
|
|
97
95
|
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
|
|
98
96
|
{live !== null && <LivePart live={live} />}
|
|
99
97
|
{answered !== null && (
|
|
@@ -189,16 +187,28 @@ function useRisingFloor(active: boolean): [RefObject<HTMLDivElement | null>, num
|
|
|
189
187
|
}
|
|
190
188
|
|
|
191
189
|
/*
|
|
192
|
-
* The
|
|
190
|
+
* The band of one block, with its footer. Named by its Summary and busy while it streams, but never
|
|
193
191
|
* live: reading out every compile would be noise. A node fades in as it arrives, without a stagger,
|
|
194
192
|
* and for a person who asked for less motion it only fades; a node inside another only fades
|
|
195
193
|
* (`chat-ui-nodes`). Until the first node a skeleton holds the place of the head.
|
|
196
194
|
*/
|
|
197
195
|
export function UiBlockFrame({ nodes, phase, children, className, ...footer }: UiBlockFrameProps) {
|
|
198
196
|
const { t } = useTranslation('agent-chat');
|
|
197
|
+
const host = useChatHost();
|
|
199
198
|
const streaming = phase === 'streaming';
|
|
200
199
|
const head = uiBlockHead(nodes);
|
|
201
200
|
const [ref, floor] = useRisingFloor(streaming);
|
|
201
|
+
// Only while the focus is inside the block, so the composer keeps the same keys for sending.
|
|
202
|
+
const pickPrimary = (event: KeyboardEvent<HTMLDivElement>): void => {
|
|
203
|
+
if (!matchesShortcut(KEY_SHORTCUTS.modEnter, event, host.isApplePlatform())) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const primary = event.currentTarget.querySelector<HTMLButtonElement>('[data-ui-primary-choice]');
|
|
207
|
+
if (primary !== null) {
|
|
208
|
+
event.preventDefault();
|
|
209
|
+
primary.click();
|
|
210
|
+
}
|
|
211
|
+
};
|
|
202
212
|
return (
|
|
203
213
|
<UiHeadContext value={head?.id ?? null}>
|
|
204
214
|
<div
|
|
@@ -206,13 +216,16 @@ export function UiBlockFrame({ nodes, phase, children, className, ...footer }: U
|
|
|
206
216
|
role="group"
|
|
207
217
|
aria-label={head === null || head.label === '' ? t('blocks.label') : head.label}
|
|
208
218
|
aria-busy={streaming || undefined}
|
|
209
|
-
className={clsx('flex min-h-11 flex-col
|
|
219
|
+
className={clsx('chat-ui-band flex min-h-11 flex-col', className)}
|
|
210
220
|
style={streaming && floor > 0 ? { minHeight: floor } : undefined}
|
|
221
|
+
onKeyDown={pickPrimary}
|
|
211
222
|
>
|
|
212
|
-
<div className="flex flex-col gap-3
|
|
213
|
-
|
|
223
|
+
<div className="flex flex-col gap-3 py-5">
|
|
224
|
+
<div className="flex flex-col gap-3.5 *:transition-[opacity,translate] *:duration-160 *:ease-out *:starting:opacity-0 motion-safe:*:starting:translate-y-0.5">
|
|
225
|
+
{nodes.length === 0 && streaming ? <Skeleton className="my-1 w-2/5" /> : children}
|
|
226
|
+
</div>
|
|
227
|
+
<UiBlockFooter {...footer} />
|
|
214
228
|
</div>
|
|
215
|
-
<UiBlockFooter {...footer} />
|
|
216
229
|
</div>
|
|
217
230
|
</UiHeadContext>
|
|
218
231
|
);
|
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
import { useTranslation } from 'react-i18next';
|
|
2
|
+
import { Info } from 'lucide-react';
|
|
3
|
+
import { Icon } from '@adecore/ui';
|
|
2
4
|
import { Markdown } from '../Markdown';
|
|
3
5
|
|
|
4
6
|
/* Why a part is drawn as its text: a component this version does not know, or one it could not draw. */
|
|
5
7
|
export type UiFallbackProblem = { kind: 'unknown'; component: string } | { kind: 'failed' };
|
|
6
8
|
|
|
7
9
|
/*
|
|
8
|
-
* One part of a block as the markdown it stands for, in a quiet
|
|
10
|
+
* One part of a block as the markdown it stands for, on a card in a quiet tile under a line that says why.
|
|
9
11
|
* A person sees it as a repair, never as an error, so nothing here is red.
|
|
10
12
|
*/
|
|
11
13
|
export function UiFallbackPart({ fallback, problem }: { fallback: string; problem: UiFallbackProblem }) {
|
|
12
14
|
const { t } = useTranslation('agent-chat');
|
|
13
15
|
return (
|
|
14
|
-
<div className="flex flex-col gap-1 rounded-
|
|
15
|
-
<span className="text-
|
|
16
|
+
<div className="chat-ui-flat flex flex-col gap-1.5 rounded-xl bg-ui-tile p-1.5">
|
|
17
|
+
<span className="chat-ui-flat-inner flex items-center gap-1.5 px-1.5 pt-0.5 text-2xs text-text-muted">
|
|
18
|
+
<Icon icon={Info} size={12} className="shrink-0" />
|
|
16
19
|
{problem.kind === 'unknown' ? t('blocks.unknownComponent', { component: problem.component }) : t('blocks.renderFailed')}
|
|
17
20
|
</span>
|
|
18
21
|
{fallback !== '' && (
|
|
19
22
|
// A fallback is often data without a space in it, such as the JSON of a chart.
|
|
20
|
-
<div className="min-w-0 wrap-anywhere">
|
|
23
|
+
<div className="min-w-0 rounded-lg bg-ui-inset px-3 py-2.5 wrap-anywhere shadow-[inset_0_0_0_1px_var(--border)]">
|
|
21
24
|
<Markdown text={fallback} fileLinks={false} />
|
|
22
25
|
</div>
|
|
23
26
|
)}
|
|
@@ -68,8 +68,8 @@ const LITERAL_PARENTS = new Set(['CodeBlock', 'Tag', 'Choice', 'Item', 'Switch',
|
|
|
68
68
|
|
|
69
69
|
/* Parents that stack their children, where a run of text and inline nodes is one paragraph. */
|
|
70
70
|
const PARAGRAPHS: Readonly<Record<string, string>> = {
|
|
71
|
-
[BLOCK]: '
|
|
72
|
-
Tab: '
|
|
71
|
+
[BLOCK]: 'text-xs text-text',
|
|
72
|
+
Tab: 'text-xs text-text',
|
|
73
73
|
Section: 'text-xs text-text-muted'
|
|
74
74
|
};
|
|
75
75
|
|
|
@@ -198,7 +198,7 @@ function ProseLink({ href, children }: { href?: string; children?: ReactNode })
|
|
|
198
198
|
|
|
199
199
|
const PROSE_COMPONENTS: Components = {
|
|
200
200
|
p: ({ children }) => children,
|
|
201
|
-
code: ({ children }) => <code className="rounded-sm bg-
|
|
201
|
+
code: ({ children }) => <code className="rounded-sm bg-ui-tile px-1 py-px font-mono text-code">{children}</code>,
|
|
202
202
|
a: ({ href, children }) => <ProseLink href={href}>{children}</ProseLink>,
|
|
203
203
|
span: ({ node, children, className }) =>
|
|
204
204
|
node?.properties['data-ui-citation'] !== undefined ? (
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { UiRenderers } from './render-context';
|
|
2
2
|
import { ChartRenderer } from './renderers/chart';
|
|
3
3
|
import { ChoiceRenderer, ChoicesRenderer } from './renderers/choices';
|
|
4
|
-
import { CodeBlockRenderer, CompareRenderer, GalleryRenderer, ImageRenderer, SourceRenderer, SourcesRenderer } from './renderers/content';
|
|
4
|
+
import { CodeBlockRenderer, CompareRenderer, GalleryRenderer, ImageRenderer, ModelRenderer, SourceRenderer, SourcesRenderer } from './renderers/content';
|
|
5
5
|
import { EntityListRenderer, EntryRenderer, StatRenderer, StatsRenderer, TableRenderer } from './renderers/data';
|
|
6
6
|
import {
|
|
7
7
|
ButtonRenderer,
|
|
@@ -57,6 +57,7 @@ export const UI_RENDERERS: UiRenderers = {
|
|
|
57
57
|
Image: ImageRenderer,
|
|
58
58
|
Gallery: GalleryRenderer,
|
|
59
59
|
Compare: CompareRenderer,
|
|
60
|
+
Model: ModelRenderer,
|
|
60
61
|
Map: MapRenderer,
|
|
61
62
|
Place: null,
|
|
62
63
|
Sources: SourcesRenderer,
|
|
@@ -442,7 +442,7 @@ function DrawnChart({
|
|
|
442
442
|
const values = useMemo(() => uiChartValues(data), [data]);
|
|
443
443
|
const { kind } = chart;
|
|
444
444
|
return (
|
|
445
|
-
<figure className="flex flex-col gap-2
|
|
445
|
+
<figure className="chat-ui-flat flex flex-col gap-2.5 rounded-xl bg-ui-tile p-3.5">
|
|
446
446
|
{chart.series.length > 1 && <Legend chart={chart} />}
|
|
447
447
|
{kind !== 'stacked' && unitApart(unit) && <p className="text-xs text-text-faint">{unit}</p>}
|
|
448
448
|
<div ref={measure} role="img" aria-label={chartSummary(values, unit, t)} className="relative">
|
|
@@ -475,7 +475,7 @@ export function ChartRenderer({ node, context }: UiRendererProps<UiProps<'Chart'
|
|
|
475
475
|
const { kind, data, unit } = node.props;
|
|
476
476
|
const chart = useMemo(() => (node.complete ? uiChartData(data, kind) : null), [node.complete, data, kind]);
|
|
477
477
|
if (!node.complete) {
|
|
478
|
-
return <div aria-hidden className="
|
|
478
|
+
return <div aria-hidden className="rounded-xl bg-ui-tile" style={{ height: chartHeight(kind, data.length) }} />;
|
|
479
479
|
}
|
|
480
480
|
if (chart === null) {
|
|
481
481
|
return <UiFallbackPart fallback={node.fallback} problem={{ kind: 'failed' }} />;
|