@datalayer/agent-runtimes 1.5.90 → 1.5.91
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/README.md +29 -297
- package/dist-embed/datalayer-app-main.js +2 -2
- package/lib/components/teams/A2ATeamGraph.d.ts +40 -155
- package/lib/components/teams/A2ATeamGraph.js +122 -400
- package/lib/components/teams/SceneTranscript.d.ts +5 -1
- package/lib/components/teams/SceneTranscript.js +2 -2
- package/lib/components/teams/SceneView.d.ts +17 -5
- package/lib/components/teams/SceneView.js +7 -3
- package/lib/components/teams/backdrops/catalogue.d.ts +35 -0
- package/lib/components/teams/backdrops/catalogue.js +34 -0
- package/lib/components/teams/backdrops/edges.d.ts +20 -0
- package/lib/components/teams/backdrops/edges.js +29 -0
- package/lib/components/teams/backdrops/faces.d.ts +24 -0
- package/lib/components/teams/backdrops/faces.js +39 -0
- package/lib/components/teams/backdrops/halo.d.ts +14 -0
- package/lib/components/teams/backdrops/halo.js +5 -0
- package/lib/components/teams/backdrops/hero.d.ts +14 -0
- package/lib/components/teams/backdrops/hero.js +178 -0
- package/lib/components/teams/backdrops/index.d.ts +22 -0
- package/lib/components/teams/backdrops/index.js +38 -0
- package/lib/components/teams/backdrops/nodes.d.ts +123 -0
- package/lib/components/teams/backdrops/nodes.js +265 -0
- package/lib/components/teams/backdrops/plain.d.ts +14 -0
- package/lib/components/teams/backdrops/plain.js +116 -0
- package/lib/components/teams/backdrops/studio.d.ts +17 -0
- package/lib/components/teams/backdrops/studio.js +122 -0
- package/lib/components/teams/backdrops/theme.d.ts +14 -0
- package/lib/components/teams/backdrops/theme.js +19 -0
- package/lib/components/teams/backdrops/types.d.ts +214 -0
- package/lib/components/teams/backdrops/types.js +5 -0
- package/lib/components/teams/index.d.ts +2 -0
- package/lib/components/teams/index.js +2 -0
- package/lib/components/teams/sceneTranscriptRules.d.ts +32 -1
- package/lib/components/teams/sceneTranscriptRules.js +104 -2
- package/lib/components/teams/transcript/RecordingLabel.d.ts +17 -0
- package/lib/components/teams/transcript/RecordingLabel.js +7 -0
- package/lib/components/teams/transcript/SequenceDiagram.d.ts +39 -0
- package/lib/components/teams/transcript/SequenceDiagram.js +190 -0
- package/lib/components/teams/transcript/TranscriptDetail.d.ts +27 -0
- package/lib/components/teams/transcript/TranscriptDetail.js +48 -0
- package/lib/components/teams/transcript/TranscriptTable.d.ts +23 -0
- package/lib/components/teams/transcript/TranscriptTable.js +112 -0
- package/lib/components/teams/transcript/TranscriptViews.d.ts +66 -0
- package/lib/components/teams/transcript/TranscriptViews.js +135 -0
- package/lib/components/teams/transcript/TranscriptViewsTypes.d.ts +13 -0
- package/lib/components/teams/transcript/TranscriptViewsTypes.js +5 -0
- package/lib/components/teams/transcript/index.d.ts +23 -0
- package/lib/components/teams/transcript/index.js +27 -0
- package/lib/components/teams/transcript/recordingPlayback.d.ts +52 -0
- package/lib/components/teams/transcript/recordingPlayback.js +146 -0
- package/lib/components/teams/transcript/selection.d.ts +59 -0
- package/lib/components/teams/transcript/selection.js +79 -0
- package/lib/components/teams/transcript/sequenceDiagramRules.d.ts +35 -0
- package/lib/components/teams/transcript/sequenceDiagramRules.js +158 -0
- package/lib/components/teams/transcript/transcriptDetailRules.d.ts +80 -0
- package/lib/components/teams/transcript/transcriptDetailRules.js +208 -0
- package/lib/components/teams/transcript/transcriptExport.d.ts +31 -0
- package/lib/components/teams/transcript/transcriptExport.js +82 -0
- package/lib/components/teams/transcript/transcriptRows.d.ts +76 -0
- package/lib/components/teams/transcript/transcriptRows.js +187 -0
- package/lib/components/teams/transcript/useSceneRecording.d.ts +45 -0
- package/lib/components/teams/transcript/useSceneRecording.js +96 -0
- package/lib/examples/ScenesExample.js +16 -3
- package/lib/specs/actions.js +2 -0
- package/lib/specs/apps.d.ts +1 -0
- package/lib/specs/apps.js +219 -0
- package/lib/specs/sceneRecordings.d.ts +14 -0
- package/lib/specs/sceneRecordings.js +12 -0
- package/lib/specs/sceneSchema.js +11 -0
- package/lib/specs/scenes.d.ts +1 -0
- package/lib/specs/scenes.js +159 -0
- package/lib/types/scenes.d.ts +54 -0
- package/package.json +2 -1
- package/scripts/codegen/generate_scenes.py +80 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the three views share: the lines. In its own module so that the
|
|
3
|
+
* table and the diagram say the same shape without reading each other.
|
|
4
|
+
*
|
|
5
|
+
* @module components/teams/transcript/TranscriptViewsTypes
|
|
6
|
+
*/
|
|
7
|
+
import type { SceneTranscriptLine } from '../sceneTranscriptRules';
|
|
8
|
+
/** The lines a view draws, in order: already filtered. */
|
|
9
|
+
export type TranscriptRowsLines = {
|
|
10
|
+
lines: readonly SceneTranscriptLine[];
|
|
11
|
+
};
|
|
12
|
+
/** Which of the three views shows. */
|
|
13
|
+
export type TranscriptViewChoice = 'lines' | 'table' | 'sequence';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The message views of a scene (SCENES S2, S3): the transcript as rows and
|
|
3
|
+
* as a mermaid sequence diagram from the one list of lines, the three
|
|
4
|
+
* views under one switch, the drill-down, the selection the stage shares,
|
|
5
|
+
* the JSON export, and a recording played at its recorded pace
|
|
6
|
+
* (`useSceneRecording`, `RecordingLabel`). mermaid is loaded by `SequenceDiagram` when the
|
|
7
|
+
* view opens, never from here.
|
|
8
|
+
*
|
|
9
|
+
* @module components/teams/transcript
|
|
10
|
+
*/
|
|
11
|
+
export * from './transcriptRows';
|
|
12
|
+
export * from './sequenceDiagramRules';
|
|
13
|
+
export * from './selection';
|
|
14
|
+
export * from './transcriptExport';
|
|
15
|
+
export * from './transcriptDetailRules';
|
|
16
|
+
export * from './TranscriptViewsTypes';
|
|
17
|
+
export * from './TranscriptTable';
|
|
18
|
+
export * from './SequenceDiagram';
|
|
19
|
+
export * from './TranscriptDetail';
|
|
20
|
+
export * from './TranscriptViews';
|
|
21
|
+
export * from './recordingPlayback';
|
|
22
|
+
export * from './useSceneRecording';
|
|
23
|
+
export * from './RecordingLabel';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The message views of a scene (SCENES S2, S3): the transcript as rows and
|
|
7
|
+
* as a mermaid sequence diagram from the one list of lines, the three
|
|
8
|
+
* views under one switch, the drill-down, the selection the stage shares,
|
|
9
|
+
* the JSON export, and a recording played at its recorded pace
|
|
10
|
+
* (`useSceneRecording`, `RecordingLabel`). mermaid is loaded by `SequenceDiagram` when the
|
|
11
|
+
* view opens, never from here.
|
|
12
|
+
*
|
|
13
|
+
* @module components/teams/transcript
|
|
14
|
+
*/
|
|
15
|
+
export * from './transcriptRows.js';
|
|
16
|
+
export * from './sequenceDiagramRules.js';
|
|
17
|
+
export * from './selection.js';
|
|
18
|
+
export * from './transcriptExport.js';
|
|
19
|
+
export * from './transcriptDetailRules.js';
|
|
20
|
+
export * from './TranscriptViewsTypes.js';
|
|
21
|
+
export * from './TranscriptTable.js';
|
|
22
|
+
export * from './SequenceDiagram.js';
|
|
23
|
+
export * from './TranscriptDetail.js';
|
|
24
|
+
export * from './TranscriptViews.js';
|
|
25
|
+
export * from './recordingPlayback.js';
|
|
26
|
+
export * from './useSceneRecording.js';
|
|
27
|
+
export * from './RecordingLabel.js';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A recording played back: the transcript of a passing rehearsal
|
|
3
|
+
* (`<id>/recording.json`, `SceneRecordingFile`), its lines shown at the pace
|
|
4
|
+
* they were recorded, and the stage drawn as it was at each moment — which
|
|
5
|
+
* A2A links carried a message, which tool calls ran.
|
|
6
|
+
*
|
|
7
|
+
* Pure: a recording and a moment of it in, the lines said by then and the
|
|
8
|
+
* graph's `flows` and `calls` out. {@link useSceneRecording} drives it with
|
|
9
|
+
* a clock; a test reads it at any moment.
|
|
10
|
+
*
|
|
11
|
+
* @module components/teams/transcript/recordingPlayback
|
|
12
|
+
*/
|
|
13
|
+
import type { SceneRecordingFile } from '../../../types/scenes';
|
|
14
|
+
import { type A2ATeamCall, type A2ATeamFlow } from '../a2aTeamFlow';
|
|
15
|
+
import type { SceneTranscriptLine, SceneTranscriptMember } from '../sceneTranscriptRules';
|
|
16
|
+
/** What a recording's label says: *Recording, taken 29 October 2026*. */
|
|
17
|
+
export declare function recordingLabel(taken: string | undefined, locale?: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* The lines of a recording, as a stage draws them: every line, or one
|
|
20
|
+
* beat's (by its id, as `beats` says which lines it played). None for a
|
|
21
|
+
* beat the recording did not play.
|
|
22
|
+
*/
|
|
23
|
+
export declare function recordedLinesOf(recording: Pick<SceneRecordingFile, 'lines' | 'beats'>, beat?: string): SceneTranscriptLine[];
|
|
24
|
+
/**
|
|
25
|
+
* When each line is shown, in milliseconds from the start of the playback:
|
|
26
|
+
* the recorded pace, divided by `speed`, with no pause longer than
|
|
27
|
+
* `maxGapMs` (a runtime starting between two beats is not worth watching).
|
|
28
|
+
*/
|
|
29
|
+
export declare function playbackOffsets(lines: readonly Pick<SceneTranscriptLine, 'at'>[], { speed, maxGapMs }?: {
|
|
30
|
+
speed?: number;
|
|
31
|
+
maxGapMs?: number;
|
|
32
|
+
}): number[];
|
|
33
|
+
/**
|
|
34
|
+
* The moment of the recording a playback offset stands for, in recorded
|
|
35
|
+
* milliseconds: the line shown last, plus the time since, at the same pace.
|
|
36
|
+
*/
|
|
37
|
+
export declare function recordedMomentAt(lines: readonly Pick<SceneTranscriptLine, 'at'>[], offsets: readonly number[], elapsed: number, speed?: number): number;
|
|
38
|
+
/** What the stage shows at a moment of the recording: the links' flows and the calls running. */
|
|
39
|
+
export type RecordedStage = {
|
|
40
|
+
/** Which way each peer's link carries a message, by the peer's id (`A2ATeamGraph`'s `flows`). */
|
|
41
|
+
flows: Record<string, A2ATeamFlow>;
|
|
42
|
+
/** The tool calls to the members' connections (`A2ATeamGraph`'s `calls`). */
|
|
43
|
+
calls: A2ATeamCall[];
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* The stage at a moment of the recording (`at`, recorded milliseconds), from
|
|
47
|
+
* the lines said by then: a link from the entry to a peer is `asking` while
|
|
48
|
+
* the peer's answer has not come, `answering` for {@link ANSWER_SHOWN_MS}
|
|
49
|
+
* after it; a call to a connection runs until it ended and shows its result
|
|
50
|
+
* coming back for {@link RESULT_SHOWN_MS}.
|
|
51
|
+
*/
|
|
52
|
+
export declare function recordedStageAt(lines: readonly SceneTranscriptLine[], at: number, members: readonly SceneTranscriptMember[], entry: string): RecordedStage;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { ANSWER_SHOWN_MS, RESULT_SHOWN_MS, } from '../a2aTeamFlow.js';
|
|
6
|
+
import { exchangeOf } from './transcriptRows.js';
|
|
7
|
+
/** What a recording's label says: *Recording, taken 29 October 2026*. */
|
|
8
|
+
export function recordingLabel(taken, locale = 'en-GB') {
|
|
9
|
+
const day = (taken ?? '').trim();
|
|
10
|
+
if (!day) {
|
|
11
|
+
return 'Recording';
|
|
12
|
+
}
|
|
13
|
+
const parsed = /^\d{4}-\d{2}-\d{2}$/.test(day)
|
|
14
|
+
? new Date(`${day}T12:00:00Z`)
|
|
15
|
+
: new Date(day);
|
|
16
|
+
const shown = Number.isFinite(parsed.getTime())
|
|
17
|
+
? parsed.toLocaleDateString(locale, {
|
|
18
|
+
day: 'numeric',
|
|
19
|
+
month: 'long',
|
|
20
|
+
year: 'numeric',
|
|
21
|
+
timeZone: 'UTC',
|
|
22
|
+
})
|
|
23
|
+
: day;
|
|
24
|
+
return `Recording, taken ${shown}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The lines of a recording, as a stage draws them: every line, or one
|
|
28
|
+
* beat's (by its id, as `beats` says which lines it played). None for a
|
|
29
|
+
* beat the recording did not play.
|
|
30
|
+
*/
|
|
31
|
+
export function recordedLinesOf(recording, beat) {
|
|
32
|
+
const lines = recording.lines.map(line => ({ ...line }));
|
|
33
|
+
if (!beat) {
|
|
34
|
+
return lines;
|
|
35
|
+
}
|
|
36
|
+
const found = recording.beats?.find(item => item.beat === beat);
|
|
37
|
+
return found ? lines.slice(found.first, found.first + found.count) : [];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* When each line is shown, in milliseconds from the start of the playback:
|
|
41
|
+
* the recorded pace, divided by `speed`, with no pause longer than
|
|
42
|
+
* `maxGapMs` (a runtime starting between two beats is not worth watching).
|
|
43
|
+
*/
|
|
44
|
+
export function playbackOffsets(lines, { speed = 1, maxGapMs } = {}) {
|
|
45
|
+
const pace = speed > 0 ? speed : 1;
|
|
46
|
+
const offsets = [];
|
|
47
|
+
let offset = 0;
|
|
48
|
+
lines.forEach((line, index) => {
|
|
49
|
+
if (index > 0) {
|
|
50
|
+
let gap = Math.max(0, line.at - lines[index - 1].at) / pace;
|
|
51
|
+
if (maxGapMs !== undefined) {
|
|
52
|
+
gap = Math.min(gap, maxGapMs);
|
|
53
|
+
}
|
|
54
|
+
offset += gap;
|
|
55
|
+
}
|
|
56
|
+
offsets.push(Math.round(offset));
|
|
57
|
+
});
|
|
58
|
+
return offsets;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The moment of the recording a playback offset stands for, in recorded
|
|
62
|
+
* milliseconds: the line shown last, plus the time since, at the same pace.
|
|
63
|
+
*/
|
|
64
|
+
export function recordedMomentAt(lines, offsets, elapsed, speed = 1) {
|
|
65
|
+
if (lines.length === 0) {
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
let last = 0;
|
|
69
|
+
for (let index = 0; index < offsets.length; index += 1) {
|
|
70
|
+
if (offsets[index] <= elapsed) {
|
|
71
|
+
last = index;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const since = Math.max(0, elapsed - offsets[last]) * (speed > 0 ? speed : 1);
|
|
75
|
+
const next = lines[last + 1]?.at;
|
|
76
|
+
const moment = lines[last].at + since;
|
|
77
|
+
return next === undefined ? moment : Math.min(moment, next);
|
|
78
|
+
}
|
|
79
|
+
function memberIdOf(name, members) {
|
|
80
|
+
if (!name) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
return (members.find(member => member.name === name)?.id ??
|
|
84
|
+
members.find(member => member.id === name)?.id);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The stage at a moment of the recording (`at`, recorded milliseconds), from
|
|
88
|
+
* the lines said by then: a link from the entry to a peer is `asking` while
|
|
89
|
+
* the peer's answer has not come, `answering` for {@link ANSWER_SHOWN_MS}
|
|
90
|
+
* after it; a call to a connection runs until it ended and shows its result
|
|
91
|
+
* coming back for {@link RESULT_SHOWN_MS}.
|
|
92
|
+
*/
|
|
93
|
+
export function recordedStageAt(lines, at, members, entry) {
|
|
94
|
+
const flows = {};
|
|
95
|
+
const calls = [];
|
|
96
|
+
const answers = new Map();
|
|
97
|
+
for (const line of lines) {
|
|
98
|
+
if (line.kind === 'said') {
|
|
99
|
+
answers.set(exchangeOf(line.key), line);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
if (line.at > at) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (line.kind === 'asked') {
|
|
107
|
+
const asker = memberIdOf(line.from, members);
|
|
108
|
+
const asked = memberIdOf(line.to, members);
|
|
109
|
+
if (!asker || !asked || asker !== entry) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const answer = answers.get(exchangeOf(line.key));
|
|
113
|
+
const answeredAt = answer?.at ?? line.ended;
|
|
114
|
+
if (answeredAt === undefined || at < answeredAt) {
|
|
115
|
+
flows[asked] = 'asking';
|
|
116
|
+
}
|
|
117
|
+
else if (at < answeredAt + ANSWER_SHOWN_MS) {
|
|
118
|
+
flows[asked] = 'answering';
|
|
119
|
+
}
|
|
120
|
+
else if (!flows[asked]) {
|
|
121
|
+
flows[asked] = 'still';
|
|
122
|
+
}
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (line.kind === 'called') {
|
|
126
|
+
const member = members.find(item => item.name === line.from || item.id === line.from);
|
|
127
|
+
const connection = member?.connections?.find(item => item.label === line.to || item.id === line.to);
|
|
128
|
+
if (!member || !connection) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const ended = line.ended !== undefined && line.ended <= at ? line.ended : undefined;
|
|
132
|
+
if (ended !== undefined && at >= ended + RESULT_SHOWN_MS) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
calls.push({
|
|
136
|
+
member: member.id,
|
|
137
|
+
connection: connection.id,
|
|
138
|
+
tool: line.text,
|
|
139
|
+
id: line.key,
|
|
140
|
+
started: line.at,
|
|
141
|
+
...(ended !== undefined ? { ended } : {}),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { flows, calls };
|
|
146
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The selection the transcript's views and the stage share (SCENES S3-02).
|
|
3
|
+
*
|
|
4
|
+
* A line selected in the Lines, Table or Sequence view — or a row, or an
|
|
5
|
+
* arrow — is one {@link TranscriptSelection}: the line's key, who it is from
|
|
6
|
+
* and to, its kind and time. The page holds it (`TranscriptViews`'s
|
|
7
|
+
* `selected`/`onSelect`) and hands the graph the edge it is on,
|
|
8
|
+
* {@link selectedEdgeOf}: the member ids and, for a call, the connection's
|
|
9
|
+
* node id, as `A2ATeamGraph` names its edges (`from->to` for an A2A link,
|
|
10
|
+
* `member->member/connection` for a connection's call edge).
|
|
11
|
+
*
|
|
12
|
+
* **The contract with the graph and its backdrops.** The graph takes
|
|
13
|
+
* `selected?: SelectedEdge | null` and draws that edge highlighted on every
|
|
14
|
+
* backdrop — by its `id`, or by its endpoints in either direction, since an
|
|
15
|
+
* answer travels the asker's edge back. A click on an edge gives the page the
|
|
16
|
+
* edge's latest exchange: the graph calls `onSelectEdge?(edge)` and the page
|
|
17
|
+
* answers with `latestLineOnEdge` to find the line to select.
|
|
18
|
+
*
|
|
19
|
+
* @module components/teams/transcript/selection
|
|
20
|
+
*/
|
|
21
|
+
import type { SceneTranscriptKind, SceneTranscriptLine, SceneTranscriptMember } from '../sceneTranscriptRules';
|
|
22
|
+
/** A line selected, as the views and the stage share it. */
|
|
23
|
+
export type TranscriptSelection = {
|
|
24
|
+
/** The line's key. */
|
|
25
|
+
key: string;
|
|
26
|
+
from: string;
|
|
27
|
+
to?: string;
|
|
28
|
+
kind: SceneTranscriptKind;
|
|
29
|
+
at: number;
|
|
30
|
+
failed?: boolean;
|
|
31
|
+
};
|
|
32
|
+
/** The edge of the graph a selection is on. */
|
|
33
|
+
export type SelectedEdge = {
|
|
34
|
+
/** As the graph names it: `sales->accounting`, `accounting->accounting/odoo-accounting`. */
|
|
35
|
+
id: string;
|
|
36
|
+
/** The asker's or caller's member id. */
|
|
37
|
+
from: string;
|
|
38
|
+
/** The member asked (by id), or the connection's node id. */
|
|
39
|
+
to: string;
|
|
40
|
+
over: 'a2a' | 'mcp';
|
|
41
|
+
/** The selection it is for. */
|
|
42
|
+
key: string;
|
|
43
|
+
};
|
|
44
|
+
/** The selection a line is. */
|
|
45
|
+
export declare function selectionOfLine(line: SceneTranscriptLine): TranscriptSelection;
|
|
46
|
+
/** A connection's node id in the graph: the member's id, a slash, the connection's. */
|
|
47
|
+
export declare const connectionNodeIdOf: (member: string, connection: string) => string;
|
|
48
|
+
/**
|
|
49
|
+
* The edge a selection is on, or none: a line between two members is on
|
|
50
|
+
* their A2A link, oriented from the asker (an answer travels it back); a
|
|
51
|
+
* call is on the caller's edge to the connection whose label it reaches.
|
|
52
|
+
* A line the person speaks, or a call to a tool's home, is on no edge.
|
|
53
|
+
*/
|
|
54
|
+
export declare function selectedEdgeOf(selection: TranscriptSelection | null | undefined, members: readonly SceneTranscriptMember[]): SelectedEdge | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* The latest line on an edge: what a click on the edge selects. The edge's
|
|
57
|
+
* ends are member ids (or a connection's node id); the lines name members.
|
|
58
|
+
*/
|
|
59
|
+
export declare function latestLineOnEdge(edge: Pick<SelectedEdge, 'from' | 'to'>, lines: readonly SceneTranscriptLine[], members: readonly SceneTranscriptMember[]): SceneTranscriptLine | undefined;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/** The selection a line is. */
|
|
6
|
+
export function selectionOfLine(line) {
|
|
7
|
+
return {
|
|
8
|
+
key: line.key,
|
|
9
|
+
from: line.from,
|
|
10
|
+
...(line.to ? { to: line.to } : {}),
|
|
11
|
+
kind: line.kind,
|
|
12
|
+
at: line.at,
|
|
13
|
+
...(line.failed ? { failed: true } : {}),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** A connection's node id in the graph: the member's id, a slash, the connection's. */
|
|
17
|
+
export const connectionNodeIdOf = (member, connection) => `${member}/${connection}`;
|
|
18
|
+
/** The member a name is: by name, then by id. */
|
|
19
|
+
function memberOf(name, members) {
|
|
20
|
+
return name
|
|
21
|
+
? (members.find(member => member.name === name) ??
|
|
22
|
+
members.find(member => member.id === name))
|
|
23
|
+
: undefined;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The edge a selection is on, or none: a line between two members is on
|
|
27
|
+
* their A2A link, oriented from the asker (an answer travels it back); a
|
|
28
|
+
* call is on the caller's edge to the connection whose label it reaches.
|
|
29
|
+
* A line the person speaks, or a call to a tool's home, is on no edge.
|
|
30
|
+
*/
|
|
31
|
+
export function selectedEdgeOf(selection, members) {
|
|
32
|
+
if (!selection || !selection.to) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const from = memberOf(selection.from, members);
|
|
36
|
+
const to = memberOf(selection.to, members);
|
|
37
|
+
if (selection.kind === 'called') {
|
|
38
|
+
const connection = from?.connections?.find(one => one.label === selection.to || one.name === selection.to);
|
|
39
|
+
if (!from || !connection) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const node = connectionNodeIdOf(from.id, connection.id);
|
|
43
|
+
return {
|
|
44
|
+
id: `${from.id}->${node}`,
|
|
45
|
+
from: from.id,
|
|
46
|
+
to: node,
|
|
47
|
+
over: 'mcp',
|
|
48
|
+
key: selection.key,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (!from || !to) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
// An answer (`said`, with `to`) goes back along the ask's edge.
|
|
55
|
+
const [asker, asked] = selection.kind === 'said' ? [to, from] : [from, to];
|
|
56
|
+
return {
|
|
57
|
+
id: `${asker.id}->${asked.id}`,
|
|
58
|
+
from: asker.id,
|
|
59
|
+
to: asked.id,
|
|
60
|
+
over: 'a2a',
|
|
61
|
+
key: selection.key,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The latest line on an edge: what a click on the edge selects. The edge's
|
|
66
|
+
* ends are member ids (or a connection's node id); the lines name members.
|
|
67
|
+
*/
|
|
68
|
+
export function latestLineOnEdge(edge, lines, members) {
|
|
69
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
70
|
+
const line = lines[index];
|
|
71
|
+
const on = selectedEdgeOf(selectionOfLine(line), members);
|
|
72
|
+
if (on &&
|
|
73
|
+
((on.from === edge.from && on.to === edge.to) ||
|
|
74
|
+
(on.from === edge.to && on.to === edge.from))) {
|
|
75
|
+
return line;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The transcript as a mermaid sequence diagram (SCENES S2-02): one
|
|
3
|
+
* participant per member and system in the cast's order (the person first,
|
|
4
|
+
* as an actor, when they speak), `->>` for an ask or a call, `-->>` for
|
|
5
|
+
* the answer, `activate`/`deactivate` around an exchange that is open or
|
|
6
|
+
* answered, `Note over` for words said to all, and a `rect` around an
|
|
7
|
+
* exchange that failed.
|
|
8
|
+
*
|
|
9
|
+
* Pure: lines in, source out — a golden file holds the source of a recorded
|
|
10
|
+
* transcript — and, beside it, which line each arrow and each note is, so
|
|
11
|
+
* that a click on the drawing finds its line ({@link SequenceDiagram}).
|
|
12
|
+
*
|
|
13
|
+
* @module components/teams/transcript/sequenceDiagramRules
|
|
14
|
+
*/
|
|
15
|
+
import { type SceneTranscriptLine } from '../sceneTranscriptRules';
|
|
16
|
+
/** One of the cast, as the diagram stands them: a member, or a system a member reaches. */
|
|
17
|
+
export type SequenceCastMember = {
|
|
18
|
+
name: string;
|
|
19
|
+
/** A system (a server reached over MCP), drawn after the members. */
|
|
20
|
+
system?: boolean;
|
|
21
|
+
};
|
|
22
|
+
/** The source, and which line each arrow and each note is, in drawing order. */
|
|
23
|
+
export type SequenceDiagramPlan = {
|
|
24
|
+
source: string;
|
|
25
|
+
/** The key of the line each arrow (a message) carries, in the order drawn. */
|
|
26
|
+
arrows: string[];
|
|
27
|
+
/** The key of the line each note carries, in the order drawn. */
|
|
28
|
+
notes: string[];
|
|
29
|
+
};
|
|
30
|
+
/** Words on a diagram: one line, no statement end, no entity, clipped. */
|
|
31
|
+
export declare function diagramText(text: string): string;
|
|
32
|
+
/** The source and the map of its arrows and notes back to the lines. */
|
|
33
|
+
export declare function sequenceDiagramPlan(lines: readonly SceneTranscriptLine[], cast?: readonly SequenceCastMember[]): SequenceDiagramPlan;
|
|
34
|
+
/** The mermaid source of a transcript: what *Copy as mermaid* copies. */
|
|
35
|
+
export declare function sequenceDiagramOf(lines: readonly SceneTranscriptLine[], cast?: readonly SequenceCastMember[]): string;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The transcript as a mermaid sequence diagram (SCENES S2-02): one
|
|
7
|
+
* participant per member and system in the cast's order (the person first,
|
|
8
|
+
* as an actor, when they speak), `->>` for an ask or a call, `-->>` for
|
|
9
|
+
* the answer, `activate`/`deactivate` around an exchange that is open or
|
|
10
|
+
* answered, `Note over` for words said to all, and a `rect` around an
|
|
11
|
+
* exchange that failed.
|
|
12
|
+
*
|
|
13
|
+
* Pure: lines in, source out — a golden file holds the source of a recorded
|
|
14
|
+
* transcript — and, beside it, which line each arrow and each note is, so
|
|
15
|
+
* that a click on the drawing finds its line ({@link SequenceDiagram}).
|
|
16
|
+
*
|
|
17
|
+
* @module components/teams/transcript/sequenceDiagramRules
|
|
18
|
+
*/
|
|
19
|
+
import { PERSON } from '../sceneTranscriptRules.js';
|
|
20
|
+
import { durationText, exchangeOf } from './transcriptRows.js';
|
|
21
|
+
/** The most characters of a message: the rest is an ellipsis. */
|
|
22
|
+
const MESSAGE_LENGTH = 72;
|
|
23
|
+
/** The tint behind an exchange that failed: Primer's danger, faint, in light and dark. */
|
|
24
|
+
const FAILED_RECT = 'rect rgba(248, 81, 73, 0.14)';
|
|
25
|
+
/** Words on a diagram: one line, no statement end, no entity, clipped. */
|
|
26
|
+
export function diagramText(text) {
|
|
27
|
+
const one = text.replace(/\s+/g, ' ').trim();
|
|
28
|
+
const clipped = one.length > MESSAGE_LENGTH ? `${one.slice(0, MESSAGE_LENGTH - 1)}…` : one;
|
|
29
|
+
return (clipped.replace(/[#;]/g, mark => (mark === '#' ? '#35;' : '#59;')) || ' ');
|
|
30
|
+
}
|
|
31
|
+
/** Each participant's alias: `p0`, `p1`…, in the order they stand. */
|
|
32
|
+
function participantsOf(lines, cast) {
|
|
33
|
+
const names = [];
|
|
34
|
+
const spoken = new Set();
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
spoken.add(line.from);
|
|
37
|
+
if (line.to) {
|
|
38
|
+
spoken.add(line.to);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (spoken.has(PERSON)) {
|
|
42
|
+
names.push(PERSON);
|
|
43
|
+
}
|
|
44
|
+
for (const member of [...cast].sort((a, b) => Number(Boolean(a.system)) - Number(Boolean(b.system)))) {
|
|
45
|
+
if (member.name !== PERSON && !names.includes(member.name)) {
|
|
46
|
+
names.push(member.name);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
for (const name of [line.from, line.to]) {
|
|
51
|
+
if (name && !names.includes(name)) {
|
|
52
|
+
names.push(name);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const alias = new Map(names.map((name, index) => [name, `p${index}`]));
|
|
57
|
+
return { names, alias, actors: new Set([PERSON]) };
|
|
58
|
+
}
|
|
59
|
+
/** The source and the map of its arrows and notes back to the lines. */
|
|
60
|
+
export function sequenceDiagramPlan(lines, cast = []) {
|
|
61
|
+
const { names, alias, actors } = participantsOf(lines, cast);
|
|
62
|
+
const id = (name) => alias.get(name) ?? name;
|
|
63
|
+
const out = ['sequenceDiagram'];
|
|
64
|
+
for (const name of names) {
|
|
65
|
+
out.push(` ${actors.has(name) ? 'actor' : 'participant'} ${id(name)} as ${diagramText(name)}`);
|
|
66
|
+
}
|
|
67
|
+
const arrows = [];
|
|
68
|
+
const notes = [];
|
|
69
|
+
// The answers, by exchange: what an ask or a call waits for.
|
|
70
|
+
const answers = new Map();
|
|
71
|
+
for (const line of lines) {
|
|
72
|
+
if (line.kind === 'said') {
|
|
73
|
+
answers.set(exchangeOf(line.key), line);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Who an exchange activated, until its answer deactivates them.
|
|
77
|
+
const active = new Map();
|
|
78
|
+
// The exchanges whose rect is open, closed by their failed answer.
|
|
79
|
+
const rects = new Set();
|
|
80
|
+
const failed = new Set();
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
if (line.failed) {
|
|
83
|
+
failed.add(exchangeOf(line.key));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const indent = () => (rects.size ? ' ' : ' ');
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
const exchange = exchangeOf(line.key);
|
|
89
|
+
const answer = answers.get(exchange);
|
|
90
|
+
if (line.kind === 'asked' || line.kind === 'called') {
|
|
91
|
+
if (failed.has(exchange) && answer && !rects.has(exchange)) {
|
|
92
|
+
out.push(` ${FAILED_RECT}`);
|
|
93
|
+
rects.add(exchange);
|
|
94
|
+
}
|
|
95
|
+
const to = line.to ?? '';
|
|
96
|
+
out.push(`${indent()}${id(line.from)}->>${id(to)}: ${diagramText(line.text)}${line.open ? '…' : ''}`);
|
|
97
|
+
arrows.push(line.key);
|
|
98
|
+
if (line.open || answer || line.ended !== undefined) {
|
|
99
|
+
out.push(`${indent()}activate ${id(to)}`);
|
|
100
|
+
active.set(exchange, to);
|
|
101
|
+
}
|
|
102
|
+
if (line.kind === 'called' && !line.open && !answer) {
|
|
103
|
+
// A call's answer is its end: the tool answered, in so long, or failed.
|
|
104
|
+
const took = line.ended !== undefined && line.ended > line.at
|
|
105
|
+
? ` in ${durationText(line.ended - line.at)}`
|
|
106
|
+
: '';
|
|
107
|
+
const opened = !rects.has(exchange) && line.failed;
|
|
108
|
+
if (opened) {
|
|
109
|
+
out.push(` ${FAILED_RECT}`);
|
|
110
|
+
rects.add(exchange);
|
|
111
|
+
}
|
|
112
|
+
out.push(`${indent()}${id(to)}-->>${id(line.from)}: ${line.failed ? 'failed' : `answered${took}`}`);
|
|
113
|
+
arrows.push(line.key);
|
|
114
|
+
if (active.get(exchange) === to) {
|
|
115
|
+
out.push(`${indent()}deactivate ${id(to)}`);
|
|
116
|
+
active.delete(exchange);
|
|
117
|
+
}
|
|
118
|
+
if (rects.has(exchange)) {
|
|
119
|
+
rects.delete(exchange);
|
|
120
|
+
out.push(' end');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
// Words said: back to who asked, or to all.
|
|
126
|
+
const asker = line.to ??
|
|
127
|
+
(lines.some(other => other.kind === 'asked' &&
|
|
128
|
+
other.from === PERSON &&
|
|
129
|
+
exchangeOf(other.key) === exchange)
|
|
130
|
+
? PERSON
|
|
131
|
+
: undefined);
|
|
132
|
+
if (line.failed && !rects.has(exchange)) {
|
|
133
|
+
out.push(` ${FAILED_RECT}`);
|
|
134
|
+
rects.add(exchange);
|
|
135
|
+
}
|
|
136
|
+
if (asker) {
|
|
137
|
+
out.push(`${indent()}${id(line.from)}-->>${id(asker)}: ${diagramText(line.text)}${line.failed ? ' (failed)' : ''}`);
|
|
138
|
+
arrows.push(line.key);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
out.push(`${indent()}Note over ${id(line.from)}: ${diagramText(line.text)}${line.failed ? ' (failed)' : ''}`);
|
|
142
|
+
notes.push(line.key);
|
|
143
|
+
}
|
|
144
|
+
if (active.get(exchange) === line.from) {
|
|
145
|
+
out.push(`${indent()}deactivate ${id(line.from)}`);
|
|
146
|
+
active.delete(exchange);
|
|
147
|
+
}
|
|
148
|
+
if (rects.has(exchange)) {
|
|
149
|
+
rects.delete(exchange);
|
|
150
|
+
out.push(' end');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return { source: `${out.join('\n')}\n`, arrows, notes };
|
|
154
|
+
}
|
|
155
|
+
/** The mermaid source of a transcript: what *Copy as mermaid* copies. */
|
|
156
|
+
export function sequenceDiagramOf(lines, cast = []) {
|
|
157
|
+
return sequenceDiagramPlan(lines, cast).source;
|
|
158
|
+
}
|