@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.
Files changed (43) hide show
  1. package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +19 -9
  2. package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +4 -2
  3. package/dist/chat/ui/intelligent-ui/UiReply.js +3 -3
  4. package/dist/chat/ui/intelligent-ui/registry.js +2 -1
  5. package/dist/chat/ui/intelligent-ui/renderers/chart.js +2 -2
  6. package/dist/chat/ui/intelligent-ui/renderers/choices.js +13 -8
  7. package/dist/chat/ui/intelligent-ui/renderers/content.d.ts +1 -0
  8. package/dist/chat/ui/intelligent-ui/renderers/content.js +38 -11
  9. package/dist/chat/ui/intelligent-ui/renderers/data.js +19 -6
  10. package/dist/chat/ui/intelligent-ui/renderers/inputs.js +10 -9
  11. package/dist/chat/ui/intelligent-ui/renderers/links.js +5 -3
  12. package/dist/chat/ui/intelligent-ui/renderers/map.js +7 -7
  13. package/dist/chat/ui/intelligent-ui/renderers/structure.js +5 -5
  14. package/dist/chat/ui/intelligent-ui/renderers/text.js +25 -14
  15. package/dist/chat/ui/intelligent-ui/tones.d.ts +2 -0
  16. package/dist/chat/ui/intelligent-ui/tones.js +16 -0
  17. package/dist/host.d.ts +10 -0
  18. package/dist/index.d.ts +1 -1
  19. package/dist/locales/en/agent-chat.d.ts +5 -0
  20. package/dist/locales/en/agent-chat.js +5 -0
  21. package/dist/locales/nl/agent-chat.d.ts +5 -0
  22. package/dist/locales/nl/agent-chat.js +5 -0
  23. package/dist/theme.css +38 -0
  24. package/package.json +8 -8
  25. package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +26 -13
  26. package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +7 -4
  27. package/src/chat/ui/intelligent-ui/UiReply.tsx +3 -3
  28. package/src/chat/ui/intelligent-ui/registry.ts +2 -1
  29. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +2 -2
  30. package/src/chat/ui/intelligent-ui/renderers/choices.tsx +33 -19
  31. package/src/chat/ui/intelligent-ui/renderers/content.tsx +82 -19
  32. package/src/chat/ui/intelligent-ui/renderers/data.tsx +61 -19
  33. package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +21 -14
  34. package/src/chat/ui/intelligent-ui/renderers/links.tsx +7 -4
  35. package/src/chat/ui/intelligent-ui/renderers/map.tsx +15 -12
  36. package/src/chat/ui/intelligent-ui/renderers/structure.tsx +6 -6
  37. package/src/chat/ui/intelligent-ui/renderers/text.tsx +63 -42
  38. package/src/chat/ui/intelligent-ui/tones.ts +18 -0
  39. package/src/host.ts +19 -0
  40. package/src/index.ts +1 -0
  41. package/src/locales/en/agent-chat.ts +5 -0
  42. package/src/locales/nl/agent-chat.ts +5 -0
  43. 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-full text-xs tabular-nums', selected ? 'bg-accent text-accent-text' : 'bg-map-marker text-map-marker-text'), children: number }));
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-sm 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 w-full min-h-8 items-start gap-2 rounded-md px-2 py-1.5 text-left';
28
- return (_jsx("li", { children: onPick === null ? (_jsx("div", { className: clsx(row, selected && 'bg-surface-hover'), children: body })) : (_jsx("button", { type: "button", "aria-pressed": selected, className: clsx(row, 'hover:bg-surface-hover', selected && 'bg-surface-hover'), onClick: onPick, children: body })) }));
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-lg border border-border bg-surface-sunken @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 })] }));
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 px-2', !node.complete && PENDING), children: [frame, _jsx("ol", { "aria-label": t('blocks.map.label', { count: places.length }), className: "flex flex-col", children: places.map((listed, index) => {
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 && (_jsx("p", { className: "px-2 text-xs 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 })] }));
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: "px-2", children: tabs.map((tab) => (_jsx(Tabs.Tab, { value: tab.id, "data-ui-reveal": tab.id, children: tab.props.title }, tab.id))) }), children] }));
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 divide-y divide-border-soft", children: children }) }));
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 starts under the title. */
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-8 w-full items-center gap-2 rounded-md px-2 text-left text-sm font-medium text-text hover:bg-surface-hover", onClick: () => {
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 pt-1 pr-2 pb-2 pl-7.5", children: children })] }));
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 badge longer than this is a sentence, which a tag cannot wrap; it stands under the title as text instead. */
30
- const BADGE_AS_TAG = 32;
31
- /* The head of the block when it is the first node, a subheading without an icon further down. */
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 tag = badge !== undefined && badge !== '' && badge.length <= BADGE_AS_TAG;
37
- 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 })] })] }));
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('flex gap-2 rounded-md p-2 text-sm', UI_TONE_SURFACE[tone]), children: [_jsx(Icon, { icon: UI_TONE_ICONS[tone], size: 16, className: clsx('mt-0.75 shrink-0', UI_TONE_TEXT[tone]) }), _jsxs("div", { className: "min-w-0 grow", children: [title !== undefined && title !== '' && _jsx("div", { className: "font-medium text-text", children: title }), _jsx("div", { className: "text-text-muted", children: children })] })] }));
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-1 px-2", 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 })] }));
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 px-2", children: children });
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-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 })] })] }));
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-2", children: children });
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
- /* Each row draws its own stretch of the line, down to the next dot, so a row that failed to draw leaves the others whole. */
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
- return (_jsxs("li", { "aria-label": label, className: "group/event relative flex min-h-7 gap-3 pb-3 last:pb-0", children: [_jsx("span", { "aria-hidden": true, className: clsx('mt-1 size-2 shrink-0 rounded-full bg-current', UI_TONE_TEXT[tone]) }), _jsx("span", { "aria-hidden": true, className: "absolute top-3.5 bottom-0 left-0.75 w-px bg-border group-last/event:hidden" }), _jsxs("div", { className: "flex min-w-0 grow flex-col gap-0.5", children: [parseUiEventTime(at) === null ? (_jsx("span", { className: "text-xs wrap-anywhere text-text-muted tabular-nums", children: time })) : (_jsx("time", { dateTime: at, className: "text-xs text-text-muted tabular-nums", children: time })), _jsx("div", { className: "text-sm wrap-break-word text-text", children: children }), detail !== undefined && detail !== '' && _jsx("div", { className: "text-xs wrap-anywhere text-text-muted", children: detail })] })] }));
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 @@ declare const _default: {
621
621
  label: string;
622
622
  unavailable: string;
623
623
  };
624
+ model: {
625
+ label: string;
626
+ unavailable: string;
627
+ crashed: string;
628
+ };
624
629
  sources: string;
625
630
  citation: string;
626
631
  outsideProject: string;
@@ -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 @@ declare const _default: {
621
621
  label: string;
622
622
  unavailable: string;
623
623
  };
624
+ model: {
625
+ label: string;
626
+ unavailable: string;
627
+ crashed: string;
628
+ };
624
629
  sources: string;
625
630
  citation: string;
626
631
  outsideProject: string;
@@ -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.27.5",
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.27.5",
71
- "@adecore/editor": "0.27.5",
72
- "@adecore/editor-react": "0.27.5",
73
- "@adecore/intelligent-ui": "0.27.5",
74
- "@adecore/merge": "0.27.5",
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.27.5",
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.27.5",
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-status-idle" />
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="-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">
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 card of one block, with its footer. Named by its Summary and busy while it streams, but never
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 rounded-2xl border border-border bg-surface-raised p-2.25', className)}
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 *:transition-[opacity,translate] *:duration-160 *:ease-out *:starting:opacity-0 motion-safe:*:starting:translate-y-0.5">
213
- {nodes.length === 0 && streaming ? <Skeleton className="mx-2 my-1 w-2/5" /> : children}
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 surface with a line that says why.
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-md bg-surface-hover p-2">
15
- <span className="text-xs text-text-faint">
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]: 'px-2 text-xs text-text',
72
- Tab: 'px-2 text-xs text-text',
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-surface-sunken px-1 py-px font-mono text-code">{children}</code>,
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 px-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="mx-2 rounded-md bg-surface-hover" style={{ height: chartHeight(kind, data.length) }} />;
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' }} />;