@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
|
@@ -34,8 +34,8 @@ function NumberChip({ number, selected }: { number: number; selected: boolean })
|
|
|
34
34
|
<span
|
|
35
35
|
aria-hidden
|
|
36
36
|
className={clsx(
|
|
37
|
-
'mt-px flex size-5 shrink-0 items-center justify-center rounded-
|
|
38
|
-
selected ? 'bg-accent text-accent-text' : 'bg-
|
|
37
|
+
'mt-px flex size-5 shrink-0 items-center justify-center rounded-md text-2xs font-semibold tabular-nums',
|
|
38
|
+
selected ? 'bg-accent text-accent-text' : 'bg-ui-inset text-text'
|
|
39
39
|
)}
|
|
40
40
|
>
|
|
41
41
|
{number}
|
|
@@ -50,7 +50,7 @@ function PlaceRow({ listed, number, selected, onPick }: { listed: ListedPlace; n
|
|
|
50
50
|
<>
|
|
51
51
|
<NumberChip number={number} selected={selected} />
|
|
52
52
|
<span className="flex min-w-0 grow flex-col gap-0.5">
|
|
53
|
-
<span className="text-
|
|
53
|
+
<span className="text-xs font-medium wrap-break-word text-text">{listed.name}</span>
|
|
54
54
|
{detail !== undefined && detail !== '' && <span className="text-xs wrap-break-word text-text-muted">{detail}</span>}
|
|
55
55
|
{address !== undefined && address !== '' && <span className="text-xs wrap-break-word text-text-faint">{address}</span>}
|
|
56
56
|
{listed.point === undefined && <span className="text-xs text-text-faint">{t('blocks.map.lookingUp')}</span>}
|
|
@@ -58,13 +58,18 @@ function PlaceRow({ listed, number, selected, onPick }: { listed: ListedPlace; n
|
|
|
58
58
|
</span>
|
|
59
59
|
</>
|
|
60
60
|
);
|
|
61
|
-
const row = 'flex w-full min-h-
|
|
61
|
+
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';
|
|
62
62
|
return (
|
|
63
63
|
<li>
|
|
64
64
|
{onPick === null ? (
|
|
65
|
-
<div className={clsx(row, selected && '
|
|
65
|
+
<div className={clsx(row, selected && 'ring-2 ring-accent ring-inset')}>{body}</div>
|
|
66
66
|
) : (
|
|
67
|
-
<button
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
aria-pressed={selected}
|
|
70
|
+
className={clsx(row, 'hover:bg-surface-active', selected && 'ring-2 ring-accent ring-inset')}
|
|
71
|
+
onClick={onPick}
|
|
72
|
+
>
|
|
68
73
|
{body}
|
|
69
74
|
</button>
|
|
70
75
|
)}
|
|
@@ -150,7 +155,7 @@ function MapFrame({ markers, source, selected, onPick }: FrameProps) {
|
|
|
150
155
|
}
|
|
151
156
|
return (
|
|
152
157
|
<>
|
|
153
|
-
<div ref={setElement} className="relative h-50 overflow-hidden rounded-
|
|
158
|
+
<div ref={setElement} className="relative h-50 overflow-hidden rounded-xl bg-ui-tile @min-[420px]:h-60">
|
|
154
159
|
{body}
|
|
155
160
|
{waiting && (
|
|
156
161
|
<div className="pointer-events-none absolute inset-0 grid place-items-center">
|
|
@@ -200,9 +205,9 @@ export function MapRenderer({ node, children, context }: UiRendererProps<UiProps
|
|
|
200
205
|
<MapFrame markers={markers} source={null} selected={selectedId} onPick={onPick} />
|
|
201
206
|
);
|
|
202
207
|
return (
|
|
203
|
-
<div className={clsx('@container flex flex-col gap-1.5
|
|
208
|
+
<div className={clsx('@container flex flex-col gap-1.5', !node.complete && PENDING)}>
|
|
204
209
|
{frame}
|
|
205
|
-
<ol aria-label={t('blocks.map.label', { count: places.length })} className="
|
|
210
|
+
<ol aria-label={t('blocks.map.label', { count: places.length })} className="grid gap-1.5 @min-[420px]:grid-cols-2">
|
|
206
211
|
{places.map((listed, index) => {
|
|
207
212
|
const { value } = listed.node.props;
|
|
208
213
|
return (
|
|
@@ -216,9 +221,7 @@ export function MapRenderer({ node, children, context }: UiRendererProps<UiProps
|
|
|
216
221
|
);
|
|
217
222
|
})}
|
|
218
223
|
</ol>
|
|
219
|
-
{node.omitted !== undefined && node.omitted > 0 && (
|
|
220
|
-
<p className="px-2 text-xs text-text-faint">{t('blocks.map.omitted', { count: node.omitted })}</p>
|
|
221
|
-
)}
|
|
224
|
+
{node.omitted !== undefined && node.omitted > 0 && <p className="text-2xs text-text-faint">{t('blocks.map.omitted', { count: node.omitted })}</p>}
|
|
222
225
|
{/* A place the catalog refused draws as its text, which is all its node renders. */}
|
|
223
226
|
<div className="chat-ui-nodes flex flex-col gap-1.5 empty:hidden">{children}</div>
|
|
224
227
|
</div>
|
|
@@ -22,7 +22,7 @@ export function TabsRenderer({ node, children, context }: UiRendererProps<UiProp
|
|
|
22
22
|
const value = picked !== null && tabs.some((tab) => tab.id === picked) ? picked : (tabs[0]?.id ?? null);
|
|
23
23
|
return (
|
|
24
24
|
<Tabs.Root value={value} onValueChange={(next) => setPicked(String(next))} className="flex flex-col gap-3">
|
|
25
|
-
<Tabs.List className="
|
|
25
|
+
<Tabs.List variant="pill" overflow="scroll" className="w-fit max-w-full rounded-xl bg-ui-tile p-0.75">
|
|
26
26
|
{tabs.map((tab) => (
|
|
27
27
|
<Tabs.Tab key={tab.id} value={tab.id} data-ui-reveal={tab.id}>
|
|
28
28
|
{tab.props.title}
|
|
@@ -46,24 +46,24 @@ export function SectionsRenderer({ node, children, context }: UiRendererProps<Ui
|
|
|
46
46
|
const [touched, setTouched] = useBlockLocal(context, node.id, () => false);
|
|
47
47
|
return (
|
|
48
48
|
<SectionsContext value={{ touched, touch: () => setTouched(true) }}>
|
|
49
|
-
<div className="chat-ui-nodes flex flex-col
|
|
49
|
+
<div className="chat-ui-nodes flex flex-col gap-1.5">{children}</div>
|
|
50
50
|
</SectionsContext>
|
|
51
51
|
);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/* Arrives open until the person folds a section of the set themselves; the content
|
|
54
|
+
/* Arrives open until the person folds a section of the set themselves; the content keeps one margin on both sides. */
|
|
55
55
|
export function SectionRenderer({ node, children, context }: UiRendererProps<UiProps<'Section'>>) {
|
|
56
56
|
const fold = useContext(SectionsContext);
|
|
57
57
|
const [open, setOpen] = useBlockLocal(context, node.id, () => !fold.touched);
|
|
58
58
|
const contentId = useId();
|
|
59
59
|
return (
|
|
60
|
-
<section>
|
|
60
|
+
<section className="rounded-xl bg-ui-tile">
|
|
61
61
|
<button
|
|
62
62
|
type="button"
|
|
63
63
|
data-ui-reveal={node.id}
|
|
64
64
|
aria-expanded={open}
|
|
65
65
|
aria-controls={contentId}
|
|
66
|
-
className="flex h-
|
|
66
|
+
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"
|
|
67
67
|
onClick={() => {
|
|
68
68
|
fold.touch();
|
|
69
69
|
setOpen(!open);
|
|
@@ -76,7 +76,7 @@ export function SectionRenderer({ node, children, context }: UiRendererProps<UiP
|
|
|
76
76
|
/>
|
|
77
77
|
<span className="min-w-0 truncate">{node.props.title}</span>
|
|
78
78
|
</button>
|
|
79
|
-
<div id={contentId} hidden={!open} className="chat-ui-nodes flex flex-col gap-3
|
|
79
|
+
<div id={contentId} hidden={!open} className="chat-ui-section-body chat-ui-nodes flex flex-col gap-3 px-3 pb-3">
|
|
80
80
|
{children}
|
|
81
81
|
</div>
|
|
82
82
|
</section>
|
|
@@ -7,7 +7,7 @@ import { Icon, Meter, Pill, Spinner } from '@adecore/ui';
|
|
|
7
7
|
import { formatNumber, formatPercent } from '@adecore/ui/format';
|
|
8
8
|
import { UiHeadContext, type UiRenderContext, type UiRendererProps } from '../render-context';
|
|
9
9
|
import { uiEventTime, uiNodeLabel } from '../node-text';
|
|
10
|
-
import { UI_TONE_ICONS, UI_TONE_PILL, UI_TONE_SURFACE, UI_TONE_TEXT } from '../tones';
|
|
10
|
+
import { UI_STEP_PILL, UI_TONE_FILL, UI_TONE_ICONS, UI_TONE_PILL, UI_TONE_SURFACE, UI_TONE_TEXT } from '../tones';
|
|
11
11
|
|
|
12
12
|
type StepState = UiProps<'Step'>['state'];
|
|
13
13
|
|
|
@@ -26,43 +26,56 @@ const STEP_TONES: Readonly<Record<StepState, string>> = {
|
|
|
26
26
|
skipped: 'text-text-faint'
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
/* A detail longer than this reads as a sentence, which a tag cannot wrap. */
|
|
30
|
+
const DETAIL_AS_TAG = 24;
|
|
31
|
+
|
|
29
32
|
/* Whether the block still changes: while it streams, or while it reads live data. Only then does anything spin. */
|
|
30
33
|
function isLive(context: UiRenderContext): boolean {
|
|
31
34
|
return context.phase === 'streaming' || (context.live !== undefined && context.live !== null && context.live.state !== 'refused');
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
/* A
|
|
35
|
-
const
|
|
37
|
+
/* A Summary and a Callout share one row: the icon in a square, and beside it text that centers on the square while it is shorter and starts at its top once it is taller. */
|
|
38
|
+
const ICON_ROW = 'flex items-start gap-3 rounded-xl p-3';
|
|
39
|
+
const ICON_SQUARE = 'grid size-8.5 shrink-0 place-items-center rounded-lg';
|
|
40
|
+
const ICON_TEXT = 'flex min-h-8.5 min-w-0 grow flex-col justify-center gap-0.5';
|
|
36
41
|
|
|
37
|
-
/*
|
|
42
|
+
/*
|
|
43
|
+
* A tile in its tone; the badge stands under the title in the tone's color. The head of the block
|
|
44
|
+
* carries its icon in a square of its own, a Summary further down is a subheading without one. The
|
|
45
|
+
* icon is the ground's ink in the dark theme, where its tones are too light for white.
|
|
46
|
+
*/
|
|
38
47
|
export function SummaryRenderer({ node, children }: UiRendererProps<UiProps<'Summary'>>) {
|
|
39
48
|
const headId = useContext(UiHeadContext);
|
|
40
49
|
const head = headId === undefined || headId === node.id;
|
|
41
50
|
const { tone, badge } = node.props;
|
|
42
|
-
const
|
|
51
|
+
const shown = tone ?? 'neutral';
|
|
43
52
|
return (
|
|
44
|
-
<div className=
|
|
45
|
-
{head &&
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
<div className={clsx(ICON_ROW, tone === undefined ? 'bg-ui-tile' : UI_TONE_SURFACE[tone])}>
|
|
54
|
+
{head && (
|
|
55
|
+
<span aria-hidden className={clsx(ICON_SQUARE, 'text-accent-text dark:text-surface', UI_TONE_FILL[shown])}>
|
|
56
|
+
<Icon icon={UI_TONE_ICONS[shown]} size={16} />
|
|
57
|
+
</span>
|
|
58
|
+
)}
|
|
59
|
+
<div className={ICON_TEXT}>
|
|
60
|
+
<div className="text-base font-semibold text-balance wrap-break-word text-text">{children}</div>
|
|
61
|
+
{badge !== undefined && badge !== '' && (
|
|
62
|
+
<p className={clsx('text-2xs font-medium wrap-anywhere', tone === undefined ? 'text-text-muted' : UI_TONE_TEXT[tone])}>{badge}</p>
|
|
52
63
|
)}
|
|
53
|
-
{!tag && badge !== undefined && badge !== '' && <p className="w-full text-xs wrap-anywhere text-text-muted">{badge}</p>}
|
|
54
64
|
</div>
|
|
55
65
|
</div>
|
|
56
66
|
);
|
|
57
67
|
}
|
|
58
68
|
|
|
69
|
+
/* A quiet tile whatever its tone: only the square of its icon wears the tone. */
|
|
59
70
|
export function CalloutRenderer({ node, children }: UiRendererProps<UiProps<'Callout'>>) {
|
|
60
71
|
const { tone, title } = node.props;
|
|
61
72
|
return (
|
|
62
|
-
<div className={clsx('
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
<div className={clsx('chat-ui-flat bg-ui-tile text-xs', ICON_ROW)}>
|
|
74
|
+
<span aria-hidden className={clsx(ICON_SQUARE, UI_TONE_SURFACE[tone], UI_TONE_TEXT[tone])}>
|
|
75
|
+
<Icon icon={UI_TONE_ICONS[tone]} size={16} />
|
|
76
|
+
</span>
|
|
77
|
+
<div className={ICON_TEXT}>
|
|
78
|
+
{title !== undefined && title !== '' && <div className="font-semibold text-text">{title}</div>}
|
|
66
79
|
<div className="text-text-muted">{children}</div>
|
|
67
80
|
</div>
|
|
68
81
|
</div>
|
|
@@ -87,7 +100,7 @@ export function ProgressRenderer({ node, children, context }: UiRendererProps<Ui
|
|
|
87
100
|
value === undefined ? null : limit === 100 ? formatPercent(value) : t('blocks.progress.of', { value: formatNumber(value), max: formatNumber(limit) });
|
|
88
101
|
const label = uiNodeLabel(node);
|
|
89
102
|
return (
|
|
90
|
-
<div className="flex flex-col gap-
|
|
103
|
+
<div className="chat-ui-flat flex flex-col gap-2 rounded-xl bg-ui-tile p-3">
|
|
91
104
|
<div className="flex items-baseline justify-between gap-3 text-xs">
|
|
92
105
|
<span className="min-w-0 text-text">{children}</span>
|
|
93
106
|
{reading !== null ? (
|
|
@@ -102,17 +115,17 @@ export function ProgressRenderer({ node, children, context }: UiRendererProps<Ui
|
|
|
102
115
|
}
|
|
103
116
|
|
|
104
117
|
export function StepsRenderer({ children }: UiRendererProps<UiProps<'Steps'>>) {
|
|
105
|
-
return <ol className="chat-ui-nodes flex flex-col
|
|
118
|
+
return <ol className="chat-ui-nodes flex flex-col gap-1.5">{children}</ol>;
|
|
106
119
|
}
|
|
107
120
|
|
|
108
|
-
/* A running step spins only while the block is live; in a quiet block it is a still dot. */
|
|
121
|
+
/* A tile per step with its state in words at the end. A running step spins only while the block is live; in a quiet block it is a still dot. */
|
|
109
122
|
export function StepRenderer({ node, children, context }: UiRendererProps<UiProps<'Step'>>) {
|
|
110
123
|
const { t } = useTranslation('agent-chat');
|
|
111
124
|
const { state, detail } = node.props;
|
|
112
125
|
const quiet = state === 'pending' || state === 'skipped';
|
|
113
126
|
return (
|
|
114
|
-
<li className="flex min-h-
|
|
115
|
-
<span className={clsx('grid size-4 shrink-0 place-items-center', STEP_TONES[state])}>
|
|
127
|
+
<li className="flex min-h-10 items-start gap-3 rounded-xl bg-ui-tile px-3 py-2.5 text-xs">
|
|
128
|
+
<span className={clsx('mt-0.5 grid size-4 shrink-0 place-items-center', STEP_TONES[state])}>
|
|
116
129
|
{state === 'running' ? (
|
|
117
130
|
isLive(context) ? (
|
|
118
131
|
<Spinner size={14} />
|
|
@@ -123,21 +136,23 @@ export function StepRenderer({ node, children, context }: UiRendererProps<UiProp
|
|
|
123
136
|
<Icon icon={STEP_ICONS[state]} size={14} />
|
|
124
137
|
)}
|
|
125
138
|
</span>
|
|
126
|
-
<span className="sr-only">{t(`blocks.step.${state}`)}</span>
|
|
127
139
|
{/* A short detail stays beside the title; one that does not fit beside it takes the line under it. */}
|
|
128
|
-
<span className="flex min-w-0 grow flex-wrap items-baseline
|
|
129
|
-
<span className={clsx('max-w-full min-w-0
|
|
130
|
-
{detail !== undefined && detail !== '' && <span className="max-w-full text-
|
|
140
|
+
<span className="flex min-w-0 grow flex-wrap items-baseline gap-x-2">
|
|
141
|
+
<span className={clsx('max-w-full min-w-0 font-medium wrap-break-word', quiet ? 'text-text-muted' : 'text-text')}>{children}</span>
|
|
142
|
+
{detail !== undefined && detail !== '' && <span className="max-w-full text-2xs wrap-anywhere text-text-muted tabular-nums">{detail}</span>}
|
|
131
143
|
</span>
|
|
144
|
+
<Pill shape="tag" size="sm" tone={UI_STEP_PILL[state]} className="shrink-0">
|
|
145
|
+
{t(`blocks.step.${state}`)}
|
|
146
|
+
</Pill>
|
|
132
147
|
</li>
|
|
133
148
|
);
|
|
134
149
|
}
|
|
135
150
|
|
|
136
151
|
export function TimelineRenderer({ children }: UiRendererProps<UiProps<'Timeline'>>) {
|
|
137
|
-
return <ol className="chat-ui-nodes flex flex-col px-
|
|
152
|
+
return <ol className="chat-ui-nodes chat-ui-flat flex flex-col rounded-xl bg-ui-tile px-3 py-1.5">{children}</ol>;
|
|
138
153
|
}
|
|
139
154
|
|
|
140
|
-
/*
|
|
155
|
+
/* A row of the tile: the time, a dot in the tone, the title, and a short detail as a tag at the end or a longer one under the title. */
|
|
141
156
|
export function EventRenderer({ node, children }: UiRendererProps<UiProps<'Event'>>) {
|
|
142
157
|
const { t } = useTranslation('agent-chat');
|
|
143
158
|
const { at, detail } = node.props;
|
|
@@ -145,22 +160,28 @@ export function EventRenderer({ node, children }: UiRendererProps<UiProps<'Event
|
|
|
145
160
|
const time = uiEventTime(at);
|
|
146
161
|
// A neutral dot says nothing the title does not, and a detail is part of what the row says.
|
|
147
162
|
const label = [time, uiNodeLabel(node), tone === 'neutral' ? '' : t(`blocks.event.tone.${tone}`), detail ?? ''].filter((part) => part !== '').join(', ');
|
|
163
|
+
const described = detail !== undefined && detail !== '';
|
|
164
|
+
const tagged = described && detail.length <= DETAIL_AS_TAG;
|
|
148
165
|
return (
|
|
149
|
-
<li aria-label={label} className="
|
|
150
|
-
|
|
151
|
-
|
|
166
|
+
<li aria-label={label} className="flex min-h-9 items-start gap-3 py-2 text-xs">
|
|
167
|
+
{/* A time nobody could read stays as written and is no `dateTime` for a machine to read. */}
|
|
168
|
+
{parseUiEventTime(at) === null ? (
|
|
169
|
+
<span className="w-24 shrink-0 text-2xs leading-5 wrap-anywhere text-text-muted tabular-nums">{time}</span>
|
|
170
|
+
) : (
|
|
171
|
+
<time dateTime={at} className="w-24 shrink-0 text-2xs leading-5 text-text-muted tabular-nums">
|
|
172
|
+
{time}
|
|
173
|
+
</time>
|
|
174
|
+
)}
|
|
175
|
+
<span aria-hidden className={clsx('mt-1.5 size-2 shrink-0 rounded-full bg-current', tone === 'neutral' ? 'text-text-faint' : UI_TONE_TEXT[tone])} />
|
|
152
176
|
<div className="flex min-w-0 grow flex-col gap-0.5">
|
|
153
|
-
|
|
154
|
-
{
|
|
155
|
-
<span className="text-xs wrap-anywhere text-text-muted tabular-nums">{time}</span>
|
|
156
|
-
) : (
|
|
157
|
-
<time dateTime={at} className="text-xs text-text-muted tabular-nums">
|
|
158
|
-
{time}
|
|
159
|
-
</time>
|
|
160
|
-
)}
|
|
161
|
-
<div className="text-sm wrap-break-word text-text">{children}</div>
|
|
162
|
-
{detail !== undefined && detail !== '' && <div className="text-xs wrap-anywhere text-text-muted">{detail}</div>}
|
|
177
|
+
<div className="wrap-break-word text-text">{children}</div>
|
|
178
|
+
{described && !tagged && <div className="text-2xs wrap-anywhere text-text-muted">{detail}</div>}
|
|
163
179
|
</div>
|
|
180
|
+
{tagged && (
|
|
181
|
+
<Pill shape="tag" size="sm" tone={UI_TONE_PILL[tone]} className="shrink-0">
|
|
182
|
+
{detail}
|
|
183
|
+
</Pill>
|
|
184
|
+
)}
|
|
164
185
|
</li>
|
|
165
186
|
);
|
|
166
187
|
}
|
|
@@ -35,3 +35,21 @@ export const UI_TONE_PILL: Readonly<Record<UiTone, NonNullable<PillProps['tone']
|
|
|
35
35
|
warning: 'needsYou',
|
|
36
36
|
danger: 'error'
|
|
37
37
|
};
|
|
38
|
+
|
|
39
|
+
/* The tone at full strength, for the square that carries the icon of a Summary. */
|
|
40
|
+
export const UI_TONE_FILL: Readonly<Record<UiTone, string>> = {
|
|
41
|
+
neutral: 'bg-text-muted',
|
|
42
|
+
info: 'bg-status-running',
|
|
43
|
+
success: 'bg-positive',
|
|
44
|
+
warning: 'bg-status-needs-you',
|
|
45
|
+
danger: 'bg-status-error'
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/* The tag at the end of a step, in words, so a state never rests on its icon alone. */
|
|
49
|
+
export const UI_STEP_PILL: Readonly<Record<'done' | 'running' | 'pending' | 'failed' | 'skipped', NonNullable<PillProps['tone']>>> = {
|
|
50
|
+
done: 'idle',
|
|
51
|
+
running: 'accent',
|
|
52
|
+
pending: 'muted',
|
|
53
|
+
failed: 'error',
|
|
54
|
+
skipped: 'muted'
|
|
55
|
+
};
|
package/src/host.ts
CHANGED
|
@@ -260,6 +260,23 @@ export type UiMapSource =
|
|
|
260
260
|
}
|
|
261
261
|
| { state: 'off'; reason: 'disabled' | 'signed-out' | 'unavailable' };
|
|
262
262
|
|
|
263
|
+
/*
|
|
264
|
+
* What the host's view of a Model block gets. It is drawn only once the attachment has a URL, inside a frame
|
|
265
|
+
* of fixed height that the block sizes and captions itself, so the view fills its parent and draws no caption.
|
|
266
|
+
*/
|
|
267
|
+
export interface UiModelViewProps {
|
|
268
|
+
scopeId: string;
|
|
269
|
+
chatId: string;
|
|
270
|
+
attachmentId: string;
|
|
271
|
+
/* The attachment's bytes, as `attachments.useUrl` resolved them. */
|
|
272
|
+
url: string;
|
|
273
|
+
/* What the model shows, for a screen reader: the block's alt, its caption, or a word for a 3D model. */
|
|
274
|
+
label: string;
|
|
275
|
+
alt?: string;
|
|
276
|
+
/* The caption as plain text, empty without one. */
|
|
277
|
+
caption: string;
|
|
278
|
+
}
|
|
279
|
+
|
|
263
280
|
/*
|
|
264
281
|
* What the app around the chat decides and the chat cannot, handed to the views by a `ChatHostProvider`.
|
|
265
282
|
* Whatever an app leaves out keeps the default here, which is the chat without that part. One host may
|
|
@@ -313,6 +330,8 @@ export interface ChatHost {
|
|
|
313
330
|
};
|
|
314
331
|
/* The image a tool read, drawn under its row; null draws none. */
|
|
315
332
|
ReadImage: ComponentType<{ path: string }> | null;
|
|
333
|
+
/* The 3D view of a Model block; left out, a model shows as a row with its name and caption. */
|
|
334
|
+
ModelView?: ComponentType<UiModelViewProps>;
|
|
316
335
|
/* An image a reply's markdown names by a path on the host's machine, absolute or under the chat's cwd; left out, the `<img>` stays as written. */
|
|
317
336
|
MarkdownImage?: ComponentType<{ path: string; alt: string }>;
|
|
318
337
|
code: {
|
package/src/index.ts
CHANGED
|
@@ -621,6 +621,11 @@ export default {
|
|
|
621
621
|
label: 'Image',
|
|
622
622
|
unavailable: 'The image is not available'
|
|
623
623
|
},
|
|
624
|
+
model: {
|
|
625
|
+
label: '3D model',
|
|
626
|
+
unavailable: 'The model is not available',
|
|
627
|
+
crashed: 'This model failed to render'
|
|
628
|
+
},
|
|
624
629
|
sources: 'Sources',
|
|
625
630
|
citation: 'Source {{number}}: {{title}}',
|
|
626
631
|
outsideProject: 'Outside the project, shown as text',
|
|
@@ -621,6 +621,11 @@ export default {
|
|
|
621
621
|
label: 'Afbeelding',
|
|
622
622
|
unavailable: 'De afbeelding is niet beschikbaar'
|
|
623
623
|
},
|
|
624
|
+
model: {
|
|
625
|
+
label: '3D-model',
|
|
626
|
+
unavailable: 'Het model is niet beschikbaar',
|
|
627
|
+
crashed: 'Dit model kon niet worden getekend'
|
|
628
|
+
},
|
|
624
629
|
sources: 'Bronnen',
|
|
625
630
|
citation: 'Bron {{number}}: {{title}}',
|
|
626
631
|
outsideProject: 'Buiten het project, als tekst getoond',
|
package/src/theme.css
CHANGED
|
@@ -30,6 +30,12 @@
|
|
|
30
30
|
/* The two grounds of the map a page draws without tiles. */
|
|
31
31
|
--map-land: #f2f1ed;
|
|
32
32
|
--map-water: #d3dde5;
|
|
33
|
+
/* The parts of an intelligent UI block, from the tokens an app's palette sets: a tile on the thread's
|
|
34
|
+
ground, a card inset in a tile, and the hatch of its band. The tile mixes the ink in so lightly that
|
|
35
|
+
muted text keeps 4.5 to 1 on it; the design's #f0f0f3 did not. */
|
|
36
|
+
--chat-ui-tile: color-mix(in srgb, var(--text) 4.5%, var(--surface));
|
|
37
|
+
--chat-ui-inset: var(--surface);
|
|
38
|
+
--chat-ui-hatch: var(--separator);
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
[data-theme="dark"] {
|
|
@@ -51,6 +57,9 @@
|
|
|
51
57
|
--chart-6: #008300;
|
|
52
58
|
--map-land: #2c3135;
|
|
53
59
|
--map-water: #1a2026;
|
|
60
|
+
--chat-ui-tile: color-mix(in srgb, var(--text) 3%, var(--surface));
|
|
61
|
+
--chat-ui-inset: var(--surface);
|
|
62
|
+
--chat-ui-hatch: var(--separator);
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
/* Output and diffs take the terminal's colors when the app loads `@adecore/terminal/terminal.css`,
|
|
@@ -82,6 +91,8 @@
|
|
|
82
91
|
--color-chat-added: var(--chat-added);
|
|
83
92
|
--color-chat-removed: var(--chat-removed);
|
|
84
93
|
--color-map-marker: var(--map-marker);
|
|
94
|
+
--color-ui-tile: var(--chat-ui-tile);
|
|
95
|
+
--color-ui-inset: var(--chat-ui-inset);
|
|
85
96
|
--color-map-marker-text: var(--map-marker-text);
|
|
86
97
|
}
|
|
87
98
|
|
|
@@ -155,6 +166,24 @@
|
|
|
155
166
|
@media (prefers-reduced-motion: reduce) {
|
|
156
167
|
.chat-ui-changed { animation: none; }
|
|
157
168
|
}
|
|
169
|
+
/*
|
|
170
|
+
* A block stands in a band across the whole thread, hatched above and below, with its tiles at the
|
|
171
|
+
* reading width. The thread's scroller is the container the band measures, so a block anywhere
|
|
172
|
+
* else keeps the width of its column.
|
|
173
|
+
*/
|
|
174
|
+
.chat-scroll { container-type: inline-size; }
|
|
175
|
+
.chat-ui-band { position: relative; padding-block: 14px; }
|
|
176
|
+
.chat-ui-band::before, .chat-ui-band::after {
|
|
177
|
+
content: "";
|
|
178
|
+
position: absolute;
|
|
179
|
+
inset-inline: 0;
|
|
180
|
+
height: 14px;
|
|
181
|
+
border-block: 1px solid var(--border);
|
|
182
|
+
background-image: repeating-linear-gradient(-45deg, var(--chat-ui-hatch) 0 1px, transparent 1px 7px);
|
|
183
|
+
}
|
|
184
|
+
.chat-ui-band::before { top: 0; }
|
|
185
|
+
.chat-ui-band::after { bottom: 0; }
|
|
186
|
+
.chat-scroll .chat-ui-band::before, .chat-scroll .chat-ui-band::after { inset-inline: calc(50% - 50cqw); }
|
|
158
187
|
/* A source row a citation revealed. The row only takes focus from code, since its own button takes a click. */
|
|
159
188
|
.chat-ui-source:focus { animation: chat-flash 1600ms ease-out both; }
|
|
160
189
|
/* A visual still on its way says so only once it takes long enough to be noticed. */
|
|
@@ -225,6 +254,15 @@
|
|
|
225
254
|
|
|
226
255
|
/* A code block is shiki's own `<pre>`, which brings a background of its own to override. */
|
|
227
256
|
.chat-code { position: relative; min-height: 38px; border-radius: 8px; border: 1px solid var(--border); background: var(--chat-code-bg); overflow: hidden; }
|
|
257
|
+
/* A code block in an intelligent UI block is a card inset in its tile. */
|
|
258
|
+
.chat-ui-code .chat-code { background: var(--chat-ui-inset); }
|
|
259
|
+
/* Inside a Section the section is the tile. A tile that only frames its parts goes flat, so they keep the
|
|
260
|
+
section's own margin, and a card among them takes the inset ground so it still reads as one. Unlayered, so it outweighs
|
|
261
|
+
the utilities it overrides. */
|
|
262
|
+
.chat-ui-section-body .bg-ui-tile { background-color: var(--chat-ui-inset); }
|
|
263
|
+
.chat-ui-section-body .chat-ui-flat { background-color: transparent; padding: 0; }
|
|
264
|
+
.chat-ui-section-body .chat-ui-flat .chat-ui-flat-inner { padding-inline: 0; }
|
|
265
|
+
.chat-ui-section-body .chat-ui-flat thead { background-color: var(--chat-ui-tile); }
|
|
228
266
|
.chat-code pre { margin: 0; padding: round(0.625rem, 1px) round(0.75rem, 1px); overflow: auto; user-select: text; background: transparent !important; font-family: var(--font-mono); font-size: var(--text-code); line-height: var(--text-code--line-height); }
|
|
229
267
|
.chat-code code { font-family: inherit; }
|
|
230
268
|
.chat-code-actions { position: absolute; top: 4px; right: 4px; border-radius: 6px; background: var(--surface-raised); color: var(--text-muted); box-shadow: 0 0 0 1px var(--border); }
|