@adecore/agents-react 0.20.1 → 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/logic/generated-image.d.ts +27 -0
- package/dist/chat/logic/generated-image.js +32 -0
- package/dist/chat/logic/timeline-target.d.ts +13 -1
- package/dist/chat/logic/timeline-target.js +26 -0
- package/dist/chat/logic/timeline.js +16 -10
- 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/ImageView.d.ts +8 -2
- package/dist/chat/ui/ImageView.js +12 -6
- package/dist/chat/ui/SubagentTimeline.js +6 -2
- package/dist/chat/ui/Timeline.js +67 -7
- package/dist/chat/ui/intelligent-ui/UiBlockFrame.d.ts +23 -0
- package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +103 -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 +15 -0
- package/dist/chat/ui/intelligent-ui/UiReply.js +329 -0
- package/dist/chat/ui/intelligent-ui/block-local.d.ts +11 -0
- package/dist/chat/ui/intelligent-ui/block-local.js +42 -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 +15 -0
- package/dist/chat/ui/intelligent-ui/node-text.js +106 -0
- package/dist/chat/ui/intelligent-ui/registry.d.ts +2 -0
- package/dist/chat/ui/intelligent-ui/registry.js +51 -0
- package/dist/chat/ui/intelligent-ui/render-context.d.ts +53 -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 +123 -0
- package/dist/chat/ui/intelligent-ui/renderers/choices.d.ts +4 -0
- package/dist/chat/ui/intelligent-ui/renderers/choices.js +45 -0
- package/dist/chat/ui/intelligent-ui/renderers/content.d.ts +14 -0
- package/dist/chat/ui/intelligent-ui/renderers/content.js +59 -0
- package/dist/chat/ui/intelligent-ui/renderers/data.d.ts +7 -0
- package/dist/chat/ui/intelligent-ui/renderers/data.js +80 -0
- package/dist/chat/ui/intelligent-ui/renderers/inputs.d.ts +8 -0
- package/dist/chat/ui/intelligent-ui/renderers/inputs.js +72 -0
- package/dist/chat/ui/intelligent-ui/renderers/links.d.ts +10 -0
- package/dist/chat/ui/intelligent-ui/renderers/links.js +87 -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 +126 -0
- package/dist/chat/ui/intelligent-ui/use-ui-queries.d.ts +21 -0
- package/dist/chat/ui/intelligent-ui/use-ui-queries.js +272 -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 +21 -4
- 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 +11 -1
- package/dist/locales/en/agent-chat.json +101 -0
- package/dist/locales/nl/agent-chat.json +101 -0
- package/dist/state/chats.js +13 -0
- package/dist/theme.css +19 -0
- package/package.json +128 -7
- package/src/chat/logic/generated-image.ts +48 -0
- package/src/chat/logic/timeline-target.ts +41 -1
- package/src/chat/logic/timeline.ts +19 -10
- package/src/chat/subagent-conversation.ts +13 -2
- package/src/chat/timeline-flash.ts +5 -3
- package/src/chat/ui/ImageView.tsx +51 -8
- package/src/chat/ui/SubagentTimeline.tsx +24 -1
- package/src/chat/ui/Timeline.tsx +97 -21
- package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +219 -0
- package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +21 -0
- package/src/chat/ui/intelligent-ui/UiReply.tsx +541 -0
- package/src/chat/ui/intelligent-ui/block-local.ts +51 -0
- package/src/chat/ui/intelligent-ui/chart-data.ts +67 -0
- package/src/chat/ui/intelligent-ui/node-text.ts +119 -0
- package/src/chat/ui/intelligent-ui/registry.ts +53 -0
- package/src/chat/ui/intelligent-ui/render-context.ts +84 -0
- package/src/chat/ui/intelligent-ui/renderers/chart.tsx +319 -0
- package/src/chat/ui/intelligent-ui/renderers/choices.tsx +102 -0
- package/src/chat/ui/intelligent-ui/renderers/content.tsx +140 -0
- package/src/chat/ui/intelligent-ui/renderers/data.tsx +181 -0
- package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +144 -0
- package/src/chat/ui/intelligent-ui/renderers/links.tsx +158 -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 +143 -0
- package/src/chat/ui/intelligent-ui/use-ui-queries.ts +308 -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 +60 -4
- 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 +27 -1
- package/src/locales/en/agent-chat.json +101 -0
- package/src/locales/nl/agent-chat.json +101 -0
- package/src/state/chats.ts +13 -0
- package/src/theme.css +19 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Markdown } from '../Markdown.js';
|
|
4
|
+
/*
|
|
5
|
+
* One part of a block as the markdown it stands for, in a quiet surface with a line that says why.
|
|
6
|
+
* A person sees it as a repair, never as an error, so nothing here is red.
|
|
7
|
+
*/
|
|
8
|
+
export function UiFallbackPart({ fallback, problem }) {
|
|
9
|
+
const { t } = useTranslation('agent-chat');
|
|
10
|
+
return (_jsxs("div", { className: "flex flex-col gap-1 rounded-md bg-surface-hover p-2", children: [_jsx("span", { className: "text-xs text-text-faint", children: problem.kind === 'unknown' ? t('blocks.unknownComponent', { component: problem.component }) : t('blocks.renderFailed') }), fallback !== '' && _jsx(Markdown, { text: fallback, fileLinks: false })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactNode } from 'react';
|
|
2
|
+
import { type UiBlock } from '@adecore/intelligent-ui';
|
|
3
|
+
import type { ChatUiAnswer, ChatUiQueryState } from '@adecore/agent-contracts';
|
|
4
|
+
import { ReplyMarkdown } from '../Markdown.js';
|
|
5
|
+
import type { UiRenderContext } from './render-context.js';
|
|
6
|
+
type UiBlockIdentity = Pick<UiRenderContext, 'scopeId' | 'chatId' | 'itemId' | 'blockId' | 'phase'>;
|
|
7
|
+
export declare function UiReply({ text, blocks, context, answers, queries, reply }: {
|
|
8
|
+
text: string;
|
|
9
|
+
blocks: readonly UiBlock[];
|
|
10
|
+
context: Omit<UiBlockIdentity, 'blockId'>;
|
|
11
|
+
answers?: Readonly<Record<string, ChatUiAnswer>>;
|
|
12
|
+
queries?: ChatUiQueryState;
|
|
13
|
+
reply?: ComponentProps<typeof ReplyMarkdown>['reply'];
|
|
14
|
+
}): ReactNode;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Component, createContext, Fragment, memo, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
3
|
+
import ReactMarkdown, {} from 'react-markdown';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { evaluateUiBlock, uiInputValues, isUiComponent, UI_CATALOG_VERSION, UiState } from '@adecore/intelligent-ui';
|
|
6
|
+
import { ErrorBoundary, Tooltip } from '@adecore/ui';
|
|
7
|
+
import { chatHost } from '../../../host.js';
|
|
8
|
+
import { UiReplyNavigationContext } from '../reply-context.js';
|
|
9
|
+
import { ReplyMarkdown } from '../Markdown.js';
|
|
10
|
+
import { rehypeFadeWords } from '../rehype-fade.js';
|
|
11
|
+
import { UI_RENDERERS } from './registry.js';
|
|
12
|
+
import { UiPageMemory } from './block-local.js';
|
|
13
|
+
import { UiBlockFrame } from './UiBlockFrame.js';
|
|
14
|
+
import { UiFallbackPart } from './UiFallbackPart.js';
|
|
15
|
+
import { useUiLinks } from './use-ui-links.js';
|
|
16
|
+
import { useUiQueries, useUiLiveValues } from './use-ui-queries.js';
|
|
17
|
+
import { UiSourceCitation } from './renderers/content.js';
|
|
18
|
+
import { uiChildrenOf, revealUiSource, uiLiveChoiceReason, uiUrlDomain } from './node-text.js';
|
|
19
|
+
import { uiReplyParts } from './reply-parts.js';
|
|
20
|
+
const localStates = new UiPageMemory(64);
|
|
21
|
+
function localState(key, block, answered) {
|
|
22
|
+
let state = localStates.get(key);
|
|
23
|
+
if (!state) {
|
|
24
|
+
state = new UiState(block);
|
|
25
|
+
if (answered && answered.revision === block.revision) {
|
|
26
|
+
for (const [name, value] of Object.entries(answered.values ?? {})) {
|
|
27
|
+
state.set(name, value);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
localStates.set(key, state);
|
|
31
|
+
}
|
|
32
|
+
return state;
|
|
33
|
+
}
|
|
34
|
+
/* The block itself, as the parent of its top-level nodes. */
|
|
35
|
+
const BLOCK = '$block';
|
|
36
|
+
/*
|
|
37
|
+
* Parents whose text is read as written: code, a short tag, and the labels of controls, whose
|
|
38
|
+
* accessible name and sent text are that same string.
|
|
39
|
+
*/
|
|
40
|
+
const LITERAL_PARENTS = new Set(['CodeBlock', 'Tag', 'Choice', 'Item', 'Switch', 'Slider', 'Segmented', 'Option', 'Button', 'Column']);
|
|
41
|
+
/* Parents that stack their children, where a run of text and inline nodes is one paragraph. */
|
|
42
|
+
const PARAGRAPHS = {
|
|
43
|
+
[BLOCK]: 'px-2 text-xs text-text',
|
|
44
|
+
Tab: 'px-2 text-xs text-text',
|
|
45
|
+
Section: 'text-xs text-text-muted'
|
|
46
|
+
};
|
|
47
|
+
const LINKS = new Set(['File', 'Diff', 'Commit', 'Node']);
|
|
48
|
+
/* Constructs that would start a block or load an image stay text; without GFM a table never forms. */
|
|
49
|
+
const BLOCK_CONSTRUCTS = [
|
|
50
|
+
'blockQuote',
|
|
51
|
+
'codeFenced',
|
|
52
|
+
'codeIndented',
|
|
53
|
+
'definition',
|
|
54
|
+
'headingAtx',
|
|
55
|
+
'htmlFlow',
|
|
56
|
+
'htmlText',
|
|
57
|
+
'labelStartImage',
|
|
58
|
+
'list',
|
|
59
|
+
'setextUnderline',
|
|
60
|
+
'thematicBreak'
|
|
61
|
+
];
|
|
62
|
+
/* Prose between the tags of a block is inline: only emphasis, code and links are Markdown, and a blank line is a line break. */
|
|
63
|
+
function remarkInlineProse() {
|
|
64
|
+
// The field remark-parse reads its syntax extensions from, as remark-gfm adds its own.
|
|
65
|
+
const data = this.data();
|
|
66
|
+
data.micromarkExtensions = [...(data.micromarkExtensions ?? []), { disable: { null: BLOCK_CONSTRUCTS } }];
|
|
67
|
+
return (tree) => {
|
|
68
|
+
tree.children = [
|
|
69
|
+
{
|
|
70
|
+
type: 'paragraph',
|
|
71
|
+
children: (tree.children ?? []).flatMap((paragraph, index) => [...(index > 0 ? [{ type: 'break' }] : []), ...(paragraph.children ?? [])])
|
|
72
|
+
}
|
|
73
|
+
];
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function remarkCitations() {
|
|
77
|
+
return (tree) => {
|
|
78
|
+
const visit = (parent) => {
|
|
79
|
+
if (['link', 'inlineCode', 'code'].includes(parent.type)) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
parent.children = parent.children?.flatMap((child) => {
|
|
83
|
+
if (child.type !== 'text' || !child.value) {
|
|
84
|
+
visit(child);
|
|
85
|
+
return [child];
|
|
86
|
+
}
|
|
87
|
+
const parts = [];
|
|
88
|
+
let start = 0;
|
|
89
|
+
for (const match of child.value.matchAll(/\[(\d{1,3})\]/g)) {
|
|
90
|
+
parts.push({ type: 'text', value: child.value.slice(start, match.index) });
|
|
91
|
+
parts.push({
|
|
92
|
+
type: 'citation',
|
|
93
|
+
data: { hName: 'span', hProperties: { 'data-ui-citation': match[1] } },
|
|
94
|
+
children: [{ type: 'text', value: match[0] }]
|
|
95
|
+
});
|
|
96
|
+
start = match.index + match[0].length;
|
|
97
|
+
}
|
|
98
|
+
parts.push({ type: 'text', value: child.value.slice(start) });
|
|
99
|
+
return parts;
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
visit(tree);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const CitationContext = createContext({ sources: new Map() });
|
|
106
|
+
function ProseCitation({ number, children }) {
|
|
107
|
+
const { sources, reveal } = useContext(CitationContext);
|
|
108
|
+
const source = sources.get(number);
|
|
109
|
+
return source ? _jsx(UiSourceCitation, { number: number, title: source.title, url: source.url, onReveal: reveal ? () => reveal(source) : undefined }) : children;
|
|
110
|
+
}
|
|
111
|
+
function sourceTargets(nodes) {
|
|
112
|
+
const lists = [];
|
|
113
|
+
const visit = (nodes, ancestors = []) => {
|
|
114
|
+
for (const node of nodes) {
|
|
115
|
+
const path = ['Tab', 'Section'].includes(node.type) ? [...ancestors, node.id] : ancestors;
|
|
116
|
+
if (node.type === 'Sources') {
|
|
117
|
+
lists.push(uiChildrenOf(node, 'Source').map((source) => ({ id: source.id, title: source.props.title, url: source.props.url, ancestors: path })));
|
|
118
|
+
}
|
|
119
|
+
visit(node.children, path);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
visit(nodes);
|
|
123
|
+
// Numbers restart in each Sources list, so multiple lists have no unambiguous prose target.
|
|
124
|
+
return new Map(lists.length === 1 ? lists[0].map((source, index) => [index + 1, source]) : []);
|
|
125
|
+
}
|
|
126
|
+
const PROSE_PLUGINS = [remarkInlineProse, remarkCitations];
|
|
127
|
+
const PROSE_ELEMENTS = ['p', 'br', 'strong', 'em', 'code', 'a', 'span'];
|
|
128
|
+
const FADE_PLUGINS = [rehypeFadeWords];
|
|
129
|
+
const NO_PLUGINS = [];
|
|
130
|
+
const ProseUrlContext = createContext(undefined);
|
|
131
|
+
/* Through the host only, like a Source: the address is the model's, so nothing here navigates by itself. */
|
|
132
|
+
function ProseLink({ href, children }) {
|
|
133
|
+
const openUrl = useContext(ProseUrlContext);
|
|
134
|
+
if (openUrl === undefined || href === undefined || !/^https?:\/\//i.test(href)) {
|
|
135
|
+
return children;
|
|
136
|
+
}
|
|
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 }) }));
|
|
138
|
+
}
|
|
139
|
+
const PROSE_COMPONENTS = {
|
|
140
|
+
p: ({ children }) => children,
|
|
141
|
+
code: ({ children }) => _jsx("code", { className: "rounded-sm bg-surface-sunken px-1 py-px font-mono text-code", children: children }),
|
|
142
|
+
a: ({ href, children }) => _jsx(ProseLink, { href: href, children: children }),
|
|
143
|
+
span: ({ node, children, className }) => node?.properties['data-ui-citation'] !== undefined ? (_jsx(ProseCitation, { number: Number(node.properties['data-ui-citation']), children: children })) : (_jsx("span", { className: className, children: children }))
|
|
144
|
+
};
|
|
145
|
+
/*
|
|
146
|
+
* Text an agent wrote in a block, as inline Markdown without raw HTML. Markdown trims a paragraph,
|
|
147
|
+
* so the whitespace at either end stays text: it is the space between a word and the tag beside it.
|
|
148
|
+
* While the block streams every new word fades in, as in the prose of the reply around it.
|
|
149
|
+
*/
|
|
150
|
+
const UiProse = memo(function UiProse({ text, fade, openUrl }) {
|
|
151
|
+
const [, lead = '', body = '', trail = ''] = /^(\s*)([\s\S]*?)(\s*)$/.exec(text) ?? [];
|
|
152
|
+
if (body === '') {
|
|
153
|
+
return text;
|
|
154
|
+
}
|
|
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] }));
|
|
156
|
+
});
|
|
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. */
|
|
158
|
+
function writtenTexts(nodes, into = new Map()) {
|
|
159
|
+
for (const node of nodes) {
|
|
160
|
+
if (node.type === '$text') {
|
|
161
|
+
into.set(node.id, node.expressions.text === undefined);
|
|
162
|
+
}
|
|
163
|
+
writtenTexts(node.children, into);
|
|
164
|
+
}
|
|
165
|
+
return into;
|
|
166
|
+
}
|
|
167
|
+
function isInline(node) {
|
|
168
|
+
if (node.type === '$text') {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
return node.error === undefined && (node.type === 'Tag' || (LINKS.has(node.type) && node.children.length === 0));
|
|
172
|
+
}
|
|
173
|
+
function isBlank(node) {
|
|
174
|
+
return node.type === '$text' && String(node.props.text ?? '').trim() === '';
|
|
175
|
+
}
|
|
176
|
+
/* The children of one parent, each in its own boundary. Where the parent stacks them, a run of text and inline nodes becomes a paragraph. */
|
|
177
|
+
function UiNodes({ nodes, parent, texts, context }) {
|
|
178
|
+
const drawn = (node) => (_jsx(UiNodeBoundary, { node: node, children: _jsx(UiNodeBody, { node: node, parent: parent, texts: texts, context: context }) }, node.id));
|
|
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
|
+
}
|
|
184
|
+
if (paragraph === undefined) {
|
|
185
|
+
return nodes.map(drawn);
|
|
186
|
+
}
|
|
187
|
+
const output = [];
|
|
188
|
+
let run = [];
|
|
189
|
+
const close = () => {
|
|
190
|
+
if (run.some((node) => !isBlank(node))) {
|
|
191
|
+
output.push(_jsx("div", { className: paragraph, children: run.map(drawn) }, `run:${run[0].id}`));
|
|
192
|
+
}
|
|
193
|
+
run = [];
|
|
194
|
+
};
|
|
195
|
+
for (const node of nodes) {
|
|
196
|
+
if (isInline(node)) {
|
|
197
|
+
run.push(node);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
close();
|
|
201
|
+
output.push(drawn(node));
|
|
202
|
+
}
|
|
203
|
+
close();
|
|
204
|
+
return output;
|
|
205
|
+
}
|
|
206
|
+
class UiNodeBoundary extends Component {
|
|
207
|
+
state = { failed: false, node: this.props.node };
|
|
208
|
+
static getDerivedStateFromError() {
|
|
209
|
+
return { failed: true };
|
|
210
|
+
}
|
|
211
|
+
static getDerivedStateFromProps(props, state) {
|
|
212
|
+
return props.node === state.node ? null : { failed: false, node: props.node };
|
|
213
|
+
}
|
|
214
|
+
render() {
|
|
215
|
+
return this.state.failed ? _jsx(UiFallbackPart, { fallback: this.props.node.fallback, problem: { kind: 'failed' } }) : this.props.children;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function UiNodeBody({ node, parent, texts, context }) {
|
|
219
|
+
if (node.type === '$text') {
|
|
220
|
+
const text = String(node.props.text ?? '');
|
|
221
|
+
return LITERAL_PARENTS.has(parent) || texts.get(node.sourceId ?? node.id) !== true ? (text) : (_jsx(UiProse, { text: text, fade: context.phase === 'streaming', openUrl: context.openUrl }));
|
|
222
|
+
}
|
|
223
|
+
if (node.error !== undefined) {
|
|
224
|
+
return _jsx(UiFallbackPart, { fallback: node.fallback, problem: { kind: 'failed' } });
|
|
225
|
+
}
|
|
226
|
+
if (!isUiComponent(node.type)) {
|
|
227
|
+
return _jsx(UiFallbackPart, { fallback: node.fallback, problem: { kind: 'unknown', component: node.type } });
|
|
228
|
+
}
|
|
229
|
+
const Renderer = UI_RENDERERS[node.type];
|
|
230
|
+
if (Renderer === null) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
return (_jsx(Renderer, { node: node, context: context, children: _jsx(UiNodes, { nodes: node.children, parent: node.type, texts: texts, context: context }) }));
|
|
234
|
+
}
|
|
235
|
+
function UiBlockBody({ block, context, answered, frozen }) {
|
|
236
|
+
const { t } = useTranslation('agent-chat');
|
|
237
|
+
const key = JSON.stringify([context.scopeId, context.chatId, context.itemId, block.id]);
|
|
238
|
+
const [state] = useState(() => localState(key, block, answered));
|
|
239
|
+
state.sync(block);
|
|
240
|
+
const stateRevision = useSyncExternalStore((listener) => state.subscribe(listener), () => state.snapshot(), () => state.snapshot());
|
|
241
|
+
const [optimistic, setOptimistic] = useState(null);
|
|
242
|
+
const [failedChoiceId, setFailedChoiceId] = useState(null);
|
|
243
|
+
const pending = useRef(false);
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
if (answered && answered.revision === block.revision && answered.values) {
|
|
246
|
+
for (const [name, value] of Object.entries(answered.values)) {
|
|
247
|
+
state.set(name, value);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}, [state, answered, block.revision]);
|
|
251
|
+
const answer = answered && answered.revision === block.revision
|
|
252
|
+
? { choiceId: answered.choiceId, state: answered.queued ? 'queued' : 'sent' }
|
|
253
|
+
: optimistic?.revision === block.revision
|
|
254
|
+
? optimistic
|
|
255
|
+
: null;
|
|
256
|
+
const [element, setElement] = useState(null);
|
|
257
|
+
const navigation = useContext(UiReplyNavigationContext);
|
|
258
|
+
const flash = navigation?.flash;
|
|
259
|
+
const flashing = navigation?.chatId === context.chatId && flash?.itemId === context.itemId && flash.blockId === block.id && flash.revision === block.revision;
|
|
260
|
+
const queries = useUiQueries(block, state, context, element, frozen);
|
|
261
|
+
const links = useUiLinks(block, state, context, queries.reads, frozen);
|
|
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]);
|
|
275
|
+
const liveValue = useUiLiveValues(block, evaluation.nodes, queries.currentReadings);
|
|
276
|
+
const sendChoice = chatHost().intelligentUi?.sendChoice;
|
|
277
|
+
const rendering = {
|
|
278
|
+
...context,
|
|
279
|
+
...links,
|
|
280
|
+
answer,
|
|
281
|
+
failedChoiceId,
|
|
282
|
+
live: queries.live,
|
|
283
|
+
liveValue,
|
|
284
|
+
choiceReason: uiLiveChoiceReason(t, queries.live),
|
|
285
|
+
onChoose: !queries.ready || queries.reading
|
|
286
|
+
? undefined
|
|
287
|
+
: sendChoice && block.revision && block.complete && context.phase === 'final'
|
|
288
|
+
? (choiceId) => {
|
|
289
|
+
if (pending.current || answer || queries.reading) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const values = uiInputValues(block, state);
|
|
293
|
+
const reads = queries.readsFor(values);
|
|
294
|
+
if (reads === null) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
pending.current = true;
|
|
298
|
+
setFailedChoiceId(null);
|
|
299
|
+
const revision = block.revision;
|
|
300
|
+
setOptimistic({ revision, choiceId, state: 'sending' });
|
|
301
|
+
void sendChoice(context.scopeId, {
|
|
302
|
+
chatId: context.chatId,
|
|
303
|
+
itemId: context.itemId,
|
|
304
|
+
blockId: block.id,
|
|
305
|
+
revision,
|
|
306
|
+
choiceId,
|
|
307
|
+
reads,
|
|
308
|
+
values
|
|
309
|
+
})
|
|
310
|
+
.then((state) => setOptimistic({ revision, choiceId, state }))
|
|
311
|
+
.catch(() => {
|
|
312
|
+
setOptimistic(null);
|
|
313
|
+
setFailedChoiceId(choiceId);
|
|
314
|
+
})
|
|
315
|
+
.finally(() => {
|
|
316
|
+
pending.current = false;
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
: undefined
|
|
320
|
+
};
|
|
321
|
+
const unknown = block.catalogVersion !== UI_CATALOG_VERSION;
|
|
322
|
+
const failed = !unknown && !block.nodes.length && block.diagnostics.length > 0;
|
|
323
|
+
const shownAsText = unknown ? t('blocks.unreadable') : failed ? block.diagnostics[0].message : undefined;
|
|
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 }) })) })] }));
|
|
325
|
+
}
|
|
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. */
|
|
327
|
+
export function UiReply({ text, blocks, context, answers, queries, reply }) {
|
|
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])))) }));
|
|
329
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
}
|
|
11
|
+
export declare function useBlockLocal<Value>(context: UiRenderContext, nodeId: string, initial: () => Value): [Value, (next: Value) => void];
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
}
|
|
31
|
+
// Which tab or section a person opened, so a block the thread unmounts while scrolling comes back as they left it.
|
|
32
|
+
const remembered = new UiPageMemory(512);
|
|
33
|
+
/* State of one node that lasts as long as the page, such as a fold; local input state belongs to the runtime instead. */
|
|
34
|
+
export function useBlockLocal(context, nodeId, initial) {
|
|
35
|
+
const key = `${context.scopeId}\n${context.chatId}\n${context.itemId}\n${context.blockId}\n${nodeId}`;
|
|
36
|
+
const [value, setValue] = useState(() => (remembered.has(key) ? remembered.get(key) : initial()));
|
|
37
|
+
const set = (next) => {
|
|
38
|
+
remembered.set(key, next);
|
|
39
|
+
setValue(next);
|
|
40
|
+
};
|
|
41
|
+
return [value, set];
|
|
42
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { UiProps } from '@adecore/intelligent-ui';
|
|
2
|
+
export declare const UI_CHART_SERIES = 6;
|
|
3
|
+
export declare const UI_CHART_ROWS = 60;
|
|
4
|
+
export interface UiChartSeries {
|
|
5
|
+
key: string;
|
|
6
|
+
values: (number | null)[];
|
|
7
|
+
}
|
|
8
|
+
export interface UiChartData {
|
|
9
|
+
labels: string[];
|
|
10
|
+
series: UiChartSeries[];
|
|
11
|
+
min: number;
|
|
12
|
+
max: number;
|
|
13
|
+
truncated: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function uiChartData(data: UiProps<'Chart'>['data'], kind: UiProps<'Chart'>['kind']): UiChartData | null;
|
|
16
|
+
export declare function niceCeiling(value: number): number;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/* The catalog promises up to six series, one per chart color. */
|
|
2
|
+
export const UI_CHART_SERIES = 6;
|
|
3
|
+
/* Categories a chart draws; more would not fit a column of a thread. */
|
|
4
|
+
export const UI_CHART_ROWS = 60;
|
|
5
|
+
/*
|
|
6
|
+
* The series a chart can draw: every key besides `label` with a finite number in some row, the
|
|
7
|
+
* first six of them. A value that is missing or no finite number is a gap. Null when there is no
|
|
8
|
+
* row or no series, which draws the fallback.
|
|
9
|
+
*/
|
|
10
|
+
export function uiChartData(data, kind) {
|
|
11
|
+
const rows = data.slice(0, UI_CHART_ROWS);
|
|
12
|
+
const keys = [];
|
|
13
|
+
for (const row of rows) {
|
|
14
|
+
for (const [key, value] of Object.entries(row)) {
|
|
15
|
+
if (key !== 'label' && !keys.includes(key) && keys.length < UI_CHART_SERIES && typeof value === 'number' && Number.isFinite(value)) {
|
|
16
|
+
keys.push(key);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (rows.length === 0 || keys.length === 0) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const series = keys.map((key) => ({
|
|
24
|
+
key,
|
|
25
|
+
values: rows.map((row) => {
|
|
26
|
+
const value = row[key];
|
|
27
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
28
|
+
})
|
|
29
|
+
}));
|
|
30
|
+
const labels = rows.map((row) => (typeof row.label === 'string' || typeof row.label === 'number' ? String(row.label) : ''));
|
|
31
|
+
const extents = kind === 'stacked'
|
|
32
|
+
? labels.map((_, index) => series.reduce((sum, { values }) => sum + Math.max(0, values[index] ?? 0), 0))
|
|
33
|
+
: series.flatMap(({ values }) => values.filter((value) => value !== null));
|
|
34
|
+
const min = Math.min(0, ...extents);
|
|
35
|
+
const max = Math.max(0, ...extents);
|
|
36
|
+
return { labels, series, min, max: max === min ? min + 1 : max, truncated: data.length > rows.length };
|
|
37
|
+
}
|
|
38
|
+
/* A round number at or above `value`, for the top of an axis: 1, 2, 2.5 or 5 times a power of ten. */
|
|
39
|
+
export function niceCeiling(value) {
|
|
40
|
+
if (value <= 0) {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
const power = 10 ** Math.floor(Math.log10(value));
|
|
44
|
+
const step = [1, 2, 2.5, 5, 10].find((factor) => factor * power >= value) ?? 10;
|
|
45
|
+
return step * power;
|
|
46
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { TFunction } from 'i18next';
|
|
2
|
+
import type { UiComponentName, UiProps, UiViewNode } from '@adecore/intelligent-ui';
|
|
3
|
+
import type { UiLiveStatus } from './render-context.js';
|
|
4
|
+
export declare function uiNodeText(node: UiViewNode): string;
|
|
5
|
+
export declare function uiNodeLabel(node: UiViewNode): string;
|
|
6
|
+
export declare function uiChildrenOf<Name extends UiComponentName>(node: UiViewNode, type: Name): UiViewNode<UiProps<Name>>[];
|
|
7
|
+
export declare function uiBlockHead(nodes: readonly UiViewNode[]): {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
} | null;
|
|
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;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/* The text a node holds, every `$text` below it joined as written, such as the code of a CodeBlock. */
|
|
2
|
+
export function uiNodeText(node) {
|
|
3
|
+
return node.children.map((child) => (child.type === '$text' ? String(child.props.text ?? '') : uiNodeText(child))).join('');
|
|
4
|
+
}
|
|
5
|
+
/* The same text on one line, for a name a control or a screen reader needs as a string. */
|
|
6
|
+
export function uiNodeLabel(node) {
|
|
7
|
+
return uiNodeText(node).replace(/\s+/g, ' ').trim();
|
|
8
|
+
}
|
|
9
|
+
/* The valid children of one kind, which a parent reads as metadata: the columns of a table, the tabs of a strip. */
|
|
10
|
+
export function uiChildrenOf(node, type) {
|
|
11
|
+
return node.children.filter((child) => child.type === type && child.error === undefined);
|
|
12
|
+
}
|
|
13
|
+
/* A Summary draws its text as inline Markdown, so the name it gives its block leaves out the code ticks, the strong marks and the address of a link. */
|
|
14
|
+
function withoutMarks(text) {
|
|
15
|
+
return text
|
|
16
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
17
|
+
.replace(/\*\*|`/g, '')
|
|
18
|
+
.trim();
|
|
19
|
+
}
|
|
20
|
+
/* The Summary that heads a block, which is its first node; a Summary further down is a subheading. */
|
|
21
|
+
export function uiBlockHead(nodes) {
|
|
22
|
+
const first = nodes[0];
|
|
23
|
+
if (first === undefined || first.type !== 'Summary' || first.error !== undefined) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return { id: first.id, label: withoutMarks(uiNodeLabel(first)) };
|
|
27
|
+
}
|
|
28
|
+
/*
|
|
29
|
+
* Scrolls the Source row of `nodeId` under `root` into view and focuses it, which lights the row once.
|
|
30
|
+
* Opens enclosing tabs and sections first; false when the source is absent.
|
|
31
|
+
*/
|
|
32
|
+
export async function revealUiSource(root, nodeId, ancestors = []) {
|
|
33
|
+
for (const id of ancestors) {
|
|
34
|
+
const control = [...root.querySelectorAll('[data-ui-reveal]')].find((element) => element.dataset.uiReveal === id);
|
|
35
|
+
if (control && (control.getAttribute('aria-expanded') === 'false' || control.getAttribute('aria-selected') === 'false')) {
|
|
36
|
+
control.click();
|
|
37
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const row = [...root.querySelectorAll('[data-ui-source]')].find((element) => element.dataset.uiSource === nodeId);
|
|
41
|
+
if (row === undefined) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
// The light runs when focus arrives, so a row that already has it lets go first.
|
|
45
|
+
if (row.ownerDocument.activeElement === row) {
|
|
46
|
+
row.blur();
|
|
47
|
+
}
|
|
48
|
+
row.focus({ preventScroll: true });
|
|
49
|
+
const reduced = row.ownerDocument.defaultView?.matchMedia('(prefers-reduced-motion: reduce)').matches === true;
|
|
50
|
+
row.scrollIntoView({ block: 'nearest', behavior: reduced ? 'auto' : 'smooth' });
|
|
51
|
+
return true;
|
|
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
|
+
}
|