@adecore/agents-react 0.28.0 → 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/renderers/chart.js +2 -2
- package/dist/chat/ui/intelligent-ui/renderers/choices.js +13 -8
- package/dist/chat/ui/intelligent-ui/renderers/content.js +15 -14
- 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/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/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 +36 -23
- 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/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
|
};
|
|
@@ -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
|
}
|
|
@@ -15,7 +15,8 @@ import { useTruncated } from '../use-truncated.js';
|
|
|
15
15
|
const SourceNumbers = createContext(new Map());
|
|
16
16
|
/* Plain text in the same measure until the node closes, so highlighting never makes it jump. */
|
|
17
17
|
export function CodeBlockRenderer({ node }) {
|
|
18
|
-
|
|
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' }) })] }));
|
|
19
20
|
}
|
|
20
21
|
// Set by a Gallery, so the images inside it draw as cells of its grid.
|
|
21
22
|
const GalleryContext = createContext(false);
|
|
@@ -27,7 +28,7 @@ function AttachedImage({ scopeId, chatId, attachmentId, alt, cell, unnamed }) {
|
|
|
27
28
|
const host = useChatHost();
|
|
28
29
|
const source = host.attachments.useUrl(scopeId, chatId, attachmentId);
|
|
29
30
|
if (!cell) {
|
|
30
|
-
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" });
|
|
31
32
|
}
|
|
32
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 })] }));
|
|
33
34
|
}
|
|
@@ -41,12 +42,12 @@ export function ImageRenderer({ node, children, context }) {
|
|
|
41
42
|
const { attachment, alt } = node.props;
|
|
42
43
|
const caption = uiNodeLabel(node);
|
|
43
44
|
const name = alt ?? (caption === '' ? t('blocks.image.label') : caption);
|
|
44
|
-
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') }))] }));
|
|
45
46
|
}
|
|
46
47
|
/* Three columns, two in a narrow block; the container decides, not the window, since a chat can be any width. */
|
|
47
48
|
export function GalleryRenderer({ node, children }) {
|
|
48
49
|
const { t } = useTranslation('agent-chat');
|
|
49
|
-
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 }) }) }));
|
|
50
51
|
}
|
|
51
52
|
// The shape of the frame until the first picture reports its own.
|
|
52
53
|
const COMPARE_ASPECT = 4 / 3;
|
|
@@ -76,7 +77,7 @@ export function CompareRenderer({ node, children, context }) {
|
|
|
76
77
|
body = _jsx(ImageThumb, { source: { url: null, failure: null }, alt: label, aspect: COMPARE_ASPECT });
|
|
77
78
|
}
|
|
78
79
|
else if (first.url !== null && second.url !== null) {
|
|
79
|
-
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) => {
|
|
80
81
|
const { naturalWidth, naturalHeight } = event.currentTarget;
|
|
81
82
|
if (naturalWidth > 0 && naturalHeight > 0) {
|
|
82
83
|
setAspect(naturalWidth / naturalHeight);
|
|
@@ -88,9 +89,9 @@ export function CompareRenderer({ node, children, context }) {
|
|
|
88
89
|
}
|
|
89
90
|
else {
|
|
90
91
|
const shown = first.url !== null ? { source: first, name: describe(beforeLabel) } : { source: second, name: describe(afterLabel) };
|
|
91
|
-
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') })] }));
|
|
92
93
|
}
|
|
93
|
-
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 })] }));
|
|
94
95
|
}
|
|
95
96
|
/* A chip on the picture gets a ground of its own, since a pill's alpha ground would vanish over an image. */
|
|
96
97
|
function CompareChip({ side, label }) {
|
|
@@ -111,21 +112,21 @@ export function ModelRenderer({ node, children, context }) {
|
|
|
111
112
|
const View = host.ModelView;
|
|
112
113
|
let body;
|
|
113
114
|
if (View !== undefined && source.url !== null) {
|
|
114
|
-
body = (_jsx("div", { role: "group", "aria-label": label, className: "relative h-80 w-full overflow-hidden rounded-
|
|
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 }) }) }));
|
|
115
116
|
}
|
|
116
117
|
else if (View !== undefined && source.failure === null) {
|
|
117
|
-
body = _jsx("div", { "aria-busy": "true", "aria-label": label, className: "h-80 w-full rounded-
|
|
118
|
+
body = _jsx("div", { "aria-busy": "true", "aria-label": label, className: "h-80 w-full rounded-xl bg-ui-tile" });
|
|
118
119
|
}
|
|
119
120
|
else {
|
|
120
|
-
body = (_jsxs("div", { className: "flex w-full min-w-0 items-center gap-2 rounded-
|
|
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') })] }));
|
|
121
122
|
}
|
|
122
|
-
return (_jsxs("figure", { className: "flex flex-col items-start gap-1.5
|
|
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 })] }));
|
|
123
124
|
}
|
|
124
125
|
export function SourcesRenderer({ node, children }) {
|
|
125
126
|
const { t } = useTranslation('agent-chat');
|
|
126
127
|
const headingId = useId();
|
|
127
128
|
const numbers = new Map(uiChildrenOf(node, 'Source').map((source, index) => [source.id, index + 1]));
|
|
128
|
-
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 })] }) }));
|
|
129
130
|
}
|
|
130
131
|
/*
|
|
131
132
|
* A source number in running text. Resting on it or focusing it shows the title and the domain, and
|
|
@@ -145,6 +146,6 @@ export function SourceRenderer({ node, context }) {
|
|
|
145
146
|
const { title, url } = node.props;
|
|
146
147
|
const { openUrl } = context;
|
|
147
148
|
const [measureDomain, domainCut] = useTruncated();
|
|
148
|
-
const face = (_jsxs(_Fragment, { children: [_jsx("span", { className: "
|
|
149
|
-
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 })) }) }));
|
|
150
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;
|
|
@@ -18,14 +18,14 @@ const MapView = lazyNamed(() => import('./map-view.js'), 'MapView');
|
|
|
18
18
|
const LOADING_SPINNER_MS = 1000;
|
|
19
19
|
/* The number that ties a row to its marker. The list numbers itself for a screen reader, so the chip is only for the eye. */
|
|
20
20
|
function NumberChip({ number, selected }) {
|
|
21
|
-
return (_jsx("span", { "aria-hidden": true, className: clsx('mt-px flex size-5 shrink-0 items-center justify-center rounded-
|
|
21
|
+
return (_jsx("span", { "aria-hidden": true, className: clsx('mt-px flex size-5 shrink-0 items-center justify-center rounded-md text-2xs font-semibold tabular-nums', selected ? 'bg-accent text-accent-text' : 'bg-ui-inset text-text'), children: number }));
|
|
22
22
|
}
|
|
23
23
|
function PlaceRow({ listed, number, selected, onPick }) {
|
|
24
24
|
const { t } = useTranslation('agent-chat');
|
|
25
25
|
const { detail, address } = listed.node.props;
|
|
26
|
-
const body = (_jsxs(_Fragment, { children: [_jsx(NumberChip, { number: number, selected: selected }), _jsxs("span", { className: "flex min-w-0 grow flex-col gap-0.5", children: [_jsx("span", { className: "text-
|
|
27
|
-
const row = 'flex w-full min-h-
|
|
28
|
-
return (_jsx("li", { children: onPick === null ? (_jsx("div", { className: clsx(row, selected && '
|
|
26
|
+
const body = (_jsxs(_Fragment, { children: [_jsx(NumberChip, { number: number, selected: selected }), _jsxs("span", { className: "flex min-w-0 grow flex-col gap-0.5", children: [_jsx("span", { className: "text-xs font-medium wrap-break-word text-text", children: listed.name }), detail !== undefined && detail !== '' && _jsx("span", { className: "text-xs wrap-break-word text-text-muted", children: detail }), address !== undefined && address !== '' && _jsx("span", { className: "text-xs wrap-break-word text-text-faint", children: address }), listed.point === undefined && _jsx("span", { className: "text-xs text-text-faint", children: t('blocks.map.lookingUp') }), listed.point === null && _jsx("span", { className: "text-xs text-text-faint", children: t('blocks.map.notFound') })] })] }));
|
|
27
|
+
const row = 'flex h-full w-full min-h-12 items-start gap-2.5 rounded-xl bg-ui-tile px-3 py-2 text-left';
|
|
28
|
+
return (_jsx("li", { children: onPick === null ? (_jsx("div", { className: clsx(row, selected && 'ring-2 ring-accent ring-inset'), children: body })) : (_jsx("button", { type: "button", "aria-pressed": selected, className: clsx(row, 'hover:bg-surface-active', selected && 'ring-2 ring-accent ring-inset'), onClick: onPick, children: body })) }));
|
|
29
29
|
}
|
|
30
30
|
/* Why the map is not the host's: its own reason when the source has none, or what the view found out on the way. */
|
|
31
31
|
function noteFor(t, source, status) {
|
|
@@ -75,7 +75,7 @@ function MapFrame({ markers, source, selected, onPick }) {
|
|
|
75
75
|
else if (drawn) {
|
|
76
76
|
body = (_jsx(DiffFallbackBoundary, { fallback: quiet(t('blocks.map.unavailable')), resetKeys: [], children: _jsx(LazyBoundary, { children: _jsx(MapView, { markers: markers, source: source ?? { state: 'off', reason: 'unavailable' }, selected: selected, onPick: onPick, onStatus: setStatus }, attempt) }) }));
|
|
77
77
|
}
|
|
78
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: setElement, className: "relative h-50 overflow-hidden rounded-
|
|
78
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: setElement, className: "relative h-50 overflow-hidden rounded-xl bg-ui-tile @min-[420px]:h-60", children: [body, waiting && (_jsx("div", { className: "pointer-events-none absolute inset-0 grid place-items-center", children: _jsx(Spinner, { size: 16 }) }))] }), note !== null && _jsx("p", { className: "px-0.5 text-xs text-text-faint", children: note })] }));
|
|
79
79
|
}
|
|
80
80
|
/* Asks the host for its tiles in a component of its own, since a host that offers none has no hook to call. */
|
|
81
81
|
function HostedFrame({ maps, scopeId, ...frame }) {
|
|
@@ -106,8 +106,8 @@ export function MapRenderer({ node, children, context }) {
|
|
|
106
106
|
};
|
|
107
107
|
const onPick = binding !== null ? pick : null;
|
|
108
108
|
const frame = maps ? (_jsx(HostedFrame, { maps: maps, scopeId: context.scopeId, markers: markers, selected: selectedId, onPick: onPick })) : (_jsx(MapFrame, { markers: markers, source: null, selected: selectedId, onPick: onPick }));
|
|
109
|
-
return (_jsxs("div", { className: clsx('@container flex flex-col gap-1.5
|
|
109
|
+
return (_jsxs("div", { className: clsx('@container flex flex-col gap-1.5', !node.complete && PENDING), children: [frame, _jsx("ol", { "aria-label": t('blocks.map.label', { count: places.length }), className: "grid gap-1.5 @min-[420px]:grid-cols-2", children: places.map((listed, index) => {
|
|
110
110
|
const { value } = listed.node.props;
|
|
111
111
|
return (_jsx(PlaceRow, { listed: listed, number: index + 1, selected: value !== undefined && value === current, onPick: binding !== null && value !== undefined ? () => binding.onValueChange(value) : null }, listed.node.id));
|
|
112
|
-
}) }), node.omitted !== undefined && node.omitted > 0 &&
|
|
112
|
+
}) }), node.omitted !== undefined && node.omitted > 0 && _jsx("p", { className: "text-2xs text-text-faint", children: t('blocks.map.omitted', { count: node.omitted }) }), _jsx("div", { className: "chat-ui-nodes flex flex-col gap-1.5 empty:hidden", children: children })] }));
|
|
113
113
|
}
|
|
@@ -11,22 +11,22 @@ export function TabsRenderer({ node, children, context }) {
|
|
|
11
11
|
const tabs = uiChildrenOf(node, 'Tab');
|
|
12
12
|
const [picked, setPicked] = useBlockLocal(context, node.id, () => null);
|
|
13
13
|
const value = picked !== null && tabs.some((tab) => tab.id === picked) ? picked : (tabs[0]?.id ?? null);
|
|
14
|
-
return (_jsxs(Tabs.Root, { value: value, onValueChange: (next) => setPicked(String(next)), className: "flex flex-col gap-3", children: [_jsx(Tabs.List, { className: "
|
|
14
|
+
return (_jsxs(Tabs.Root, { value: value, onValueChange: (next) => setPicked(String(next)), className: "flex flex-col gap-3", children: [_jsx(Tabs.List, { variant: "pill", overflow: "scroll", className: "w-fit max-w-full rounded-xl bg-ui-tile p-0.75", children: tabs.map((tab) => (_jsx(Tabs.Tab, { value: tab.id, "data-ui-reveal": tab.id, children: tab.props.title }, tab.id))) }), children] }));
|
|
15
15
|
}
|
|
16
16
|
export function TabRenderer({ node, children }) {
|
|
17
17
|
return (_jsx(Tabs.Panel, { value: node.id, className: "chat-ui-nodes flex flex-col gap-3", children: children }));
|
|
18
18
|
}
|
|
19
19
|
export function SectionsRenderer({ node, children, context }) {
|
|
20
20
|
const [touched, setTouched] = useBlockLocal(context, node.id, () => false);
|
|
21
|
-
return (_jsx(SectionsContext, { value: { touched, touch: () => setTouched(true) }, children: _jsx("div", { className: "chat-ui-nodes flex flex-col
|
|
21
|
+
return (_jsx(SectionsContext, { value: { touched, touch: () => setTouched(true) }, children: _jsx("div", { className: "chat-ui-nodes flex flex-col gap-1.5", children: children }) }));
|
|
22
22
|
}
|
|
23
|
-
/* Arrives open until the person folds a section of the set themselves; the content
|
|
23
|
+
/* Arrives open until the person folds a section of the set themselves; the content keeps one margin on both sides. */
|
|
24
24
|
export function SectionRenderer({ node, children, context }) {
|
|
25
25
|
const fold = useContext(SectionsContext);
|
|
26
26
|
const [open, setOpen] = useBlockLocal(context, node.id, () => !fold.touched);
|
|
27
27
|
const contentId = useId();
|
|
28
|
-
return (_jsxs("section", { children: [_jsxs("button", { type: "button", "data-ui-reveal": node.id, "aria-expanded": open, "aria-controls": contentId, className: "flex h-
|
|
28
|
+
return (_jsxs("section", { className: "rounded-xl bg-ui-tile", children: [_jsxs("button", { type: "button", "data-ui-reveal": node.id, "aria-expanded": open, "aria-controls": contentId, className: "flex min-h-10 w-full items-center gap-2 rounded-xl px-3 text-left text-xs font-medium text-text hover:bg-text/4", onClick: () => {
|
|
29
29
|
fold.touch();
|
|
30
30
|
setOpen(!open);
|
|
31
|
-
}, children: [_jsx(Icon, { icon: ChevronRight, size: 14, className: clsx('shrink-0 text-text-faint transition-transform duration-150 motion-reduce:transition-none', open && 'rotate-90') }), _jsx("span", { className: "min-w-0 truncate", children: node.props.title })] }), _jsx("div", { id: contentId, hidden: !open, className: "chat-ui-nodes flex flex-col gap-3
|
|
31
|
+
}, children: [_jsx(Icon, { icon: ChevronRight, size: 14, className: clsx('shrink-0 text-text-faint transition-transform duration-150 motion-reduce:transition-none', open && 'rotate-90') }), _jsx("span", { className: "min-w-0 truncate", children: node.props.title })] }), _jsx("div", { id: contentId, hidden: !open, className: "chat-ui-section-body chat-ui-nodes flex flex-col gap-3 px-3 pb-3", children: children })] }));
|
|
32
32
|
}
|