@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.
- package/dist/chat/chat-client.d.ts +1 -0
- 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 +19 -1
- package/dist/chat/logic/timeline-target.js +49 -3
- package/dist/chat/logic/timeline.js +25 -17
- 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/CodeBlock.d.ts +3 -1
- package/dist/chat/ui/CodeBlock.js +38 -7
- package/dist/chat/ui/Composer.js +13 -3
- package/dist/chat/ui/ImageView.d.ts +8 -2
- package/dist/chat/ui/ImageView.js +12 -6
- package/dist/chat/ui/Markdown.d.ts +7 -1
- package/dist/chat/ui/Markdown.js +80 -23
- package/dist/chat/ui/MessageActions.js +6 -7
- package/dist/chat/ui/SubagentTimeline.js +7 -3
- package/dist/chat/ui/Timeline.js +68 -8
- package/dist/chat/ui/TimelineMenu.js +7 -3
- package/dist/chat/ui/file-links.d.ts +2 -1
- package/dist/chat/ui/file-links.js +8 -3
- 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 +27 -7
- 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 +25 -3
- package/dist/host.js +1 -0
- package/dist/locales/en/agent-chat.json +89 -1
- package/dist/locales/nl/agent-chat.json +89 -1
- package/dist/state/chats.js +9 -0
- package/dist/theme.css +38 -2
- package/package.json +133 -7
- package/src/chat/chat-client.ts +1 -0
- package/src/chat/logic/generated-image.ts +48 -0
- package/src/chat/logic/timeline-target.ts +69 -4
- package/src/chat/logic/timeline.ts +28 -17
- package/src/chat/subagent-conversation.ts +13 -2
- package/src/chat/timeline-flash.ts +5 -3
- package/src/chat/ui/CodeBlock.tsx +70 -21
- package/src/chat/ui/Composer.tsx +22 -5
- package/src/chat/ui/ImageView.tsx +51 -8
- package/src/chat/ui/Markdown.tsx +123 -26
- package/src/chat/ui/MessageActions.tsx +15 -8
- package/src/chat/ui/SubagentTimeline.tsx +26 -1
- package/src/chat/ui/Timeline.tsx +98 -22
- package/src/chat/ui/TimelineMenu.tsx +6 -3
- package/src/chat/ui/file-links.ts +9 -3
- 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 +69 -7
- 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 +46 -3
- package/src/locales/en/agent-chat.json +89 -1
- package/src/locales/nl/agent-chat.json +89 -1
- package/src/state/chats.ts +9 -0
- package/src/theme.css +38 -2
|
@@ -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;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { uiInputValues, UI_CATALOG_VERSION, UiState } from '@adecore/intelligent-ui';
|
|
3
|
+
import { chatHost } from '../../../host.js';
|
|
4
|
+
function documentVisible() {
|
|
5
|
+
return document.visibilityState !== 'hidden';
|
|
6
|
+
}
|
|
7
|
+
export function useUiQueries(block, state, context, element, frozen) {
|
|
8
|
+
const [readings, setReadings] = useState({});
|
|
9
|
+
const [busyInput, setBusyInput] = useState(null);
|
|
10
|
+
const lastReadAt = useRef({ identity: '', at: 0 });
|
|
11
|
+
const names = Object.keys(block.queries);
|
|
12
|
+
const inputs = JSON.stringify(uiInputValues(block, state));
|
|
13
|
+
const identity = JSON.stringify([context.scopeId, context.chatId, context.itemId, block.id, block.revision]);
|
|
14
|
+
const inputKey = JSON.stringify([identity, inputs]);
|
|
15
|
+
const reading = busyInput === inputKey;
|
|
16
|
+
const defaultInputs = JSON.stringify(uiInputValues(block, new UiState(block)));
|
|
17
|
+
const initial = frozen && frozen.revision === block.revision ? frozen.readings : {};
|
|
18
|
+
const successfulFor = (name, signature) => {
|
|
19
|
+
const local = readings[name];
|
|
20
|
+
if (local?.identity === identity && local.successful?.inputs === signature) {
|
|
21
|
+
return local.successful.reading;
|
|
22
|
+
}
|
|
23
|
+
return signature === defaultInputs && initial[name]?.state === 'fresh' ? initial[name] : undefined;
|
|
24
|
+
};
|
|
25
|
+
const readsFor = (values) => {
|
|
26
|
+
const signature = JSON.stringify(values);
|
|
27
|
+
const reads = {};
|
|
28
|
+
for (const name of names) {
|
|
29
|
+
const local = readings[name];
|
|
30
|
+
const successful = successfulFor(name, signature);
|
|
31
|
+
if (!successful?.readId || (local?.identity === identity && local.reading.state === 'refused')) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
reads[name] = successful.readId;
|
|
35
|
+
}
|
|
36
|
+
return reads;
|
|
37
|
+
};
|
|
38
|
+
const reads = readsFor(JSON.parse(inputs));
|
|
39
|
+
const ready = reads !== null;
|
|
40
|
+
const query = chatHost().intelligentUi?.query;
|
|
41
|
+
const subscribe = chatHost().intelligentUi?.subscribe;
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const scope = state.scope();
|
|
44
|
+
for (const name of names) {
|
|
45
|
+
const successful = successfulFor(name, inputs);
|
|
46
|
+
if (successful && JSON.stringify(scope[name]) !== JSON.stringify(successful.value)) {
|
|
47
|
+
state.setQuery(name, successful.value, block);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!query ||
|
|
53
|
+
!element ||
|
|
54
|
+
!block.complete ||
|
|
55
|
+
block.catalogVersion !== UI_CATALOG_VERSION ||
|
|
56
|
+
!block.revision ||
|
|
57
|
+
!Object.keys(block.queries).length ||
|
|
58
|
+
context.phase !== 'final') {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
let stopped = false;
|
|
62
|
+
let visible = typeof IntersectionObserver === 'undefined';
|
|
63
|
+
let busy = false;
|
|
64
|
+
let timer = null;
|
|
65
|
+
const refresh = async () => {
|
|
66
|
+
if (stopped || !visible || !documentVisible() || busy) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const wait = lastReadAt.current.identity === identity ? 10_000 - (Date.now() - lastReadAt.current.at) : 0;
|
|
70
|
+
if (wait > 0) {
|
|
71
|
+
if (timer === null) {
|
|
72
|
+
if (!visible || !documentVisible()) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
timer = setTimeout(() => {
|
|
76
|
+
timer = null;
|
|
77
|
+
void refresh();
|
|
78
|
+
}, wait);
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
busy = true;
|
|
83
|
+
lastReadAt.current = { identity, at: Date.now() };
|
|
84
|
+
setBusyInput(inputKey);
|
|
85
|
+
for (const name of Object.keys(block.queries).slice(0, 8)) {
|
|
86
|
+
if (!visible || !documentVisible()) {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const reading = await query(context.scopeId, {
|
|
91
|
+
chatId: context.chatId,
|
|
92
|
+
itemId: context.itemId,
|
|
93
|
+
blockId: block.id,
|
|
94
|
+
revision: block.revision,
|
|
95
|
+
query: name,
|
|
96
|
+
values: JSON.parse(inputs)
|
|
97
|
+
});
|
|
98
|
+
if (stopped) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (reading.state === 'fresh') {
|
|
102
|
+
state.setQuery(name, reading.value, block);
|
|
103
|
+
}
|
|
104
|
+
setReadings((previous) => ({
|
|
105
|
+
...previous,
|
|
106
|
+
[name]: {
|
|
107
|
+
identity,
|
|
108
|
+
reading,
|
|
109
|
+
successful: reading.state === 'fresh' ? { inputs, reading } : previous[name]?.identity === identity ? previous[name].successful : undefined
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (stopped) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
setReadings((previous) => ({
|
|
118
|
+
...previous,
|
|
119
|
+
[name]: {
|
|
120
|
+
identity,
|
|
121
|
+
reading: { state: 'failed', readAt: Date.now(), reason: error instanceof Error ? error.message : 'This query could not be read.' },
|
|
122
|
+
successful: previous[name]?.identity === identity ? previous[name].successful : undefined
|
|
123
|
+
}
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
busy = false;
|
|
128
|
+
if (stopped) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
setBusyInput(null);
|
|
132
|
+
timer = setTimeout(() => {
|
|
133
|
+
timer = null;
|
|
134
|
+
void refresh();
|
|
135
|
+
}, 10_000);
|
|
136
|
+
};
|
|
137
|
+
const observer = typeof IntersectionObserver === 'undefined'
|
|
138
|
+
? null
|
|
139
|
+
: new IntersectionObserver((entries) => {
|
|
140
|
+
visible = entries.some((entry) => entry.isIntersecting);
|
|
141
|
+
if (visible) {
|
|
142
|
+
void refresh();
|
|
143
|
+
}
|
|
144
|
+
else if (timer !== null) {
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
timer = null;
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
if (element) {
|
|
150
|
+
observer?.observe(element);
|
|
151
|
+
}
|
|
152
|
+
const focus = () => {
|
|
153
|
+
void refresh();
|
|
154
|
+
};
|
|
155
|
+
document.addEventListener('visibilitychange', focus);
|
|
156
|
+
const unsubscribe = subscribe?.(context.scopeId, context.chatId, focus);
|
|
157
|
+
void refresh();
|
|
158
|
+
return () => {
|
|
159
|
+
stopped = true;
|
|
160
|
+
observer?.disconnect();
|
|
161
|
+
unsubscribe?.();
|
|
162
|
+
document.removeEventListener('visibilitychange', focus);
|
|
163
|
+
if (timer !== null) {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}, [block, state, context.scopeId, context.chatId, context.itemId, context.phase, inputs, query, subscribe, element, identity, inputKey]);
|
|
168
|
+
const currentReadings = Object.fromEntries(names.flatMap((name) => {
|
|
169
|
+
const local = readings[name];
|
|
170
|
+
const reading = local?.identity === identity ? local.reading : initial[name];
|
|
171
|
+
return reading ? [[name, reading]] : [];
|
|
172
|
+
}));
|
|
173
|
+
const failed = Object.entries(currentReadings).find(([, value]) => value.state !== 'fresh');
|
|
174
|
+
const live = names.length
|
|
175
|
+
? {
|
|
176
|
+
state: reading || (!ready && !failed) ? 'reading' : (failed?.[1].state ?? 'fresh'),
|
|
177
|
+
sources: names.map((name) => block.queries[name].source),
|
|
178
|
+
readAt: Object.values(currentReadings)
|
|
179
|
+
.filter((reading) => reading.state === 'fresh')
|
|
180
|
+
.reduce((at, value) => (at === null ? value.readAt : Math.min(at, value.readAt)), null),
|
|
181
|
+
source: failed ? block.queries[failed[0]]?.source : undefined,
|
|
182
|
+
reason: failed?.[1].reason
|
|
183
|
+
}
|
|
184
|
+
: undefined;
|
|
185
|
+
return { live, reads: reads ?? {}, reading, ready, readsFor, currentReadings };
|
|
186
|
+
}
|
|
187
|
+
export function useUiLiveValues(block, nodes, readings) {
|
|
188
|
+
const [committed, setCommitted] = useState({
|
|
189
|
+
token: '',
|
|
190
|
+
values: new Map(),
|
|
191
|
+
previous: new Map()
|
|
192
|
+
});
|
|
193
|
+
const dependencies = new Map();
|
|
194
|
+
const names = new Set(Object.keys(block.queries));
|
|
195
|
+
const references = (value, into) => {
|
|
196
|
+
if (!value || typeof value !== 'object') {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const expression = value;
|
|
200
|
+
if (expression.kind === 'reference' && names.has(String(expression.name))) {
|
|
201
|
+
into.add(String(expression.name));
|
|
202
|
+
}
|
|
203
|
+
for (const child of Object.values(expression)) {
|
|
204
|
+
if (Array.isArray(child)) {
|
|
205
|
+
child.forEach((item) => references(item, into));
|
|
206
|
+
}
|
|
207
|
+
else if (typeof child === 'object') {
|
|
208
|
+
references(child, into);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
const written = (nodes, inherited = new Set()) => {
|
|
213
|
+
for (const node of nodes) {
|
|
214
|
+
const related = new Set(inherited);
|
|
215
|
+
references(node.expressions, related);
|
|
216
|
+
dependencies.set(node.id, related);
|
|
217
|
+
written(node.children, node.type === 'Each' ? related : inherited);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
written(block.nodes);
|
|
221
|
+
const values = new Map();
|
|
222
|
+
const sources = new Map();
|
|
223
|
+
const collect = (nodes) => {
|
|
224
|
+
for (const node of nodes) {
|
|
225
|
+
const related = dependencies.get(node.sourceId ?? node.id);
|
|
226
|
+
if (related?.size && !node.error) {
|
|
227
|
+
values.set(node.id, node.props);
|
|
228
|
+
sources.set(node.id, related);
|
|
229
|
+
}
|
|
230
|
+
collect(node.children);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
collect(nodes);
|
|
234
|
+
const token = JSON.stringify([block.revision, Object.entries(readings).map(([name, reading]) => [name, reading.state, reading.readId, reading.readAt])]);
|
|
235
|
+
const previous = committed.token === token ? committed.previous : committed.values;
|
|
236
|
+
if (committed.token !== token) {
|
|
237
|
+
setCommitted({ token, values, previous });
|
|
238
|
+
}
|
|
239
|
+
if (!names.size) {
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
return (id, prop, path) => {
|
|
243
|
+
const related = sources.get(id);
|
|
244
|
+
if (!related) {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
const get = (props) => {
|
|
248
|
+
const value = props?.[prop];
|
|
249
|
+
return path && Array.isArray(value) ? value[path[0]]?.[path[1]] : value;
|
|
250
|
+
};
|
|
251
|
+
const before = get(previous.get(id));
|
|
252
|
+
const value = get(values.get(id));
|
|
253
|
+
return {
|
|
254
|
+
previous: before,
|
|
255
|
+
changed: before !== undefined && JSON.stringify(before) !== JSON.stringify(value),
|
|
256
|
+
stale: [...related].some((name) => readings[name]?.state === 'failed' || readings[name]?.state === 'refused')
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
}
|