@adecore/agents-react 0.21.0 → 0.21.1
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.d.ts +14 -1
- package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +47 -5
- package/dist/chat/ui/intelligent-ui/UiReply.d.ts +3 -1
- package/dist/chat/ui/intelligent-ui/UiReply.js +34 -30
- package/dist/chat/ui/intelligent-ui/block-local.d.ts +9 -0
- package/dist/chat/ui/intelligent-ui/block-local.js +30 -1
- package/dist/chat/ui/intelligent-ui/node-text.d.ts +6 -0
- package/dist/chat/ui/intelligent-ui/node-text.js +54 -0
- package/dist/chat/ui/intelligent-ui/registry.js +2 -1
- package/dist/chat/ui/intelligent-ui/render-context.d.ts +3 -0
- package/dist/chat/ui/intelligent-ui/renderers/chart.js +5 -7
- package/dist/chat/ui/intelligent-ui/renderers/choices.js +6 -2
- package/dist/chat/ui/intelligent-ui/renderers/content.js +6 -13
- package/dist/chat/ui/intelligent-ui/renderers/data.js +8 -15
- package/dist/chat/ui/intelligent-ui/renderers/inputs.d.ts +1 -0
- package/dist/chat/ui/intelligent-ui/renderers/inputs.js +16 -4
- package/dist/chat/ui/intelligent-ui/renderers/links.js +2 -1
- package/dist/chat/ui/intelligent-ui/use-ui-links.d.ts +4 -4
- package/dist/chat/ui/intelligent-ui/use-ui-links.js +29 -10
- package/dist/chat/ui/intelligent-ui/use-ui-queries.d.ts +2 -1
- package/dist/chat/ui/intelligent-ui/use-ui-queries.js +34 -21
- package/dist/chat/ui/rows/MessageRows.js +1 -1
- package/dist/locales/en/agent-chat.json +23 -4
- package/dist/locales/nl/agent-chat.json +23 -4
- package/dist/state/chats.js +5 -1
- package/package.json +24 -29
- package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +135 -5
- package/src/chat/ui/intelligent-ui/UiReply.tsx +48 -38
- package/src/chat/ui/intelligent-ui/block-local.ts +36 -1
- package/src/chat/ui/intelligent-ui/node-text.ts +60 -0
- package/src/chat/ui/intelligent-ui/registry.ts +2 -1
- package/src/chat/ui/intelligent-ui/render-context.ts +6 -0
- package/src/chat/ui/intelligent-ui/renderers/chart.tsx +5 -7
- package/src/chat/ui/intelligent-ui/renderers/choices.tsx +6 -2
- package/src/chat/ui/intelligent-ui/renderers/content.tsx +9 -14
- package/src/chat/ui/intelligent-ui/renderers/data.tsx +13 -19
- package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +28 -5
- package/src/chat/ui/intelligent-ui/renderers/links.tsx +2 -1
- package/src/chat/ui/intelligent-ui/use-ui-links.ts +31 -11
- package/src/chat/ui/intelligent-ui/use-ui-queries.ts +48 -22
- package/src/chat/ui/rows/MessageRows.tsx +1 -1
- package/src/locales/en/agent-chat.json +23 -4
- package/src/locales/nl/agent-chat.json +23 -4
- package/src/state/chats.ts +5 -1
- package/dist/chat/ui/intelligent-ui/UiBlockFooter.d.ts +0 -15
- package/dist/chat/ui/intelligent-ui/UiBlockFooter.js +0 -50
- package/src/chat/ui/intelligent-ui/UiBlockFooter.tsx +0 -139
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
2
|
import type { UiViewNode } from '@adecore/intelligent-ui';
|
|
3
|
-
import { type
|
|
3
|
+
import { type UiLiveStatus } from './render-context.js';
|
|
4
|
+
export interface UiBlockFix {
|
|
5
|
+
code: string;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
export interface UiBlockFooterProps {
|
|
9
|
+
live?: UiLiveStatus | null;
|
|
10
|
+
answered?: {
|
|
11
|
+
label: string;
|
|
12
|
+
at: number;
|
|
13
|
+
} | null;
|
|
14
|
+
fixes?: readonly UiBlockFix[];
|
|
15
|
+
shownAsText?: string | null;
|
|
16
|
+
}
|
|
4
17
|
export interface UiBlockFrameProps extends UiBlockFooterProps {
|
|
5
18
|
nodes: readonly UiViewNode[];
|
|
6
19
|
phase: 'streaming' | 'final';
|
|
@@ -1,11 +1,53 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useLayoutEffect, useRef, useState } from 'react';
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId, useLayoutEffect, useRef, useState } from 'react';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { Lock, Radio, TriangleAlert, Wrench } from 'lucide-react';
|
|
6
|
+
import { Icon, Skeleton, Spinner, Tooltip, useTickingText } from '@adecore/ui';
|
|
7
|
+
import { formatAgo, formatMoment } from '@adecore/ui/format';
|
|
8
|
+
import { uiBlockHead, uiReasonText } from './node-text.js';
|
|
7
9
|
import { UiHeadContext } from './render-context.js';
|
|
8
|
-
import {
|
|
10
|
+
import { useLater } from './use-later.js';
|
|
11
|
+
const NO_FIXES = [];
|
|
12
|
+
const READING_SPINNER_MS = 1000;
|
|
13
|
+
/* The one sign of a live block: what it reads, and when it last read it. A reading spins only after a second. */
|
|
14
|
+
function LivePart({ live }) {
|
|
15
|
+
const { t } = useTranslation('agent-chat');
|
|
16
|
+
const slow = useLater(live.state === 'reading', READING_SPINNER_MS);
|
|
17
|
+
const { readAt } = live;
|
|
18
|
+
const fresh = live.state === 'fresh' || live.state === 'reading';
|
|
19
|
+
const ago = useTickingText(() => {
|
|
20
|
+
if (readAt === null) {
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
const time = formatAgo(Date.now() - readAt);
|
|
24
|
+
return fresh ? t('blocks.live.read', { time }) : time;
|
|
25
|
+
});
|
|
26
|
+
const source = live.source ?? live.sources[0] ?? '';
|
|
27
|
+
const icon = slow ? (_jsx(Spinner, { size: 12 })) : live.state === 'failed' ? (_jsx(Icon, { icon: TriangleAlert, size: 12, className: "text-status-needs-you" })) : live.state === 'refused' ? (_jsx(Icon, { icon: Lock, size: 12 })) : (_jsx(Icon, { icon: Radio, size: 12, className: "text-status-idle" }));
|
|
28
|
+
const words = live.state === 'failed'
|
|
29
|
+
? t('blocks.live.failed', { source })
|
|
30
|
+
: live.state === 'refused'
|
|
31
|
+
? t('blocks.live.refused', { source })
|
|
32
|
+
: t('blocks.live.label');
|
|
33
|
+
const reason = live.state === 'failed' || live.state === 'refused' ? uiReasonText(t, live.code, live.reason) : undefined;
|
|
34
|
+
const hint = reason !== undefined ? reason : live.sources.length > 0 ? t('blocks.live.sources', { sources: live.sources.join(', ') }) : null;
|
|
35
|
+
const body = (_jsxs("span", { tabIndex: hint === null ? undefined : 0, className: "flex items-center gap-1.5", children: [icon, _jsx("span", { children: words }), readAt !== null && live.state !== 'refused' && (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx("time", { dateTime: new Date(readAt).toISOString(), children: _jsx("span", { ref: ago, className: "tabular-nums" }) })] })), readAt === null && fresh && (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx("span", { children: t('blocks.live.notRead') })] }))] }));
|
|
36
|
+
return hint === null ? body : _jsx(Tooltip, { label: hint, children: body });
|
|
37
|
+
}
|
|
38
|
+
/*
|
|
39
|
+
* The line under a block, only when it has something to say: live, answered, repaired or drawn as
|
|
40
|
+
* text, in that order. A hairline of the soft border runs the full width of the card over it.
|
|
41
|
+
*/
|
|
42
|
+
function UiBlockFooter({ live = null, answered = null, fixes = NO_FIXES, shownAsText = null }) {
|
|
43
|
+
const { t } = useTranslation('agent-chat');
|
|
44
|
+
const [fixesOpen, setFixesOpen] = useState(false);
|
|
45
|
+
const listId = useId();
|
|
46
|
+
if (live === null && answered === null && fixes.length === 0 && shownAsText === null) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return (_jsxs("div", { className: "-mx-2.25 mt-2.25 -mb-2.25 flex flex-col gap-1.5 border-t border-border-soft px-4.25 py-2 text-xs text-text-faint", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1", children: [live !== null && _jsx(LivePart, { live: live }), answered !== null && (_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { children: t('blocks.answered', { label: answered.label }) }), _jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx("time", { dateTime: new Date(answered.at).toISOString(), children: formatMoment(answered.at) })] })), fixes.length > 0 && (_jsxs("button", { type: "button", "aria-expanded": fixesOpen, "aria-controls": listId, className: "flex items-center gap-1.5 rounded-sm hover:text-text-muted", onClick: () => setFixesOpen(!fixesOpen), children: [_jsx(Icon, { icon: Wrench, size: 12 }), t('blocks.fixes', { count: fixes.length })] })), shownAsText !== null && (_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { children: t('blocks.shownAsText') }), _jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx("span", { children: shownAsText })] }))] }), fixes.length > 0 && (_jsx("ul", { id: listId, hidden: !fixesOpen, className: "flex flex-col gap-0.5 text-text-muted", children: fixes.map((fix, index) => (_jsxs("li", { className: "flex gap-2", children: [_jsx("span", { className: "shrink-0 font-mono", children: fix.code }), _jsx("span", { className: "min-w-0", children: fix.message })] }, index))) }))] }));
|
|
50
|
+
}
|
|
9
51
|
const RELEASE_MS = 200;
|
|
10
52
|
/*
|
|
11
53
|
* The tallest the card has been, while `active`: a streaming block grows and never shrinks until its
|
|
@@ -3,11 +3,13 @@ import { type UiBlock } from '@adecore/intelligent-ui';
|
|
|
3
3
|
import type { ChatUiAnswer, ChatUiQueryState } from '@adecore/agent-contracts';
|
|
4
4
|
import { ReplyMarkdown } from '../Markdown.js';
|
|
5
5
|
import type { UiRenderContext } from './render-context.js';
|
|
6
|
+
type UiBlockIdentity = Pick<UiRenderContext, 'scopeId' | 'chatId' | 'itemId' | 'blockId' | 'phase'>;
|
|
6
7
|
export declare function UiReply({ text, blocks, context, answers, queries, reply }: {
|
|
7
8
|
text: string;
|
|
8
9
|
blocks: readonly UiBlock[];
|
|
9
|
-
context: Omit<
|
|
10
|
+
context: Omit<UiBlockIdentity, 'blockId'>;
|
|
10
11
|
answers?: Readonly<Record<string, ChatUiAnswer>>;
|
|
11
12
|
queries?: ChatUiQueryState;
|
|
12
13
|
reply?: ComponentProps<typeof ReplyMarkdown>['reply'];
|
|
13
14
|
}): ReactNode;
|
|
15
|
+
export {};
|
|
@@ -1,26 +1,25 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Component, createContext, Fragment, memo, useContext, useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
2
|
+
import { Component, createContext, Fragment, memo, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
3
3
|
import ReactMarkdown, {} from 'react-markdown';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import { evaluateUiBlock, uiInputValues, isUiComponent, UI_CATALOG_VERSION, UiState } from '@adecore/intelligent-ui';
|
|
6
|
-
import { ErrorBoundary } from '@adecore/ui';
|
|
6
|
+
import { ErrorBoundary, Tooltip } from '@adecore/ui';
|
|
7
7
|
import { chatHost } from '../../../host.js';
|
|
8
8
|
import { UiReplyNavigationContext } from '../reply-context.js';
|
|
9
9
|
import { ReplyMarkdown } from '../Markdown.js';
|
|
10
10
|
import { rehypeFadeWords } from '../rehype-fade.js';
|
|
11
11
|
import { UI_RENDERERS } from './registry.js';
|
|
12
|
+
import { UiPageMemory } from './block-local.js';
|
|
12
13
|
import { UiBlockFrame } from './UiBlockFrame.js';
|
|
13
14
|
import { UiFallbackPart } from './UiFallbackPart.js';
|
|
14
15
|
import { useUiLinks } from './use-ui-links.js';
|
|
15
16
|
import { useUiQueries, useUiLiveValues } from './use-ui-queries.js';
|
|
16
17
|
import { UiSourceCitation } from './renderers/content.js';
|
|
17
|
-
import { uiChildrenOf, revealUiSource } from './node-text.js';
|
|
18
|
+
import { uiChildrenOf, revealUiSource, uiLiveChoiceReason, uiUrlDomain } from './node-text.js';
|
|
18
19
|
import { uiReplyParts } from './reply-parts.js';
|
|
19
|
-
const localStates = new
|
|
20
|
-
const LOCAL_STATE_LIMIT = 64;
|
|
20
|
+
const localStates = new UiPageMemory(64);
|
|
21
21
|
function localState(key, block, answered) {
|
|
22
22
|
let state = localStates.get(key);
|
|
23
|
-
localStates.delete(key);
|
|
24
23
|
if (!state) {
|
|
25
24
|
state = new UiState(block);
|
|
26
25
|
if (answered && answered.revision === block.revision) {
|
|
@@ -28,10 +27,7 @@ function localState(key, block, answered) {
|
|
|
28
27
|
state.set(name, value);
|
|
29
28
|
}
|
|
30
29
|
}
|
|
31
|
-
|
|
32
|
-
localStates.set(key, state);
|
|
33
|
-
if (localStates.size > LOCAL_STATE_LIMIT) {
|
|
34
|
-
localStates.delete(localStates.keys().next().value);
|
|
30
|
+
localStates.set(key, state);
|
|
35
31
|
}
|
|
36
32
|
return state;
|
|
37
33
|
}
|
|
@@ -41,7 +37,7 @@ const BLOCK = '$block';
|
|
|
41
37
|
* Parents whose text is read as written: code, a short tag, and the labels of controls, whose
|
|
42
38
|
* accessible name and sent text are that same string.
|
|
43
39
|
*/
|
|
44
|
-
const LITERAL_PARENTS = new Set(['CodeBlock', 'Tag', 'Choice', 'Item', 'Switch', 'Slider', 'Segmented', 'Option', 'Column']);
|
|
40
|
+
const LITERAL_PARENTS = new Set(['CodeBlock', 'Tag', 'Choice', 'Item', 'Switch', 'Slider', 'Segmented', 'Option', 'Button', 'Column']);
|
|
45
41
|
/* Parents that stack their children, where a run of text and inline nodes is one paragraph. */
|
|
46
42
|
const PARAGRAPHS = {
|
|
47
43
|
[BLOCK]: 'px-2 text-xs text-text',
|
|
@@ -129,8 +125,6 @@ function sourceTargets(nodes) {
|
|
|
129
125
|
}
|
|
130
126
|
const PROSE_PLUGINS = [remarkInlineProse, remarkCitations];
|
|
131
127
|
const PROSE_ELEMENTS = ['p', 'br', 'strong', 'em', 'code', 'a', 'span'];
|
|
132
|
-
// Raw HTML never parses here, so the only spans are the words the fade wraps.
|
|
133
|
-
const FADING_PROSE_ELEMENTS = [...PROSE_ELEMENTS, 'span'];
|
|
134
128
|
const FADE_PLUGINS = [rehypeFadeWords];
|
|
135
129
|
const NO_PLUGINS = [];
|
|
136
130
|
const ProseUrlContext = createContext(undefined);
|
|
@@ -140,7 +134,7 @@ function ProseLink({ href, children }) {
|
|
|
140
134
|
if (openUrl === undefined || href === undefined || !/^https?:\/\//i.test(href)) {
|
|
141
135
|
return children;
|
|
142
136
|
}
|
|
143
|
-
return (_jsx("button", { type: "button", className: "cursor-pointer text-accent underline underline-offset-2 select-text", onClick: () => openUrl(href), children: children }));
|
|
137
|
+
return (_jsx(Tooltip, { label: uiUrlDomain(href), children: _jsx("button", { type: "button", className: "cursor-pointer text-accent underline underline-offset-2 select-text", onClick: () => openUrl(href), children: children }) }));
|
|
144
138
|
}
|
|
145
139
|
const PROSE_COMPONENTS = {
|
|
146
140
|
p: ({ children }) => children,
|
|
@@ -158,7 +152,7 @@ const UiProse = memo(function UiProse({ text, fade, openUrl }) {
|
|
|
158
152
|
if (body === '') {
|
|
159
153
|
return text;
|
|
160
154
|
}
|
|
161
|
-
return (_jsxs(ProseUrlContext, { value: openUrl, children: [lead, _jsx(ReactMarkdown, { remarkPlugins: PROSE_PLUGINS, rehypePlugins: fade ? FADE_PLUGINS : NO_PLUGINS, allowedElements:
|
|
155
|
+
return (_jsxs(ProseUrlContext, { value: openUrl, children: [lead, _jsx(ReactMarkdown, { remarkPlugins: PROSE_PLUGINS, rehypePlugins: fade ? FADE_PLUGINS : NO_PLUGINS, allowedElements: PROSE_ELEMENTS, unwrapDisallowed: true, components: PROSE_COMPONENTS, children: body }), trail] }));
|
|
162
156
|
});
|
|
163
157
|
/* Which text nodes of a block the agent wrote out, by id; the value of an expression is data and stays as it is. */
|
|
164
158
|
function writtenTexts(nodes, into = new Map()) {
|
|
@@ -183,6 +177,10 @@ function isBlank(node) {
|
|
|
183
177
|
function UiNodes({ nodes, parent, texts, context }) {
|
|
184
178
|
const drawn = (node) => (_jsx(UiNodeBoundary, { node: node, children: _jsx(UiNodeBody, { node: node, parent: parent, texts: texts, context: context }) }, node.id));
|
|
185
179
|
const paragraph = PARAGRAPHS[parent];
|
|
180
|
+
if (parent === 'Choices') {
|
|
181
|
+
// A primary choice stands first in the order a keyboard and a screen reader meet it too, not only on screen.
|
|
182
|
+
return [...nodes].sort((a, b) => Number(b.props.primary === true) - Number(a.props.primary === true)).map(drawn);
|
|
183
|
+
}
|
|
186
184
|
if (paragraph === undefined) {
|
|
187
185
|
return nodes.map(drawn);
|
|
188
186
|
}
|
|
@@ -239,7 +237,7 @@ function UiBlockBody({ block, context, answered, frozen }) {
|
|
|
239
237
|
const key = JSON.stringify([context.scopeId, context.chatId, context.itemId, block.id]);
|
|
240
238
|
const [state] = useState(() => localState(key, block, answered));
|
|
241
239
|
state.sync(block);
|
|
242
|
-
useSyncExternalStore((listener) => state.subscribe(listener), () => state.snapshot(), () => state.snapshot());
|
|
240
|
+
const stateRevision = useSyncExternalStore((listener) => state.subscribe(listener), () => state.snapshot(), () => state.snapshot());
|
|
243
241
|
const [optimistic, setOptimistic] = useState(null);
|
|
244
242
|
const [failedChoiceId, setFailedChoiceId] = useState(null);
|
|
245
243
|
const pending = useRef(false);
|
|
@@ -254,14 +252,26 @@ function UiBlockBody({ block, context, answered, frozen }) {
|
|
|
254
252
|
? { choiceId: answered.choiceId, state: answered.queued ? 'queued' : 'sent' }
|
|
255
253
|
: optimistic?.revision === block.revision
|
|
256
254
|
? optimistic
|
|
257
|
-
:
|
|
255
|
+
: null;
|
|
258
256
|
const [element, setElement] = useState(null);
|
|
259
257
|
const navigation = useContext(UiReplyNavigationContext);
|
|
260
258
|
const flash = navigation?.flash;
|
|
261
259
|
const flashing = navigation?.chatId === context.chatId && flash?.itemId === context.itemId && flash.blockId === block.id && flash.revision === block.revision;
|
|
262
260
|
const queries = useUiQueries(block, state, context, element, frozen);
|
|
263
261
|
const links = useUiLinks(block, state, context, queries.reads, frozen);
|
|
264
|
-
|
|
262
|
+
// The state changes in place, so its revision is what tells this evaluation it went stale.
|
|
263
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
|
264
|
+
const evaluation = useMemo(() => evaluateUiBlock(block, state), [block, state, stateRevision]);
|
|
265
|
+
const texts = useMemo(() => writtenTexts(block.nodes), [block]);
|
|
266
|
+
const sources = useMemo(() => sourceTargets(evaluation.nodes), [evaluation]);
|
|
267
|
+
const citations = useMemo(() => ({
|
|
268
|
+
sources,
|
|
269
|
+
reveal: element
|
|
270
|
+
? (source) => {
|
|
271
|
+
void revealUiSource(element, source.id, source.ancestors);
|
|
272
|
+
}
|
|
273
|
+
: undefined
|
|
274
|
+
}), [sources, element]);
|
|
265
275
|
const liveValue = useUiLiveValues(block, evaluation.nodes, queries.currentReadings);
|
|
266
276
|
const sendChoice = chatHost().intelligentUi?.sendChoice;
|
|
267
277
|
const rendering = {
|
|
@@ -269,8 +279,9 @@ function UiBlockBody({ block, context, answered, frozen }) {
|
|
|
269
279
|
...links,
|
|
270
280
|
answer,
|
|
271
281
|
failedChoiceId,
|
|
272
|
-
live: queries.live
|
|
273
|
-
liveValue
|
|
282
|
+
live: queries.live,
|
|
283
|
+
liveValue,
|
|
284
|
+
choiceReason: uiLiveChoiceReason(t, queries.live),
|
|
274
285
|
onChoose: !queries.ready || queries.reading
|
|
275
286
|
? undefined
|
|
276
287
|
: sendChoice && block.revision && block.complete && context.phase === 'final'
|
|
@@ -305,21 +316,14 @@ function UiBlockBody({ block, context, answered, frozen }) {
|
|
|
305
316
|
pending.current = false;
|
|
306
317
|
});
|
|
307
318
|
}
|
|
308
|
-
:
|
|
319
|
+
: undefined
|
|
309
320
|
};
|
|
310
321
|
const unknown = block.catalogVersion !== UI_CATALOG_VERSION;
|
|
311
322
|
const failed = !unknown && !block.nodes.length && block.diagnostics.length > 0;
|
|
312
323
|
const shownAsText = unknown ? t('blocks.unreadable') : failed ? block.diagnostics[0].message : undefined;
|
|
313
|
-
return (_jsxs("div", { ref: setElement, className: "relative", "data-ui-block": block.id, "data-ui-revision": block.revision, tabIndex: -1, children: [flashing && _jsx("span", { className: "chat-flash pointer-events-none absolute inset-0 rounded-lg", "aria-hidden": true }, flash.nonce), _jsx(UiBlockFrame, { live: queries.live, nodes: evaluation.nodes, phase: context.phase, fixes: [...block.diagnostics, ...evaluation.diagnostics], shownAsText: shownAsText, answered: answered, children: unknown || failed ? (_jsx(ReplyMarkdown, { text: block.fallback, streaming: false })) : (_jsx(CitationContext, { value: {
|
|
314
|
-
sources: sourceTargets(evaluation.nodes),
|
|
315
|
-
reveal: element
|
|
316
|
-
? (source) => {
|
|
317
|
-
void revealUiSource(element, source.id, source.ancestors);
|
|
318
|
-
}
|
|
319
|
-
: undefined
|
|
320
|
-
}, children: _jsx(UiNodes, { nodes: evaluation.nodes, parent: BLOCK, texts: writtenTexts(block.nodes), context: rendering }) })) })] }));
|
|
324
|
+
return (_jsxs("div", { ref: setElement, className: "relative", "data-ui-block": block.id, "data-ui-revision": block.revision, tabIndex: -1, children: [flashing && _jsx("span", { className: "chat-flash pointer-events-none absolute inset-0 rounded-lg", "aria-hidden": true }, flash.nonce), _jsx(UiBlockFrame, { live: queries.live, nodes: evaluation.nodes, phase: context.phase, fixes: [...block.diagnostics, ...evaluation.diagnostics], shownAsText: shownAsText, answered: answered, children: unknown || failed ? (_jsx(ReplyMarkdown, { text: block.fallback, streaming: false })) : (_jsx(CitationContext, { value: citations, children: _jsx(UiNodes, { nodes: evaluation.nodes, parent: BLOCK, texts: texts, context: rendering }) })) })] }));
|
|
321
325
|
}
|
|
322
326
|
/* Prose and cards a block gap apart, as the rows of a turn are; text that is only the newline between two fences draws nothing. */
|
|
323
327
|
export function UiReply({ text, blocks, context, answers, queries, reply }) {
|
|
324
|
-
return (_jsx("div", { className: "flex flex-col gap-(--chat-block-gap)", children: uiReplyParts(text, blocks, context.phase === 'streaming').map((part, index) => part.kind === 'text' ? (part.text.trim() !== '' && (_jsx(Fragment, { children: _jsx(ReplyMarkdown, { text: part.text, streaming: context.phase === 'streaming', reply: reply }) }, `text:${index}`))) : (_jsx(ErrorBoundary, { label: "UI block",
|
|
328
|
+
return (_jsx("div", { className: "flex flex-col gap-(--chat-block-gap)", children: uiReplyParts(text, blocks, context.phase === 'streaming').map((part, index) => part.kind === 'text' ? (part.text.trim() !== '' && (_jsx(Fragment, { children: _jsx(ReplyMarkdown, { text: part.text, streaming: context.phase === 'streaming', reply: reply }) }, `text:${index}`))) : (_jsx(ErrorBoundary, { label: "UI block", children: _jsx(UiBlockBody, { block: part.block, frozen: queries?.blocks[part.block.id], answered: answers?.[part.block.id], context: { scopeId: context.scopeId, chatId: context.chatId, itemId: context.itemId, phase: context.phase, blockId: part.block.id } }) }, JSON.stringify([context.scopeId, context.chatId, context.itemId, part.block.id])))) }));
|
|
325
329
|
}
|
|
@@ -1,2 +1,11 @@
|
|
|
1
1
|
import type { UiRenderContext } from './render-context.js';
|
|
2
|
+
export declare class UiPageMemory<Value> {
|
|
3
|
+
private readonly entries;
|
|
4
|
+
private readonly limit;
|
|
5
|
+
constructor(limit: number);
|
|
6
|
+
has(key: string): boolean;
|
|
7
|
+
get(key: string): Value | undefined;
|
|
8
|
+
set(key: string, value: Value): void;
|
|
9
|
+
get size(): number;
|
|
10
|
+
}
|
|
2
11
|
export declare function useBlockLocal<Value>(context: UiRenderContext, nodeId: string, initial: () => Value): [Value, (next: Value) => void];
|
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
+
/* Values that last as long as the page, past `limit` forgetting the one used longest ago. */
|
|
3
|
+
export class UiPageMemory {
|
|
4
|
+
entries = new Map();
|
|
5
|
+
limit;
|
|
6
|
+
constructor(limit) {
|
|
7
|
+
this.limit = limit;
|
|
8
|
+
}
|
|
9
|
+
has(key) {
|
|
10
|
+
return this.entries.has(key);
|
|
11
|
+
}
|
|
12
|
+
/* Reading a value counts as using it. */
|
|
13
|
+
get(key) {
|
|
14
|
+
const value = this.entries.get(key);
|
|
15
|
+
if (value !== undefined) {
|
|
16
|
+
this.set(key, value);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
set(key, value) {
|
|
21
|
+
this.entries.delete(key);
|
|
22
|
+
this.entries.set(key, value);
|
|
23
|
+
if (this.entries.size > this.limit) {
|
|
24
|
+
this.entries.delete(this.entries.keys().next().value);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
get size() {
|
|
28
|
+
return this.entries.size;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
2
31
|
// Which tab or section a person opened, so a block the thread unmounts while scrolling comes back as they left it.
|
|
3
|
-
const remembered = new
|
|
32
|
+
const remembered = new UiPageMemory(512);
|
|
4
33
|
/* State of one node that lasts as long as the page, such as a fold; local input state belongs to the runtime instead. */
|
|
5
34
|
export function useBlockLocal(context, nodeId, initial) {
|
|
6
35
|
const key = `${context.scopeId}\n${context.chatId}\n${context.itemId}\n${context.blockId}\n${nodeId}`;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { TFunction } from 'i18next';
|
|
1
2
|
import type { UiComponentName, UiProps, UiViewNode } from '@adecore/intelligent-ui';
|
|
3
|
+
import type { UiLiveStatus } from './render-context.js';
|
|
2
4
|
export declare function uiNodeText(node: UiViewNode): string;
|
|
3
5
|
export declare function uiNodeLabel(node: UiViewNode): string;
|
|
4
6
|
export declare function uiChildrenOf<Name extends UiComponentName>(node: UiViewNode, type: Name): UiViewNode<UiProps<Name>>[];
|
|
@@ -7,3 +9,7 @@ export declare function uiBlockHead(nodes: readonly UiViewNode[]): {
|
|
|
7
9
|
label: string;
|
|
8
10
|
} | null;
|
|
9
11
|
export declare function revealUiSource(root: ParentNode, nodeId: string, ancestors?: readonly string[]): Promise<boolean>;
|
|
12
|
+
export declare function uiReasonText(t: TFunction<'agent-chat'>, code: string | undefined, reason: string | undefined): string | undefined;
|
|
13
|
+
export declare function uiWithUnit(text: string, unit: string | undefined): string;
|
|
14
|
+
export declare function uiLiveChoiceReason(t: TFunction<'agent-chat'>, live: UiLiveStatus | null | undefined): string | null;
|
|
15
|
+
export declare function uiUrlDomain(url: string): string;
|
|
@@ -50,3 +50,57 @@ export async function revealUiSource(root, nodeId, ancestors = []) {
|
|
|
50
50
|
row.scrollIntoView({ block: 'nearest', behavior: reduced ? 'auto' : 'smooth' });
|
|
51
51
|
return true;
|
|
52
52
|
}
|
|
53
|
+
/* The reason codes this version says in its own words; any other code reads as the reason the machine gave. */
|
|
54
|
+
const UI_REASON_CODES = new Set([
|
|
55
|
+
'access-unavailable',
|
|
56
|
+
'block-stale',
|
|
57
|
+
'busy',
|
|
58
|
+
'link-unchecked',
|
|
59
|
+
'link-unsupported',
|
|
60
|
+
'links-unsupported',
|
|
61
|
+
'no-unique-match',
|
|
62
|
+
'outside-project',
|
|
63
|
+
'query-undeclared',
|
|
64
|
+
'refresh-limit',
|
|
65
|
+
'result-too-large',
|
|
66
|
+
'snapshot-too-large',
|
|
67
|
+
'source-unregistered',
|
|
68
|
+
'stale-read',
|
|
69
|
+
'target-unavailable',
|
|
70
|
+
'timed-out',
|
|
71
|
+
'unreadable'
|
|
72
|
+
]);
|
|
73
|
+
/* Why a reading failed or a link stayed plain, in the person's language where the code is known. */
|
|
74
|
+
export function uiReasonText(t, code, reason) {
|
|
75
|
+
return code !== undefined && UI_REASON_CODES.has(code) ? t(`blocks.live.reasons.${code}`) : reason;
|
|
76
|
+
}
|
|
77
|
+
/*
|
|
78
|
+
* A number with its unit, where a percent sign hugs the number and every other unit stands apart
|
|
79
|
+
* from it, by a space that never lets the two wrap apart.
|
|
80
|
+
*/
|
|
81
|
+
export function uiWithUnit(text, unit) {
|
|
82
|
+
if (unit === undefined || unit === '') {
|
|
83
|
+
return text;
|
|
84
|
+
}
|
|
85
|
+
return unit === '%' ? `${text}${unit}` : `${text}\u00a0${unit}`;
|
|
86
|
+
}
|
|
87
|
+
/* Why the choices of a live block are closed: its data is still being read, or the reason a reading failed. Null while nothing live holds them. */
|
|
88
|
+
export function uiLiveChoiceReason(t, live) {
|
|
89
|
+
if (live === null || live === undefined || live.state === 'fresh') {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (live.state === 'reading') {
|
|
93
|
+
return t('blocks.choice.reading');
|
|
94
|
+
}
|
|
95
|
+
const source = live.source ?? live.sources[0] ?? '';
|
|
96
|
+
return uiReasonText(t, live.code, live.reason) ?? t(live.state === 'failed' ? 'blocks.live.failed' : 'blocks.live.refused', { source });
|
|
97
|
+
}
|
|
98
|
+
/* The host of an address without its `www.`, which is how a source or a link in prose says where it goes. */
|
|
99
|
+
export function uiUrlDomain(url) {
|
|
100
|
+
try {
|
|
101
|
+
return new URL(url).hostname.replace(/^www\./, '');
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return url;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -2,7 +2,7 @@ import { ChartRenderer } from './renderers/chart.js';
|
|
|
2
2
|
import { ChoiceRenderer, ChoicesRenderer } from './renderers/choices.js';
|
|
3
3
|
import { CodeBlockRenderer, ImageRenderer, SourceRenderer, SourcesRenderer } from './renderers/content.js';
|
|
4
4
|
import { EntityListRenderer, EntryRenderer, StatRenderer, StatsRenderer, TableRenderer } from './renderers/data.js';
|
|
5
|
-
import { ChecklistRenderer, ItemRenderer, SegmentedRenderer, SliderRenderer, SwitchRenderer } from './renderers/inputs.js';
|
|
5
|
+
import { ButtonRenderer, ChecklistRenderer, ItemRenderer, SegmentedRenderer, SliderRenderer, SwitchRenderer } from './renderers/inputs.js';
|
|
6
6
|
import { CommitRenderer, DiffRenderer, FileRenderer, NodeRenderer } from './renderers/links.js';
|
|
7
7
|
import { SectionRenderer, SectionsRenderer, TabRenderer, TabsRenderer } from './renderers/structure.js';
|
|
8
8
|
import { CalloutRenderer, ProgressRenderer, StepRenderer, StepsRenderer, SummaryRenderer, TagRenderer } from './renderers/text.js';
|
|
@@ -43,6 +43,7 @@ export const UI_RENDERERS = {
|
|
|
43
43
|
Slider: SliderRenderer,
|
|
44
44
|
Segmented: SegmentedRenderer,
|
|
45
45
|
Option: null,
|
|
46
|
+
Button: ButtonRenderer,
|
|
46
47
|
Show: null,
|
|
47
48
|
Each: null,
|
|
48
49
|
Choices: ChoicesRenderer,
|
|
@@ -6,6 +6,7 @@ export interface UiLink {
|
|
|
6
6
|
state: 'chip' | 'plain';
|
|
7
7
|
label?: string;
|
|
8
8
|
reason?: string;
|
|
9
|
+
code?: string;
|
|
9
10
|
}
|
|
10
11
|
export interface UiAnswer {
|
|
11
12
|
choiceId: string;
|
|
@@ -17,6 +18,7 @@ export interface UiLiveStatus {
|
|
|
17
18
|
readAt: number | null;
|
|
18
19
|
source?: string;
|
|
19
20
|
reason?: string;
|
|
21
|
+
code?: string;
|
|
20
22
|
}
|
|
21
23
|
export interface UiLiveValue {
|
|
22
24
|
previous?: unknown;
|
|
@@ -36,6 +38,7 @@ export interface UiRenderContext {
|
|
|
36
38
|
openLink?(target: UiLinkTarget): void;
|
|
37
39
|
openUrl?(url: string): void;
|
|
38
40
|
onChoose?(nodeId: string): void;
|
|
41
|
+
choiceReason?: string | null;
|
|
39
42
|
liveValue?(nodeId: string, prop: string, path?: readonly [row: number, key: string]): UiLiveValue | undefined;
|
|
40
43
|
}
|
|
41
44
|
export interface UiRendererProps<Props> {
|
|
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
5
5
|
import { SegmentBar, useMeasuredWidth } from '@adecore/ui';
|
|
6
6
|
import { formatRounded } from '@adecore/ui/format';
|
|
7
7
|
import { niceCeiling, UI_CHART_ROWS, uiChartData } from '../chart-data.js';
|
|
8
|
+
import { uiWithUnit } from '../node-text.js';
|
|
8
9
|
import { UiFallbackPart } from '../UiFallbackPart.js';
|
|
9
10
|
const HEIGHT = 160;
|
|
10
11
|
const HBAR_ROW = 24;
|
|
@@ -21,11 +22,7 @@ function seriesColor(index) {
|
|
|
21
22
|
return `var(--chart-${index + 1})`;
|
|
22
23
|
}
|
|
23
24
|
function valueText(value, unit) {
|
|
24
|
-
|
|
25
|
-
return '';
|
|
26
|
-
}
|
|
27
|
-
const text = formatRounded(value, 2);
|
|
28
|
-
return unit === undefined || unit === '' ? text : unit === '%' ? `${text}%` : `${text} ${unit}`;
|
|
25
|
+
return value === null ? '' : uiWithUnit(formatRounded(value, 2), unit);
|
|
29
26
|
}
|
|
30
27
|
/* The height a chart will take, which its place holds while the node streams. */
|
|
31
28
|
function chartHeight(kind, rows) {
|
|
@@ -87,11 +84,12 @@ function VerticalChart({ chart, kind, unit, width }) {
|
|
|
87
84
|
}, [])
|
|
88
85
|
.join(' ');
|
|
89
86
|
return (_jsxs(_Fragment, { children: [_jsxs("svg", { width: width, height: HEIGHT, "aria-hidden": true, className: "block overflow-visible", onPointerLeave: () => setHover(null), children: [hover !== null && (_jsx("rect", { x: Math.round(AXIS_WIDTH + band * hover), y: PLOT_TOP, width: Math.round(band), height: plotHeight, className: "fill-surface-hover" })), ticks.map((tick) => (_jsxs("g", { children: [_jsx("line", { x1: AXIS_WIDTH, x2: width, y1: y(tick), y2: y(tick), className: tick === 0 ? 'stroke-border' : 'stroke-border-soft', shapeRendering: "crispEdges" }), _jsx("text", { x: AXIS_WIDTH - 6, y: y(tick), dy: "0.32em", textAnchor: "end", className: "fill-text-faint text-xs tabular-nums", children: valueText(tick, unit) })] }, tick))), chart.labels.map((label, index) => index % labelEvery === 0 ? (_jsx("text", { x: center(index), y: HEIGHT - 4, textAnchor: "middle", className: "fill-text-faint text-xs", children: label }, index)) : null), kind === 'bar' ? (_jsx("g", { className: clsx(GROW, 'starting:scale-y-0'), style: { transformOrigin: `0px ${baseline}px` }, children: chart.series.map((series, at) => series.values.map((value, index) => value === null ? null : (_jsx("rect", { x: Math.round(AXIS_WIDTH + band * index + gap / 2 + barWidth * at), y: Math.min(y(value), baseline), width: Math.max(1, Math.round(barWidth) - 1), height: Math.abs(y(value) - baseline), rx: 2, style: { fill: seriesColor(at) } }, `${series.key}:${index}`)))) })) : (_jsx("g", { className: clsx(GROW, 'starting:opacity-0'), children: chart.series.map((series, at) => (_jsxs("g", { style: { color: seriesColor(at) }, children: [_jsx("path", { d: segments(series.values), fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinejoin: "round", strokeLinecap: "round" }), chart.labels.length <= DOTS_UP_TO &&
|
|
90
|
-
series.values.map((value, index) => value === null ? null : _jsx("circle", { cx: center(index), cy: y(value), r:
|
|
87
|
+
series.values.map((value, index) => value === null ? null : _jsx("circle", { cx: center(index), cy: y(value), r: 3, fill: "currentColor" }, index))] }, series.key))) })), chart.labels.map((_, index) => (_jsx("rect", { x: Math.round(AXIS_WIDTH + band * index), y: 0, width: Math.max(1, Math.round(band)), height: HEIGHT, fill: "transparent", onPointerEnter: () => setHover(index) }, index)))] }), hover !== null && _jsx(Readout, { chart: chart, index: hover, unit: unit, left: Math.min(Math.max(center(hover), 64), Math.max(64, width - 64)) })] }));
|
|
91
88
|
}
|
|
92
89
|
/* A row of 24px per category, its label beside the bars. */
|
|
93
90
|
function HorizontalChart({ chart, unit }) {
|
|
94
|
-
|
|
91
|
+
// Bars grow right from zero only, so a chart of only negative values has no length to share and keeps a unit scale.
|
|
92
|
+
const high = niceCeiling(chart.max) || 1;
|
|
95
93
|
const single = chart.series.length === 1;
|
|
96
94
|
return (_jsx("div", { "aria-hidden": true, className: "flex flex-col", children: chart.labels.map((label, index) => (_jsxs("div", { className: "flex h-6 items-center gap-2 text-xs", children: [_jsx("span", { className: "w-2/5 max-w-36 shrink-0 truncate text-text-muted", children: label }), _jsx("span", { className: "flex min-w-0 grow flex-col justify-center gap-px", children: chart.series.map((series, at) => (_jsx("span", { className: clsx(GROW, 'block origin-left rounded-sm starting:scale-x-0', single ? 'h-3' : 'h-1'), style: { width: `${(Math.max(0, series.values[index] ?? 0) / high) * 100}%`, backgroundColor: seriesColor(at) } }, series.key))) }), single && (_jsx("span", { className: "w-16 shrink-0 text-right text-text-faint tabular-nums", children: valueText(chart.series[0].values[index] ?? null, unit) }))] }, index))) }));
|
|
97
95
|
}
|
|
@@ -30,12 +30,16 @@ export function ChoiceRenderer({ node, children, context }) {
|
|
|
30
30
|
const failed = answer === null && context.failedChoiceId === node.id;
|
|
31
31
|
const closed = waiting || unavailable || answer !== null;
|
|
32
32
|
const slow = useLater(chosen && answer.state === 'sending', SPINNER_AFTER_MS);
|
|
33
|
-
const reason = waiting
|
|
33
|
+
const reason = waiting
|
|
34
|
+
? t('blocks.choice.waiting')
|
|
35
|
+
: answer === null && unavailable
|
|
36
|
+
? ((disabled === true ? null : context.choiceReason) ?? t('blocks.choice.unavailable'))
|
|
37
|
+
: null;
|
|
34
38
|
const described = detail !== undefined && detail !== '';
|
|
35
39
|
const row = (_jsxs("button", { type: "button", "aria-disabled": closed || undefined, "aria-describedby": described ? descriptionId : undefined, className: clsx('flex w-full items-start gap-2 rounded-md border px-2 py-1.5 text-left transition-[opacity,scale,background-color] duration-150 motion-reduce:transition-none', chosen ? 'border-accent bg-accent-soft' : failed ? 'border-status-error bg-surface-hover' : 'border-border bg-surface-hover', !closed && 'hover:bg-surface-active active:scale-99', waiting ? 'opacity-60' : closed && !chosen && 'opacity-50'), onClick: () => {
|
|
36
40
|
if (!closed) {
|
|
37
41
|
onChoose?.(node.id);
|
|
38
42
|
}
|
|
39
43
|
}, children: [_jsx(Icon, { icon: ArrowRight, size: 14, className: clsx('mt-1 shrink-0', primary === true ? 'text-accent' : 'text-text-faint') }), _jsxs("span", { className: "flex min-w-0 grow flex-col", children: [_jsx("span", { className: "text-sm font-semibold text-text", children: children }), described && (_jsx("span", { id: descriptionId, className: "line-clamp-2 text-xs text-text-muted", children: detail }))] }), chosen && (_jsxs("span", { className: "mt-0.5 flex shrink-0 items-center gap-1 text-xs text-accent", children: [slow ? _jsx(Spinner, { size: 12 }) : _jsx(Icon, { icon: Check, size: 12 }), answer.state === 'queued' ? t('blocks.choice.queued') : t('blocks.choice.sent')] }))] }));
|
|
40
|
-
return (_jsxs("div", { className:
|
|
44
|
+
return (_jsxs("div", { className: "flex flex-col gap-1", children: [reason !== null ? (_jsx(DisabledReason, { reason: reason, children: row })) : described && detail.length > LONG_CONTEXT ? (_jsx(Tooltip, { label: detail, children: row })) : (row), failed && (_jsx("p", { role: "alert", className: "px-2 text-xs text-status-error", children: t('blocks.choice.failed') }))] }));
|
|
41
45
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import clsx from 'clsx';
|
|
3
|
-
import { createContext, useContext } from 'react';
|
|
3
|
+
import { createContext, useContext, useId } from 'react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import { PreviewCard } from '@adecore/ui';
|
|
6
6
|
import { formatNumber } from '@adecore/ui/format';
|
|
@@ -8,7 +8,7 @@ import { chatHost } from '../../../../host.js';
|
|
|
8
8
|
import { CodeBlock } from '../../CodeBlock.js';
|
|
9
9
|
import { CodeStreamingContext } from '../../code-streaming.js';
|
|
10
10
|
import { ImageThumb } from '../../ImageView.js';
|
|
11
|
-
import { uiChildrenOf, uiNodeLabel, uiNodeText } from '../node-text.js';
|
|
11
|
+
import { uiChildrenOf, uiNodeLabel, uiNodeText, uiUrlDomain } from '../node-text.js';
|
|
12
12
|
// The number of every source by its node id, which the list counts in its own order.
|
|
13
13
|
const SourceNumbers = createContext(new Map());
|
|
14
14
|
/* Plain text in the same measure until the node closes, so highlighting never makes it jump. */
|
|
@@ -33,16 +33,9 @@ export function ImageRenderer({ node, children, context }) {
|
|
|
33
33
|
}
|
|
34
34
|
export function SourcesRenderer({ node, children }) {
|
|
35
35
|
const { t } = useTranslation('agent-chat');
|
|
36
|
+
const headingId = useId();
|
|
36
37
|
const numbers = new Map(uiChildrenOf(node, 'Source').map((source, index) => [source.id, index + 1]));
|
|
37
|
-
return (_jsx(SourceNumbers, { value: numbers, children: _jsxs("section", { "aria-
|
|
38
|
-
}
|
|
39
|
-
function domainOf(url) {
|
|
40
|
-
try {
|
|
41
|
-
return new URL(url).hostname.replace(/^www\./, '');
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
return url;
|
|
45
|
-
}
|
|
38
|
+
return (_jsx(SourceNumbers, { value: numbers, children: _jsxs("section", { "aria-labelledby": headingId, className: "flex flex-col", children: [_jsx("span", { id: headingId, className: "px-2 pb-1 text-xs font-medium text-text-muted", children: t('blocks.sources') }), _jsx("ol", { className: "chat-ui-nodes flex flex-col", children: children })] }) }));
|
|
46
39
|
}
|
|
47
40
|
/*
|
|
48
41
|
* A source number in running text. Resting on it or focusing it shows the title and the domain, and
|
|
@@ -51,7 +44,7 @@ function domainOf(url) {
|
|
|
51
44
|
*/
|
|
52
45
|
export function UiSourceCitation({ number, title, url, onReveal, className }) {
|
|
53
46
|
const { t } = useTranslation('agent-chat');
|
|
54
|
-
return (_jsxs(PreviewCard.Root, { children: [_jsx(PreviewCard.Trigger, { delay: 300, closeDelay: 150, render: onReveal === undefined ? _jsx("span", {}) : _jsx("button", { type: "button" }), "aria-label": onReveal === undefined ? undefined : t('blocks.citation', { number: formatNumber(number), title }), className: clsx('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', onReveal !== undefined && 'cursor-pointer hover:bg-text/12 hover:text-text', className), onClick: onReveal, children: formatNumber(number) }), _jsxs(PreviewCard.Popup, { side: "top", align: "center", sideOffset: 4, className: "flex max-w-72 flex-col px-2.5 py-1.5 text-xs", children: [_jsx("span", { className: "line-clamp-2 text-text", children: title }), _jsx("span", { className: "truncate text-text-faint", children:
|
|
47
|
+
return (_jsxs(PreviewCard.Root, { children: [_jsx(PreviewCard.Trigger, { delay: 300, closeDelay: 150, render: onReveal === undefined ? _jsx("span", {}) : _jsx("button", { type: "button" }), "aria-label": onReveal === undefined ? undefined : t('blocks.citation', { number: formatNumber(number), title }), className: clsx('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', onReveal !== undefined && 'cursor-pointer hover:bg-text/12 hover:text-text', className), onClick: onReveal, children: formatNumber(number) }), _jsxs(PreviewCard.Popup, { side: "top", align: "center", sideOffset: 4, className: "flex max-w-72 flex-col px-2.5 py-1.5 text-xs", children: [_jsx("span", { className: "line-clamp-2 text-text", children: title }), _jsx("span", { className: "truncate text-text-faint", children: uiUrlDomain(url) })] })] }));
|
|
55
48
|
}
|
|
56
49
|
/*
|
|
57
50
|
* Opens through the host and loads nothing before, not even an icon. The row itself takes focus
|
|
@@ -61,6 +54,6 @@ export function SourceRenderer({ node, context }) {
|
|
|
61
54
|
const number = useContext(SourceNumbers).get(node.id);
|
|
62
55
|
const { title, url } = node.props;
|
|
63
56
|
const { openUrl } = context;
|
|
64
|
-
const face = (_jsxs(_Fragment, { children: [_jsx("span", { className: "w-5 shrink-0 text-right text-text-faint tabular-nums", children: number }), _jsx("span", { className: "min-w-0 truncate text-text", children: title }), _jsx("span", { className: "shrink-0 text-text-faint", children:
|
|
57
|
+
const face = (_jsxs(_Fragment, { children: [_jsx("span", { className: "w-5 shrink-0 text-right text-text-faint tabular-nums", children: number }), _jsx("span", { className: "min-w-0 truncate text-text", children: title }), _jsx("span", { className: "shrink-0 text-text-faint", children: uiUrlDomain(url) })] }));
|
|
65
58
|
return (_jsx("li", { "data-ui-source": node.id, tabIndex: -1, className: "chat-ui-source scroll-my-2 rounded-md", children: openUrl === undefined ? (_jsx("span", { className: "flex h-8 items-center gap-2 px-2 text-xs", children: face })) : (_jsx("button", { type: "button", className: "flex h-8 w-full items-center gap-2 rounded-md px-2 text-left text-xs hover:bg-surface-hover", onClick: () => openUrl(url), children: face })) }));
|
|
66
59
|
}
|
|
@@ -5,20 +5,12 @@ import { ArrowDown, ArrowRight, ArrowUp } from 'lucide-react';
|
|
|
5
5
|
import { Button, Icon, KeyValueList, Pill } from '@adecore/ui';
|
|
6
6
|
import { formatBytes, formatDuration, formatMoment, formatNumber, formatPercent, formatRounded } from '@adecore/ui/format';
|
|
7
7
|
import { useBlockLocal } from '../block-local.js';
|
|
8
|
-
import { uiChildrenOf } from '../node-text.js';
|
|
8
|
+
import { uiChildrenOf, uiWithUnit } from '../node-text.js';
|
|
9
9
|
import { isNumericColumn, UI_TABLE_ROWS, uiTableCell, uiTableColumns } from '../table-data.js';
|
|
10
10
|
import { UI_TONE_TEXT } from '../tones.js';
|
|
11
11
|
import { UiFallbackPart } from '../UiFallbackPart.js';
|
|
12
|
-
import { UiLinkChip } from './links.js';
|
|
13
12
|
/* EntityList draws a hairline between its rows from this many on. */
|
|
14
13
|
const DIVIDED_FROM = 7;
|
|
15
|
-
/* A number with its unit, where a percent sign hugs the number and every other unit stands apart from it. */
|
|
16
|
-
function withUnit(text, unit) {
|
|
17
|
-
if (unit === undefined || unit === '') {
|
|
18
|
-
return text;
|
|
19
|
-
}
|
|
20
|
-
return unit === '%' ? `${text}${unit}` : `${text} ${unit}`;
|
|
21
|
-
}
|
|
22
14
|
/*
|
|
23
15
|
* A number that live readings keep up to date. Keyed by its text, so a value that changed mounts
|
|
24
16
|
* again and lights up once, even when it changes again before the last light settled.
|
|
@@ -37,10 +29,10 @@ export function StatRenderer({ node, context }) {
|
|
|
37
29
|
const live = context.liveValue?.(node.id, 'value');
|
|
38
30
|
const change = previous === undefined ? null : value - previous;
|
|
39
31
|
const arrow = change === null || change === 0 ? ArrowRight : change > 0 ? ArrowUp : ArrowDown;
|
|
40
|
-
const from = previous === undefined ? '' :
|
|
32
|
+
const from = previous === undefined ? '' : uiWithUnit(formatRounded(previous, 2), unit);
|
|
41
33
|
// A change from zero has no percentage.
|
|
42
34
|
const percent = change === null || previous === undefined || previous === 0 ? null : formatPercent(Math.abs((change / previous) * 100));
|
|
43
|
-
return (_jsxs("div", { className: "flex min-w-0 flex-col gap-0.5 rounded-md bg-surface-hover px-2 py-1.5", children: [_jsx("span", { className: "truncate text-xs text-text-muted", children: label }), _jsx(LiveFigure, { text:
|
|
35
|
+
return (_jsxs("div", { className: "flex min-w-0 flex-col gap-0.5 rounded-md bg-surface-hover px-2 py-1.5", children: [_jsx("span", { className: "truncate text-xs text-text-muted", children: label }), _jsx(LiveFigure, { text: uiWithUnit(formatRounded(value, 2), unit), previous: typeof live?.previous === 'number' ? uiWithUnit(formatRounded(live.previous, 2), unit) : undefined, live: live, className: "text-lg font-semibold tabular-nums" }), change !== null && (_jsxs("span", { className: clsx('flex items-center gap-1 text-xs tabular-nums', tone === undefined ? 'text-text-faint' : UI_TONE_TEXT[tone]), children: [_jsx(Icon, { icon: arrow, size: 12, className: "shrink-0" }), _jsx("span", { className: "min-w-0 truncate", children: percent === null ? t('blocks.stat.from', { previous: from }) : t('blocks.stat.change', { change: percent, previous: from }) })] }))] }));
|
|
44
36
|
}
|
|
45
37
|
export function EntityListRenderer({ node, children }) {
|
|
46
38
|
return (_jsx(KeyValueList.Root, { divided: uiChildrenOf(node, 'Entry').length >= DIVIDED_FROM, className: "chat-ui-nodes px-2", children: children }));
|
|
@@ -49,9 +41,9 @@ export function EntryRenderer({ node, children }) {
|
|
|
49
41
|
return (_jsxs(KeyValueList.Item, { children: [_jsx(KeyValueList.Name, { children: node.props.label }), _jsx(KeyValueList.Value, { children: children })] }));
|
|
50
42
|
}
|
|
51
43
|
function numberText(cell, unit) {
|
|
52
|
-
return cell.as === 'bytes' ? formatBytes(cell.value) : cell.as === 'duration' ? formatDuration(cell.value) :
|
|
44
|
+
return cell.as === 'bytes' ? formatBytes(cell.value) : cell.as === 'duration' ? formatDuration(cell.value) : uiWithUnit(formatRounded(cell.value, 2), unit);
|
|
53
45
|
}
|
|
54
|
-
function Cell({ value, column, live
|
|
46
|
+
function Cell({ value, column, live }) {
|
|
55
47
|
const cell = uiTableCell(value, column);
|
|
56
48
|
switch (cell.kind) {
|
|
57
49
|
case 'empty':
|
|
@@ -65,7 +57,8 @@ function Cell({ value, column, live, context }) {
|
|
|
65
57
|
case 'date':
|
|
66
58
|
return _jsx("time", { dateTime: new Date(cell.at).toISOString(), children: formatMoment(cell.at) });
|
|
67
59
|
case 'file':
|
|
68
|
-
|
|
60
|
+
// A cell is no link target the host checked, so its path stays text.
|
|
61
|
+
return _jsx("span", { className: "font-mono text-code break-all", children: cell.path });
|
|
69
62
|
case 'tag':
|
|
70
63
|
return (_jsx(Pill, { shape: "tag", size: "sm", children: cell.text }));
|
|
71
64
|
}
|
|
@@ -83,5 +76,5 @@ export function TableRenderer({ node, context }) {
|
|
|
83
76
|
}
|
|
84
77
|
const { rows } = node.props;
|
|
85
78
|
const shown = all ? rows : rows.slice(0, UI_TABLE_ROWS);
|
|
86
|
-
return (_jsxs("div", { className: "flex flex-col items-start gap-1.5", children: [_jsx("div", { className: "max-h-80 w-full overflow-auto rounded-md border border-border-soft", children: _jsxs("table", { className: "w-full border-collapse text-xs tabular-nums", children: [_jsx("thead", { className: "sticky top-0 z-1 bg-surface-hover", children: _jsx("tr", { children: columns.map((column) => (_jsx("th", { scope: "col", className: clsx('h-8 px-2 font-medium whitespace-nowrap text-text-muted', isNumericColumn(column) ? 'text-right' : 'text-left'), children: column.title }, column.key))) }) }), _jsx("tbody", { children: shown.map((row, index) => (_jsx("tr", { className: "border-t border-border-soft", children: columns.map((column) => (_jsx("td", { className: clsx('h-8 px-2 text-text', isNumericColumn(column) ? 'text-right whitespace-nowrap' : 'text-left'), children: _jsx(Cell, { value: row[column.key], column: column, live: isNumericColumn(column) ? context.liveValue?.(node.id, 'rows', [index, column.key]) : undefined
|
|
79
|
+
return (_jsxs("div", { className: "flex flex-col items-start gap-1.5", children: [_jsx("div", { className: "max-h-80 w-full overflow-auto rounded-md border border-border-soft", children: _jsxs("table", { "aria-label": t('blocks.table.label', { columns: columns.map((column) => column.title).join(', ') }), className: "w-full border-collapse text-xs tabular-nums", children: [_jsx("thead", { className: "sticky top-0 z-1 bg-surface-hover", children: _jsx("tr", { children: columns.map((column) => (_jsx("th", { scope: "col", className: clsx('h-8 px-2 font-medium whitespace-nowrap text-text-muted', isNumericColumn(column) ? 'text-right' : 'text-left'), children: column.title }, column.key))) }) }), _jsx("tbody", { children: shown.map((row, index) => (_jsx("tr", { className: "border-t border-border-soft", children: columns.map((column) => (_jsx("td", { className: clsx('h-8 px-2 text-text', isNumericColumn(column) ? 'text-right whitespace-nowrap' : 'text-left'), children: _jsx(Cell, { value: row[column.key], column: column, live: isNumericColumn(column) ? context.liveValue?.(node.id, 'rows', [index, column.key]) : undefined }) }, column.key))) }, index))) })] }) }), !all && rows.length > UI_TABLE_ROWS && (_jsx(Button, { size: "xs", variant: "secondary", onClick: () => setAll(true), children: t('blocks.table.showAll', { total: formatNumber(rows.length) }) }))] }));
|
|
87
80
|
}
|