@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
|
@@ -3,7 +3,8 @@ import { useId, useLayoutEffect, useRef, useState } from 'react';
|
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
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.js';
|
|
7
8
|
import { formatAgo, formatMoment } from '@adecore/ui/format';
|
|
8
9
|
import { uiBlockHead, uiReasonText } from './node-text.js';
|
|
9
10
|
import { UiHeadContext } from './render-context.js';
|
|
@@ -24,7 +25,7 @@ function LivePart({ live }) {
|
|
|
24
25
|
return fresh ? t('blocks.live.read', { time }) : time;
|
|
25
26
|
});
|
|
26
27
|
const source = live.source ?? live.sources[0] ?? '';
|
|
27
|
-
const icon = slow ? (_jsx(Spinner, { size: 12 })) : live.state === 'failed' ? (_jsx(Icon, { icon: TriangleAlert, size: 12, className: "text-status-needs-you" })) : live.state === 'refused' ? (_jsx(Icon, { icon: Lock, size: 12 })) : (_jsx(Icon, { icon: Radio, size: 12, className: "text-
|
|
28
|
+
const icon = slow ? (_jsx(Spinner, { size: 12 })) : live.state === 'failed' ? (_jsx(Icon, { icon: TriangleAlert, size: 12, className: "text-status-needs-you" })) : live.state === 'refused' ? (_jsx(Icon, { icon: Lock, size: 12 })) : (_jsx(Icon, { icon: Radio, size: 12, className: "text-positive" }));
|
|
28
29
|
const words = live.state === 'failed'
|
|
29
30
|
? t('blocks.live.failed', { source })
|
|
30
31
|
: live.state === 'refused'
|
|
@@ -35,10 +36,7 @@ function LivePart({ live }) {
|
|
|
35
36
|
const body = (_jsxs("span", { tabIndex: hint === null ? undefined : 0, className: "flex items-center gap-1.5", children: [icon, _jsx("span", { children: words }), readAt !== null && live.state !== 'refused' && (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx("time", { dateTime: new Date(readAt).toISOString(), children: _jsx("span", { ref: ago, className: "tabular-nums" }) })] })), readAt === null && fresh && (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx("span", { children: t('blocks.live.notRead') })] }))] }));
|
|
36
37
|
return hint === null ? body : _jsx(Tooltip, { label: hint, children: body });
|
|
37
38
|
}
|
|
38
|
-
/*
|
|
39
|
-
* The line under a block, only when it has something to say: live, answered, noted or drawn as
|
|
40
|
-
* text, in that order. A hairline of the soft border runs the full width of the card over it.
|
|
41
|
-
*/
|
|
39
|
+
/* The line under a block, only when it has something to say: live, answered, noted or drawn as text, in that order. */
|
|
42
40
|
function UiBlockFooter({ live = null, answered = null, fixes = NO_FIXES, shownAsText = null }) {
|
|
43
41
|
const { t } = useTranslation('agent-chat');
|
|
44
42
|
const [fixesOpen, setFixesOpen] = useState(false);
|
|
@@ -46,7 +44,7 @@ function UiBlockFooter({ live = null, answered = null, fixes = NO_FIXES, shownAs
|
|
|
46
44
|
if (live === null && answered === null && fixes.length === 0 && shownAsText === null) {
|
|
47
45
|
return null;
|
|
48
46
|
}
|
|
49
|
-
return (_jsxs("div", { className: "
|
|
47
|
+
return (_jsxs("div", { className: "flex flex-col gap-1.5 text-2xs 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
48
|
}
|
|
51
49
|
const RELEASE_MS = 200;
|
|
52
50
|
/*
|
|
@@ -89,15 +87,27 @@ function useRisingFloor(active) {
|
|
|
89
87
|
return [ref, floor];
|
|
90
88
|
}
|
|
91
89
|
/*
|
|
92
|
-
* The
|
|
90
|
+
* The band of one block, with its footer. Named by its Summary and busy while it streams, but never
|
|
93
91
|
* live: reading out every compile would be noise. A node fades in as it arrives, without a stagger,
|
|
94
92
|
* and for a person who asked for less motion it only fades; a node inside another only fades
|
|
95
93
|
* (`chat-ui-nodes`). Until the first node a skeleton holds the place of the head.
|
|
96
94
|
*/
|
|
97
95
|
export function UiBlockFrame({ nodes, phase, children, className, ...footer }) {
|
|
98
96
|
const { t } = useTranslation('agent-chat');
|
|
97
|
+
const host = useChatHost();
|
|
99
98
|
const streaming = phase === 'streaming';
|
|
100
99
|
const head = uiBlockHead(nodes);
|
|
101
100
|
const [ref, floor] = useRisingFloor(streaming);
|
|
102
|
-
|
|
101
|
+
// Only while the focus is inside the block, so the composer keeps the same keys for sending.
|
|
102
|
+
const pickPrimary = (event) => {
|
|
103
|
+
if (!matchesShortcut(KEY_SHORTCUTS.modEnter, event, host.isApplePlatform())) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const primary = event.currentTarget.querySelector('[data-ui-primary-choice]');
|
|
107
|
+
if (primary !== null) {
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
primary.click();
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
return (_jsx(UiHeadContext, { value: head?.id ?? null, children: _jsx("div", { ref: ref, role: "group", "aria-label": head === null || head.label === '' ? t('blocks.label') : head.label, "aria-busy": streaming || undefined, className: clsx('chat-ui-band flex min-h-11 flex-col', className), style: streaming && floor > 0 ? { minHeight: floor } : undefined, onKeyDown: pickPrimary, children: _jsxs("div", { className: "flex flex-col gap-3 py-5", children: [_jsx("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", children: nodes.length === 0 && streaming ? _jsx(Skeleton, { className: "my-1 w-2/5" }) : children }), _jsx(UiBlockFooter, { ...footer })] }) }) }));
|
|
103
113
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Info } from 'lucide-react';
|
|
4
|
+
import { Icon } from '@adecore/ui';
|
|
3
5
|
import { Markdown } from '../Markdown.js';
|
|
4
6
|
/*
|
|
5
|
-
* One part of a block as the markdown it stands for, in a quiet
|
|
7
|
+
* One part of a block as the markdown it stands for, on a card in a quiet tile under a line that says why.
|
|
6
8
|
* A person sees it as a repair, never as an error, so nothing here is red.
|
|
7
9
|
*/
|
|
8
10
|
export function UiFallbackPart({ fallback, problem }) {
|
|
9
11
|
const { t } = useTranslation('agent-chat');
|
|
10
|
-
return (_jsxs("div", { className: "flex flex-col gap-1 rounded-
|
|
12
|
+
return (_jsxs("div", { className: "chat-ui-flat flex flex-col gap-1.5 rounded-xl bg-ui-tile p-1.5", children: [_jsxs("span", { className: "chat-ui-flat-inner flex items-center gap-1.5 px-1.5 pt-0.5 text-2xs text-text-muted", children: [_jsx(Icon, { icon: Info, size: 12, className: "shrink-0" }), problem.kind === 'unknown' ? t('blocks.unknownComponent', { component: problem.component }) : t('blocks.renderFailed')] }), fallback !== '' && (_jsx("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)]", children: _jsx(Markdown, { text: fallback, fileLinks: false }) }))] }));
|
|
11
13
|
}
|
|
@@ -40,8 +40,8 @@ const BLOCK = '$block';
|
|
|
40
40
|
const LITERAL_PARENTS = new Set(['CodeBlock', 'Tag', 'Choice', 'Item', 'Switch', 'Slider', 'Segmented', 'Option', 'Button', 'Column']);
|
|
41
41
|
/* Parents that stack their children, where a run of text and inline nodes is one paragraph. */
|
|
42
42
|
const PARAGRAPHS = {
|
|
43
|
-
[BLOCK]: '
|
|
44
|
-
Tab: '
|
|
43
|
+
[BLOCK]: 'text-xs text-text',
|
|
44
|
+
Tab: 'text-xs text-text',
|
|
45
45
|
Section: 'text-xs text-text-muted'
|
|
46
46
|
};
|
|
47
47
|
const LINKS = new Set(['File', 'Diff', 'Commit', 'Node']);
|
|
@@ -138,7 +138,7 @@ function ProseLink({ href, children }) {
|
|
|
138
138
|
}
|
|
139
139
|
const PROSE_COMPONENTS = {
|
|
140
140
|
p: ({ children }) => children,
|
|
141
|
-
code: ({ children }) => _jsx("code", { className: "rounded-sm bg-
|
|
141
|
+
code: ({ children }) => _jsx("code", { className: "rounded-sm bg-ui-tile px-1 py-px font-mono text-code", children: children }),
|
|
142
142
|
a: ({ href, children }) => _jsx(ProseLink, { href: href, children: children }),
|
|
143
143
|
span: ({ node, children, className }) => node?.properties['data-ui-citation'] !== undefined ? (_jsx(ProseCitation, { number: Number(node.properties['data-ui-citation']), children: children })) : (_jsx("span", { className: className, children: children }))
|
|
144
144
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ChartRenderer } from './renderers/chart.js';
|
|
2
2
|
import { ChoiceRenderer, ChoicesRenderer } from './renderers/choices.js';
|
|
3
|
-
import { CodeBlockRenderer, CompareRenderer, GalleryRenderer, ImageRenderer, SourceRenderer, SourcesRenderer } from './renderers/content.js';
|
|
3
|
+
import { CodeBlockRenderer, CompareRenderer, GalleryRenderer, ImageRenderer, ModelRenderer, SourceRenderer, SourcesRenderer } from './renderers/content.js';
|
|
4
4
|
import { EntityListRenderer, EntryRenderer, StatRenderer, StatsRenderer, TableRenderer } from './renderers/data.js';
|
|
5
5
|
import { ButtonRenderer, ChecklistRenderer, ItemRenderer, SegmentedRenderer, SelectRenderer, SliderRenderer, SwitchRenderer, TextFieldRenderer } from './renderers/inputs.js';
|
|
6
6
|
import { CommitRenderer, DiffRenderer, FileRenderer, NodeRenderer } from './renderers/links.js';
|
|
@@ -37,6 +37,7 @@ export const UI_RENDERERS = {
|
|
|
37
37
|
Image: ImageRenderer,
|
|
38
38
|
Gallery: GalleryRenderer,
|
|
39
39
|
Compare: CompareRenderer,
|
|
40
|
+
Model: ModelRenderer,
|
|
40
41
|
Map: MapRenderer,
|
|
41
42
|
Place: null,
|
|
42
43
|
Sources: SourcesRenderer,
|
|
@@ -168,7 +168,7 @@ function DrawnChart({ chart, requested, data, unit, context, nodeId }) {
|
|
|
168
168
|
const [measureRows, rowsCut] = useTruncated(true);
|
|
169
169
|
const values = useMemo(() => uiChartValues(data), [data]);
|
|
170
170
|
const { kind } = chart;
|
|
171
|
-
return (_jsxs("figure", { className: "flex flex-col gap-2
|
|
171
|
+
return (_jsxs("figure", { className: "chat-ui-flat flex flex-col gap-2.5 rounded-xl bg-ui-tile p-3.5", 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 })] }));
|
|
172
172
|
}
|
|
173
173
|
/*
|
|
174
174
|
* Draws only once its node closed; until then a quiet surface of the height it will take holds its
|
|
@@ -178,7 +178,7 @@ export function ChartRenderer({ node, context }) {
|
|
|
178
178
|
const { kind, data, unit } = node.props;
|
|
179
179
|
const chart = useMemo(() => (node.complete ? uiChartData(data, kind) : null), [node.complete, data, kind]);
|
|
180
180
|
if (!node.complete) {
|
|
181
|
-
return _jsx("div", { "aria-hidden": true, className: "
|
|
181
|
+
return _jsx("div", { "aria-hidden": true, className: "rounded-xl bg-ui-tile", style: { height: chartHeight(kind, data.length) } });
|
|
182
182
|
}
|
|
183
183
|
if (chart === null) {
|
|
184
184
|
return _jsx(UiFallbackPart, { fallback: node.fallback, problem: { kind: 'failed' } });
|
|
@@ -2,9 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useId, useState } from 'react';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
|
-
import {
|
|
5
|
+
import { Check } from 'lucide-react';
|
|
6
6
|
import { uiChoiceLabel } from '@adecore/intelligent-ui';
|
|
7
|
-
import { DisabledReason, Icon, Spinner, Tooltip } from '@adecore/ui';
|
|
7
|
+
import { DisabledReason, formatShortcut, Icon, KEY_SHORTCUTS, Spinner, Tooltip } from '@adecore/ui';
|
|
8
|
+
import { useChatHost } from '../../../../host.js';
|
|
8
9
|
import { UiFallbackPart } from '../UiFallbackPart.js';
|
|
9
10
|
import { useLater } from '../use-later.js';
|
|
10
11
|
/* A context this long may run past its two lines, so the row offers all of it in a tooltip. */
|
|
@@ -14,16 +15,18 @@ const SPINNER_AFTER_MS = 300;
|
|
|
14
15
|
export function ChoicesRenderer({ children, context }) {
|
|
15
16
|
const { t } = useTranslation('agent-chat');
|
|
16
17
|
const [streamed] = useState(context.phase === 'streaming');
|
|
17
|
-
return (_jsxs("div", { role: "group", "aria-label": t('blocks.choices'), className: "chat-ui-nodes flex flex-
|
|
18
|
+
return (_jsxs("div", { role: "group", "aria-label": t('blocks.choices'), className: "chat-ui-nodes @container flex flex-wrap items-start gap-2", children: [children, _jsx("span", { className: "sr-only", role: "status", children: streamed && context.phase === 'final' ? t('blocks.choice.ready') : '' })] }));
|
|
18
19
|
}
|
|
19
20
|
/*
|
|
20
|
-
* A
|
|
21
|
+
* A button that shows what it sends: the label, and under it the context the agent receives. It stays
|
|
21
22
|
* closed while the reply streams, once the block is answered and without a host to send it, and
|
|
22
23
|
* remains focusable then so a screen reader hears why. A finished Choice the host would refuse for
|
|
23
|
-
* its label is no
|
|
24
|
+
* its label is no button at all, but its text. The primary one says the key that picks it while the
|
|
25
|
+
* focus is in the block (`UiBlockFrame`).
|
|
24
26
|
*/
|
|
25
27
|
export function ChoiceRenderer({ node, children, context }) {
|
|
26
28
|
const { t } = useTranslation('agent-chat');
|
|
29
|
+
const host = useChatHost();
|
|
27
30
|
const descriptionId = useId();
|
|
28
31
|
const { context: detail, primary, disabled } = node.props;
|
|
29
32
|
const { answer, onChoose } = context;
|
|
@@ -42,10 +45,12 @@ export function ChoiceRenderer({ node, children, context }) {
|
|
|
42
45
|
? ((disabled === true ? null : context.choiceReason) ?? t('blocks.choice.unavailable'))
|
|
43
46
|
: null;
|
|
44
47
|
const described = detail !== undefined && detail !== '';
|
|
45
|
-
const
|
|
48
|
+
const strong = primary === true;
|
|
49
|
+
const button = (_jsxs("button", { type: "button", "aria-disabled": closed || undefined, "aria-describedby": described ? descriptionId : undefined, "aria-keyshortcuts": strong && !closed ? 'Meta+Enter Control+Enter' : undefined, "data-ui-primary-choice": strong && !closed ? '' : undefined, className: clsx('flex max-w-full min-h-9.5 items-center gap-2.5 rounded-xl px-4 py-2 text-left text-xs transition-[opacity,scale,background-color,filter] duration-150 motion-reduce:transition-none', strong ? 'bg-accent text-accent-text' : 'bg-ui-tile text-text', chosen && 'ring-2 ring-accent ring-offset-2 ring-offset-surface', failed && 'ring-2 ring-status-error', !closed && (strong ? 'hover:brightness-110' : 'hover:bg-surface-active'), !closed && 'active:scale-99', waiting ? 'opacity-60' : closed && !chosen && 'opacity-50'), onClick: () => {
|
|
46
50
|
if (!closed) {
|
|
47
51
|
onChoose?.(node.id);
|
|
48
52
|
}
|
|
49
|
-
}, children: [
|
|
50
|
-
|
|
53
|
+
}, children: [_jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: clsx('wrap-anywhere', strong ? 'font-semibold' : 'font-medium'), children: children }), described && (_jsx("span", { id: descriptionId, className: clsx('line-clamp-2 text-2xs wrap-anywhere', strong ? 'text-accent-text/80' : 'text-text-muted'), children: detail }))] }), strong && !closed && (_jsx("kbd", { "aria-hidden": true, className: "shrink-0 rounded-md bg-accent-text/18 px-1.5 font-sans text-2xs font-medium", children: formatShortcut(KEY_SHORTCUTS.modEnter, host.isApplePlatform()) })), chosen && (_jsxs("span", { className: clsx('flex shrink-0 items-center gap-1 text-2xs', strong ? 'text-accent-text' : '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')] }))] }));
|
|
54
|
+
// At most half the row less half the gap, so two long choices stand side by side and end on one edge; a narrow block gives each the row.
|
|
55
|
+
return (_jsxs("div", { className: "flex max-w-full flex-col gap-1 @min-[420px]:max-w-[calc(50%-4px)]", children: [reason !== null ? (_jsx(DisabledReason, { reason: reason, children: button })) : described && detail.length > LONG_CONTEXT ? (_jsx(Tooltip, { label: detail, children: button })) : (button), failed && (_jsx("p", { role: "alert", className: "px-1 text-2xs text-status-error", children: t('blocks.choice.failed') }))] }));
|
|
51
56
|
}
|
|
@@ -4,6 +4,7 @@ export declare function CodeBlockRenderer({ node }: UiRendererProps<UiProps<'Cod
|
|
|
4
4
|
export declare function ImageRenderer({ node, children, context }: UiRendererProps<UiProps<'Image'>>): import("react").JSX.Element;
|
|
5
5
|
export declare function GalleryRenderer({ node, children }: UiRendererProps<UiProps<'Gallery'>>): import("react").JSX.Element;
|
|
6
6
|
export declare function CompareRenderer({ node, children, context }: UiRendererProps<UiProps<'Compare'>>): import("react").JSX.Element;
|
|
7
|
+
export declare function ModelRenderer({ node, children, context }: UiRendererProps<UiProps<'Model'>>): import("react").JSX.Element;
|
|
7
8
|
export declare function SourcesRenderer({ node, children }: UiRendererProps<UiProps<'Sources'>>): import("react").JSX.Element;
|
|
8
9
|
export interface UiSourceCitationProps {
|
|
9
10
|
number: number;
|
|
@@ -2,7 +2,8 @@ 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, useState } from 'react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
|
-
import {
|
|
5
|
+
import { Box } from 'lucide-react';
|
|
6
|
+
import { ErrorBoundary, Icon, Pill, PreviewCard, Tooltip, Wipe } from '@adecore/ui';
|
|
6
7
|
import { formatNumber } from '@adecore/ui/format';
|
|
7
8
|
import { useChatHost } from '../../../../host.js';
|
|
8
9
|
import { CodeBlock } from '../../CodeBlock.js';
|
|
@@ -14,7 +15,8 @@ import { useTruncated } from '../use-truncated.js';
|
|
|
14
15
|
const SourceNumbers = createContext(new Map());
|
|
15
16
|
/* Plain text in the same measure until the node closes, so highlighting never makes it jump. */
|
|
16
17
|
export function CodeBlockRenderer({ node }) {
|
|
17
|
-
|
|
18
|
+
const { language } = node.props;
|
|
19
|
+
return (_jsxs("div", { className: "chat-ui-code chat-ui-flat flex flex-col rounded-xl bg-ui-tile p-1.5", children: [language !== undefined && language !== '' && (_jsx("div", { className: "chat-ui-flat-inner flex px-1.5 pt-0.5 pb-1.5", children: _jsx("span", { className: "rounded-md bg-ui-inset px-1.5 font-mono text-2xs text-text shadow-[inset_0_0_0_1px_var(--border)]", children: language }) })), _jsx(CodeStreamingContext.Provider, { value: !node.complete, children: _jsx(CodeBlock, { code: uiNodeText(node).replace(/^\n|\n$/g, ''), lang: language ?? 'text' }) })] }));
|
|
18
20
|
}
|
|
19
21
|
// Set by a Gallery, so the images inside it draw as cells of its grid.
|
|
20
22
|
const GalleryContext = createContext(false);
|
|
@@ -26,7 +28,7 @@ function AttachedImage({ scopeId, chatId, attachmentId, alt, cell, unnamed }) {
|
|
|
26
28
|
const host = useChatHost();
|
|
27
29
|
const source = host.attachments.useUrl(scopeId, chatId, attachmentId);
|
|
28
30
|
if (!cell) {
|
|
29
|
-
return _jsx(ImageThumb, { source: source, alt: alt, className: "max-h-70 max-w-full object-contain" });
|
|
31
|
+
return _jsx(ImageThumb, { source: source, alt: alt, className: "max-h-70 max-w-full rounded-xl object-contain" });
|
|
30
32
|
}
|
|
31
33
|
return (_jsxs(_Fragment, { children: [_jsx(ImageThumb, { source: source, alt: alt, aspect: 1 }), unnamed && source.failure !== null && _jsx("figcaption", { className: "text-xs wrap-anywhere text-text-faint", children: alt })] }));
|
|
32
34
|
}
|
|
@@ -40,12 +42,12 @@ export function ImageRenderer({ node, children, context }) {
|
|
|
40
42
|
const { attachment, alt } = node.props;
|
|
41
43
|
const caption = uiNodeLabel(node);
|
|
42
44
|
const name = alt ?? (caption === '' ? t('blocks.image.label') : caption);
|
|
43
|
-
return (_jsxs("figure", { className: cell ? 'flex min-w-0 flex-col gap-1' : 'flex flex-col items-start gap-1.5', children: [attachment === undefined ? (_jsx(ImageThumb, { source: { url: null, failure: t('blocks.image.unavailable') }, alt: name, aspect: cell ? 1 : undefined })) : (_jsx(AttachedImage, { scopeId: context.scopeId, chatId: context.chatId, attachmentId: attachment, alt: name, cell: cell, unnamed: caption === '' })), caption !== '' && _jsx("figcaption", { className:
|
|
45
|
+
return (_jsxs("figure", { className: cell ? 'flex min-w-0 flex-col gap-1' : 'flex flex-col items-start gap-1.5', children: [attachment === undefined ? (_jsx(ImageThumb, { source: { url: null, failure: t('blocks.image.unavailable') }, alt: name, aspect: cell ? 1 : undefined })) : (_jsx(AttachedImage, { scopeId: context.scopeId, chatId: context.chatId, attachmentId: attachment, alt: name, cell: cell, unnamed: caption === '' })), caption !== '' && _jsx("figcaption", { className: "px-0.5 text-2xs wrap-anywhere text-text-muted", children: children }), cell && caption === '' && attachment === undefined && (_jsx("figcaption", { className: "text-xs wrap-anywhere text-text-faint", children: alt ?? t('blocks.image.unavailable') }))] }));
|
|
44
46
|
}
|
|
45
47
|
/* Three columns, two in a narrow block; the container decides, not the window, since a chat can be any width. */
|
|
46
48
|
export function GalleryRenderer({ node, children }) {
|
|
47
49
|
const { t } = useTranslation('agent-chat');
|
|
48
|
-
return (_jsx(GalleryContext, { value: true, children: _jsx("div", { role: "group", "aria-label": t('blocks.gallery.label', { count: uiChildrenOf(node, 'Image').length }), className: "@container
|
|
50
|
+
return (_jsx(GalleryContext, { value: true, children: _jsx("div", { role: "group", "aria-label": t('blocks.gallery.label', { count: uiChildrenOf(node, 'Image').length }), className: "@container", children: _jsx("div", { className: "chat-ui-nodes grid grid-cols-2 gap-2 @min-[420px]:grid-cols-3", children: children }) }) }));
|
|
49
51
|
}
|
|
50
52
|
// The shape of the frame until the first picture reports its own.
|
|
51
53
|
const COMPARE_ASPECT = 4 / 3;
|
|
@@ -75,7 +77,7 @@ export function CompareRenderer({ node, children, context }) {
|
|
|
75
77
|
body = _jsx(ImageThumb, { source: { url: null, failure: null }, alt: label, aspect: COMPARE_ASPECT });
|
|
76
78
|
}
|
|
77
79
|
else if (first.url !== null && second.url !== null) {
|
|
78
|
-
body = (_jsxs("div", { className: "relative max-h-96 overflow-hidden rounded-
|
|
80
|
+
body = (_jsxs("div", { className: "relative max-h-96 overflow-hidden rounded-xl bg-ui-tile", style: { aspectRatio: aspect, width: `min(100%, ${COMPARE_MAX_HEIGHT * aspect}px)` }, children: [_jsx(Wipe, { value: split, onValueChange: setSplit, label: label, before: _jsx("img", { src: first.url, alt: describe(beforeLabel), draggable: false, className: "size-full object-contain select-none", onLoad: (event) => {
|
|
79
81
|
const { naturalWidth, naturalHeight } = event.currentTarget;
|
|
80
82
|
if (naturalWidth > 0 && naturalHeight > 0) {
|
|
81
83
|
setAspect(naturalWidth / naturalHeight);
|
|
@@ -87,19 +89,44 @@ export function CompareRenderer({ node, children, context }) {
|
|
|
87
89
|
}
|
|
88
90
|
else {
|
|
89
91
|
const shown = first.url !== null ? { source: first, name: describe(beforeLabel) } : { source: second, name: describe(afterLabel) };
|
|
90
|
-
body = (_jsxs(_Fragment, { children: [_jsx(ImageThumb, { source: shown.source, alt: shown.name, className: "max-h-70 max-w-full object-contain" }), _jsx("span", { className: "
|
|
92
|
+
body = (_jsxs(_Fragment, { children: [_jsx(ImageThumb, { source: shown.source, alt: shown.name, className: "max-h-70 max-w-full object-contain" }), _jsx("span", { className: "text-2xs text-text-faint", children: t('blocks.compare.missing') })] }));
|
|
91
93
|
}
|
|
92
|
-
return (_jsxs("figure", { className: "flex flex-col items-start gap-1.5
|
|
94
|
+
return (_jsxs("figure", { className: "flex w-full flex-col items-start gap-1.5", children: [body, caption !== '' && _jsx("figcaption", { className: "px-0.5 text-2xs text-text-muted", children: children })] }));
|
|
93
95
|
}
|
|
94
96
|
/* A chip on the picture gets a ground of its own, since a pill's alpha ground would vanish over an image. */
|
|
95
97
|
function CompareChip({ side, label }) {
|
|
96
98
|
return (_jsx("span", { className: clsx('pointer-events-none absolute top-2 z-20 max-w-2/5 truncate rounded-md bg-surface-raised', side === 'left' ? 'left-2' : 'right-2'), children: _jsx(Pill, { shape: "tag", size: "sm", tone: "muted", children: label }) }));
|
|
97
99
|
}
|
|
100
|
+
/*
|
|
101
|
+
* The host draws the model, since the chat knows no 3D format. The frame has a fixed height in the reply column,
|
|
102
|
+
* so a view that loads late never moves the thread. Without a view of the host, or without the bytes, the model
|
|
103
|
+
* stays a quiet row that names it.
|
|
104
|
+
*/
|
|
105
|
+
export function ModelRenderer({ node, children, context }) {
|
|
106
|
+
const { t } = useTranslation('agent-chat');
|
|
107
|
+
const host = useChatHost();
|
|
108
|
+
const { attachment, alt } = node.props;
|
|
109
|
+
const source = host.attachments.useUrl(context.scopeId, context.chatId, attachment);
|
|
110
|
+
const caption = uiNodeLabel(node);
|
|
111
|
+
const label = alt ?? (caption === '' ? t('blocks.model.label') : caption);
|
|
112
|
+
const View = host.ModelView;
|
|
113
|
+
let body;
|
|
114
|
+
if (View !== undefined && source.url !== null) {
|
|
115
|
+
body = (_jsx("div", { role: "group", "aria-label": label, className: "relative h-80 w-full overflow-hidden rounded-xl bg-ui-tile", children: _jsx(ErrorBoundary, { label: t('blocks.model.crashed'), resetKeys: [attachment, source.url], compact: true, children: _jsx(View, { scopeId: context.scopeId, chatId: context.chatId, attachmentId: attachment, url: source.url, label: label, alt: alt, caption: caption }) }) }));
|
|
116
|
+
}
|
|
117
|
+
else if (View !== undefined && source.failure === null) {
|
|
118
|
+
body = _jsx("div", { "aria-busy": "true", "aria-label": label, className: "h-80 w-full rounded-xl bg-ui-tile" });
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
body = (_jsxs("div", { className: "flex w-full min-w-0 items-center gap-2 rounded-xl bg-ui-tile px-3 py-2 text-xs", children: [_jsx(Icon, { icon: Box, size: 14, className: "shrink-0 text-text-muted" }), _jsx("span", { className: "min-w-0 truncate text-text", children: alt ?? attachment }), _jsx("span", { className: "ml-auto shrink-0 text-text-faint", children: source.failure === null ? t('blocks.model.label') : t('blocks.model.unavailable') })] }));
|
|
122
|
+
}
|
|
123
|
+
return (_jsxs("figure", { className: "flex w-full flex-col items-start gap-1.5", children: [body, caption !== '' && _jsx("figcaption", { className: "px-0.5 text-2xs text-text-muted", children: children })] }));
|
|
124
|
+
}
|
|
98
125
|
export function SourcesRenderer({ node, children }) {
|
|
99
126
|
const { t } = useTranslation('agent-chat');
|
|
100
127
|
const headingId = useId();
|
|
101
128
|
const numbers = new Map(uiChildrenOf(node, 'Source').map((source, index) => [source.id, index + 1]));
|
|
102
|
-
return (_jsx(SourceNumbers, { value: numbers, children: _jsxs("section", { "aria-labelledby": headingId, className: "flex flex-col", children: [_jsx("span", { id: headingId, className: "px-
|
|
129
|
+
return (_jsx(SourceNumbers, { value: numbers, children: _jsxs("section", { "aria-labelledby": headingId, className: "chat-ui-flat flex flex-col rounded-xl bg-ui-tile p-1.5", children: [_jsx("span", { id: headingId, className: "chat-ui-flat-inner px-1.5 pt-0.5 pb-1 text-2xs font-medium text-text-muted", children: t('blocks.sources') }), _jsx("ol", { className: "chat-ui-nodes flex flex-col", children: children })] }) }));
|
|
103
130
|
}
|
|
104
131
|
/*
|
|
105
132
|
* A source number in running text. Resting on it or focusing it shows the title and the domain, and
|
|
@@ -119,6 +146,6 @@ export function SourceRenderer({ node, context }) {
|
|
|
119
146
|
const { title, url } = node.props;
|
|
120
147
|
const { openUrl } = context;
|
|
121
148
|
const [measureDomain, domainCut] = useTruncated();
|
|
122
|
-
const face = (_jsxs(_Fragment, { children: [_jsx("span", { className: "
|
|
123
|
-
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-
|
|
149
|
+
const face = (_jsxs(_Fragment, { children: [_jsx("span", { className: "grid size-5 shrink-0 place-items-center rounded-md bg-ui-inset text-2xs font-semibold text-text tabular-nums", children: number }), _jsxs("span", { className: "flex min-w-0 grow flex-col", children: [_jsx("span", { className: "font-medium wrap-anywhere text-text", children: title }), _jsx("span", { ref: measureDomain, className: "truncate text-text-muted", children: uiUrlDomain(url) })] })] }));
|
|
150
|
+
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: "chat-ui-flat-inner flex min-h-8 items-start gap-2.5 rounded-lg px-1.5 py-1.5 text-2xs", children: face })) : (_jsx("button", { type: "button", className: "chat-ui-flat-inner flex min-h-8 w-full items-start gap-2.5 rounded-lg px-1.5 py-1.5 text-left text-2xs hover:bg-text/4", onClick: () => openUrl(url), children: face })) }) }));
|
|
124
151
|
}
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
2
3
|
import clsx from 'clsx';
|
|
3
4
|
import { useTranslation } from 'react-i18next';
|
|
4
5
|
import { ArrowDown, ArrowRight, ArrowUp } from 'lucide-react';
|
|
5
|
-
import { Button, Icon, KeyValueList, Pill } from '@adecore/ui';
|
|
6
|
+
import { Button, Icon, KeyValueList, Pill, Tooltip } from '@adecore/ui';
|
|
6
7
|
import { formatBytes, formatDuration, formatMoment, formatNumber, formatPercent, formatRounded } from '@adecore/ui/format';
|
|
7
8
|
import { useBlockLocal } from '../block-local.js';
|
|
8
9
|
import { uiChildrenOf, uiWithUnit } from '../node-text.js';
|
|
9
10
|
import { isNumericColumn, UI_TABLE_ROWS, uiTableCell, uiTableColumns } from '../table-data.js';
|
|
10
|
-
import {
|
|
11
|
+
import { UI_TONE_PILL } from '../tones.js';
|
|
11
12
|
import { UiFallbackPart } from '../UiFallbackPart.js';
|
|
12
13
|
/* EntityList draws a hairline between its rows from this many on. */
|
|
13
14
|
const DIVIDED_FROM = 7;
|
|
15
|
+
/* Up to this many entries an EntityList is a row of tiles; a longer one is a list in one tile. */
|
|
16
|
+
const TILED_UP_TO = 4;
|
|
17
|
+
const EntityTiles = createContext(false);
|
|
14
18
|
/*
|
|
15
19
|
* A number that live readings keep up to date. Keyed by its text, so a value that changed mounts
|
|
16
20
|
* again and lights up once, even when it changes again before the last light settled.
|
|
@@ -22,7 +26,7 @@ function LiveFigure({ text, previous, live, className }) {
|
|
|
22
26
|
export function StatsRenderer({ children }) {
|
|
23
27
|
return _jsx("div", { className: "chat-ui-nodes grid grid-cols-[repeat(auto-fit,minmax(128px,1fr))] gap-2", children: children });
|
|
24
28
|
}
|
|
25
|
-
/* The arrow and the number say which way a value went; only a tone says whether that is good. */
|
|
29
|
+
/* The arrow and the number say which way a value went; only a tone says whether that is good. The tag says the change, its tooltip from what. */
|
|
26
30
|
export function StatRenderer({ node, context }) {
|
|
27
31
|
const { t } = useTranslation('agent-chat');
|
|
28
32
|
const { label, value, previous, unit, tone } = node.props;
|
|
@@ -32,12 +36,21 @@ export function StatRenderer({ node, context }) {
|
|
|
32
36
|
const from = previous === undefined ? '' : uiWithUnit(formatRounded(previous, 2), unit);
|
|
33
37
|
// A change from zero has no percentage.
|
|
34
38
|
const percent = change === null || previous === undefined || previous === 0 ? null : formatPercent(Math.abs((change / previous) * 100));
|
|
35
|
-
|
|
39
|
+
const said = percent === null ? t('blocks.stat.from', { previous: from }) : t('blocks.stat.change', { change: percent, previous: from });
|
|
40
|
+
return (_jsxs("div", { className: "flex min-w-0 flex-col items-start gap-1.5 rounded-xl bg-ui-tile p-3", children: [_jsx("span", { className: "max-w-full truncate text-2xs text-text-muted", children: label }), _jsx(LiveFigure, { text: uiWithUnit(formatRounded(value, 2), unit), previous: typeof live?.previous === 'number' ? uiWithUnit(formatRounded(live.previous, 2), unit) : undefined, live: live, className: "max-w-full truncate text-lg font-semibold tabular-nums" }), change !== null && (_jsx(Tooltip, { label: said, children: _jsx("span", { tabIndex: 0, className: "max-w-full rounded-md", children: _jsxs(Pill, { shape: "tag", size: "sm", tone: tone === undefined ? 'muted' : UI_TONE_PILL[tone], icon: _jsx(Icon, { icon: arrow, size: 12, className: "shrink-0" }), children: [_jsx("span", { "aria-hidden": true, className: "tabular-nums", children: percent ?? from }), _jsx("span", { className: "sr-only", children: said })] }) }) }))] }));
|
|
36
41
|
}
|
|
37
42
|
export function EntityListRenderer({ node, children }) {
|
|
38
|
-
|
|
43
|
+
const count = uiChildrenOf(node, 'Entry').length;
|
|
44
|
+
if (count <= TILED_UP_TO) {
|
|
45
|
+
return (_jsx(EntityTiles, { value: true, children: _jsx("div", { className: "chat-ui-nodes grid grid-cols-[repeat(auto-fit,minmax(140px,1fr))] gap-2", children: children }) }));
|
|
46
|
+
}
|
|
47
|
+
return (_jsx(KeyValueList.Root, { divided: count >= DIVIDED_FROM, className: "chat-ui-nodes chat-ui-flat rounded-xl bg-ui-tile px-3 py-2", children: children }));
|
|
39
48
|
}
|
|
40
49
|
export function EntryRenderer({ node, children }) {
|
|
50
|
+
const tiled = useContext(EntityTiles);
|
|
51
|
+
if (tiled) {
|
|
52
|
+
return (_jsxs("div", { className: "flex min-w-0 flex-col gap-0.5 rounded-xl bg-ui-tile px-3 py-2", children: [_jsx("span", { className: "truncate text-2xs text-text-muted", children: node.props.label }), _jsx("span", { className: "text-2xs font-semibold wrap-break-word text-text", children: children })] }));
|
|
53
|
+
}
|
|
41
54
|
return (_jsxs(KeyValueList.Item, { children: [_jsx(KeyValueList.Name, { children: node.props.label }), _jsx(KeyValueList.Value, { children: children })] }));
|
|
42
55
|
}
|
|
43
56
|
function numberText(cell, unit) {
|
|
@@ -76,5 +89,5 @@ export function TableRenderer({ node, context }) {
|
|
|
76
89
|
}
|
|
77
90
|
const { rows } = node.props;
|
|
78
91
|
const shown = all ? rows : rows.slice(0, UI_TABLE_ROWS);
|
|
79
|
-
return (_jsxs("div", { className: "flex flex-col items-start gap-1.5", children: [_jsx("div", { className: "max-h-80 w-full overflow-auto rounded-
|
|
92
|
+
return (_jsxs("div", { className: "flex flex-col items-start gap-1.5", children: [_jsx("div", { className: "chat-ui-flat max-h-80 w-full overflow-auto rounded-xl bg-ui-tile px-1.5 pb-1.5", children: _jsxs("table", { "aria-label": t('blocks.table.label', { columns: columns.map((column) => column.title).join(', ') }), className: clsx('w-full border-separate border-spacing-0 text-xs tabular-nums', '[&_tbody_tr:first-child_td:first-child]:rounded-tl-lg [&_tbody_tr:first-child_td:last-child]:rounded-tr-lg', '[&_tbody_tr:last-child_td:first-child]:rounded-bl-lg [&_tbody_tr:last-child_td:last-child]:rounded-br-lg'), children: [_jsx("thead", { className: "sticky top-0 z-1 bg-ui-tile", children: _jsx("tr", { children: columns.map((column) => (_jsx("th", { scope: "col", className: clsx('h-8 px-2 text-2xs font-medium whitespace-nowrap text-text-muted', isNumericColumn(column) ? 'text-right' : 'text-left'), children: column.title }, column.key))) }) }), _jsx("tbody", { children: shown.map((row, index) => (_jsx("tr", { children: columns.map((column, at) => (_jsx("td", { className: clsx('h-9 bg-ui-inset px-2 text-text', index > 0 && 'border-t border-border-soft', at === 0 && 'font-medium', isNumericColumn(column) ? 'text-right whitespace-nowrap' : 'text-left'), children: _jsx(Cell, { value: row[column.key], column: column, live: isNumericColumn(column) ? context.liveValue?.(node.id, 'rows', [index, column.key]) : undefined }) }, column.key))) }, index))) })] }) }), !all && rows.length > UI_TABLE_ROWS && (_jsx(Button, { size: "xs", variant: "secondary", onClick: () => setAll(true), children: t('blocks.table.showAll', { total: formatNumber(rows.length) }) }))] }));
|
|
80
93
|
}
|
|
@@ -11,7 +11,7 @@ const ChecklistContext = createContext({ values: [], enabled: false, toggle: ()
|
|
|
11
11
|
/* The rows of a checklist as one named group. */
|
|
12
12
|
function ChecklistGroup({ pending, children }) {
|
|
13
13
|
const { t } = useTranslation('agent-chat');
|
|
14
|
-
return (_jsx("div", { role: "group", "aria-label": t('blocks.checklist'), className: clsx('chat-ui-nodes flex flex-col', pending && PENDING), children: children }));
|
|
14
|
+
return (_jsx("div", { role: "group", "aria-label": t('blocks.checklist'), className: clsx('chat-ui-nodes flex flex-col gap-1.5', pending && PENDING), children: children }));
|
|
15
15
|
}
|
|
16
16
|
export function ChecklistRenderer({ node, children, context }) {
|
|
17
17
|
const binding = bindingOf(node, context);
|
|
@@ -21,20 +21,21 @@ export function ChecklistRenderer({ node, children, context }) {
|
|
|
21
21
|
};
|
|
22
22
|
return (_jsx(ChecklistContext, { value: { values, enabled: binding !== null, toggle }, children: _jsx(ChecklistGroup, { pending: !node.complete, children: children }) }));
|
|
23
23
|
}
|
|
24
|
-
/* The whole
|
|
24
|
+
/* The whole tile is the label, ringed in the accent while checked; a link inside it keeps its own click and checks nothing. */
|
|
25
25
|
export function ItemRenderer({ node, children }) {
|
|
26
26
|
const list = useContext(ChecklistContext);
|
|
27
27
|
const { value } = node.props;
|
|
28
|
+
const checked = list.values.includes(value);
|
|
28
29
|
const toggleRow = (event) => {
|
|
29
30
|
if (list.enabled && !event.target.closest('button, a')) {
|
|
30
31
|
list.toggle(value);
|
|
31
32
|
}
|
|
32
33
|
};
|
|
33
|
-
return (_jsxs("div", { className: clsx('flex min-h-
|
|
34
|
+
return (_jsxs("div", { className: clsx('flex min-h-10 items-start gap-3 rounded-xl bg-ui-tile px-3 py-2.5 text-xs text-text', checked && 'ring-2 ring-accent ring-inset', list.enabled && 'hover:bg-surface-active'), onClick: toggleRow, children: [_jsx(Checkbox, { className: "mt-0.5", checked: checked, onCheckedChange: () => list.toggle(value), label: uiNodeLabel(node), disabled: !list.enabled }), _jsx("div", { className: "min-w-0 grow", children: children })] }));
|
|
34
35
|
}
|
|
35
36
|
export function SwitchRenderer({ node, children, context }) {
|
|
36
37
|
const binding = bindingOf(node, context);
|
|
37
|
-
return (_jsxs("div", { className: clsx('flex min-h-
|
|
38
|
+
return (_jsxs("div", { className: clsx('chat-ui-flat flex min-h-11 items-center justify-between gap-3 rounded-xl bg-ui-tile px-3 py-2 text-xs', !node.complete && PENDING), children: [_jsx("span", { className: "min-w-0 font-medium text-text", children: children }), _jsx(Switch, { checked: binding?.value ?? node.props.value, onCheckedChange: (next) => binding?.onValueChange(next), label: uiNodeLabel(node), disabled: binding === null })] }));
|
|
38
39
|
}
|
|
39
40
|
/* Its children are the label, read as text: the slider writes its own label line. */
|
|
40
41
|
export function SliderRenderer({ node, context }) {
|
|
@@ -42,7 +43,7 @@ export function SliderRenderer({ node, context }) {
|
|
|
42
43
|
const { value, min, max, step, unit } = node.props;
|
|
43
44
|
return (_jsx(Slider, { value: binding?.value ?? value, onValueChange: (next) => binding?.onValueChange(next), min: min, max: max, step: step,
|
|
44
45
|
// The slider prints its unit right after the number, so the space a unit stands apart with comes from here.
|
|
45
|
-
unit: uiWithUnit('', unit), label: uiNodeLabel(node), disabled: binding === null, className: clsx('
|
|
46
|
+
unit: uiWithUnit('', unit), label: uiNodeLabel(node), disabled: binding === null, className: clsx('chat-ui-flat rounded-xl bg-ui-tile p-3', !node.complete && PENDING) }));
|
|
46
47
|
}
|
|
47
48
|
/* A label longer than this reads as a sentence, which wraps badly beside another; the options then stand in a column. */
|
|
48
49
|
const SIDE_BY_SIDE_LABEL = 24;
|
|
@@ -53,7 +54,7 @@ export function SegmentedRenderer({ node, context }) {
|
|
|
53
54
|
const current = binding?.value ?? node.props.value;
|
|
54
55
|
const options = uiChildrenOf(node, 'Option').map((option, index) => ({ id: String(index), label: uiNodeLabel(option), value: option.props.value }));
|
|
55
56
|
const picked = options.find((option) => option.value === current)?.id ?? '';
|
|
56
|
-
return (_jsx("div", { className: clsx(
|
|
57
|
+
return (_jsx("div", { className: clsx(!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) => {
|
|
57
58
|
const option = options[Number(id)];
|
|
58
59
|
if (option !== undefined) {
|
|
59
60
|
binding?.onValueChange(option.value);
|
|
@@ -76,7 +77,7 @@ export function TextFieldRenderer({ node, children, context }) {
|
|
|
76
77
|
const value = binding?.value ?? node.props.value;
|
|
77
78
|
const countId = `${id}-count`;
|
|
78
79
|
const counting = value.length >= maxLength * COUNT_FROM;
|
|
79
|
-
return (_jsxs("div", { className: clsx('flex flex-col gap-1
|
|
80
|
+
return (_jsxs("div", { className: clsx('flex flex-col gap-1.5', !node.complete && PENDING), children: [_jsx("label", { htmlFor: id, className: "px-0.5 text-2xs text-text-muted", children: children }), _jsx(Input, { id: id, value: value, size: "sm", maxLength: maxLength, placeholder: placeholder, disabled: binding === null, "aria-describedby": counting ? countId : undefined, className: "border-transparent bg-ui-tile", onChange: (event) => binding?.onValueChange(event.target.value) }), counting && (_jsx("span", { id: countId, className: "text-xs text-text-muted tabular-nums", children: t('blocks.textField.count', { used: formatNumber(value.length), max: formatNumber(maxLength) }) }))] }));
|
|
80
81
|
}
|
|
81
82
|
/*
|
|
82
83
|
* Left aligned, never the full width. Like Segmented, each Option is an item by its place, since a value may be any scalar.
|
|
@@ -88,7 +89,7 @@ export function SelectRenderer({ node, context }) {
|
|
|
88
89
|
const current = binding?.value ?? node.props.value;
|
|
89
90
|
const options = uiChildrenOf(node, 'Option').map((option, index) => ({ id: String(index), label: uiNodeLabel(option), value: option.props.value }));
|
|
90
91
|
const label = uiOwnLabel(node);
|
|
91
|
-
return (_jsxs("div", { className: clsx('flex flex-col items-start gap-1
|
|
92
|
+
return (_jsxs("div", { className: clsx('flex flex-col items-start gap-1.5', !node.complete && PENDING), children: [label !== '' && _jsx("span", { className: "px-0.5 text-2xs text-text-muted", children: label }), _jsx(Select, { value: options.find((option) => option.value === current)?.id ?? null, items: options.map((option) => ({ value: option.id, label: option.label })), label: label === '' ? t('blocks.options') : label, disabled: binding === null || options.length === 0, onValueChange: (id) => {
|
|
92
93
|
const option = options[Number(id)];
|
|
93
94
|
if (option !== undefined) {
|
|
94
95
|
binding?.onValueChange(option.value);
|
|
@@ -100,5 +101,5 @@ export function ButtonRenderer({ node, children, context }) {
|
|
|
100
101
|
const enabled = node.onAction !== undefined && node.complete && context.answer === null && node.props.disabled !== true;
|
|
101
102
|
const label = uiNodeLabel(node);
|
|
102
103
|
const [measure, truncated] = useTruncated();
|
|
103
|
-
return (_jsx("div", { className: clsx(
|
|
104
|
+
return (_jsx("div", { className: clsx(!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 }) }) }) }));
|
|
104
105
|
}
|
|
@@ -3,9 +3,11 @@ import clsx from 'clsx';
|
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { FileDiff, GitCommitHorizontal, SquareDashed } from 'lucide-react';
|
|
5
5
|
import { FileIcon, Icon, Tooltip } from '@adecore/ui';
|
|
6
|
-
import { CHIP_IN_MESSAGE
|
|
6
|
+
import { CHIP_IN_MESSAGE } from '../../chips.js';
|
|
7
7
|
import { uiReasonText } from '../node-text.js';
|
|
8
8
|
const PLAIN = { state: 'plain' };
|
|
9
|
+
/* A link in a block is a quiet card on its tile, unlike a mention in a message, which wears the accent. */
|
|
10
|
+
const UI_CHIP = 'bg-ui-inset font-mono text-text shadow-[inset_0_0_0_1px_var(--border)]';
|
|
9
11
|
function baseName(path) {
|
|
10
12
|
const trimmed = path.replace(/\/+$/, '');
|
|
11
13
|
return trimmed.slice(trimmed.lastIndexOf('/') + 1) || path;
|
|
@@ -62,7 +64,7 @@ export function UiLinkChip({ target, context }) {
|
|
|
62
64
|
}
|
|
63
65
|
const { face, hint } = chipFace(target, link);
|
|
64
66
|
const { openLink } = context;
|
|
65
|
-
const chip = openLink === undefined ? (_jsx("span", { className: clsx(CHIP_IN_MESSAGE,
|
|
67
|
+
const chip = openLink === undefined ? (_jsx("span", { className: clsx(CHIP_IN_MESSAGE, UI_CHIP, 'transition-opacity duration-120 starting:opacity-0'), children: face })) : (_jsx("button", { type: "button", className: clsx(CHIP_IN_MESSAGE, UI_CHIP, 'transition-opacity duration-120 hover:bg-surface-hover starting:opacity-0'), onClick: () => openLink(target), children: face }));
|
|
66
68
|
return hint === null ? chip : _jsx(Tooltip, { label: hint, children: chip });
|
|
67
69
|
}
|
|
68
70
|
/* A link with children is a row: the chip and what the agent says about it. */
|
|
@@ -70,7 +72,7 @@ function LinkRow({ target, context, described, children }) {
|
|
|
70
72
|
if (!described) {
|
|
71
73
|
return _jsx(UiLinkChip, { target: target, context: context });
|
|
72
74
|
}
|
|
73
|
-
return (_jsxs("div", { className: "flex min-w-0 flex-wrap items-baseline gap-x-2
|
|
75
|
+
return (_jsxs("div", { className: "flex min-w-0 flex-wrap items-baseline gap-x-2 text-xs", 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
76
|
}
|
|
75
77
|
export function FileRenderer({ node, children, context }) {
|
|
76
78
|
const { path, line } = node.props;
|