@adecore/agents-react 0.20.0 → 0.21.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/chat-client.d.ts +1 -0
- package/dist/chat/logic/generated-image.d.ts +27 -0
- package/dist/chat/logic/generated-image.js +32 -0
- package/dist/chat/logic/timeline-target.d.ts +19 -1
- package/dist/chat/logic/timeline-target.js +49 -3
- package/dist/chat/logic/timeline.js +25 -17
- package/dist/chat/subagent-conversation.d.ts +2 -1
- package/dist/chat/subagent-conversation.js +7 -1
- package/dist/chat/timeline-flash.d.ts +3 -1
- package/dist/chat/timeline-flash.js +2 -2
- package/dist/chat/ui/CodeBlock.d.ts +3 -1
- package/dist/chat/ui/CodeBlock.js +38 -7
- package/dist/chat/ui/Composer.js +13 -3
- package/dist/chat/ui/ImageView.d.ts +8 -2
- package/dist/chat/ui/ImageView.js +12 -6
- package/dist/chat/ui/Markdown.d.ts +7 -1
- package/dist/chat/ui/Markdown.js +80 -23
- package/dist/chat/ui/MessageActions.js +6 -7
- package/dist/chat/ui/SubagentTimeline.js +7 -3
- package/dist/chat/ui/Timeline.js +68 -8
- package/dist/chat/ui/TimelineMenu.js +7 -3
- package/dist/chat/ui/file-links.d.ts +2 -1
- package/dist/chat/ui/file-links.js +8 -3
- package/dist/chat/ui/intelligent-ui/UiBlockFooter.d.ts +15 -0
- package/dist/chat/ui/intelligent-ui/UiBlockFooter.js +50 -0
- package/dist/chat/ui/intelligent-ui/UiBlockFrame.d.ts +10 -0
- package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +61 -0
- package/dist/chat/ui/intelligent-ui/UiFallbackPart.d.ts +10 -0
- package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +11 -0
- package/dist/chat/ui/intelligent-ui/UiReply.d.ts +13 -0
- package/dist/chat/ui/intelligent-ui/UiReply.js +325 -0
- package/dist/chat/ui/intelligent-ui/block-local.d.ts +2 -0
- package/dist/chat/ui/intelligent-ui/block-local.js +13 -0
- package/dist/chat/ui/intelligent-ui/chart-data.d.ts +16 -0
- package/dist/chat/ui/intelligent-ui/chart-data.js +46 -0
- package/dist/chat/ui/intelligent-ui/node-text.d.ts +9 -0
- package/dist/chat/ui/intelligent-ui/node-text.js +52 -0
- package/dist/chat/ui/intelligent-ui/registry.d.ts +2 -0
- package/dist/chat/ui/intelligent-ui/registry.js +50 -0
- package/dist/chat/ui/intelligent-ui/render-context.d.ts +50 -0
- package/dist/chat/ui/intelligent-ui/render-context.js +3 -0
- package/dist/chat/ui/intelligent-ui/renderers/chart.d.ts +3 -0
- package/dist/chat/ui/intelligent-ui/renderers/chart.js +125 -0
- package/dist/chat/ui/intelligent-ui/renderers/choices.d.ts +4 -0
- package/dist/chat/ui/intelligent-ui/renderers/choices.js +41 -0
- package/dist/chat/ui/intelligent-ui/renderers/content.d.ts +14 -0
- package/dist/chat/ui/intelligent-ui/renderers/content.js +66 -0
- package/dist/chat/ui/intelligent-ui/renderers/data.d.ts +7 -0
- package/dist/chat/ui/intelligent-ui/renderers/data.js +87 -0
- package/dist/chat/ui/intelligent-ui/renderers/inputs.d.ts +7 -0
- package/dist/chat/ui/intelligent-ui/renderers/inputs.js +60 -0
- package/dist/chat/ui/intelligent-ui/renderers/links.d.ts +10 -0
- package/dist/chat/ui/intelligent-ui/renderers/links.js +86 -0
- package/dist/chat/ui/intelligent-ui/renderers/structure.d.ts +6 -0
- package/dist/chat/ui/intelligent-ui/renderers/structure.js +32 -0
- package/dist/chat/ui/intelligent-ui/renderers/text.d.ts +8 -0
- package/dist/chat/ui/intelligent-ui/renderers/text.js +61 -0
- package/dist/chat/ui/intelligent-ui/reply-parts.d.ts +9 -0
- package/dist/chat/ui/intelligent-ui/reply-parts.js +18 -0
- package/dist/chat/ui/intelligent-ui/table-data.d.ts +31 -0
- package/dist/chat/ui/intelligent-ui/table-data.js +79 -0
- package/dist/chat/ui/intelligent-ui/tones.d.ts +7 -0
- package/dist/chat/ui/intelligent-ui/tones.js +31 -0
- package/dist/chat/ui/intelligent-ui/use-later.d.ts +1 -0
- package/dist/chat/ui/intelligent-ui/use-later.js +16 -0
- package/dist/chat/ui/intelligent-ui/use-ui-links.d.ts +9 -0
- package/dist/chat/ui/intelligent-ui/use-ui-links.js +107 -0
- package/dist/chat/ui/intelligent-ui/use-ui-queries.d.ts +20 -0
- package/dist/chat/ui/intelligent-ui/use-ui-queries.js +259 -0
- package/dist/chat/ui/reply-context.d.ts +8 -0
- package/dist/chat/ui/reply-context.js +1 -0
- package/dist/chat/ui/rows/GeneratedImageRow.d.ts +5 -0
- package/dist/chat/ui/rows/GeneratedImageRow.js +121 -0
- package/dist/chat/ui/rows/MessageRows.js +27 -7
- package/dist/chat/ui/rows/Rows.js +4 -2
- package/dist/chat/ui/rows/SubagentRow.js +5 -0
- package/dist/chat/ui/rows/WorkRows.d.ts +4 -1
- package/dist/chat/ui/rows/WorkRows.js +4 -3
- package/dist/host.d.ts +25 -3
- package/dist/host.js +1 -0
- package/dist/locales/en/agent-chat.json +89 -1
- package/dist/locales/nl/agent-chat.json +89 -1
- package/dist/state/chats.js +9 -0
- package/dist/theme.css +38 -2
- package/package.json +133 -7
- package/src/chat/chat-client.ts +1 -0
- package/src/chat/logic/generated-image.ts +48 -0
- package/src/chat/logic/timeline-target.ts +69 -4
- package/src/chat/logic/timeline.ts +28 -17
- package/src/chat/subagent-conversation.ts +13 -2
- package/src/chat/timeline-flash.ts +5 -3
- package/src/chat/ui/CodeBlock.tsx +70 -21
- package/src/chat/ui/Composer.tsx +22 -5
- package/src/chat/ui/ImageView.tsx +51 -8
- package/src/chat/ui/Markdown.tsx +123 -26
- package/src/chat/ui/MessageActions.tsx +15 -8
- package/src/chat/ui/SubagentTimeline.tsx +26 -1
- package/src/chat/ui/Timeline.tsx +98 -22
- package/src/chat/ui/TimelineMenu.tsx +6 -3
- package/src/chat/ui/file-links.ts +9 -3
- package/src/chat/ui/intelligent-ui/UiBlockFooter.tsx +139 -0
- package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +89 -0
- package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +21 -0
- package/src/chat/ui/intelligent-ui/UiReply.tsx +531 -0
- package/src/chat/ui/intelligent-ui/block-local.ts +16 -0
- package/src/chat/ui/intelligent-ui/chart-data.ts +67 -0
- package/src/chat/ui/intelligent-ui/node-text.ts +59 -0
- package/src/chat/ui/intelligent-ui/registry.ts +52 -0
- package/src/chat/ui/intelligent-ui/render-context.ts +78 -0
- package/src/chat/ui/intelligent-ui/renderers/chart.tsx +321 -0
- package/src/chat/ui/intelligent-ui/renderers/choices.tsx +98 -0
- package/src/chat/ui/intelligent-ui/renderers/content.tsx +145 -0
- package/src/chat/ui/intelligent-ui/renderers/data.tsx +187 -0
- package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +121 -0
- package/src/chat/ui/intelligent-ui/renderers/links.tsx +157 -0
- package/src/chat/ui/intelligent-ui/renderers/structure.tsx +84 -0
- package/src/chat/ui/intelligent-ui/renderers/text.tsx +124 -0
- package/src/chat/ui/intelligent-ui/reply-parts.ts +22 -0
- package/src/chat/ui/intelligent-ui/table-data.ts +104 -0
- package/src/chat/ui/intelligent-ui/tones.ts +37 -0
- package/src/chat/ui/intelligent-ui/use-later.ts +17 -0
- package/src/chat/ui/intelligent-ui/use-ui-links.ts +123 -0
- package/src/chat/ui/intelligent-ui/use-ui-queries.ts +282 -0
- package/src/chat/ui/reply-context.ts +7 -0
- package/src/chat/ui/rows/GeneratedImageRow.tsx +244 -0
- package/src/chat/ui/rows/MessageRows.tsx +69 -7
- package/src/chat/ui/rows/Rows.tsx +4 -2
- package/src/chat/ui/rows/SubagentRow.tsx +5 -0
- package/src/chat/ui/rows/WorkRows.tsx +20 -5
- package/src/host.ts +46 -3
- package/src/locales/en/agent-chat.json +89 -1
- package/src/locales/nl/agent-chat.json +89 -1
- package/src/state/chats.ts +9 -0
- package/src/theme.css +38 -2
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import type { UiProps } from '@adecore/intelligent-ui';
|
|
5
|
+
import { PreviewCard } from '@adecore/ui';
|
|
6
|
+
import { formatNumber } from '@adecore/ui/format';
|
|
7
|
+
import { chatHost } from '../../../../host';
|
|
8
|
+
import { CodeBlock } from '../../CodeBlock';
|
|
9
|
+
import { CodeStreamingContext } from '../../code-streaming';
|
|
10
|
+
import { ImageThumb } from '../../ImageView';
|
|
11
|
+
import { uiChildrenOf, uiNodeLabel, uiNodeText } from '../node-text';
|
|
12
|
+
import type { UiRendererProps } from '../render-context';
|
|
13
|
+
|
|
14
|
+
// The number of every source by its node id, which the list counts in its own order.
|
|
15
|
+
const SourceNumbers = createContext<ReadonlyMap<string, number>>(new Map());
|
|
16
|
+
|
|
17
|
+
/* Plain text in the same measure until the node closes, so highlighting never makes it jump. */
|
|
18
|
+
export function CodeBlockRenderer({ node }: UiRendererProps<UiProps<'CodeBlock'>>) {
|
|
19
|
+
return (
|
|
20
|
+
<CodeStreamingContext.Provider value={!node.complete}>
|
|
21
|
+
<CodeBlock code={uiNodeText(node).replace(/^\n|\n$/g, '')} lang={node.props.language ?? 'text'} />
|
|
22
|
+
</CodeStreamingContext.Provider>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* Its own component, so the host's hook is only called for an image that names an attachment. */
|
|
27
|
+
function AttachedImage({ scopeId, chatId, attachmentId, alt }: { scopeId: string; chatId: string; attachmentId: string; alt: string }) {
|
|
28
|
+
const source = chatHost().attachments.useUrl(scopeId, chatId, attachmentId);
|
|
29
|
+
return <ImageThumb source={source} alt={alt} className="max-h-70 max-w-full object-contain" />;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* An attachment of the chat, never a path or an address the model wrote. An image of the latest
|
|
34
|
+
* generation stays a quiet square until the daemon resolved it to an attachment.
|
|
35
|
+
*/
|
|
36
|
+
export function ImageRenderer({ node, children, context }: UiRendererProps<UiProps<'Image'>>) {
|
|
37
|
+
const { t } = useTranslation('agent-chat');
|
|
38
|
+
const { attachment, alt } = node.props;
|
|
39
|
+
const caption = uiNodeLabel(node);
|
|
40
|
+
const name = alt ?? (caption === '' ? t('blocks.image.label') : caption);
|
|
41
|
+
return (
|
|
42
|
+
<figure className="flex flex-col items-start gap-1.5">
|
|
43
|
+
{attachment === undefined ? (
|
|
44
|
+
<ImageThumb source={{ url: null, failure: t('blocks.image.unavailable') }} alt={name} />
|
|
45
|
+
) : (
|
|
46
|
+
<AttachedImage scopeId={context.scopeId} chatId={context.chatId} attachmentId={attachment} alt={name} />
|
|
47
|
+
)}
|
|
48
|
+
{caption !== '' && <figcaption className="px-2 text-xs text-text-muted">{children}</figcaption>}
|
|
49
|
+
</figure>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function SourcesRenderer({ node, children }: UiRendererProps<UiProps<'Sources'>>) {
|
|
54
|
+
const { t } = useTranslation('agent-chat');
|
|
55
|
+
const numbers = new Map(uiChildrenOf(node, 'Source').map((source, index) => [source.id, index + 1]));
|
|
56
|
+
return (
|
|
57
|
+
<SourceNumbers value={numbers}>
|
|
58
|
+
<section aria-label={t('blocks.sources')} className="flex flex-col">
|
|
59
|
+
<span className="px-2 pb-1 text-xs font-medium text-text-muted">{t('blocks.sources')}</span>
|
|
60
|
+
<ol className="chat-ui-nodes flex flex-col">{children}</ol>
|
|
61
|
+
</section>
|
|
62
|
+
</SourceNumbers>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function domainOf(url: string): string {
|
|
67
|
+
try {
|
|
68
|
+
return new URL(url).hostname.replace(/^www\./, '');
|
|
69
|
+
} catch {
|
|
70
|
+
return url;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface UiSourceCitationProps {
|
|
75
|
+
/* The number of the Source, as its row in the list counts it. */
|
|
76
|
+
number: number;
|
|
77
|
+
title: string;
|
|
78
|
+
url: string;
|
|
79
|
+
/* Shows the Source row the number stands for. Without it the number is no button. */
|
|
80
|
+
onReveal?: () => void;
|
|
81
|
+
className?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/*
|
|
85
|
+
* A source number in running text. Resting on it or focusing it shows the title and the domain, and
|
|
86
|
+
* a tap reveals the row in the list of sources; only that row opens the address, so nothing loads
|
|
87
|
+
* before a person asks for it.
|
|
88
|
+
*/
|
|
89
|
+
export function UiSourceCitation({ number, title, url, onReveal, className }: UiSourceCitationProps) {
|
|
90
|
+
const { t } = useTranslation('agent-chat');
|
|
91
|
+
return (
|
|
92
|
+
<PreviewCard.Root>
|
|
93
|
+
<PreviewCard.Trigger
|
|
94
|
+
delay={300}
|
|
95
|
+
closeDelay={150}
|
|
96
|
+
render={onReveal === undefined ? <span /> : <button type="button" />}
|
|
97
|
+
aria-label={onReveal === undefined ? undefined : t('blocks.citation', { number: formatNumber(number), title })}
|
|
98
|
+
className={clsx(
|
|
99
|
+
'inline-flex h-4 min-w-4.5 items-center justify-center rounded-sm bg-text/7 px-1 align-[1px] text-2xs leading-4 text-text-muted tabular-nums',
|
|
100
|
+
onReveal !== undefined && 'cursor-pointer hover:bg-text/12 hover:text-text',
|
|
101
|
+
className
|
|
102
|
+
)}
|
|
103
|
+
onClick={onReveal}
|
|
104
|
+
>
|
|
105
|
+
{formatNumber(number)}
|
|
106
|
+
</PreviewCard.Trigger>
|
|
107
|
+
<PreviewCard.Popup side="top" align="center" sideOffset={4} className="flex max-w-72 flex-col px-2.5 py-1.5 text-xs">
|
|
108
|
+
<span className="line-clamp-2 text-text">{title}</span>
|
|
109
|
+
<span className="truncate text-text-faint">{domainOf(url)}</span>
|
|
110
|
+
</PreviewCard.Popup>
|
|
111
|
+
</PreviewCard.Root>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/*
|
|
116
|
+
* Opens through the host and loads nothing before, not even an icon. The row itself takes focus
|
|
117
|
+
* when a citation reveals it, apart from the button that opens it.
|
|
118
|
+
*/
|
|
119
|
+
export function SourceRenderer({ node, context }: UiRendererProps<UiProps<'Source'>>) {
|
|
120
|
+
const number = useContext(SourceNumbers).get(node.id);
|
|
121
|
+
const { title, url } = node.props;
|
|
122
|
+
const { openUrl } = context;
|
|
123
|
+
const face = (
|
|
124
|
+
<>
|
|
125
|
+
<span className="w-5 shrink-0 text-right text-text-faint tabular-nums">{number}</span>
|
|
126
|
+
<span className="min-w-0 truncate text-text">{title}</span>
|
|
127
|
+
<span className="shrink-0 text-text-faint">{domainOf(url)}</span>
|
|
128
|
+
</>
|
|
129
|
+
);
|
|
130
|
+
return (
|
|
131
|
+
<li data-ui-source={node.id} tabIndex={-1} className="chat-ui-source scroll-my-2 rounded-md">
|
|
132
|
+
{openUrl === undefined ? (
|
|
133
|
+
<span className="flex h-8 items-center gap-2 px-2 text-xs">{face}</span>
|
|
134
|
+
) : (
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
className="flex h-8 w-full items-center gap-2 rounded-md px-2 text-left text-xs hover:bg-surface-hover"
|
|
138
|
+
onClick={() => openUrl(url)}
|
|
139
|
+
>
|
|
140
|
+
{face}
|
|
141
|
+
</button>
|
|
142
|
+
)}
|
|
143
|
+
</li>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { ArrowDown, ArrowRight, ArrowUp } from 'lucide-react';
|
|
4
|
+
import type { UiProps } from '@adecore/intelligent-ui';
|
|
5
|
+
import { Button, Icon, KeyValueList, Pill } from '@adecore/ui';
|
|
6
|
+
import { formatBytes, formatDuration, formatMoment, formatNumber, formatPercent, formatRounded } from '@adecore/ui/format';
|
|
7
|
+
import { useBlockLocal } from '../block-local';
|
|
8
|
+
import { uiChildrenOf } from '../node-text';
|
|
9
|
+
import type { UiLiveValue, UiRenderContext, UiRendererProps } from '../render-context';
|
|
10
|
+
import { isNumericColumn, UI_TABLE_ROWS, uiTableCell, uiTableColumns, type UiTableCell, type UiTableColumn } from '../table-data';
|
|
11
|
+
import { UI_TONE_TEXT } from '../tones';
|
|
12
|
+
import { UiFallbackPart } from '../UiFallbackPart';
|
|
13
|
+
import { UiLinkChip } from './links';
|
|
14
|
+
|
|
15
|
+
/* EntityList draws a hairline between its rows from this many on. */
|
|
16
|
+
const DIVIDED_FROM = 7;
|
|
17
|
+
|
|
18
|
+
/* A number with its unit, where a percent sign hugs the number and every other unit stands apart from it. */
|
|
19
|
+
function withUnit(text: string, unit: string | undefined): string {
|
|
20
|
+
if (unit === undefined || unit === '') {
|
|
21
|
+
return text;
|
|
22
|
+
}
|
|
23
|
+
return unit === '%' ? `${text}${unit}` : `${text} ${unit}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
* A number that live readings keep up to date. Keyed by its text, so a value that changed mounts
|
|
28
|
+
* again and lights up once, even when it changes again before the last light settled.
|
|
29
|
+
*/
|
|
30
|
+
function LiveFigure({ text, previous, live, className }: { text: string; previous?: string; live?: UiLiveValue; className?: string }) {
|
|
31
|
+
const lit = live !== undefined && live.changed && !live.stale && previous !== text;
|
|
32
|
+
return (
|
|
33
|
+
<span key={text} className={clsx(live?.stale ? 'text-text-muted' : 'text-text', lit && 'chat-ui-changed', className)}>
|
|
34
|
+
{text}
|
|
35
|
+
</span>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function StatsRenderer({ children }: UiRendererProps<UiProps<'Stats'>>) {
|
|
40
|
+
return <div className="chat-ui-nodes grid grid-cols-[repeat(auto-fit,minmax(128px,1fr))] gap-2">{children}</div>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* The arrow and the number say which way a value went; only a tone says whether that is good. */
|
|
44
|
+
export function StatRenderer({ node, context }: UiRendererProps<UiProps<'Stat'>>) {
|
|
45
|
+
const { t } = useTranslation('agent-chat');
|
|
46
|
+
const { label, value, previous, unit, tone } = node.props;
|
|
47
|
+
const live = context.liveValue?.(node.id, 'value');
|
|
48
|
+
const change = previous === undefined ? null : value - previous;
|
|
49
|
+
const arrow = change === null || change === 0 ? ArrowRight : change > 0 ? ArrowUp : ArrowDown;
|
|
50
|
+
const from = previous === undefined ? '' : withUnit(formatRounded(previous, 2), unit);
|
|
51
|
+
// A change from zero has no percentage.
|
|
52
|
+
const percent = change === null || previous === undefined || previous === 0 ? null : formatPercent(Math.abs((change / previous) * 100));
|
|
53
|
+
return (
|
|
54
|
+
<div className="flex min-w-0 flex-col gap-0.5 rounded-md bg-surface-hover px-2 py-1.5">
|
|
55
|
+
<span className="truncate text-xs text-text-muted">{label}</span>
|
|
56
|
+
<LiveFigure
|
|
57
|
+
text={withUnit(formatRounded(value, 2), unit)}
|
|
58
|
+
previous={typeof live?.previous === 'number' ? withUnit(formatRounded(live.previous, 2), unit) : undefined}
|
|
59
|
+
live={live}
|
|
60
|
+
className="text-lg font-semibold tabular-nums"
|
|
61
|
+
/>
|
|
62
|
+
{change !== null && (
|
|
63
|
+
<span className={clsx('flex items-center gap-1 text-xs tabular-nums', tone === undefined ? 'text-text-faint' : UI_TONE_TEXT[tone])}>
|
|
64
|
+
<Icon icon={arrow} size={12} className="shrink-0" />
|
|
65
|
+
<span className="min-w-0 truncate">
|
|
66
|
+
{percent === null ? t('blocks.stat.from', { previous: from }) : t('blocks.stat.change', { change: percent, previous: from })}
|
|
67
|
+
</span>
|
|
68
|
+
</span>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function EntityListRenderer({ node, children }: UiRendererProps<UiProps<'EntityList'>>) {
|
|
75
|
+
return (
|
|
76
|
+
<KeyValueList.Root divided={uiChildrenOf(node, 'Entry').length >= DIVIDED_FROM} className="chat-ui-nodes px-2">
|
|
77
|
+
{children}
|
|
78
|
+
</KeyValueList.Root>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function EntryRenderer({ node, children }: UiRendererProps<UiProps<'Entry'>>) {
|
|
83
|
+
return (
|
|
84
|
+
<KeyValueList.Item>
|
|
85
|
+
<KeyValueList.Name>{node.props.label}</KeyValueList.Name>
|
|
86
|
+
<KeyValueList.Value>{children}</KeyValueList.Value>
|
|
87
|
+
</KeyValueList.Item>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function numberText(cell: Extract<UiTableCell, { kind: 'number' }>, unit: string | undefined): string {
|
|
92
|
+
return cell.as === 'bytes' ? formatBytes(cell.value) : cell.as === 'duration' ? formatDuration(cell.value) : withUnit(formatRounded(cell.value, 2), unit);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function Cell({ value, column, live, context }: { value: unknown; column: UiTableColumn; live?: UiLiveValue; context: UiRenderContext }) {
|
|
96
|
+
const cell = uiTableCell(value, column);
|
|
97
|
+
switch (cell.kind) {
|
|
98
|
+
case 'empty':
|
|
99
|
+
return null;
|
|
100
|
+
case 'text':
|
|
101
|
+
return <>{cell.text}</>;
|
|
102
|
+
case 'number': {
|
|
103
|
+
const before = live?.previous === undefined ? undefined : uiTableCell(live.previous, column);
|
|
104
|
+
return (
|
|
105
|
+
<LiveFigure
|
|
106
|
+
text={numberText(cell, column.unit)}
|
|
107
|
+
previous={before?.kind === 'number' ? numberText(before, column.unit) : undefined}
|
|
108
|
+
live={live}
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
case 'date':
|
|
113
|
+
return <time dateTime={new Date(cell.at).toISOString()}>{formatMoment(cell.at)}</time>;
|
|
114
|
+
case 'file':
|
|
115
|
+
return <UiLinkChip target={{ type: 'File', path: cell.path }} context={context} />;
|
|
116
|
+
case 'tag':
|
|
117
|
+
return (
|
|
118
|
+
<Pill shape="tag" size="sm">
|
|
119
|
+
{cell.text}
|
|
120
|
+
</Pill>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/*
|
|
126
|
+
* Rows under a sticky head, scrolling inside 320px and sideways when wider than the column. Rows
|
|
127
|
+
* arrive one by one while the block streams, without animation; past fifty a person asks for the rest.
|
|
128
|
+
*/
|
|
129
|
+
export function TableRenderer({ node, context }: UiRendererProps<UiProps<'Table'>>) {
|
|
130
|
+
const { t } = useTranslation('agent-chat');
|
|
131
|
+
const [all, setAll] = useBlockLocal(context, node.id, () => false);
|
|
132
|
+
const columns = uiTableColumns(node);
|
|
133
|
+
if (columns.length === 0) {
|
|
134
|
+
return <UiFallbackPart fallback={node.fallback} problem={{ kind: 'failed' }} />;
|
|
135
|
+
}
|
|
136
|
+
const { rows } = node.props;
|
|
137
|
+
const shown = all ? rows : rows.slice(0, UI_TABLE_ROWS);
|
|
138
|
+
return (
|
|
139
|
+
<div className="flex flex-col items-start gap-1.5">
|
|
140
|
+
<div className="max-h-80 w-full overflow-auto rounded-md border border-border-soft">
|
|
141
|
+
<table className="w-full border-collapse text-xs tabular-nums">
|
|
142
|
+
<thead className="sticky top-0 z-1 bg-surface-hover">
|
|
143
|
+
<tr>
|
|
144
|
+
{columns.map((column) => (
|
|
145
|
+
<th
|
|
146
|
+
key={column.key}
|
|
147
|
+
scope="col"
|
|
148
|
+
className={clsx(
|
|
149
|
+
'h-8 px-2 font-medium whitespace-nowrap text-text-muted',
|
|
150
|
+
isNumericColumn(column) ? 'text-right' : 'text-left'
|
|
151
|
+
)}
|
|
152
|
+
>
|
|
153
|
+
{column.title}
|
|
154
|
+
</th>
|
|
155
|
+
))}
|
|
156
|
+
</tr>
|
|
157
|
+
</thead>
|
|
158
|
+
<tbody>
|
|
159
|
+
{shown.map((row, index) => (
|
|
160
|
+
// Rows only ever grow at the end while a block streams, so the place of a row is a stable key.
|
|
161
|
+
<tr key={index} className="border-t border-border-soft">
|
|
162
|
+
{columns.map((column) => (
|
|
163
|
+
<td
|
|
164
|
+
key={column.key}
|
|
165
|
+
className={clsx('h-8 px-2 text-text', isNumericColumn(column) ? 'text-right whitespace-nowrap' : 'text-left')}
|
|
166
|
+
>
|
|
167
|
+
<Cell
|
|
168
|
+
value={row[column.key]}
|
|
169
|
+
column={column}
|
|
170
|
+
live={isNumericColumn(column) ? context.liveValue?.(node.id, 'rows', [index, column.key]) : undefined}
|
|
171
|
+
context={context}
|
|
172
|
+
/>
|
|
173
|
+
</td>
|
|
174
|
+
))}
|
|
175
|
+
</tr>
|
|
176
|
+
))}
|
|
177
|
+
</tbody>
|
|
178
|
+
</table>
|
|
179
|
+
</div>
|
|
180
|
+
{!all && rows.length > UI_TABLE_ROWS && (
|
|
181
|
+
<Button size="xs" variant="secondary" onClick={() => setAll(true)}>
|
|
182
|
+
{t('blocks.table.showAll', { total: formatNumber(rows.length) })}
|
|
183
|
+
</Button>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { createContext, useContext, type MouseEvent } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import type { UiBinding, UiProps, UiViewNode } from '@adecore/intelligent-ui';
|
|
5
|
+
import { Checkbox, Segmented, Slider, Switch } from '@adecore/ui';
|
|
6
|
+
import { uiChildrenOf, uiNodeLabel } from '../node-text';
|
|
7
|
+
import type { UiRenderContext, UiRendererProps } from '../render-context';
|
|
8
|
+
|
|
9
|
+
type Scalar = UiProps<'Item'>['value'];
|
|
10
|
+
|
|
11
|
+
interface ChecklistState {
|
|
12
|
+
values: readonly Scalar[];
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
toggle(value: Scalar): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ChecklistContext = createContext<ChecklistState>({ values: [], enabled: false, toggle: () => undefined });
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* The binding of an input, or null for a read-only node. An input works once its own node closed and
|
|
21
|
+
* only through its binding, so the value never leaves the block until a Choice carries it.
|
|
22
|
+
*/
|
|
23
|
+
function bindingOf<Value>(node: UiViewNode<{ value: Value }>, context: UiRenderContext): UiBinding<Value> | null {
|
|
24
|
+
const binding = node.bindings.value as UiBinding<Value> | undefined;
|
|
25
|
+
return binding === undefined || !node.complete || context.answer !== null ? null : binding;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* An input whose node has not closed yet stands at 60% and takes no pointer. */
|
|
29
|
+
const PENDING = 'pointer-events-none opacity-60';
|
|
30
|
+
|
|
31
|
+
export function ChecklistRenderer({ node, children, context }: UiRendererProps<UiProps<'Checklist'>>) {
|
|
32
|
+
const binding = bindingOf(node, context);
|
|
33
|
+
const values = binding?.value ?? node.props.value;
|
|
34
|
+
const toggle = (value: Scalar): void => {
|
|
35
|
+
binding?.onValueChange(values.includes(value) ? values.filter((entry) => entry !== value) : [...values, value]);
|
|
36
|
+
};
|
|
37
|
+
return (
|
|
38
|
+
<ChecklistContext value={{ values, enabled: binding !== null, toggle }}>
|
|
39
|
+
<div className={clsx('chat-ui-nodes flex flex-col', !node.complete && PENDING)}>{children}</div>
|
|
40
|
+
</ChecklistContext>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* The whole row is the label; a link inside it keeps its own click and checks nothing. */
|
|
45
|
+
export function ItemRenderer({ node, children }: UiRendererProps<UiProps<'Item'>>) {
|
|
46
|
+
const list = useContext(ChecklistContext);
|
|
47
|
+
const { value } = node.props;
|
|
48
|
+
const toggleRow = (event: MouseEvent<HTMLDivElement>): void => {
|
|
49
|
+
if (list.enabled && !(event.target as Element).closest('button, a')) {
|
|
50
|
+
list.toggle(value);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
return (
|
|
54
|
+
<div
|
|
55
|
+
className={clsx('flex min-h-8 items-center gap-2 rounded-md px-2 text-sm text-text', list.enabled && 'hover:bg-surface-hover')}
|
|
56
|
+
onClick={toggleRow}
|
|
57
|
+
>
|
|
58
|
+
<Checkbox checked={list.values.includes(value)} onCheckedChange={() => list.toggle(value)} label={uiNodeLabel(node)} disabled={!list.enabled} />
|
|
59
|
+
<div className="min-w-0 grow">{children}</div>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function SwitchRenderer({ node, children, context }: UiRendererProps<UiProps<'Switch'>>) {
|
|
65
|
+
const binding = bindingOf(node, context);
|
|
66
|
+
return (
|
|
67
|
+
<div className={clsx('flex min-h-7 items-center justify-between gap-3 px-2 text-sm', !node.complete && PENDING)}>
|
|
68
|
+
<span className="min-w-0 text-text">{children}</span>
|
|
69
|
+
<Switch
|
|
70
|
+
checked={binding?.value ?? node.props.value}
|
|
71
|
+
onCheckedChange={(next) => binding?.onValueChange(next)}
|
|
72
|
+
label={uiNodeLabel(node)}
|
|
73
|
+
disabled={binding === null}
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Its children are the label, read as text: the slider writes its own label line. */
|
|
80
|
+
export function SliderRenderer({ node, context }: UiRendererProps<UiProps<'Slider'>>) {
|
|
81
|
+
const binding = bindingOf(node, context);
|
|
82
|
+
const { value, min, max, step, unit } = node.props;
|
|
83
|
+
return (
|
|
84
|
+
<Slider
|
|
85
|
+
value={binding?.value ?? value}
|
|
86
|
+
onValueChange={(next) => binding?.onValueChange(next)}
|
|
87
|
+
min={min}
|
|
88
|
+
max={max}
|
|
89
|
+
step={step}
|
|
90
|
+
unit={unit}
|
|
91
|
+
label={uiNodeLabel(node)}
|
|
92
|
+
disabled={binding === null}
|
|
93
|
+
className={clsx('px-2', !node.complete && PENDING)}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* Left aligned, never the full width. Each Option is a segment by its place, since a value may be any scalar. */
|
|
99
|
+
export function SegmentedRenderer({ node, context }: UiRendererProps<UiProps<'Segmented'>>) {
|
|
100
|
+
const { t } = useTranslation('agent-chat');
|
|
101
|
+
const binding = bindingOf(node, context);
|
|
102
|
+
const current = binding?.value ?? node.props.value;
|
|
103
|
+
const options = uiChildrenOf(node, 'Option').map((option, index) => ({ id: String(index), label: uiNodeLabel(option), value: option.props.value }));
|
|
104
|
+
const picked = options.find((option) => option.value === current)?.id ?? '';
|
|
105
|
+
return (
|
|
106
|
+
<div className={clsx('px-2', !node.complete && PENDING)}>
|
|
107
|
+
<Segmented
|
|
108
|
+
value={picked}
|
|
109
|
+
options={options}
|
|
110
|
+
label={t('blocks.options')}
|
|
111
|
+
disabled={binding === null}
|
|
112
|
+
onValueChange={(id) => {
|
|
113
|
+
const option = options[Number(id)];
|
|
114
|
+
if (option !== undefined) {
|
|
115
|
+
binding?.onValueChange(option.value);
|
|
116
|
+
}
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { FileDiff, GitCommitHorizontal, SquareDashed } from 'lucide-react';
|
|
5
|
+
import type { UiProps } from '@adecore/intelligent-ui';
|
|
6
|
+
import { FileIcon, Icon, Tooltip } from '@adecore/ui';
|
|
7
|
+
import { CHIP_IN_MESSAGE, MENTION_TONE } from '../../chips';
|
|
8
|
+
import type { UiLink, UiLinkTarget, UiRenderContext, UiRendererProps } from '../render-context';
|
|
9
|
+
|
|
10
|
+
const PLAIN: UiLink = { state: 'plain' };
|
|
11
|
+
|
|
12
|
+
function baseName(path: string): string {
|
|
13
|
+
const trimmed = path.replace(/\/+$/, '');
|
|
14
|
+
return trimmed.slice(trimmed.lastIndexOf('/') + 1) || path;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function plainText(target: UiLinkTarget, link: UiLink): string {
|
|
18
|
+
switch (target.type) {
|
|
19
|
+
case 'File':
|
|
20
|
+
return target.line === undefined ? target.path : `${target.path}:${target.line}`;
|
|
21
|
+
case 'Diff':
|
|
22
|
+
return target.path;
|
|
23
|
+
case 'Commit':
|
|
24
|
+
return target.sha.slice(0, 7);
|
|
25
|
+
case 'Node':
|
|
26
|
+
return link.label ?? target.id;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function chipFace(target: UiLinkTarget, link: UiLink): { face: ReactNode; hint: string | null } {
|
|
31
|
+
switch (target.type) {
|
|
32
|
+
case 'File':
|
|
33
|
+
return {
|
|
34
|
+
face: (
|
|
35
|
+
<>
|
|
36
|
+
<FileIcon path={target.path} size={14} />
|
|
37
|
+
<span className="truncate">{baseName(target.path)}</span>
|
|
38
|
+
{target.line !== undefined && <span className="opacity-70">:{target.line}</span>}
|
|
39
|
+
</>
|
|
40
|
+
),
|
|
41
|
+
hint: target.line === undefined ? target.path : `${target.path}:${target.line}`
|
|
42
|
+
};
|
|
43
|
+
case 'Diff':
|
|
44
|
+
return {
|
|
45
|
+
face: (
|
|
46
|
+
<>
|
|
47
|
+
<Icon icon={FileDiff} size={12} />
|
|
48
|
+
<span className="truncate">{baseName(target.path)}</span>
|
|
49
|
+
</>
|
|
50
|
+
),
|
|
51
|
+
hint: target.path
|
|
52
|
+
};
|
|
53
|
+
case 'Commit':
|
|
54
|
+
return {
|
|
55
|
+
face: (
|
|
56
|
+
<>
|
|
57
|
+
<Icon icon={GitCommitHorizontal} size={12} />
|
|
58
|
+
<span className="font-mono">{target.sha.slice(0, 7)}</span>
|
|
59
|
+
</>
|
|
60
|
+
),
|
|
61
|
+
hint: link.label ?? target.sha
|
|
62
|
+
};
|
|
63
|
+
case 'Node':
|
|
64
|
+
return {
|
|
65
|
+
face: (
|
|
66
|
+
<>
|
|
67
|
+
<Icon icon={SquareDashed} size={12} />
|
|
68
|
+
<span className="truncate">{link.label ?? target.id}</span>
|
|
69
|
+
</>
|
|
70
|
+
),
|
|
71
|
+
hint: null
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/*
|
|
77
|
+
* A resource of the host as a chip, the shape a mention has in a message. The host says whether it
|
|
78
|
+
* can open the target; one it cannot, outside the project or gone, stays mono text with the reason
|
|
79
|
+
* in a tooltip. A chip fades in, since a link reads as text until the host checked it.
|
|
80
|
+
*/
|
|
81
|
+
export function UiLinkChip({ target, context }: { target: UiLinkTarget; context: UiRenderContext }) {
|
|
82
|
+
const { t } = useTranslation('agent-chat');
|
|
83
|
+
const link = context.link?.(target) ?? PLAIN;
|
|
84
|
+
if (link.state === 'plain') {
|
|
85
|
+
const reason = link.reason ?? (context.link === undefined ? null : t('blocks.outsideProject'));
|
|
86
|
+
// Focusable only with a reason, so a keyboard reaches the tooltip.
|
|
87
|
+
const text = (
|
|
88
|
+
<span tabIndex={reason === null ? undefined : 0} className="font-mono text-code break-all text-text-muted">
|
|
89
|
+
{plainText(target, link)}
|
|
90
|
+
</span>
|
|
91
|
+
);
|
|
92
|
+
return reason === null ? text : <Tooltip label={reason}>{text}</Tooltip>;
|
|
93
|
+
}
|
|
94
|
+
const { face, hint } = chipFace(target, link);
|
|
95
|
+
const { openLink } = context;
|
|
96
|
+
const chip =
|
|
97
|
+
openLink === undefined ? (
|
|
98
|
+
<span className={clsx(CHIP_IN_MESSAGE, MENTION_TONE, 'transition-opacity duration-120 starting:opacity-0')}>{face}</span>
|
|
99
|
+
) : (
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
className={clsx(CHIP_IN_MESSAGE, MENTION_TONE, 'transition-opacity duration-120 hover:brightness-95 starting:opacity-0')}
|
|
103
|
+
onClick={() => openLink(target)}
|
|
104
|
+
>
|
|
105
|
+
{face}
|
|
106
|
+
</button>
|
|
107
|
+
);
|
|
108
|
+
return hint === null ? chip : <Tooltip label={hint}>{chip}</Tooltip>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* A link with children is a row: the chip and what the agent says about it. */
|
|
112
|
+
function LinkRow({ target, context, described, children }: { target: UiLinkTarget; context: UiRenderContext; described: boolean; children: ReactNode }) {
|
|
113
|
+
if (!described) {
|
|
114
|
+
return <UiLinkChip target={target} context={context} />;
|
|
115
|
+
}
|
|
116
|
+
return (
|
|
117
|
+
<div className="flex min-w-0 items-baseline gap-2 px-2 text-sm">
|
|
118
|
+
<span className="shrink-0">
|
|
119
|
+
<UiLinkChip target={target} context={context} />
|
|
120
|
+
</span>
|
|
121
|
+
<span className="min-w-0 text-text-muted">{children}</span>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function FileRenderer({ node, children, context }: UiRendererProps<UiProps<'File'>>) {
|
|
127
|
+
const { path, line } = node.props;
|
|
128
|
+
return (
|
|
129
|
+
<LinkRow target={{ type: 'File', path, line }} context={context} described={node.children.length > 0}>
|
|
130
|
+
{children}
|
|
131
|
+
</LinkRow>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function DiffRenderer({ node, children, context }: UiRendererProps<UiProps<'Diff'>>) {
|
|
136
|
+
return (
|
|
137
|
+
<LinkRow target={{ type: 'Diff', path: node.props.path }} context={context} described={node.children.length > 0}>
|
|
138
|
+
{children}
|
|
139
|
+
</LinkRow>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function CommitRenderer({ node, children, context }: UiRendererProps<UiProps<'Commit'>>) {
|
|
144
|
+
return (
|
|
145
|
+
<LinkRow target={{ type: 'Commit', sha: node.props.sha }} context={context} described={node.children.length > 0}>
|
|
146
|
+
{children}
|
|
147
|
+
</LinkRow>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function NodeRenderer({ node, children, context }: UiRendererProps<UiProps<'Node'>>) {
|
|
152
|
+
return (
|
|
153
|
+
<LinkRow target={{ type: 'Node', id: node.props.id }} context={context} described={node.children.length > 0}>
|
|
154
|
+
{children}
|
|
155
|
+
</LinkRow>
|
|
156
|
+
);
|
|
157
|
+
}
|