@adecore/agents-react 0.20.1 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat/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/UiBlockFooter.d.ts +15 -0
- package/dist/chat/ui/intelligent-ui/UiBlockFooter.js +50 -0
- package/dist/chat/ui/intelligent-ui/UiBlockFrame.d.ts +10 -0
- package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +61 -0
- package/dist/chat/ui/intelligent-ui/UiFallbackPart.d.ts +10 -0
- package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +11 -0
- package/dist/chat/ui/intelligent-ui/UiReply.d.ts +13 -0
- package/dist/chat/ui/intelligent-ui/UiReply.js +325 -0
- package/dist/chat/ui/intelligent-ui/block-local.d.ts +2 -0
- package/dist/chat/ui/intelligent-ui/block-local.js +13 -0
- package/dist/chat/ui/intelligent-ui/chart-data.d.ts +16 -0
- package/dist/chat/ui/intelligent-ui/chart-data.js +46 -0
- package/dist/chat/ui/intelligent-ui/node-text.d.ts +9 -0
- package/dist/chat/ui/intelligent-ui/node-text.js +52 -0
- package/dist/chat/ui/intelligent-ui/registry.d.ts +2 -0
- package/dist/chat/ui/intelligent-ui/registry.js +50 -0
- package/dist/chat/ui/intelligent-ui/render-context.d.ts +50 -0
- package/dist/chat/ui/intelligent-ui/render-context.js +3 -0
- package/dist/chat/ui/intelligent-ui/renderers/chart.d.ts +3 -0
- package/dist/chat/ui/intelligent-ui/renderers/chart.js +125 -0
- package/dist/chat/ui/intelligent-ui/renderers/choices.d.ts +4 -0
- package/dist/chat/ui/intelligent-ui/renderers/choices.js +41 -0
- package/dist/chat/ui/intelligent-ui/renderers/content.d.ts +14 -0
- package/dist/chat/ui/intelligent-ui/renderers/content.js +66 -0
- package/dist/chat/ui/intelligent-ui/renderers/data.d.ts +7 -0
- package/dist/chat/ui/intelligent-ui/renderers/data.js +87 -0
- package/dist/chat/ui/intelligent-ui/renderers/inputs.d.ts +7 -0
- package/dist/chat/ui/intelligent-ui/renderers/inputs.js +60 -0
- package/dist/chat/ui/intelligent-ui/renderers/links.d.ts +10 -0
- package/dist/chat/ui/intelligent-ui/renderers/links.js +86 -0
- package/dist/chat/ui/intelligent-ui/renderers/structure.d.ts +6 -0
- package/dist/chat/ui/intelligent-ui/renderers/structure.js +32 -0
- package/dist/chat/ui/intelligent-ui/renderers/text.d.ts +8 -0
- package/dist/chat/ui/intelligent-ui/renderers/text.js +61 -0
- package/dist/chat/ui/intelligent-ui/reply-parts.d.ts +9 -0
- package/dist/chat/ui/intelligent-ui/reply-parts.js +18 -0
- package/dist/chat/ui/intelligent-ui/table-data.d.ts +31 -0
- package/dist/chat/ui/intelligent-ui/table-data.js +79 -0
- package/dist/chat/ui/intelligent-ui/tones.d.ts +7 -0
- package/dist/chat/ui/intelligent-ui/tones.js +31 -0
- package/dist/chat/ui/intelligent-ui/use-later.d.ts +1 -0
- package/dist/chat/ui/intelligent-ui/use-later.js +16 -0
- package/dist/chat/ui/intelligent-ui/use-ui-links.d.ts +9 -0
- package/dist/chat/ui/intelligent-ui/use-ui-links.js +107 -0
- package/dist/chat/ui/intelligent-ui/use-ui-queries.d.ts +20 -0
- package/dist/chat/ui/intelligent-ui/use-ui-queries.js +259 -0
- package/dist/chat/ui/reply-context.d.ts +8 -0
- package/dist/chat/ui/reply-context.js +1 -0
- package/dist/chat/ui/rows/GeneratedImageRow.d.ts +5 -0
- package/dist/chat/ui/rows/GeneratedImageRow.js +121 -0
- package/dist/chat/ui/rows/MessageRows.js +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 +82 -0
- package/dist/locales/nl/agent-chat.json +82 -0
- package/dist/state/chats.js +9 -0
- package/dist/theme.css +19 -0
- package/package.json +133 -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/UiBlockFooter.tsx +139 -0
- package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +89 -0
- package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +21 -0
- package/src/chat/ui/intelligent-ui/UiReply.tsx +531 -0
- package/src/chat/ui/intelligent-ui/block-local.ts +16 -0
- package/src/chat/ui/intelligent-ui/chart-data.ts +67 -0
- package/src/chat/ui/intelligent-ui/node-text.ts +59 -0
- package/src/chat/ui/intelligent-ui/registry.ts +52 -0
- package/src/chat/ui/intelligent-ui/render-context.ts +78 -0
- package/src/chat/ui/intelligent-ui/renderers/chart.tsx +321 -0
- package/src/chat/ui/intelligent-ui/renderers/choices.tsx +98 -0
- package/src/chat/ui/intelligent-ui/renderers/content.tsx +145 -0
- package/src/chat/ui/intelligent-ui/renderers/data.tsx +187 -0
- package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +121 -0
- package/src/chat/ui/intelligent-ui/renderers/links.tsx +157 -0
- package/src/chat/ui/intelligent-ui/renderers/structure.tsx +84 -0
- package/src/chat/ui/intelligent-ui/renderers/text.tsx +124 -0
- package/src/chat/ui/intelligent-ui/reply-parts.ts +22 -0
- package/src/chat/ui/intelligent-ui/table-data.ts +104 -0
- package/src/chat/ui/intelligent-ui/tones.ts +37 -0
- package/src/chat/ui/intelligent-ui/use-later.ts +17 -0
- package/src/chat/ui/intelligent-ui/use-ui-links.ts +123 -0
- package/src/chat/ui/intelligent-ui/use-ui-queries.ts +282 -0
- package/src/chat/ui/reply-context.ts +7 -0
- package/src/chat/ui/rows/GeneratedImageRow.tsx +244 -0
- package/src/chat/ui/rows/MessageRows.tsx +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 +82 -0
- package/src/locales/nl/agent-chat.json +82 -0
- package/src/state/chats.ts +9 -0
- package/src/theme.css +19 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useId } from 'react';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { ChevronRight } from 'lucide-react';
|
|
5
|
+
import { Icon, Tabs } from '@adecore/ui';
|
|
6
|
+
import { useBlockLocal } from '../block-local.js';
|
|
7
|
+
import { uiChildrenOf } from '../node-text.js';
|
|
8
|
+
const SectionsContext = createContext({ touched: false, touch: () => undefined });
|
|
9
|
+
/* A tab that arrives while the block streams is not picked: the one that was open stays open. */
|
|
10
|
+
export function TabsRenderer({ node, children, context }) {
|
|
11
|
+
const tabs = uiChildrenOf(node, 'Tab');
|
|
12
|
+
const [picked, setPicked] = useBlockLocal(context, node.id, () => null);
|
|
13
|
+
const value = picked !== null && tabs.some((tab) => tab.id === picked) ? picked : (tabs[0]?.id ?? null);
|
|
14
|
+
return (_jsxs(Tabs.Root, { value: value, onValueChange: (next) => setPicked(String(next)), className: "flex flex-col gap-3", children: [_jsx(Tabs.List, { className: "px-2", children: tabs.map((tab) => (_jsx(Tabs.Tab, { value: tab.id, "data-ui-reveal": tab.id, children: tab.props.title }, tab.id))) }), children] }));
|
|
15
|
+
}
|
|
16
|
+
export function TabRenderer({ node, children }) {
|
|
17
|
+
return (_jsx(Tabs.Panel, { value: node.id, className: "chat-ui-nodes flex flex-col gap-3", children: children }));
|
|
18
|
+
}
|
|
19
|
+
export function SectionsRenderer({ node, children, context }) {
|
|
20
|
+
const [touched, setTouched] = useBlockLocal(context, node.id, () => false);
|
|
21
|
+
return (_jsx(SectionsContext, { value: { touched, touch: () => setTouched(true) }, children: _jsx("div", { className: "chat-ui-nodes flex flex-col divide-y divide-border-soft", children: children }) }));
|
|
22
|
+
}
|
|
23
|
+
/* Arrives open until the person folds a section of the set themselves; the content starts under the title. */
|
|
24
|
+
export function SectionRenderer({ node, children, context }) {
|
|
25
|
+
const fold = useContext(SectionsContext);
|
|
26
|
+
const [open, setOpen] = useBlockLocal(context, node.id, () => !fold.touched);
|
|
27
|
+
const contentId = useId();
|
|
28
|
+
return (_jsxs("section", { children: [_jsxs("button", { type: "button", "data-ui-reveal": node.id, "aria-expanded": open, "aria-controls": contentId, className: "flex h-8 w-full items-center gap-2 rounded-md px-2 text-left text-sm font-medium text-text hover:bg-surface-hover", onClick: () => {
|
|
29
|
+
fold.touch();
|
|
30
|
+
setOpen(!open);
|
|
31
|
+
}, children: [_jsx(Icon, { icon: ChevronRight, size: 14, className: clsx('shrink-0 text-text-faint transition-transform duration-150 motion-reduce:transition-none', open && 'rotate-90') }), _jsx("span", { className: "min-w-0 truncate", children: node.props.title })] }), _jsx("div", { id: contentId, hidden: !open, className: "chat-ui-nodes flex flex-col gap-3 pt-1 pr-2 pb-2 pl-7.5", children: children })] }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { UiProps } from '@adecore/intelligent-ui';
|
|
2
|
+
import { type UiRendererProps } from '../render-context.js';
|
|
3
|
+
export declare function SummaryRenderer({ node, children }: UiRendererProps<UiProps<'Summary'>>): import("react").JSX.Element;
|
|
4
|
+
export declare function CalloutRenderer({ node, children }: UiRendererProps<UiProps<'Callout'>>): import("react").JSX.Element;
|
|
5
|
+
export declare function TagRenderer({ node, children }: UiRendererProps<UiProps<'Tag'>>): import("react").JSX.Element;
|
|
6
|
+
export declare function ProgressRenderer({ node, children, context }: UiRendererProps<UiProps<'Progress'>>): import("react").JSX.Element;
|
|
7
|
+
export declare function StepsRenderer({ children }: UiRendererProps<UiProps<'Steps'>>): import("react").JSX.Element;
|
|
8
|
+
export declare function StepRenderer({ node, children, context }: UiRendererProps<UiProps<'Step'>>): import("react").JSX.Element;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useContext } from 'react';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { Circle, CircleCheck, CircleDot, CircleMinus, CircleX } from 'lucide-react';
|
|
6
|
+
import { Icon, Meter, Pill, Spinner } from '@adecore/ui';
|
|
7
|
+
import { formatNumber, formatPercent } from '@adecore/ui/format';
|
|
8
|
+
import { UiHeadContext } from '../render-context.js';
|
|
9
|
+
import { uiNodeLabel } from '../node-text.js';
|
|
10
|
+
import { UI_TONE_ICONS, UI_TONE_PILL, UI_TONE_SURFACE, UI_TONE_TEXT } from '../tones.js';
|
|
11
|
+
const STEP_ICONS = {
|
|
12
|
+
done: CircleCheck,
|
|
13
|
+
pending: Circle,
|
|
14
|
+
failed: CircleX,
|
|
15
|
+
skipped: CircleMinus
|
|
16
|
+
};
|
|
17
|
+
const STEP_TONES = {
|
|
18
|
+
done: 'text-status-idle',
|
|
19
|
+
running: 'text-accent',
|
|
20
|
+
pending: 'text-text-faint',
|
|
21
|
+
failed: 'text-status-error',
|
|
22
|
+
skipped: 'text-text-faint'
|
|
23
|
+
};
|
|
24
|
+
/* Whether the block still changes: while it streams, or while it reads live data. Only then does anything spin. */
|
|
25
|
+
function isLive(context) {
|
|
26
|
+
return context.phase === 'streaming' || (context.live !== undefined && context.live !== null && context.live.state !== 'refused');
|
|
27
|
+
}
|
|
28
|
+
/* The head of the block when it is the first node, a subheading without an icon further down. */
|
|
29
|
+
export function SummaryRenderer({ node, children }) {
|
|
30
|
+
const headId = useContext(UiHeadContext);
|
|
31
|
+
const head = headId === undefined || headId === node.id;
|
|
32
|
+
const { tone, badge } = node.props;
|
|
33
|
+
return (_jsxs("div", { className: "flex items-start gap-2 px-2", children: [head && tone !== undefined && _jsx(Icon, { icon: UI_TONE_ICONS[tone], size: 16, className: clsx('mt-0.75 shrink-0', UI_TONE_TEXT[tone]) }), _jsx("div", { className: "min-w-0 grow text-sm font-semibold text-balance text-text", children: children }), badge !== undefined && badge !== '' && (_jsx(Pill, { shape: "tag", tone: tone === undefined ? 'muted' : UI_TONE_PILL[tone], className: "shrink-0", children: badge }))] }));
|
|
34
|
+
}
|
|
35
|
+
export function CalloutRenderer({ node, children }) {
|
|
36
|
+
const { tone, title } = node.props;
|
|
37
|
+
return (_jsxs("div", { className: clsx('flex gap-2 rounded-md p-2 text-sm', UI_TONE_SURFACE[tone]), children: [_jsx(Icon, { icon: UI_TONE_ICONS[tone], size: 16, className: clsx('mt-0.75 shrink-0', UI_TONE_TEXT[tone]) }), _jsxs("div", { className: "min-w-0 grow", children: [title !== undefined && title !== '' && _jsx("div", { className: "font-medium text-text", children: title }), _jsx("div", { className: "text-text-muted", children: children })] })] }));
|
|
38
|
+
}
|
|
39
|
+
export function TagRenderer({ node, children }) {
|
|
40
|
+
const { tone } = node.props;
|
|
41
|
+
return (_jsx(Pill, { shape: "tag", size: "sm", tone: tone === undefined ? 'muted' : UI_TONE_PILL[tone], children: children }));
|
|
42
|
+
}
|
|
43
|
+
/* Without a value the progress is indeterminate; without a max the value is a percentage. */
|
|
44
|
+
export function ProgressRenderer({ node, children, context }) {
|
|
45
|
+
const { t } = useTranslation('agent-chat');
|
|
46
|
+
const { value, max } = node.props;
|
|
47
|
+
const limit = max ?? 100;
|
|
48
|
+
const reading = value === undefined ? null : limit === 100 ? formatPercent(value) : t('blocks.progress.of', { value: formatNumber(value), max: formatNumber(limit) });
|
|
49
|
+
const label = uiNodeLabel(node);
|
|
50
|
+
return (_jsxs("div", { className: "flex flex-col gap-1 px-2", children: [_jsxs("div", { className: "flex items-baseline justify-between gap-3 text-xs", children: [_jsx("span", { className: "min-w-0 text-text", children: children }), reading !== null ? (_jsx("span", { className: "shrink-0 text-text-muted tabular-nums", children: reading })) : (isLive(context) && _jsx(Spinner, { size: 12, className: "shrink-0 self-center text-text-faint" }))] }), _jsx(Meter, { value: value ?? null, max: limit, label: label === '' ? t('blocks.progress.label') : label, valueText: reading ?? undefined })] }));
|
|
51
|
+
}
|
|
52
|
+
export function StepsRenderer({ children }) {
|
|
53
|
+
return _jsx("ol", { className: "chat-ui-nodes flex flex-col px-2", children: children });
|
|
54
|
+
}
|
|
55
|
+
/* A running step spins only while the block is live; in a quiet block it is a still dot. */
|
|
56
|
+
export function StepRenderer({ node, children, context }) {
|
|
57
|
+
const { t } = useTranslation('agent-chat');
|
|
58
|
+
const { state, detail } = node.props;
|
|
59
|
+
const quiet = state === 'pending' || state === 'skipped';
|
|
60
|
+
return (_jsxs("li", { className: "flex min-h-7 items-center gap-2 text-sm", children: [_jsx("span", { className: clsx('grid size-4 shrink-0 place-items-center', STEP_TONES[state]), children: state === 'running' ? (isLive(context) ? (_jsx(Spinner, { size: 14 })) : (_jsx(Icon, { icon: CircleDot, size: 14 }))) : (_jsx(Icon, { icon: STEP_ICONS[state], size: 14 })) }), _jsx("span", { className: "sr-only", children: t(`blocks.step.${state}`) }), _jsx("span", { className: clsx('min-w-0 grow', quiet ? 'text-text-faint' : 'text-text'), children: children }), detail !== undefined && detail !== '' && _jsx("span", { className: "shrink-0 text-xs text-text-faint tabular-nums", children: detail })] }));
|
|
61
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { UiBlock } from '@adecore/intelligent-ui';
|
|
2
|
+
export type UiReplyPart = {
|
|
3
|
+
kind: 'text';
|
|
4
|
+
text: string;
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'ui';
|
|
7
|
+
block: UiBlock;
|
|
8
|
+
};
|
|
9
|
+
export declare function uiReplyParts(text: string, blocks: readonly UiBlock[], streaming?: boolean): UiReplyPart[];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function uiReplyParts(text, blocks, streaming = false) {
|
|
2
|
+
const parts = [];
|
|
3
|
+
let position = 0;
|
|
4
|
+
for (const block of [...blocks].sort((left, right) => left.start - right.start)) {
|
|
5
|
+
if (!Number.isInteger(block.start) || !Number.isInteger(block.end) || block.start < position || block.end < block.start || block.end > text.length) {
|
|
6
|
+
continue;
|
|
7
|
+
}
|
|
8
|
+
if (block.start > position) {
|
|
9
|
+
parts.push({ kind: 'text', text: text.slice(position, block.start) });
|
|
10
|
+
}
|
|
11
|
+
parts.push({ kind: 'ui', block });
|
|
12
|
+
position = streaming && !block.complete ? text.length : block.end;
|
|
13
|
+
}
|
|
14
|
+
if (position < text.length) {
|
|
15
|
+
parts.push({ kind: 'text', text: text.slice(position) });
|
|
16
|
+
}
|
|
17
|
+
return parts;
|
|
18
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { UiProps, UiViewNode } from '@adecore/intelligent-ui';
|
|
2
|
+
export type UiColumnKind = NonNullable<UiProps<'Column'>['as']>;
|
|
3
|
+
export interface UiTableColumn {
|
|
4
|
+
key: string;
|
|
5
|
+
title: string;
|
|
6
|
+
unit?: string;
|
|
7
|
+
as: UiColumnKind;
|
|
8
|
+
}
|
|
9
|
+
export type UiTableCell = {
|
|
10
|
+
kind: 'empty';
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'text';
|
|
13
|
+
text: string;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'number';
|
|
16
|
+
as: 'number' | 'bytes' | 'duration';
|
|
17
|
+
value: number;
|
|
18
|
+
} | {
|
|
19
|
+
kind: 'date';
|
|
20
|
+
at: number;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'file';
|
|
23
|
+
path: string;
|
|
24
|
+
} | {
|
|
25
|
+
kind: 'tag';
|
|
26
|
+
text: string;
|
|
27
|
+
};
|
|
28
|
+
export declare const UI_TABLE_ROWS = 50;
|
|
29
|
+
export declare function isNumericColumn(column: UiTableColumn): boolean;
|
|
30
|
+
export declare function uiTableColumns(node: UiViewNode<UiProps<'Table'>>): UiTableColumn[];
|
|
31
|
+
export declare function uiTableCell(value: unknown, column: UiTableColumn): UiTableCell;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { uiChildrenOf } from './node-text.js';
|
|
2
|
+
/* Rows shown before a person asks for all of them. */
|
|
3
|
+
export const UI_TABLE_ROWS = 50;
|
|
4
|
+
/* Columns a table without Column children takes from its rows. */
|
|
5
|
+
const DERIVED_COLUMNS = 8;
|
|
6
|
+
const NUMERIC_KINDS = new Set(['number', 'bytes', 'duration']);
|
|
7
|
+
export function isNumericColumn(column) {
|
|
8
|
+
return NUMERIC_KINDS.has(column.as);
|
|
9
|
+
}
|
|
10
|
+
/*
|
|
11
|
+
* The columns a table can draw: its Column children whose key some row has, each key once. A table
|
|
12
|
+
* without columns takes the keys of its rows, as numbers when every value there is one. Empty when
|
|
13
|
+
* nothing is usable, which draws the fallback.
|
|
14
|
+
*/
|
|
15
|
+
export function uiTableColumns(node) {
|
|
16
|
+
const rows = node.props.rows;
|
|
17
|
+
const present = (key) => rows.some((row) => Object.hasOwn(row, key));
|
|
18
|
+
const declared = uiChildrenOf(node, 'Column');
|
|
19
|
+
if (declared.length > 0) {
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
return declared.flatMap(({ props }) => {
|
|
22
|
+
if (seen.has(props.key) || (rows.length > 0 && !present(props.key))) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
seen.add(props.key);
|
|
26
|
+
return [{ key: props.key, title: props.title ?? props.key, unit: props.unit, as: props.as ?? 'text' }];
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
const keys = [];
|
|
30
|
+
for (const row of rows) {
|
|
31
|
+
for (const key of Object.keys(row)) {
|
|
32
|
+
if (!keys.includes(key) && keys.length < DERIVED_COLUMNS) {
|
|
33
|
+
keys.push(key);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return keys.map((key) => {
|
|
38
|
+
const values = rows.map((row) => row[key]).filter((value) => value !== undefined && value !== null);
|
|
39
|
+
const numeric = values.length > 0 && values.every((value) => typeof value === 'number' && Number.isFinite(value));
|
|
40
|
+
return { key, title: key, as: numeric ? 'number' : 'text' };
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function textOf(value) {
|
|
44
|
+
return typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
45
|
+
}
|
|
46
|
+
function numberOf(value) {
|
|
47
|
+
if (typeof value === 'number') {
|
|
48
|
+
return Number.isFinite(value) ? value : null;
|
|
49
|
+
}
|
|
50
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
51
|
+
const parsed = Number(value);
|
|
52
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
/* A duration is in milliseconds and a date is milliseconds since the epoch or an ISO string; anything else reads as text. */
|
|
57
|
+
export function uiTableCell(value, column) {
|
|
58
|
+
if (value === undefined || value === null || value === '') {
|
|
59
|
+
return { kind: 'empty' };
|
|
60
|
+
}
|
|
61
|
+
switch (column.as) {
|
|
62
|
+
case 'number':
|
|
63
|
+
case 'bytes':
|
|
64
|
+
case 'duration': {
|
|
65
|
+
const number = numberOf(value);
|
|
66
|
+
return number === null ? { kind: 'text', text: textOf(value) } : { kind: 'number', as: column.as, value: number };
|
|
67
|
+
}
|
|
68
|
+
case 'date': {
|
|
69
|
+
const at = typeof value === 'number' ? value : typeof value === 'string' ? Date.parse(value) : Number.NaN;
|
|
70
|
+
return Number.isFinite(at) ? { kind: 'date', at } : { kind: 'text', text: textOf(value) };
|
|
71
|
+
}
|
|
72
|
+
case 'file':
|
|
73
|
+
return typeof value === 'string' ? { kind: 'file', path: value } : { kind: 'text', text: textOf(value) };
|
|
74
|
+
case 'tag':
|
|
75
|
+
return { kind: 'tag', text: textOf(value) };
|
|
76
|
+
case 'text':
|
|
77
|
+
return { kind: 'text', text: textOf(value) };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type LucideIcon } from 'lucide-react';
|
|
2
|
+
import type { UiTone } from '@adecore/intelligent-ui';
|
|
3
|
+
import type { PillProps } from '@adecore/ui';
|
|
4
|
+
export declare const UI_TONE_ICONS: Readonly<Record<UiTone, LucideIcon>>;
|
|
5
|
+
export declare const UI_TONE_TEXT: Readonly<Record<UiTone, string>>;
|
|
6
|
+
export declare const UI_TONE_SURFACE: Readonly<Record<UiTone, string>>;
|
|
7
|
+
export declare const UI_TONE_PILL: Readonly<Record<UiTone, NonNullable<PillProps['tone']>>>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { CircleCheck, Info, MessageCircle, OctagonAlert, TriangleAlert } from 'lucide-react';
|
|
2
|
+
/* Every tone has an icon of its own, so color is never the only thing that tells two apart. */
|
|
3
|
+
export const UI_TONE_ICONS = {
|
|
4
|
+
neutral: MessageCircle,
|
|
5
|
+
info: Info,
|
|
6
|
+
success: CircleCheck,
|
|
7
|
+
warning: TriangleAlert,
|
|
8
|
+
danger: OctagonAlert
|
|
9
|
+
};
|
|
10
|
+
export const UI_TONE_TEXT = {
|
|
11
|
+
neutral: 'text-text-muted',
|
|
12
|
+
info: 'text-status-running',
|
|
13
|
+
success: 'text-positive',
|
|
14
|
+
warning: 'text-status-needs-you',
|
|
15
|
+
danger: 'text-status-error'
|
|
16
|
+
};
|
|
17
|
+
/* The tone mixed over the ground, for a surface such as a callout. */
|
|
18
|
+
export const UI_TONE_SURFACE = {
|
|
19
|
+
neutral: 'bg-text/7',
|
|
20
|
+
info: 'bg-status-running/10',
|
|
21
|
+
success: 'bg-positive/10',
|
|
22
|
+
warning: 'bg-status-needs-you/12',
|
|
23
|
+
danger: 'bg-status-error/10'
|
|
24
|
+
};
|
|
25
|
+
export const UI_TONE_PILL = {
|
|
26
|
+
neutral: 'muted',
|
|
27
|
+
info: 'accent',
|
|
28
|
+
success: 'idle',
|
|
29
|
+
warning: 'needsYou',
|
|
30
|
+
danger: 'error'
|
|
31
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useLater(on: boolean, ms: number): boolean;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
/* True once `on` has held for `ms`, for a spinner that only shows when waiting takes long. */
|
|
3
|
+
export function useLater(on, ms) {
|
|
4
|
+
const [late, setLate] = useState(false);
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
if (!on) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
const timer = setTimeout(() => setLate(true), ms);
|
|
10
|
+
return () => {
|
|
11
|
+
clearTimeout(timer);
|
|
12
|
+
setLate(false);
|
|
13
|
+
};
|
|
14
|
+
}, [on, ms]);
|
|
15
|
+
return on && late;
|
|
16
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ChatUiQueryState } from '@adecore/agent-contracts';
|
|
2
|
+
import { type UiLinkTarget } from '@adecore/intelligent-ui/links';
|
|
3
|
+
import { type UiBlock, UiState } from '@adecore/intelligent-ui';
|
|
4
|
+
import type { UiRenderContext } from './render-context.js';
|
|
5
|
+
export declare function useUiLinks(block: UiBlock, state: UiState, context: UiRenderContext, reads: Readonly<Record<string, string>>, frozen?: ChatUiQueryState['blocks'][string]): {
|
|
6
|
+
link: ((target: UiLinkTarget) => import("./render-context.js").UiLink) | undefined;
|
|
7
|
+
openLink: ((target: UiLinkTarget) => void) | undefined;
|
|
8
|
+
openUrl: ((url: string) => void) | undefined;
|
|
9
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { uiLinkTargets } from '@adecore/intelligent-ui/links';
|
|
3
|
+
import { uiInputValues, UiState } from '@adecore/intelligent-ui';
|
|
4
|
+
import { chatHost } from '../../../host.js';
|
|
5
|
+
export function useUiLinks(block, state, context, reads, frozen) {
|
|
6
|
+
const [checked, setChecked] = useState({});
|
|
7
|
+
const host = chatHost().intelligentUi;
|
|
8
|
+
const values = uiInputValues(block, state);
|
|
9
|
+
const scope = state.scope();
|
|
10
|
+
const queryValues = Object.fromEntries(Object.keys(block.queries)
|
|
11
|
+
.filter((name) => Object.hasOwn(scope, name))
|
|
12
|
+
.map((name) => [name, scope[name]]));
|
|
13
|
+
let targets = {};
|
|
14
|
+
try {
|
|
15
|
+
if (block.complete) {
|
|
16
|
+
targets = uiLinkTargets(block, values, queryValues);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
const signature = JSON.stringify(targets);
|
|
21
|
+
const input = JSON.stringify(values);
|
|
22
|
+
const tickets = JSON.stringify(reads);
|
|
23
|
+
const link = host?.link;
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!link || !block.revision || context.phase !== 'final') {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
let stopped = false;
|
|
29
|
+
const validate = async () => {
|
|
30
|
+
for (const [nodeId, target] of Object.entries(JSON.parse(signature)).slice(0, 64)) {
|
|
31
|
+
try {
|
|
32
|
+
const reading = await link(context.scopeId, {
|
|
33
|
+
chatId: context.chatId,
|
|
34
|
+
itemId: context.itemId,
|
|
35
|
+
blockId: block.id,
|
|
36
|
+
revision: block.revision,
|
|
37
|
+
nodeId,
|
|
38
|
+
values: JSON.parse(input),
|
|
39
|
+
reads: JSON.parse(tickets)
|
|
40
|
+
});
|
|
41
|
+
if (stopped) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
setChecked((previous) => ({ ...previous, [nodeId]: { target: JSON.stringify(target), reading } }));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
if (stopped) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
setChecked((previous) => ({ ...previous, [nodeId]: { target: JSON.stringify(target), reading: { state: 'plain' } } }));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
void validate();
|
|
55
|
+
return () => {
|
|
56
|
+
stopped = true;
|
|
57
|
+
};
|
|
58
|
+
}, [block.id, block.revision, context.scopeId, context.chatId, context.itemId, context.phase, signature, input, tickets, link]);
|
|
59
|
+
const idOf = (target) => Object.entries(targets).find(([, entry]) => JSON.stringify(entry) === JSON.stringify(target))?.[0];
|
|
60
|
+
const resolution = (target) => {
|
|
61
|
+
const id = idOf(target);
|
|
62
|
+
if (id === undefined) {
|
|
63
|
+
return { state: 'plain' };
|
|
64
|
+
}
|
|
65
|
+
if (checked[id]?.target === JSON.stringify(target)) {
|
|
66
|
+
return checked[id].reading;
|
|
67
|
+
}
|
|
68
|
+
if (input === JSON.stringify(uiInputValues(block, new UiState(block))) && frozen && frozen.revision === block.revision) {
|
|
69
|
+
const firstQueries = Object.fromEntries(Object.entries(frozen.readings)
|
|
70
|
+
.filter(([, reading]) => reading.state === 'fresh')
|
|
71
|
+
.map(([name, reading]) => [name, reading.value]));
|
|
72
|
+
if (JSON.stringify(queryValues) === JSON.stringify(firstQueries)) {
|
|
73
|
+
return frozen.links?.[id] ?? { state: 'plain' };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { state: 'plain' };
|
|
77
|
+
};
|
|
78
|
+
const open = (target) => {
|
|
79
|
+
const nodeId = idOf(target);
|
|
80
|
+
if (!nodeId || !link || !block.revision) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
void link(context.scopeId, {
|
|
84
|
+
chatId: context.chatId,
|
|
85
|
+
itemId: context.itemId,
|
|
86
|
+
blockId: block.id,
|
|
87
|
+
revision: block.revision,
|
|
88
|
+
nodeId,
|
|
89
|
+
values,
|
|
90
|
+
reads: { ...reads }
|
|
91
|
+
})
|
|
92
|
+
.then((reading) => {
|
|
93
|
+
setChecked((previous) => ({ ...previous, [nodeId]: { target: JSON.stringify(target), reading } }));
|
|
94
|
+
if (reading.state === 'chip' && reading.target) {
|
|
95
|
+
host?.openLink?.(context.scopeId, reading);
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
.catch(() => {
|
|
99
|
+
setChecked((previous) => ({ ...previous, [nodeId]: { target: JSON.stringify(target), reading: { state: 'plain' } } }));
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
link: link ? resolution : context.link,
|
|
104
|
+
openLink: link && host?.openLink ? open : context.openLink,
|
|
105
|
+
openUrl: host?.openUrl ? (url) => host.openUrl(context.scopeId, url) : context.openUrl
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ChatUiQueryReading, ChatUiQueryState } from '@adecore/agent-contracts';
|
|
2
|
+
import { UiState, type UiBlock, type UiViewNode } from '@adecore/intelligent-ui';
|
|
3
|
+
import type { UiLiveValue, UiLiveStatus, UiRenderContext } from './render-context.js';
|
|
4
|
+
export declare function useUiQueries(block: UiBlock, state: UiState, context: UiRenderContext, element: HTMLDivElement | null, frozen?: ChatUiQueryState['blocks'][string]): {
|
|
5
|
+
live: UiLiveStatus | undefined;
|
|
6
|
+
reads: Record<string, string>;
|
|
7
|
+
reading: boolean;
|
|
8
|
+
ready: boolean;
|
|
9
|
+
readsFor: (values: Readonly<Record<string, unknown>>) => Record<string, string> | null;
|
|
10
|
+
currentReadings: {
|
|
11
|
+
[k: string]: {
|
|
12
|
+
state: "failed" | "fresh" | "refused";
|
|
13
|
+
value?: unknown;
|
|
14
|
+
readId?: string | undefined;
|
|
15
|
+
readAt: number;
|
|
16
|
+
reason?: string | undefined;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export declare function useUiLiveValues(block: UiBlock, nodes: readonly UiViewNode[], readings: Readonly<Record<string, ChatUiQueryReading>>): ((id: string, prop: string, path?: readonly [number, string]) => UiLiveValue | undefined) | undefined;
|