@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
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useId, useState } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import {
|
|
4
|
+
import { Check } from 'lucide-react';
|
|
5
5
|
import { uiChoiceLabel, type UiProps } from '@adecore/intelligent-ui';
|
|
6
|
-
import { DisabledReason, Icon, Spinner, Tooltip } from '@adecore/ui';
|
|
6
|
+
import { DisabledReason, formatShortcut, Icon, KEY_SHORTCUTS, Spinner, Tooltip } from '@adecore/ui';
|
|
7
|
+
import { useChatHost } from '../../../../host';
|
|
7
8
|
import type { UiRendererProps } from '../render-context';
|
|
8
9
|
import { UiFallbackPart } from '../UiFallbackPart';
|
|
9
10
|
import { useLater } from '../use-later';
|
|
@@ -17,7 +18,7 @@ export function ChoicesRenderer({ children, context }: UiRendererProps<UiProps<'
|
|
|
17
18
|
const { t } = useTranslation('agent-chat');
|
|
18
19
|
const [streamed] = useState(context.phase === 'streaming');
|
|
19
20
|
return (
|
|
20
|
-
<div role="group" aria-label={t('blocks.choices')} className="chat-ui-nodes flex flex-
|
|
21
|
+
<div role="group" aria-label={t('blocks.choices')} className="chat-ui-nodes @container flex flex-wrap items-start gap-2">
|
|
21
22
|
{children}
|
|
22
23
|
<span className="sr-only" role="status">
|
|
23
24
|
{streamed && context.phase === 'final' ? t('blocks.choice.ready') : ''}
|
|
@@ -27,13 +28,15 @@ export function ChoicesRenderer({ children, context }: UiRendererProps<UiProps<'
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/*
|
|
30
|
-
* A
|
|
31
|
+
* A button that shows what it sends: the label, and under it the context the agent receives. It stays
|
|
31
32
|
* closed while the reply streams, once the block is answered and without a host to send it, and
|
|
32
33
|
* remains focusable then so a screen reader hears why. A finished Choice the host would refuse for
|
|
33
|
-
* its label is no
|
|
34
|
+
* its label is no button at all, but its text. The primary one says the key that picks it while the
|
|
35
|
+
* focus is in the block (`UiBlockFrame`).
|
|
34
36
|
*/
|
|
35
37
|
export function ChoiceRenderer({ node, children, context }: UiRendererProps<UiProps<'Choice'>>) {
|
|
36
38
|
const { t } = useTranslation('agent-chat');
|
|
39
|
+
const host = useChatHost();
|
|
37
40
|
const descriptionId = useId();
|
|
38
41
|
const { context: detail, primary, disabled } = node.props;
|
|
39
42
|
const { answer, onChoose } = context;
|
|
@@ -52,16 +55,22 @@ export function ChoiceRenderer({ node, children, context }: UiRendererProps<UiPr
|
|
|
52
55
|
? ((disabled === true ? null : context.choiceReason) ?? t('blocks.choice.unavailable'))
|
|
53
56
|
: null;
|
|
54
57
|
const described = detail !== undefined && detail !== '';
|
|
58
|
+
const strong = primary === true;
|
|
55
59
|
|
|
56
|
-
const
|
|
60
|
+
const button = (
|
|
57
61
|
<button
|
|
58
62
|
type="button"
|
|
59
63
|
aria-disabled={closed || undefined}
|
|
60
64
|
aria-describedby={described ? descriptionId : undefined}
|
|
65
|
+
aria-keyshortcuts={strong && !closed ? 'Meta+Enter Control+Enter' : undefined}
|
|
66
|
+
data-ui-primary-choice={strong && !closed ? '' : undefined}
|
|
61
67
|
className={clsx(
|
|
62
|
-
'flex w-full items-
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
'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',
|
|
69
|
+
strong ? 'bg-accent text-accent-text' : 'bg-ui-tile text-text',
|
|
70
|
+
chosen && 'ring-2 ring-accent ring-offset-2 ring-offset-surface',
|
|
71
|
+
failed && 'ring-2 ring-status-error',
|
|
72
|
+
!closed && (strong ? 'hover:brightness-110' : 'hover:bg-surface-active'),
|
|
73
|
+
!closed && 'active:scale-99',
|
|
65
74
|
waiting ? 'opacity-60' : closed && !chosen && 'opacity-50'
|
|
66
75
|
)}
|
|
67
76
|
onClick={() => {
|
|
@@ -70,17 +79,21 @@ export function ChoiceRenderer({ node, children, context }: UiRendererProps<UiPr
|
|
|
70
79
|
}
|
|
71
80
|
}}
|
|
72
81
|
>
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
<span className="text-sm font-semibold wrap-anywhere text-text">{children}</span>
|
|
82
|
+
<span className="flex min-w-0 flex-col">
|
|
83
|
+
<span className={clsx('wrap-anywhere', strong ? 'font-semibold' : 'font-medium')}>{children}</span>
|
|
76
84
|
{described && (
|
|
77
|
-
<span id={descriptionId} className=
|
|
85
|
+
<span id={descriptionId} className={clsx('line-clamp-2 text-2xs wrap-anywhere', strong ? 'text-accent-text/80' : 'text-text-muted')}>
|
|
78
86
|
{detail}
|
|
79
87
|
</span>
|
|
80
88
|
)}
|
|
81
89
|
</span>
|
|
90
|
+
{strong && !closed && (
|
|
91
|
+
<kbd aria-hidden className="shrink-0 rounded-md bg-accent-text/18 px-1.5 font-sans text-2xs font-medium">
|
|
92
|
+
{formatShortcut(KEY_SHORTCUTS.modEnter, host.isApplePlatform())}
|
|
93
|
+
</kbd>
|
|
94
|
+
)}
|
|
82
95
|
{chosen && (
|
|
83
|
-
<span className=
|
|
96
|
+
<span className={clsx('flex shrink-0 items-center gap-1 text-2xs', strong ? 'text-accent-text' : 'text-accent')}>
|
|
84
97
|
{slow ? <Spinner size={12} /> : <Icon icon={Check} size={12} />}
|
|
85
98
|
{answer.state === 'queued' ? t('blocks.choice.queued') : t('blocks.choice.sent')}
|
|
86
99
|
</span>
|
|
@@ -88,17 +101,18 @@ export function ChoiceRenderer({ node, children, context }: UiRendererProps<UiPr
|
|
|
88
101
|
</button>
|
|
89
102
|
);
|
|
90
103
|
|
|
104
|
+
// 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.
|
|
91
105
|
return (
|
|
92
|
-
<div className="flex flex-col gap-1">
|
|
106
|
+
<div className="flex max-w-full flex-col gap-1 @min-[420px]:max-w-[calc(50%-4px)]">
|
|
93
107
|
{reason !== null ? (
|
|
94
|
-
<DisabledReason reason={reason}>{
|
|
108
|
+
<DisabledReason reason={reason}>{button}</DisabledReason>
|
|
95
109
|
) : described && detail.length > LONG_CONTEXT ? (
|
|
96
|
-
<Tooltip label={detail}>{
|
|
110
|
+
<Tooltip label={detail}>{button}</Tooltip>
|
|
97
111
|
) : (
|
|
98
|
-
|
|
112
|
+
button
|
|
99
113
|
)}
|
|
100
114
|
{failed && (
|
|
101
|
-
<p role="alert" className="px-
|
|
115
|
+
<p role="alert" className="px-1 text-2xs text-status-error">
|
|
102
116
|
{t('blocks.choice.failed')}
|
|
103
117
|
</p>
|
|
104
118
|
)}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
2
|
import { createContext, useContext, useId, useState, type ReactNode } from 'react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { Box } from 'lucide-react';
|
|
4
5
|
import type { UiProps } from '@adecore/intelligent-ui';
|
|
5
|
-
import { Pill, PreviewCard, Tooltip, Wipe } from '@adecore/ui';
|
|
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';
|
|
8
9
|
import { CodeBlock } from '../../CodeBlock';
|
|
@@ -17,10 +18,18 @@ const SourceNumbers = createContext<ReadonlyMap<string, number>>(new Map());
|
|
|
17
18
|
|
|
18
19
|
/* Plain text in the same measure until the node closes, so highlighting never makes it jump. */
|
|
19
20
|
export function CodeBlockRenderer({ node }: UiRendererProps<UiProps<'CodeBlock'>>) {
|
|
21
|
+
const { language } = node.props;
|
|
20
22
|
return (
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
<div className="chat-ui-code chat-ui-flat flex flex-col rounded-xl bg-ui-tile p-1.5">
|
|
24
|
+
{language !== undefined && language !== '' && (
|
|
25
|
+
<div className="chat-ui-flat-inner flex px-1.5 pt-0.5 pb-1.5">
|
|
26
|
+
<span className="rounded-md bg-ui-inset px-1.5 font-mono text-2xs text-text shadow-[inset_0_0_0_1px_var(--border)]">{language}</span>
|
|
27
|
+
</div>
|
|
28
|
+
)}
|
|
29
|
+
<CodeStreamingContext.Provider value={!node.complete}>
|
|
30
|
+
<CodeBlock code={uiNodeText(node).replace(/^\n|\n$/g, '')} lang={language ?? 'text'} />
|
|
31
|
+
</CodeStreamingContext.Provider>
|
|
32
|
+
</div>
|
|
24
33
|
);
|
|
25
34
|
}
|
|
26
35
|
|
|
@@ -49,7 +58,7 @@ function AttachedImage({
|
|
|
49
58
|
const host = useChatHost();
|
|
50
59
|
const source = host.attachments.useUrl(scopeId, chatId, attachmentId);
|
|
51
60
|
if (!cell) {
|
|
52
|
-
return <ImageThumb source={source} alt={alt} className="max-h-70 max-w-full object-contain" />;
|
|
61
|
+
return <ImageThumb source={source} alt={alt} className="max-h-70 max-w-full rounded-xl object-contain" />;
|
|
53
62
|
}
|
|
54
63
|
return (
|
|
55
64
|
<>
|
|
@@ -76,7 +85,7 @@ export function ImageRenderer({ node, children, context }: UiRendererProps<UiPro
|
|
|
76
85
|
) : (
|
|
77
86
|
<AttachedImage scopeId={context.scopeId} chatId={context.chatId} attachmentId={attachment} alt={name} cell={cell} unnamed={caption === ''} />
|
|
78
87
|
)}
|
|
79
|
-
{caption !== '' && <figcaption className=
|
|
88
|
+
{caption !== '' && <figcaption className="px-0.5 text-2xs wrap-anywhere text-text-muted">{children}</figcaption>}
|
|
80
89
|
{cell && caption === '' && attachment === undefined && (
|
|
81
90
|
<figcaption className="text-xs wrap-anywhere text-text-faint">{alt ?? t('blocks.image.unavailable')}</figcaption>
|
|
82
91
|
)}
|
|
@@ -89,7 +98,7 @@ export function GalleryRenderer({ node, children }: UiRendererProps<UiProps<'Gal
|
|
|
89
98
|
const { t } = useTranslation('agent-chat');
|
|
90
99
|
return (
|
|
91
100
|
<GalleryContext value>
|
|
92
|
-
<div role="group" aria-label={t('blocks.gallery.label', { count: uiChildrenOf(node, 'Image').length })} className="@container
|
|
101
|
+
<div role="group" aria-label={t('blocks.gallery.label', { count: uiChildrenOf(node, 'Image').length })} className="@container">
|
|
93
102
|
<div className="chat-ui-nodes grid grid-cols-2 gap-2 @min-[420px]:grid-cols-3">{children}</div>
|
|
94
103
|
</div>
|
|
95
104
|
</GalleryContext>
|
|
@@ -128,7 +137,7 @@ export function CompareRenderer({ node, children, context }: UiRendererProps<UiP
|
|
|
128
137
|
body = (
|
|
129
138
|
// The frame keeps the picture's shape inside its tallest height: past it the width follows, so a portrait picture is not letterboxed.
|
|
130
139
|
<div
|
|
131
|
-
className="relative max-h-96 overflow-hidden rounded-
|
|
140
|
+
className="relative max-h-96 overflow-hidden rounded-xl bg-ui-tile"
|
|
132
141
|
style={{ aspectRatio: aspect, width: `min(100%, ${COMPARE_MAX_HEIGHT * aspect}px)` }}
|
|
133
142
|
>
|
|
134
143
|
<Wipe
|
|
@@ -162,14 +171,14 @@ export function CompareRenderer({ node, children, context }: UiRendererProps<UiP
|
|
|
162
171
|
body = (
|
|
163
172
|
<>
|
|
164
173
|
<ImageThumb source={shown.source} alt={shown.name} className="max-h-70 max-w-full object-contain" />
|
|
165
|
-
<span className="
|
|
174
|
+
<span className="text-2xs text-text-faint">{t('blocks.compare.missing')}</span>
|
|
166
175
|
</>
|
|
167
176
|
);
|
|
168
177
|
}
|
|
169
178
|
return (
|
|
170
|
-
<figure className="flex flex-col items-start gap-1.5
|
|
179
|
+
<figure className="flex w-full flex-col items-start gap-1.5">
|
|
171
180
|
{body}
|
|
172
|
-
{caption !== '' && <figcaption className="text-
|
|
181
|
+
{caption !== '' && <figcaption className="px-0.5 text-2xs text-text-muted">{children}</figcaption>}
|
|
173
182
|
</figure>
|
|
174
183
|
);
|
|
175
184
|
}
|
|
@@ -187,14 +196,63 @@ function CompareChip({ side, label }: { side: 'left' | 'right'; label: string })
|
|
|
187
196
|
);
|
|
188
197
|
}
|
|
189
198
|
|
|
199
|
+
/*
|
|
200
|
+
* The host draws the model, since the chat knows no 3D format. The frame has a fixed height in the reply column,
|
|
201
|
+
* so a view that loads late never moves the thread. Without a view of the host, or without the bytes, the model
|
|
202
|
+
* stays a quiet row that names it.
|
|
203
|
+
*/
|
|
204
|
+
export function ModelRenderer({ node, children, context }: UiRendererProps<UiProps<'Model'>>) {
|
|
205
|
+
const { t } = useTranslation('agent-chat');
|
|
206
|
+
const host = useChatHost();
|
|
207
|
+
const { attachment, alt } = node.props;
|
|
208
|
+
const source = host.attachments.useUrl(context.scopeId, context.chatId, attachment);
|
|
209
|
+
const caption = uiNodeLabel(node);
|
|
210
|
+
const label = alt ?? (caption === '' ? t('blocks.model.label') : caption);
|
|
211
|
+
const View = host.ModelView;
|
|
212
|
+
let body: ReactNode;
|
|
213
|
+
if (View !== undefined && source.url !== null) {
|
|
214
|
+
body = (
|
|
215
|
+
<div role="group" aria-label={label} className="relative h-80 w-full overflow-hidden rounded-xl bg-ui-tile">
|
|
216
|
+
<ErrorBoundary label={t('blocks.model.crashed')} resetKeys={[attachment, source.url]} compact>
|
|
217
|
+
<View
|
|
218
|
+
scopeId={context.scopeId}
|
|
219
|
+
chatId={context.chatId}
|
|
220
|
+
attachmentId={attachment}
|
|
221
|
+
url={source.url}
|
|
222
|
+
label={label}
|
|
223
|
+
alt={alt}
|
|
224
|
+
caption={caption}
|
|
225
|
+
/>
|
|
226
|
+
</ErrorBoundary>
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
} else if (View !== undefined && source.failure === null) {
|
|
230
|
+
body = <div aria-busy="true" aria-label={label} className="h-80 w-full rounded-xl bg-ui-tile" />;
|
|
231
|
+
} else {
|
|
232
|
+
body = (
|
|
233
|
+
<div className="flex w-full min-w-0 items-center gap-2 rounded-xl bg-ui-tile px-3 py-2 text-xs">
|
|
234
|
+
<Icon icon={Box} size={14} className="shrink-0 text-text-muted" />
|
|
235
|
+
<span className="min-w-0 truncate text-text">{alt ?? attachment}</span>
|
|
236
|
+
<span className="ml-auto shrink-0 text-text-faint">{source.failure === null ? t('blocks.model.label') : t('blocks.model.unavailable')}</span>
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
return (
|
|
241
|
+
<figure className="flex w-full flex-col items-start gap-1.5">
|
|
242
|
+
{body}
|
|
243
|
+
{caption !== '' && <figcaption className="px-0.5 text-2xs text-text-muted">{children}</figcaption>}
|
|
244
|
+
</figure>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
190
248
|
export function SourcesRenderer({ node, children }: UiRendererProps<UiProps<'Sources'>>) {
|
|
191
249
|
const { t } = useTranslation('agent-chat');
|
|
192
250
|
const headingId = useId();
|
|
193
251
|
const numbers = new Map(uiChildrenOf(node, 'Source').map((source, index) => [source.id, index + 1]));
|
|
194
252
|
return (
|
|
195
253
|
<SourceNumbers value={numbers}>
|
|
196
|
-
<section aria-labelledby={headingId} className="flex flex-col">
|
|
197
|
-
<span id={headingId} className="px-
|
|
254
|
+
<section aria-labelledby={headingId} className="chat-ui-flat flex flex-col rounded-xl bg-ui-tile p-1.5">
|
|
255
|
+
<span id={headingId} className="chat-ui-flat-inner px-1.5 pt-0.5 pb-1 text-2xs font-medium text-text-muted">
|
|
198
256
|
{t('blocks.sources')}
|
|
199
257
|
</span>
|
|
200
258
|
<ol className="chat-ui-nodes flex flex-col">{children}</ol>
|
|
@@ -255,10 +313,12 @@ export function SourceRenderer({ node, context }: UiRendererProps<UiProps<'Sourc
|
|
|
255
313
|
const [measureDomain, domainCut] = useTruncated<HTMLSpanElement>();
|
|
256
314
|
const face = (
|
|
257
315
|
<>
|
|
258
|
-
<span className="
|
|
259
|
-
<span className="min-w-0 grow
|
|
260
|
-
|
|
261
|
-
{
|
|
316
|
+
<span className="grid size-5 shrink-0 place-items-center rounded-md bg-ui-inset text-2xs font-semibold text-text tabular-nums">{number}</span>
|
|
317
|
+
<span className="flex min-w-0 grow flex-col">
|
|
318
|
+
<span className="font-medium wrap-anywhere text-text">{title}</span>
|
|
319
|
+
<span ref={measureDomain} className="truncate text-text-muted">
|
|
320
|
+
{uiUrlDomain(url)}
|
|
321
|
+
</span>
|
|
262
322
|
</span>
|
|
263
323
|
</>
|
|
264
324
|
);
|
|
@@ -274,13 +334,16 @@ export function SourceRenderer({ node, context }: UiRendererProps<UiProps<'Sourc
|
|
|
274
334
|
}
|
|
275
335
|
>
|
|
276
336
|
{openUrl === undefined ? (
|
|
277
|
-
<span
|
|
337
|
+
<span
|
|
338
|
+
tabIndex={domainCut ? 0 : undefined}
|
|
339
|
+
className="chat-ui-flat-inner flex min-h-8 items-start gap-2.5 rounded-lg px-1.5 py-1.5 text-2xs"
|
|
340
|
+
>
|
|
278
341
|
{face}
|
|
279
342
|
</span>
|
|
280
343
|
) : (
|
|
281
344
|
<button
|
|
282
345
|
type="button"
|
|
283
|
-
className="flex min-h-8 w-full items-start gap-2 rounded-
|
|
346
|
+
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"
|
|
284
347
|
onClick={() => openUrl(url)}
|
|
285
348
|
>
|
|
286
349
|
{face}
|
|
@@ -1,19 +1,25 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
1
2
|
import clsx from 'clsx';
|
|
2
3
|
import { useTranslation } from 'react-i18next';
|
|
3
4
|
import { ArrowDown, ArrowRight, ArrowUp } from 'lucide-react';
|
|
4
5
|
import type { UiProps } from '@adecore/intelligent-ui';
|
|
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';
|
|
8
9
|
import { uiChildrenOf, uiWithUnit } from '../node-text';
|
|
9
10
|
import type { UiLiveValue, UiRendererProps } from '../render-context';
|
|
10
11
|
import { isNumericColumn, UI_TABLE_ROWS, uiTableCell, uiTableColumns, type UiTableCell, type UiTableColumn } from '../table-data';
|
|
11
|
-
import {
|
|
12
|
+
import { UI_TONE_PILL } from '../tones';
|
|
12
13
|
import { UiFallbackPart } from '../UiFallbackPart';
|
|
13
14
|
|
|
14
15
|
/* EntityList draws a hairline between its rows from this many on. */
|
|
15
16
|
const DIVIDED_FROM = 7;
|
|
16
17
|
|
|
18
|
+
/* Up to this many entries an EntityList is a row of tiles; a longer one is a list in one tile. */
|
|
19
|
+
const TILED_UP_TO = 4;
|
|
20
|
+
|
|
21
|
+
const EntityTiles = createContext(false);
|
|
22
|
+
|
|
17
23
|
/*
|
|
18
24
|
* A number that live readings keep up to date. Keyed by its text, so a value that changed mounts
|
|
19
25
|
* again and lights up once, even when it changes again before the last light settled.
|
|
@@ -31,7 +37,7 @@ export function StatsRenderer({ children }: UiRendererProps<UiProps<'Stats'>>) {
|
|
|
31
37
|
return <div className="chat-ui-nodes grid grid-cols-[repeat(auto-fit,minmax(128px,1fr))] gap-2">{children}</div>;
|
|
32
38
|
}
|
|
33
39
|
|
|
34
|
-
/* The arrow and the number say which way a value went; only a tone says whether that is good. */
|
|
40
|
+
/* 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. */
|
|
35
41
|
export function StatRenderer({ node, context }: UiRendererProps<UiProps<'Stat'>>) {
|
|
36
42
|
const { t } = useTranslation('agent-chat');
|
|
37
43
|
const { label, value, previous, unit, tone } = node.props;
|
|
@@ -41,36 +47,63 @@ export function StatRenderer({ node, context }: UiRendererProps<UiProps<'Stat'>>
|
|
|
41
47
|
const from = previous === undefined ? '' : uiWithUnit(formatRounded(previous, 2), unit);
|
|
42
48
|
// A change from zero has no percentage.
|
|
43
49
|
const percent = change === null || previous === undefined || previous === 0 ? null : formatPercent(Math.abs((change / previous) * 100));
|
|
50
|
+
const said = percent === null ? t('blocks.stat.from', { previous: from }) : t('blocks.stat.change', { change: percent, previous: from });
|
|
44
51
|
return (
|
|
45
|
-
<div className="flex min-w-0 flex-col gap-
|
|
46
|
-
<span className="truncate text-
|
|
52
|
+
<div className="flex min-w-0 flex-col items-start gap-1.5 rounded-xl bg-ui-tile p-3">
|
|
53
|
+
<span className="max-w-full truncate text-2xs text-text-muted">{label}</span>
|
|
47
54
|
<LiveFigure
|
|
48
55
|
text={uiWithUnit(formatRounded(value, 2), unit)}
|
|
49
56
|
previous={typeof live?.previous === 'number' ? uiWithUnit(formatRounded(live.previous, 2), unit) : undefined}
|
|
50
57
|
live={live}
|
|
51
|
-
className="text-lg font-semibold tabular-nums"
|
|
58
|
+
className="max-w-full truncate text-lg font-semibold tabular-nums"
|
|
52
59
|
/>
|
|
53
60
|
{change !== null && (
|
|
54
|
-
<
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
<Tooltip label={said}>
|
|
62
|
+
<span tabIndex={0} className="max-w-full rounded-md">
|
|
63
|
+
<Pill
|
|
64
|
+
shape="tag"
|
|
65
|
+
size="sm"
|
|
66
|
+
tone={tone === undefined ? 'muted' : UI_TONE_PILL[tone]}
|
|
67
|
+
icon={<Icon icon={arrow} size={12} className="shrink-0" />}
|
|
68
|
+
>
|
|
69
|
+
<span aria-hidden className="tabular-nums">
|
|
70
|
+
{percent ?? from}
|
|
71
|
+
</span>
|
|
72
|
+
<span className="sr-only">{said}</span>
|
|
73
|
+
</Pill>
|
|
58
74
|
</span>
|
|
59
|
-
</
|
|
75
|
+
</Tooltip>
|
|
60
76
|
)}
|
|
61
77
|
</div>
|
|
62
78
|
);
|
|
63
79
|
}
|
|
64
80
|
|
|
65
81
|
export function EntityListRenderer({ node, children }: UiRendererProps<UiProps<'EntityList'>>) {
|
|
82
|
+
const count = uiChildrenOf(node, 'Entry').length;
|
|
83
|
+
if (count <= TILED_UP_TO) {
|
|
84
|
+
return (
|
|
85
|
+
<EntityTiles value>
|
|
86
|
+
<div className="chat-ui-nodes grid grid-cols-[repeat(auto-fit,minmax(140px,1fr))] gap-2">{children}</div>
|
|
87
|
+
</EntityTiles>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
66
90
|
return (
|
|
67
|
-
<KeyValueList.Root divided={
|
|
91
|
+
<KeyValueList.Root divided={count >= DIVIDED_FROM} className="chat-ui-nodes chat-ui-flat rounded-xl bg-ui-tile px-3 py-2">
|
|
68
92
|
{children}
|
|
69
93
|
</KeyValueList.Root>
|
|
70
94
|
);
|
|
71
95
|
}
|
|
72
96
|
|
|
73
97
|
export function EntryRenderer({ node, children }: UiRendererProps<UiProps<'Entry'>>) {
|
|
98
|
+
const tiled = useContext(EntityTiles);
|
|
99
|
+
if (tiled) {
|
|
100
|
+
return (
|
|
101
|
+
<div className="flex min-w-0 flex-col gap-0.5 rounded-xl bg-ui-tile px-3 py-2">
|
|
102
|
+
<span className="truncate text-2xs text-text-muted">{node.props.label}</span>
|
|
103
|
+
<span className="text-2xs font-semibold wrap-break-word text-text">{children}</span>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
74
107
|
return (
|
|
75
108
|
<KeyValueList.Item>
|
|
76
109
|
<KeyValueList.Name>{node.props.label}</KeyValueList.Name>
|
|
@@ -129,19 +162,23 @@ export function TableRenderer({ node, context }: UiRendererProps<UiProps<'Table'
|
|
|
129
162
|
const shown = all ? rows : rows.slice(0, UI_TABLE_ROWS);
|
|
130
163
|
return (
|
|
131
164
|
<div className="flex flex-col items-start gap-1.5">
|
|
132
|
-
<div className="max-h-80 w-full overflow-auto rounded-
|
|
165
|
+
<div className="chat-ui-flat max-h-80 w-full overflow-auto rounded-xl bg-ui-tile px-1.5 pb-1.5">
|
|
133
166
|
<table
|
|
134
167
|
aria-label={t('blocks.table.label', { columns: columns.map((column) => column.title).join(', ') })}
|
|
135
|
-
className=
|
|
168
|
+
className={clsx(
|
|
169
|
+
'w-full border-separate border-spacing-0 text-xs tabular-nums',
|
|
170
|
+
'[&_tbody_tr:first-child_td:first-child]:rounded-tl-lg [&_tbody_tr:first-child_td:last-child]:rounded-tr-lg',
|
|
171
|
+
'[&_tbody_tr:last-child_td:first-child]:rounded-bl-lg [&_tbody_tr:last-child_td:last-child]:rounded-br-lg'
|
|
172
|
+
)}
|
|
136
173
|
>
|
|
137
|
-
<thead className="sticky top-0 z-1 bg-
|
|
174
|
+
<thead className="sticky top-0 z-1 bg-ui-tile">
|
|
138
175
|
<tr>
|
|
139
176
|
{columns.map((column) => (
|
|
140
177
|
<th
|
|
141
178
|
key={column.key}
|
|
142
179
|
scope="col"
|
|
143
180
|
className={clsx(
|
|
144
|
-
'h-8 px-2 font-medium whitespace-nowrap text-text-muted',
|
|
181
|
+
'h-8 px-2 text-2xs font-medium whitespace-nowrap text-text-muted',
|
|
145
182
|
isNumericColumn(column) ? 'text-right' : 'text-left'
|
|
146
183
|
)}
|
|
147
184
|
>
|
|
@@ -153,11 +190,16 @@ export function TableRenderer({ node, context }: UiRendererProps<UiProps<'Table'
|
|
|
153
190
|
<tbody>
|
|
154
191
|
{shown.map((row, index) => (
|
|
155
192
|
// Rows only ever grow at the end while a block streams, so the place of a row is a stable key.
|
|
156
|
-
<tr key={index}
|
|
157
|
-
{columns.map((column) => (
|
|
193
|
+
<tr key={index}>
|
|
194
|
+
{columns.map((column, at) => (
|
|
158
195
|
<td
|
|
159
196
|
key={column.key}
|
|
160
|
-
className={clsx(
|
|
197
|
+
className={clsx(
|
|
198
|
+
'h-9 bg-ui-inset px-2 text-text',
|
|
199
|
+
index > 0 && 'border-t border-border-soft',
|
|
200
|
+
at === 0 && 'font-medium',
|
|
201
|
+
isNumericColumn(column) ? 'text-right whitespace-nowrap' : 'text-left'
|
|
202
|
+
)}
|
|
161
203
|
>
|
|
162
204
|
<Cell
|
|
163
205
|
value={row[column.key]}
|
|
@@ -23,7 +23,7 @@ const ChecklistContext = createContext<ChecklistState>({ values: [], enabled: fa
|
|
|
23
23
|
function ChecklistGroup({ pending, children }: { pending: boolean; children: ReactNode }) {
|
|
24
24
|
const { t } = useTranslation('agent-chat');
|
|
25
25
|
return (
|
|
26
|
-
<div role="group" aria-label={t('blocks.checklist')} className={clsx('chat-ui-nodes flex flex-col', pending && PENDING)}>
|
|
26
|
+
<div role="group" aria-label={t('blocks.checklist')} className={clsx('chat-ui-nodes flex flex-col gap-1.5', pending && PENDING)}>
|
|
27
27
|
{children}
|
|
28
28
|
</div>
|
|
29
29
|
);
|
|
@@ -42,10 +42,11 @@ export function ChecklistRenderer({ node, children, context }: UiRendererProps<U
|
|
|
42
42
|
);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/* The whole
|
|
45
|
+
/* The whole tile is the label, ringed in the accent while checked; a link inside it keeps its own click and checks nothing. */
|
|
46
46
|
export function ItemRenderer({ node, children }: UiRendererProps<UiProps<'Item'>>) {
|
|
47
47
|
const list = useContext(ChecklistContext);
|
|
48
48
|
const { value } = node.props;
|
|
49
|
+
const checked = list.values.includes(value);
|
|
49
50
|
const toggleRow = (event: MouseEvent<HTMLDivElement>): void => {
|
|
50
51
|
if (list.enabled && !(event.target as Element).closest('button, a')) {
|
|
51
52
|
list.toggle(value);
|
|
@@ -53,10 +54,14 @@ export function ItemRenderer({ node, children }: UiRendererProps<UiProps<'Item'>
|
|
|
53
54
|
};
|
|
54
55
|
return (
|
|
55
56
|
<div
|
|
56
|
-
className={clsx(
|
|
57
|
+
className={clsx(
|
|
58
|
+
'flex min-h-10 items-start gap-3 rounded-xl bg-ui-tile px-3 py-2.5 text-xs text-text',
|
|
59
|
+
checked && 'ring-2 ring-accent ring-inset',
|
|
60
|
+
list.enabled && 'hover:bg-surface-active'
|
|
61
|
+
)}
|
|
57
62
|
onClick={toggleRow}
|
|
58
63
|
>
|
|
59
|
-
<Checkbox checked={
|
|
64
|
+
<Checkbox className="mt-0.5" checked={checked} onCheckedChange={() => list.toggle(value)} label={uiNodeLabel(node)} disabled={!list.enabled} />
|
|
60
65
|
<div className="min-w-0 grow">{children}</div>
|
|
61
66
|
</div>
|
|
62
67
|
);
|
|
@@ -65,8 +70,10 @@ export function ItemRenderer({ node, children }: UiRendererProps<UiProps<'Item'>
|
|
|
65
70
|
export function SwitchRenderer({ node, children, context }: UiRendererProps<UiProps<'Switch'>>) {
|
|
66
71
|
const binding = bindingOf(node, context);
|
|
67
72
|
return (
|
|
68
|
-
<div
|
|
69
|
-
|
|
73
|
+
<div
|
|
74
|
+
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)}
|
|
75
|
+
>
|
|
76
|
+
<span className="min-w-0 font-medium text-text">{children}</span>
|
|
70
77
|
<Switch
|
|
71
78
|
checked={binding?.value ?? node.props.value}
|
|
72
79
|
onCheckedChange={(next) => binding?.onValueChange(next)}
|
|
@@ -92,7 +99,7 @@ export function SliderRenderer({ node, context }: UiRendererProps<UiProps<'Slide
|
|
|
92
99
|
unit={uiWithUnit('', unit)}
|
|
93
100
|
label={uiNodeLabel(node)}
|
|
94
101
|
disabled={binding === null}
|
|
95
|
-
className={clsx('
|
|
102
|
+
className={clsx('chat-ui-flat rounded-xl bg-ui-tile p-3', !node.complete && PENDING)}
|
|
96
103
|
/>
|
|
97
104
|
);
|
|
98
105
|
}
|
|
@@ -108,7 +115,7 @@ export function SegmentedRenderer({ node, context }: UiRendererProps<UiProps<'Se
|
|
|
108
115
|
const options = uiChildrenOf(node, 'Option').map((option, index) => ({ id: String(index), label: uiNodeLabel(option), value: option.props.value }));
|
|
109
116
|
const picked = options.find((option) => option.value === current)?.id ?? '';
|
|
110
117
|
return (
|
|
111
|
-
<div className={clsx(
|
|
118
|
+
<div className={clsx(!node.complete && PENDING)}>
|
|
112
119
|
<Segmented
|
|
113
120
|
value={picked}
|
|
114
121
|
options={options}
|
|
@@ -145,8 +152,8 @@ export function TextFieldRenderer({ node, children, context }: UiRendererProps<U
|
|
|
145
152
|
const countId = `${id}-count`;
|
|
146
153
|
const counting = value.length >= maxLength * COUNT_FROM;
|
|
147
154
|
return (
|
|
148
|
-
<div className={clsx('flex flex-col gap-1
|
|
149
|
-
<label htmlFor={id} className="text-
|
|
155
|
+
<div className={clsx('flex flex-col gap-1.5', !node.complete && PENDING)}>
|
|
156
|
+
<label htmlFor={id} className="px-0.5 text-2xs text-text-muted">
|
|
150
157
|
{children}
|
|
151
158
|
</label>
|
|
152
159
|
<Input
|
|
@@ -157,7 +164,7 @@ export function TextFieldRenderer({ node, children, context }: UiRendererProps<U
|
|
|
157
164
|
placeholder={placeholder}
|
|
158
165
|
disabled={binding === null}
|
|
159
166
|
aria-describedby={counting ? countId : undefined}
|
|
160
|
-
className="
|
|
167
|
+
className="border-transparent bg-ui-tile"
|
|
161
168
|
onChange={(event) => binding?.onValueChange(event.target.value)}
|
|
162
169
|
/>
|
|
163
170
|
{counting && (
|
|
@@ -180,8 +187,8 @@ export function SelectRenderer({ node, context }: UiRendererProps<UiProps<'Selec
|
|
|
180
187
|
const options = uiChildrenOf(node, 'Option').map((option, index) => ({ id: String(index), label: uiNodeLabel(option), value: option.props.value }));
|
|
181
188
|
const label = uiOwnLabel(node);
|
|
182
189
|
return (
|
|
183
|
-
<div className={clsx('flex flex-col items-start gap-1
|
|
184
|
-
{label !== '' && <span className="text-
|
|
190
|
+
<div className={clsx('flex flex-col items-start gap-1.5', !node.complete && PENDING)}>
|
|
191
|
+
{label !== '' && <span className="px-0.5 text-2xs text-text-muted">{label}</span>}
|
|
185
192
|
<Select
|
|
186
193
|
value={options.find((option) => option.value === current)?.id ?? null}
|
|
187
194
|
items={options.map((option) => ({ value: option.id, label: option.label }))}
|
|
@@ -204,7 +211,7 @@ export function ButtonRenderer({ node, children, context }: UiRendererProps<UiPr
|
|
|
204
211
|
const label = uiNodeLabel(node);
|
|
205
212
|
const [measure, truncated] = useTruncated<HTMLSpanElement>();
|
|
206
213
|
return (
|
|
207
|
-
<div className={clsx(
|
|
214
|
+
<div className={clsx(!node.complete && PENDING)}>
|
|
208
215
|
<Tooltip label={label} disabled={!truncated}>
|
|
209
216
|
<Button variant="secondary" size="sm" className="max-w-full" disabled={!enabled} onClick={() => node.onAction?.()}>
|
|
210
217
|
<span ref={measure} className="min-w-0 truncate">
|
|
@@ -4,12 +4,15 @@ import { useTranslation } from 'react-i18next';
|
|
|
4
4
|
import { FileDiff, GitCommitHorizontal, SquareDashed } from 'lucide-react';
|
|
5
5
|
import type { UiProps } from '@adecore/intelligent-ui';
|
|
6
6
|
import { FileIcon, Icon, Tooltip } from '@adecore/ui';
|
|
7
|
-
import { CHIP_IN_MESSAGE
|
|
7
|
+
import { CHIP_IN_MESSAGE } from '../../chips';
|
|
8
8
|
import { uiReasonText } from '../node-text';
|
|
9
9
|
import type { UiLink, UiLinkTarget, UiRenderContext, UiRendererProps } from '../render-context';
|
|
10
10
|
|
|
11
11
|
const PLAIN: UiLink = { state: 'plain' };
|
|
12
12
|
|
|
13
|
+
/* A link in a block is a quiet card on its tile, unlike a mention in a message, which wears the accent. */
|
|
14
|
+
const UI_CHIP = 'bg-ui-inset font-mono text-text shadow-[inset_0_0_0_1px_var(--border)]';
|
|
15
|
+
|
|
13
16
|
function baseName(path: string): string {
|
|
14
17
|
const trimmed = path.replace(/\/+$/, '');
|
|
15
18
|
return trimmed.slice(trimmed.lastIndexOf('/') + 1) || path;
|
|
@@ -96,11 +99,11 @@ export function UiLinkChip({ target, context }: { target: UiLinkTarget; context:
|
|
|
96
99
|
const { openLink } = context;
|
|
97
100
|
const chip =
|
|
98
101
|
openLink === undefined ? (
|
|
99
|
-
<span className={clsx(CHIP_IN_MESSAGE,
|
|
102
|
+
<span className={clsx(CHIP_IN_MESSAGE, UI_CHIP, 'transition-opacity duration-120 starting:opacity-0')}>{face}</span>
|
|
100
103
|
) : (
|
|
101
104
|
<button
|
|
102
105
|
type="button"
|
|
103
|
-
className={clsx(CHIP_IN_MESSAGE,
|
|
106
|
+
className={clsx(CHIP_IN_MESSAGE, UI_CHIP, 'transition-opacity duration-120 hover:bg-surface-hover starting:opacity-0')}
|
|
104
107
|
onClick={() => openLink(target)}
|
|
105
108
|
>
|
|
106
109
|
{face}
|
|
@@ -115,7 +118,7 @@ function LinkRow({ target, context, described, children }: { target: UiLinkTarge
|
|
|
115
118
|
return <UiLinkChip target={target} context={context} />;
|
|
116
119
|
}
|
|
117
120
|
return (
|
|
118
|
-
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2
|
|
121
|
+
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2 text-xs">
|
|
119
122
|
<span className="max-w-full min-w-0">
|
|
120
123
|
<UiLinkChip target={target} context={context} />
|
|
121
124
|
</span>
|