@adecore/agents-react 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/chat/chat-client.d.ts +1 -0
  2. package/dist/chat/logic/generated-image.d.ts +27 -0
  3. package/dist/chat/logic/generated-image.js +32 -0
  4. package/dist/chat/logic/timeline-target.d.ts +19 -1
  5. package/dist/chat/logic/timeline-target.js +49 -3
  6. package/dist/chat/logic/timeline.js +25 -17
  7. package/dist/chat/subagent-conversation.d.ts +2 -1
  8. package/dist/chat/subagent-conversation.js +7 -1
  9. package/dist/chat/timeline-flash.d.ts +3 -1
  10. package/dist/chat/timeline-flash.js +2 -2
  11. package/dist/chat/ui/CodeBlock.d.ts +3 -1
  12. package/dist/chat/ui/CodeBlock.js +38 -7
  13. package/dist/chat/ui/Composer.js +13 -3
  14. package/dist/chat/ui/ImageView.d.ts +8 -2
  15. package/dist/chat/ui/ImageView.js +12 -6
  16. package/dist/chat/ui/Markdown.d.ts +7 -1
  17. package/dist/chat/ui/Markdown.js +80 -23
  18. package/dist/chat/ui/MessageActions.js +6 -7
  19. package/dist/chat/ui/SubagentTimeline.js +7 -3
  20. package/dist/chat/ui/Timeline.js +68 -8
  21. package/dist/chat/ui/TimelineMenu.js +7 -3
  22. package/dist/chat/ui/file-links.d.ts +2 -1
  23. package/dist/chat/ui/file-links.js +8 -3
  24. package/dist/chat/ui/intelligent-ui/UiBlockFooter.d.ts +15 -0
  25. package/dist/chat/ui/intelligent-ui/UiBlockFooter.js +50 -0
  26. package/dist/chat/ui/intelligent-ui/UiBlockFrame.d.ts +10 -0
  27. package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +61 -0
  28. package/dist/chat/ui/intelligent-ui/UiFallbackPart.d.ts +10 -0
  29. package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +11 -0
  30. package/dist/chat/ui/intelligent-ui/UiReply.d.ts +13 -0
  31. package/dist/chat/ui/intelligent-ui/UiReply.js +325 -0
  32. package/dist/chat/ui/intelligent-ui/block-local.d.ts +2 -0
  33. package/dist/chat/ui/intelligent-ui/block-local.js +13 -0
  34. package/dist/chat/ui/intelligent-ui/chart-data.d.ts +16 -0
  35. package/dist/chat/ui/intelligent-ui/chart-data.js +46 -0
  36. package/dist/chat/ui/intelligent-ui/node-text.d.ts +9 -0
  37. package/dist/chat/ui/intelligent-ui/node-text.js +52 -0
  38. package/dist/chat/ui/intelligent-ui/registry.d.ts +2 -0
  39. package/dist/chat/ui/intelligent-ui/registry.js +50 -0
  40. package/dist/chat/ui/intelligent-ui/render-context.d.ts +50 -0
  41. package/dist/chat/ui/intelligent-ui/render-context.js +3 -0
  42. package/dist/chat/ui/intelligent-ui/renderers/chart.d.ts +3 -0
  43. package/dist/chat/ui/intelligent-ui/renderers/chart.js +125 -0
  44. package/dist/chat/ui/intelligent-ui/renderers/choices.d.ts +4 -0
  45. package/dist/chat/ui/intelligent-ui/renderers/choices.js +41 -0
  46. package/dist/chat/ui/intelligent-ui/renderers/content.d.ts +14 -0
  47. package/dist/chat/ui/intelligent-ui/renderers/content.js +66 -0
  48. package/dist/chat/ui/intelligent-ui/renderers/data.d.ts +7 -0
  49. package/dist/chat/ui/intelligent-ui/renderers/data.js +87 -0
  50. package/dist/chat/ui/intelligent-ui/renderers/inputs.d.ts +7 -0
  51. package/dist/chat/ui/intelligent-ui/renderers/inputs.js +60 -0
  52. package/dist/chat/ui/intelligent-ui/renderers/links.d.ts +10 -0
  53. package/dist/chat/ui/intelligent-ui/renderers/links.js +86 -0
  54. package/dist/chat/ui/intelligent-ui/renderers/structure.d.ts +6 -0
  55. package/dist/chat/ui/intelligent-ui/renderers/structure.js +32 -0
  56. package/dist/chat/ui/intelligent-ui/renderers/text.d.ts +8 -0
  57. package/dist/chat/ui/intelligent-ui/renderers/text.js +61 -0
  58. package/dist/chat/ui/intelligent-ui/reply-parts.d.ts +9 -0
  59. package/dist/chat/ui/intelligent-ui/reply-parts.js +18 -0
  60. package/dist/chat/ui/intelligent-ui/table-data.d.ts +31 -0
  61. package/dist/chat/ui/intelligent-ui/table-data.js +79 -0
  62. package/dist/chat/ui/intelligent-ui/tones.d.ts +7 -0
  63. package/dist/chat/ui/intelligent-ui/tones.js +31 -0
  64. package/dist/chat/ui/intelligent-ui/use-later.d.ts +1 -0
  65. package/dist/chat/ui/intelligent-ui/use-later.js +16 -0
  66. package/dist/chat/ui/intelligent-ui/use-ui-links.d.ts +9 -0
  67. package/dist/chat/ui/intelligent-ui/use-ui-links.js +107 -0
  68. package/dist/chat/ui/intelligent-ui/use-ui-queries.d.ts +20 -0
  69. package/dist/chat/ui/intelligent-ui/use-ui-queries.js +259 -0
  70. package/dist/chat/ui/reply-context.d.ts +8 -0
  71. package/dist/chat/ui/reply-context.js +1 -0
  72. package/dist/chat/ui/rows/GeneratedImageRow.d.ts +5 -0
  73. package/dist/chat/ui/rows/GeneratedImageRow.js +121 -0
  74. package/dist/chat/ui/rows/MessageRows.js +27 -7
  75. package/dist/chat/ui/rows/Rows.js +4 -2
  76. package/dist/chat/ui/rows/SubagentRow.js +5 -0
  77. package/dist/chat/ui/rows/WorkRows.d.ts +4 -1
  78. package/dist/chat/ui/rows/WorkRows.js +4 -3
  79. package/dist/host.d.ts +25 -3
  80. package/dist/host.js +1 -0
  81. package/dist/locales/en/agent-chat.json +89 -1
  82. package/dist/locales/nl/agent-chat.json +89 -1
  83. package/dist/state/chats.js +9 -0
  84. package/dist/theme.css +38 -2
  85. package/package.json +133 -7
  86. package/src/chat/chat-client.ts +1 -0
  87. package/src/chat/logic/generated-image.ts +48 -0
  88. package/src/chat/logic/timeline-target.ts +69 -4
  89. package/src/chat/logic/timeline.ts +28 -17
  90. package/src/chat/subagent-conversation.ts +13 -2
  91. package/src/chat/timeline-flash.ts +5 -3
  92. package/src/chat/ui/CodeBlock.tsx +70 -21
  93. package/src/chat/ui/Composer.tsx +22 -5
  94. package/src/chat/ui/ImageView.tsx +51 -8
  95. package/src/chat/ui/Markdown.tsx +123 -26
  96. package/src/chat/ui/MessageActions.tsx +15 -8
  97. package/src/chat/ui/SubagentTimeline.tsx +26 -1
  98. package/src/chat/ui/Timeline.tsx +98 -22
  99. package/src/chat/ui/TimelineMenu.tsx +6 -3
  100. package/src/chat/ui/file-links.ts +9 -3
  101. package/src/chat/ui/intelligent-ui/UiBlockFooter.tsx +139 -0
  102. package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +89 -0
  103. package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +21 -0
  104. package/src/chat/ui/intelligent-ui/UiReply.tsx +531 -0
  105. package/src/chat/ui/intelligent-ui/block-local.ts +16 -0
  106. package/src/chat/ui/intelligent-ui/chart-data.ts +67 -0
  107. package/src/chat/ui/intelligent-ui/node-text.ts +59 -0
  108. package/src/chat/ui/intelligent-ui/registry.ts +52 -0
  109. package/src/chat/ui/intelligent-ui/render-context.ts +78 -0
  110. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +321 -0
  111. package/src/chat/ui/intelligent-ui/renderers/choices.tsx +98 -0
  112. package/src/chat/ui/intelligent-ui/renderers/content.tsx +145 -0
  113. package/src/chat/ui/intelligent-ui/renderers/data.tsx +187 -0
  114. package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +121 -0
  115. package/src/chat/ui/intelligent-ui/renderers/links.tsx +157 -0
  116. package/src/chat/ui/intelligent-ui/renderers/structure.tsx +84 -0
  117. package/src/chat/ui/intelligent-ui/renderers/text.tsx +124 -0
  118. package/src/chat/ui/intelligent-ui/reply-parts.ts +22 -0
  119. package/src/chat/ui/intelligent-ui/table-data.ts +104 -0
  120. package/src/chat/ui/intelligent-ui/tones.ts +37 -0
  121. package/src/chat/ui/intelligent-ui/use-later.ts +17 -0
  122. package/src/chat/ui/intelligent-ui/use-ui-links.ts +123 -0
  123. package/src/chat/ui/intelligent-ui/use-ui-queries.ts +282 -0
  124. package/src/chat/ui/reply-context.ts +7 -0
  125. package/src/chat/ui/rows/GeneratedImageRow.tsx +244 -0
  126. package/src/chat/ui/rows/MessageRows.tsx +69 -7
  127. package/src/chat/ui/rows/Rows.tsx +4 -2
  128. package/src/chat/ui/rows/SubagentRow.tsx +5 -0
  129. package/src/chat/ui/rows/WorkRows.tsx +20 -5
  130. package/src/host.ts +46 -3
  131. package/src/locales/en/agent-chat.json +89 -1
  132. package/src/locales/nl/agent-chat.json +89 -1
  133. package/src/state/chats.ts +9 -0
  134. package/src/theme.css +38 -2
@@ -0,0 +1,13 @@
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
+ export declare function UiReply({ text, blocks, context, answers, queries, reply }: {
7
+ text: string;
8
+ blocks: readonly UiBlock[];
9
+ context: Omit<UiRenderContext, 'blockId'>;
10
+ answers?: Readonly<Record<string, ChatUiAnswer>>;
11
+ queries?: ChatUiQueryState;
12
+ reply?: ComponentProps<typeof ReplyMarkdown>['reply'];
13
+ }): ReactNode;
@@ -0,0 +1,325 @@
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';
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 } 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 { UiBlockFrame } from './UiBlockFrame.js';
13
+ import { UiFallbackPart } from './UiFallbackPart.js';
14
+ import { useUiLinks } from './use-ui-links.js';
15
+ import { useUiQueries, useUiLiveValues } from './use-ui-queries.js';
16
+ import { UiSourceCitation } from './renderers/content.js';
17
+ import { uiChildrenOf, revealUiSource } from './node-text.js';
18
+ import { uiReplyParts } from './reply-parts.js';
19
+ const localStates = new Map();
20
+ const LOCAL_STATE_LIMIT = 64;
21
+ function localState(key, block, answered) {
22
+ let state = localStates.get(key);
23
+ localStates.delete(key);
24
+ if (!state) {
25
+ state = new UiState(block);
26
+ if (answered && answered.revision === block.revision) {
27
+ for (const [name, value] of Object.entries(answered.values ?? {})) {
28
+ state.set(name, value);
29
+ }
30
+ }
31
+ }
32
+ localStates.set(key, state);
33
+ if (localStates.size > LOCAL_STATE_LIMIT) {
34
+ localStates.delete(localStates.keys().next().value);
35
+ }
36
+ return state;
37
+ }
38
+ /* The block itself, as the parent of its top-level nodes. */
39
+ const BLOCK = '$block';
40
+ /*
41
+ * Parents whose text is read as written: code, a short tag, and the labels of controls, whose
42
+ * accessible name and sent text are that same string.
43
+ */
44
+ const LITERAL_PARENTS = new Set(['CodeBlock', 'Tag', 'Choice', 'Item', 'Switch', 'Slider', 'Segmented', 'Option', 'Column']);
45
+ /* Parents that stack their children, where a run of text and inline nodes is one paragraph. */
46
+ const PARAGRAPHS = {
47
+ [BLOCK]: 'px-2 text-xs text-text',
48
+ Tab: 'px-2 text-xs text-text',
49
+ Section: 'text-xs text-text-muted'
50
+ };
51
+ const LINKS = new Set(['File', 'Diff', 'Commit', 'Node']);
52
+ /* Constructs that would start a block or load an image stay text; without GFM a table never forms. */
53
+ const BLOCK_CONSTRUCTS = [
54
+ 'blockQuote',
55
+ 'codeFenced',
56
+ 'codeIndented',
57
+ 'definition',
58
+ 'headingAtx',
59
+ 'htmlFlow',
60
+ 'htmlText',
61
+ 'labelStartImage',
62
+ 'list',
63
+ 'setextUnderline',
64
+ 'thematicBreak'
65
+ ];
66
+ /* Prose between the tags of a block is inline: only emphasis, code and links are Markdown, and a blank line is a line break. */
67
+ function remarkInlineProse() {
68
+ // The field remark-parse reads its syntax extensions from, as remark-gfm adds its own.
69
+ const data = this.data();
70
+ data.micromarkExtensions = [...(data.micromarkExtensions ?? []), { disable: { null: BLOCK_CONSTRUCTS } }];
71
+ return (tree) => {
72
+ tree.children = [
73
+ {
74
+ type: 'paragraph',
75
+ children: (tree.children ?? []).flatMap((paragraph, index) => [...(index > 0 ? [{ type: 'break' }] : []), ...(paragraph.children ?? [])])
76
+ }
77
+ ];
78
+ };
79
+ }
80
+ function remarkCitations() {
81
+ return (tree) => {
82
+ const visit = (parent) => {
83
+ if (['link', 'inlineCode', 'code'].includes(parent.type)) {
84
+ return;
85
+ }
86
+ parent.children = parent.children?.flatMap((child) => {
87
+ if (child.type !== 'text' || !child.value) {
88
+ visit(child);
89
+ return [child];
90
+ }
91
+ const parts = [];
92
+ let start = 0;
93
+ for (const match of child.value.matchAll(/\[(\d{1,3})\]/g)) {
94
+ parts.push({ type: 'text', value: child.value.slice(start, match.index) });
95
+ parts.push({
96
+ type: 'citation',
97
+ data: { hName: 'span', hProperties: { 'data-ui-citation': match[1] } },
98
+ children: [{ type: 'text', value: match[0] }]
99
+ });
100
+ start = match.index + match[0].length;
101
+ }
102
+ parts.push({ type: 'text', value: child.value.slice(start) });
103
+ return parts;
104
+ });
105
+ };
106
+ visit(tree);
107
+ };
108
+ }
109
+ const CitationContext = createContext({ sources: new Map() });
110
+ function ProseCitation({ number, children }) {
111
+ const { sources, reveal } = useContext(CitationContext);
112
+ const source = sources.get(number);
113
+ return source ? _jsx(UiSourceCitation, { number: number, title: source.title, url: source.url, onReveal: reveal ? () => reveal(source) : undefined }) : children;
114
+ }
115
+ function sourceTargets(nodes) {
116
+ const lists = [];
117
+ const visit = (nodes, ancestors = []) => {
118
+ for (const node of nodes) {
119
+ const path = ['Tab', 'Section'].includes(node.type) ? [...ancestors, node.id] : ancestors;
120
+ if (node.type === 'Sources') {
121
+ lists.push(uiChildrenOf(node, 'Source').map((source) => ({ id: source.id, title: source.props.title, url: source.props.url, ancestors: path })));
122
+ }
123
+ visit(node.children, path);
124
+ }
125
+ };
126
+ visit(nodes);
127
+ // Numbers restart in each Sources list, so multiple lists have no unambiguous prose target.
128
+ return new Map(lists.length === 1 ? lists[0].map((source, index) => [index + 1, source]) : []);
129
+ }
130
+ const PROSE_PLUGINS = [remarkInlineProse, remarkCitations];
131
+ 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
+ const FADE_PLUGINS = [rehypeFadeWords];
135
+ const NO_PLUGINS = [];
136
+ const ProseUrlContext = createContext(undefined);
137
+ /* Through the host only, like a Source: the address is the model's, so nothing here navigates by itself. */
138
+ function ProseLink({ href, children }) {
139
+ const openUrl = useContext(ProseUrlContext);
140
+ if (openUrl === undefined || href === undefined || !/^https?:\/\//i.test(href)) {
141
+ return children;
142
+ }
143
+ return (_jsx("button", { type: "button", className: "cursor-pointer text-accent underline underline-offset-2 select-text", onClick: () => openUrl(href), children: children }));
144
+ }
145
+ const PROSE_COMPONENTS = {
146
+ p: ({ children }) => children,
147
+ code: ({ children }) => _jsx("code", { className: "rounded-sm bg-surface-sunken px-1 py-px font-mono text-code", children: children }),
148
+ a: ({ href, children }) => _jsx(ProseLink, { href: href, children: children }),
149
+ 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 }))
150
+ };
151
+ /*
152
+ * Text an agent wrote in a block, as inline Markdown without raw HTML. Markdown trims a paragraph,
153
+ * so the whitespace at either end stays text: it is the space between a word and the tag beside it.
154
+ * While the block streams every new word fades in, as in the prose of the reply around it.
155
+ */
156
+ const UiProse = memo(function UiProse({ text, fade, openUrl }) {
157
+ const [, lead = '', body = '', trail = ''] = /^(\s*)([\s\S]*?)(\s*)$/.exec(text) ?? [];
158
+ if (body === '') {
159
+ return text;
160
+ }
161
+ return (_jsxs(ProseUrlContext, { value: openUrl, children: [lead, _jsx(ReactMarkdown, { remarkPlugins: PROSE_PLUGINS, rehypePlugins: fade ? FADE_PLUGINS : NO_PLUGINS, allowedElements: fade ? FADING_PROSE_ELEMENTS : PROSE_ELEMENTS, unwrapDisallowed: true, components: PROSE_COMPONENTS, children: body }), trail] }));
162
+ });
163
+ /* 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
+ function writtenTexts(nodes, into = new Map()) {
165
+ for (const node of nodes) {
166
+ if (node.type === '$text') {
167
+ into.set(node.id, node.expressions.text === undefined);
168
+ }
169
+ writtenTexts(node.children, into);
170
+ }
171
+ return into;
172
+ }
173
+ function isInline(node) {
174
+ if (node.type === '$text') {
175
+ return true;
176
+ }
177
+ return node.error === undefined && (node.type === 'Tag' || (LINKS.has(node.type) && node.children.length === 0));
178
+ }
179
+ function isBlank(node) {
180
+ return node.type === '$text' && String(node.props.text ?? '').trim() === '';
181
+ }
182
+ /* 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. */
183
+ function UiNodes({ nodes, parent, texts, context }) {
184
+ const drawn = (node) => (_jsx(UiNodeBoundary, { node: node, children: _jsx(UiNodeBody, { node: node, parent: parent, texts: texts, context: context }) }, node.id));
185
+ const paragraph = PARAGRAPHS[parent];
186
+ if (paragraph === undefined) {
187
+ return nodes.map(drawn);
188
+ }
189
+ const output = [];
190
+ let run = [];
191
+ const close = () => {
192
+ if (run.some((node) => !isBlank(node))) {
193
+ output.push(_jsx("div", { className: paragraph, children: run.map(drawn) }, `run:${run[0].id}`));
194
+ }
195
+ run = [];
196
+ };
197
+ for (const node of nodes) {
198
+ if (isInline(node)) {
199
+ run.push(node);
200
+ continue;
201
+ }
202
+ close();
203
+ output.push(drawn(node));
204
+ }
205
+ close();
206
+ return output;
207
+ }
208
+ class UiNodeBoundary extends Component {
209
+ state = { failed: false, node: this.props.node };
210
+ static getDerivedStateFromError() {
211
+ return { failed: true };
212
+ }
213
+ static getDerivedStateFromProps(props, state) {
214
+ return props.node === state.node ? null : { failed: false, node: props.node };
215
+ }
216
+ render() {
217
+ return this.state.failed ? _jsx(UiFallbackPart, { fallback: this.props.node.fallback, problem: { kind: 'failed' } }) : this.props.children;
218
+ }
219
+ }
220
+ function UiNodeBody({ node, parent, texts, context }) {
221
+ if (node.type === '$text') {
222
+ const text = String(node.props.text ?? '');
223
+ return LITERAL_PARENTS.has(parent) || texts.get(node.sourceId ?? node.id) !== true ? (text) : (_jsx(UiProse, { text: text, fade: context.phase === 'streaming', openUrl: context.openUrl }));
224
+ }
225
+ if (node.error !== undefined) {
226
+ return _jsx(UiFallbackPart, { fallback: node.fallback, problem: { kind: 'failed' } });
227
+ }
228
+ if (!isUiComponent(node.type)) {
229
+ return _jsx(UiFallbackPart, { fallback: node.fallback, problem: { kind: 'unknown', component: node.type } });
230
+ }
231
+ const Renderer = UI_RENDERERS[node.type];
232
+ if (Renderer === null) {
233
+ return null;
234
+ }
235
+ return (_jsx(Renderer, { node: node, context: context, children: _jsx(UiNodes, { nodes: node.children, parent: node.type, texts: texts, context: context }) }));
236
+ }
237
+ function UiBlockBody({ block, context, answered, frozen }) {
238
+ const { t } = useTranslation('agent-chat');
239
+ const key = JSON.stringify([context.scopeId, context.chatId, context.itemId, block.id]);
240
+ const [state] = useState(() => localState(key, block, answered));
241
+ state.sync(block);
242
+ useSyncExternalStore((listener) => state.subscribe(listener), () => state.snapshot(), () => state.snapshot());
243
+ const [optimistic, setOptimistic] = useState(null);
244
+ const [failedChoiceId, setFailedChoiceId] = useState(null);
245
+ const pending = useRef(false);
246
+ useEffect(() => {
247
+ if (answered && answered.revision === block.revision && answered.values) {
248
+ for (const [name, value] of Object.entries(answered.values)) {
249
+ state.set(name, value);
250
+ }
251
+ }
252
+ }, [state, answered, block.revision]);
253
+ const answer = answered && answered.revision === block.revision
254
+ ? { choiceId: answered.choiceId, state: answered.queued ? 'queued' : 'sent' }
255
+ : optimistic?.revision === block.revision
256
+ ? optimistic
257
+ : context.answer;
258
+ const [element, setElement] = useState(null);
259
+ const navigation = useContext(UiReplyNavigationContext);
260
+ const flash = navigation?.flash;
261
+ const flashing = navigation?.chatId === context.chatId && flash?.itemId === context.itemId && flash.blockId === block.id && flash.revision === block.revision;
262
+ const queries = useUiQueries(block, state, context, element, frozen);
263
+ const links = useUiLinks(block, state, context, queries.reads, frozen);
264
+ const evaluation = evaluateUiBlock(block, state);
265
+ const liveValue = useUiLiveValues(block, evaluation.nodes, queries.currentReadings);
266
+ const sendChoice = chatHost().intelligentUi?.sendChoice;
267
+ const rendering = {
268
+ ...context,
269
+ ...links,
270
+ answer,
271
+ failedChoiceId,
272
+ live: queries.live ?? context.live,
273
+ liveValue: liveValue ?? context.liveValue,
274
+ onChoose: !queries.ready || queries.reading
275
+ ? undefined
276
+ : sendChoice && block.revision && block.complete && context.phase === 'final'
277
+ ? (choiceId) => {
278
+ if (pending.current || answer || queries.reading) {
279
+ return;
280
+ }
281
+ const values = uiInputValues(block, state);
282
+ const reads = queries.readsFor(values);
283
+ if (reads === null) {
284
+ return;
285
+ }
286
+ pending.current = true;
287
+ setFailedChoiceId(null);
288
+ const revision = block.revision;
289
+ setOptimistic({ revision, choiceId, state: 'sending' });
290
+ void sendChoice(context.scopeId, {
291
+ chatId: context.chatId,
292
+ itemId: context.itemId,
293
+ blockId: block.id,
294
+ revision,
295
+ choiceId,
296
+ reads,
297
+ values
298
+ })
299
+ .then((state) => setOptimistic({ revision, choiceId, state }))
300
+ .catch(() => {
301
+ setOptimistic(null);
302
+ setFailedChoiceId(choiceId);
303
+ })
304
+ .finally(() => {
305
+ pending.current = false;
306
+ });
307
+ }
308
+ : context.onChoose
309
+ };
310
+ const unknown = block.catalogVersion !== UI_CATALOG_VERSION;
311
+ const failed = !unknown && !block.nodes.length && block.diagnostics.length > 0;
312
+ 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 }) })) })] }));
321
+ }
322
+ /* 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
+ 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", resetKeys: [context.scopeId, context.chatId, context.itemId, part.block.id], children: _jsx(UiBlockBody, { block: part.block, frozen: queries?.blocks[part.block.id], answered: answers?.[part.block.id], context: { ...context, blockId: part.block.id } }, JSON.stringify([context.scopeId, context.chatId, context.itemId, part.block.id])) }, part.block.id))) }));
325
+ }
@@ -0,0 +1,2 @@
1
+ import type { UiRenderContext } from './render-context.js';
2
+ export declare function useBlockLocal<Value>(context: UiRenderContext, nodeId: string, initial: () => Value): [Value, (next: Value) => void];
@@ -0,0 +1,13 @@
1
+ import { useState } from 'react';
2
+ // 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 Map();
4
+ /* State of one node that lasts as long as the page, such as a fold; local input state belongs to the runtime instead. */
5
+ export function useBlockLocal(context, nodeId, initial) {
6
+ const key = `${context.scopeId}\n${context.chatId}\n${context.itemId}\n${context.blockId}\n${nodeId}`;
7
+ const [value, setValue] = useState(() => (remembered.has(key) ? remembered.get(key) : initial()));
8
+ const set = (next) => {
9
+ remembered.set(key, next);
10
+ setValue(next);
11
+ };
12
+ return [value, set];
13
+ }
@@ -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,9 @@
1
+ import type { UiComponentName, UiProps, UiViewNode } from '@adecore/intelligent-ui';
2
+ export declare function uiNodeText(node: UiViewNode): string;
3
+ export declare function uiNodeLabel(node: UiViewNode): string;
4
+ export declare function uiChildrenOf<Name extends UiComponentName>(node: UiViewNode, type: Name): UiViewNode<UiProps<Name>>[];
5
+ export declare function uiBlockHead(nodes: readonly UiViewNode[]): {
6
+ id: string;
7
+ label: string;
8
+ } | null;
9
+ export declare function revealUiSource(root: ParentNode, nodeId: string, ancestors?: readonly string[]): Promise<boolean>;
@@ -0,0 +1,52 @@
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
+ }
@@ -0,0 +1,2 @@
1
+ import type { UiRenderers } from './render-context.js';
2
+ export declare const UI_RENDERERS: UiRenderers;
@@ -0,0 +1,50 @@
1
+ import { ChartRenderer } from './renderers/chart.js';
2
+ import { ChoiceRenderer, ChoicesRenderer } from './renderers/choices.js';
3
+ import { CodeBlockRenderer, ImageRenderer, SourceRenderer, SourcesRenderer } from './renderers/content.js';
4
+ import { EntityListRenderer, EntryRenderer, StatRenderer, StatsRenderer, TableRenderer } from './renderers/data.js';
5
+ import { ChecklistRenderer, ItemRenderer, SegmentedRenderer, SliderRenderer, SwitchRenderer } from './renderers/inputs.js';
6
+ import { CommitRenderer, DiffRenderer, FileRenderer, NodeRenderer } from './renderers/links.js';
7
+ import { SectionRenderer, SectionsRenderer, TabRenderer, TabsRenderer } from './renderers/structure.js';
8
+ import { CalloutRenderer, ProgressRenderer, StepRenderer, StepsRenderer, SummaryRenderer, TagRenderer } from './renderers/text.js';
9
+ /*
10
+ * A renderer for every component of the catalog; a name missing here fails the typecheck. Null draws
11
+ * nothing for the node itself: the runtime resolves Show and Each before rendering, and Column and
12
+ * Option are metadata their parent reads from its node (a table its columns, Segmented its segments).
13
+ */
14
+ export const UI_RENDERERS = {
15
+ Summary: SummaryRenderer,
16
+ Callout: CalloutRenderer,
17
+ Tag: TagRenderer,
18
+ Progress: ProgressRenderer,
19
+ Steps: StepsRenderer,
20
+ Step: StepRenderer,
21
+ Stats: StatsRenderer,
22
+ Stat: StatRenderer,
23
+ EntityList: EntityListRenderer,
24
+ Entry: EntryRenderer,
25
+ Table: TableRenderer,
26
+ Column: null,
27
+ Chart: ChartRenderer,
28
+ Tabs: TabsRenderer,
29
+ Tab: TabRenderer,
30
+ Sections: SectionsRenderer,
31
+ Section: SectionRenderer,
32
+ CodeBlock: CodeBlockRenderer,
33
+ Image: ImageRenderer,
34
+ Sources: SourcesRenderer,
35
+ Source: SourceRenderer,
36
+ File: FileRenderer,
37
+ Diff: DiffRenderer,
38
+ Commit: CommitRenderer,
39
+ Node: NodeRenderer,
40
+ Checklist: ChecklistRenderer,
41
+ Item: ItemRenderer,
42
+ Switch: SwitchRenderer,
43
+ Slider: SliderRenderer,
44
+ Segmented: SegmentedRenderer,
45
+ Option: null,
46
+ Show: null,
47
+ Each: null,
48
+ Choices: ChoicesRenderer,
49
+ Choice: ChoiceRenderer
50
+ };
@@ -0,0 +1,50 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { UiComponentName, UiProps, UiViewNode } from '@adecore/intelligent-ui';
3
+ export type { UiLinkTarget } from '@adecore/intelligent-ui/links';
4
+ import type { UiLinkTarget } from '@adecore/intelligent-ui/links';
5
+ export interface UiLink {
6
+ state: 'chip' | 'plain';
7
+ label?: string;
8
+ reason?: string;
9
+ }
10
+ export interface UiAnswer {
11
+ choiceId: string;
12
+ state: 'sending' | 'sent' | 'queued';
13
+ }
14
+ export interface UiLiveStatus {
15
+ state: 'fresh' | 'reading' | 'failed' | 'refused';
16
+ sources: readonly string[];
17
+ readAt: number | null;
18
+ source?: string;
19
+ reason?: string;
20
+ }
21
+ export interface UiLiveValue {
22
+ previous?: unknown;
23
+ changed: boolean;
24
+ stale: boolean;
25
+ }
26
+ export interface UiRenderContext {
27
+ scopeId: string;
28
+ chatId: string;
29
+ itemId: string;
30
+ blockId: string;
31
+ phase: 'streaming' | 'final';
32
+ answer: UiAnswer | null;
33
+ failedChoiceId?: string | null;
34
+ live?: UiLiveStatus | null;
35
+ link?(target: UiLinkTarget): UiLink;
36
+ openLink?(target: UiLinkTarget): void;
37
+ openUrl?(url: string): void;
38
+ onChoose?(nodeId: string): void;
39
+ liveValue?(nodeId: string, prop: string, path?: readonly [row: number, key: string]): UiLiveValue | undefined;
40
+ }
41
+ export interface UiRendererProps<Props> {
42
+ node: UiViewNode<Props>;
43
+ children: ReactNode;
44
+ context: UiRenderContext;
45
+ }
46
+ export type UiRenderer<Props> = (props: UiRendererProps<Props>) => ReactNode;
47
+ export type UiRenderers = {
48
+ readonly [Name in UiComponentName]: UiRenderer<UiProps<Name>> | null;
49
+ };
50
+ export declare const UiHeadContext: import("react").Context<string | null | undefined>;
@@ -0,0 +1,3 @@
1
+ import { createContext } from 'react';
2
+ /* The id of the Summary that heads the block. Undefined outside a frame, where every Summary heads. */
3
+ export const UiHeadContext = createContext(undefined);
@@ -0,0 +1,3 @@
1
+ import type { UiProps } from '@adecore/intelligent-ui';
2
+ import type { UiRendererProps } from '../render-context.js';
3
+ export declare function ChartRenderer({ node }: UiRendererProps<UiProps<'Chart'>>): import("react").JSX.Element;