@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,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every backdrop's nodes share: the member's node — its figure
|
|
3
|
+
* (`AssistantStage`: the balloon that peeks and opens, the menu, the drag
|
|
4
|
+
* within the graph's box, the arrow keys), its name and where it runs — and
|
|
5
|
+
* the connection's node — the server's mark, a button whose details show
|
|
6
|
+
* and hide — each drawn in a backdrop's *look* (`MemberLook`,
|
|
7
|
+
* `ConnectionLook`): the figure a member is drawn as, the node's width, the
|
|
8
|
+
* type of its name. A backdrop's node types are these two with its look.
|
|
9
|
+
*
|
|
10
|
+
* The members and the calls are read from the graph's contexts, not from
|
|
11
|
+
* the nodes' data: a node whose object changes is taken by React Flow for
|
|
12
|
+
* a new one, measured again, and the edge is dropped until it is — the line
|
|
13
|
+
* vanished as the characters moved. The nodes stay the same objects; what
|
|
14
|
+
* they show is read from here.
|
|
15
|
+
*
|
|
16
|
+
* @module components/teams/backdrops/nodes
|
|
17
|
+
*/
|
|
18
|
+
import type { JSX } from 'react';
|
|
19
|
+
import { type Node, type NodeProps } from '@xyflow/react';
|
|
20
|
+
import { type AssistantStageProps } from '../../../chat/assistant/AssistantStage';
|
|
21
|
+
import type { A2ATeamConnection } from '../a2aTeamFlow';
|
|
22
|
+
import type { A2ATeamGraphMember, ConnectionData, MemberData, SceneBackdropFace, StageSx } from './types';
|
|
23
|
+
/** How tall the notebook in a balloon grows before it scrolls. */
|
|
24
|
+
export declare const NOTEBOOK_PREVIEW_HEIGHT = 180;
|
|
25
|
+
/** Where the edge leaves and reaches a member: the middle of its figure. */
|
|
26
|
+
export declare const HANDLE: (size: number) => {
|
|
27
|
+
opacity: number;
|
|
28
|
+
pointerEvents: "none";
|
|
29
|
+
top: number;
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
minWidth: number;
|
|
33
|
+
minHeight: number;
|
|
34
|
+
border: number;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* The handles an edge leaves and reaches a member by, named: the middle of
|
|
38
|
+
* its figure on each side, chosen by where the other member stands.
|
|
39
|
+
*/
|
|
40
|
+
export declare const MEMBER_HANDLES: {
|
|
41
|
+
readonly inLeft: "in-left";
|
|
42
|
+
readonly outRight: "out-right";
|
|
43
|
+
readonly inTop: "in-top";
|
|
44
|
+
readonly outBottom: "out-bottom";
|
|
45
|
+
readonly inRight: "in-right";
|
|
46
|
+
readonly outLeft: "out-left";
|
|
47
|
+
readonly inBottom: "in-bottom";
|
|
48
|
+
readonly outTop: "out-top";
|
|
49
|
+
readonly connections: "connections";
|
|
50
|
+
};
|
|
51
|
+
/** The members as they are now, by id (see the module's note). */
|
|
52
|
+
export declare const Members: import("react").Context<Record<string, A2ATeamGraphMember>>;
|
|
53
|
+
/** The connections being called now, by node id: read by their nodes, as the members are. */
|
|
54
|
+
export declare const Busy: import("react").Context<Record<string, string>>;
|
|
55
|
+
/** How far a member was moved from its place, in the graph's coordinates. */
|
|
56
|
+
export type MemberOffset = {
|
|
57
|
+
dx: number;
|
|
58
|
+
dy: number;
|
|
59
|
+
};
|
|
60
|
+
export declare const AT_PLACE: MemberOffset;
|
|
61
|
+
/** How far a member may move: its node stays inside the graph's box. */
|
|
62
|
+
export type MemberBounds = {
|
|
63
|
+
minDx: number;
|
|
64
|
+
maxDx: number;
|
|
65
|
+
minDy: number;
|
|
66
|
+
maxDy: number;
|
|
67
|
+
};
|
|
68
|
+
/** An offset kept inside its bounds. */
|
|
69
|
+
export declare function clampOffset(offset: MemberOffset, bounds: MemberBounds): MemberOffset;
|
|
70
|
+
/** The members' moves, read by their nodes. */
|
|
71
|
+
export declare const Moving: import("react").Context<{
|
|
72
|
+
zoom: number;
|
|
73
|
+
offsetOf: (id: string) => MemberOffset;
|
|
74
|
+
move: (id: string, offset: MemberOffset) => void;
|
|
75
|
+
reset: (id: string) => void;
|
|
76
|
+
}>;
|
|
77
|
+
/** A connection's node id: beside its member. */
|
|
78
|
+
export declare const connectionNodeId: (member: string, connection: string) => string;
|
|
79
|
+
/** A member's accessible name: its name, and where it runs. */
|
|
80
|
+
export declare function memberLabel(member: Pick<A2ATeamGraphMember, 'name' | 'where'>): string;
|
|
81
|
+
/** A connection's accessible name: `Odoo, Accounting's MCP connection`. */
|
|
82
|
+
export declare function connectionLabel(connection: Pick<A2ATeamConnection, 'label'>, member: string | undefined): string;
|
|
83
|
+
/** What a connection's details say: the server, who reaches it, and the call running now. */
|
|
84
|
+
export declare function connectionDetails(connection: Pick<A2ATeamConnection, 'name' | 'tools'>, member: string | undefined, calling?: string): string;
|
|
85
|
+
/** How a backdrop draws a member: its figure, and the words under it. */
|
|
86
|
+
export interface MemberLook {
|
|
87
|
+
/** The node's width, the layout's `nodeWidth`. */
|
|
88
|
+
width: number;
|
|
89
|
+
/**
|
|
90
|
+
* What the member is drawn as: its own character (studio), or a figure
|
|
91
|
+
* made for the backdrop from its face — its emoji, its character, its
|
|
92
|
+
* initial (hero, plain). Called on each render: give the same object
|
|
93
|
+
* for the same member, so that the figure is not drawn anew.
|
|
94
|
+
*/
|
|
95
|
+
figure: (member: SceneBackdropFace) => AssistantStageProps['character'];
|
|
96
|
+
/** The name under the figure. */
|
|
97
|
+
name: StageSx;
|
|
98
|
+
/** Where it runs, under the name; `false` leaves it to the figure's label alone. */
|
|
99
|
+
where: StageSx | false;
|
|
100
|
+
}
|
|
101
|
+
/** A member of the team, drawn in a look. */
|
|
102
|
+
export declare function MemberNodeBase({ data, look, }: NodeProps<Node<MemberData>> & {
|
|
103
|
+
look: MemberLook;
|
|
104
|
+
}): JSX.Element | null;
|
|
105
|
+
/** How a backdrop draws a member's connection: its mark's frame, and the words under it. */
|
|
106
|
+
export interface ConnectionLook {
|
|
107
|
+
/** The node's width, the layout's `connectionWidth`. */
|
|
108
|
+
width: number;
|
|
109
|
+
/** The frame round the mark: a bordered square (studio), a ring (hero). */
|
|
110
|
+
mark: (busy: boolean) => StageSx;
|
|
111
|
+
/** What the mark holds, as a fraction of the mark's size. */
|
|
112
|
+
markScale: number;
|
|
113
|
+
/** The label under the mark. */
|
|
114
|
+
label: StageSx;
|
|
115
|
+
/** How it is reached, under the label: `via MCP`. */
|
|
116
|
+
via: StageSx;
|
|
117
|
+
/** Its details, shown by a click on the mark. */
|
|
118
|
+
details: StageSx;
|
|
119
|
+
}
|
|
120
|
+
/** A connection of a member's: the server's mark, what it reaches, and how. */
|
|
121
|
+
export declare function ConnectionNodeBase({ id, data, look, }: NodeProps<Node<ConnectionData>> & {
|
|
122
|
+
look: ConnectionLook;
|
|
123
|
+
}): JSX.Element | null;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useEffect, useId, useRef, useState, } from 'react';
|
|
3
|
+
import { Handle, Position } from '@xyflow/react';
|
|
4
|
+
import { Button, Text } from '@primer/react';
|
|
5
|
+
import { Box } from '@datalayer/primer-addons';
|
|
6
|
+
import { AssistantStage, } from '../../../chat/assistant/AssistantStage.js';
|
|
7
|
+
import { SpecMark } from '../../../chat/marks/SpecMark.js';
|
|
8
|
+
import { NotebookPreview } from '../NotebookPreview.js';
|
|
9
|
+
import { notebookBalloonVisual } from '../TeamNotebook.js';
|
|
10
|
+
import { toolLineText } from '../../../chat/assistant/toolLine.js';
|
|
11
|
+
/** How tall the notebook in a balloon grows before it scrolls. */
|
|
12
|
+
export const NOTEBOOK_PREVIEW_HEIGHT = 180;
|
|
13
|
+
/** Where the edge leaves and reaches a member: the middle of its figure. */
|
|
14
|
+
export const HANDLE = (size) => ({
|
|
15
|
+
opacity: 0,
|
|
16
|
+
pointerEvents: 'none',
|
|
17
|
+
top: size / 2,
|
|
18
|
+
width: 1,
|
|
19
|
+
height: 1,
|
|
20
|
+
minWidth: 0,
|
|
21
|
+
minHeight: 0,
|
|
22
|
+
border: 0,
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* The handles an edge leaves and reaches a member by, named: the middle of
|
|
26
|
+
* its figure on each side, chosen by where the other member stands.
|
|
27
|
+
*/
|
|
28
|
+
export const MEMBER_HANDLES = {
|
|
29
|
+
inLeft: 'in-left',
|
|
30
|
+
outRight: 'out-right',
|
|
31
|
+
inTop: 'in-top',
|
|
32
|
+
outBottom: 'out-bottom',
|
|
33
|
+
inRight: 'in-right',
|
|
34
|
+
outLeft: 'out-left',
|
|
35
|
+
inBottom: 'in-bottom',
|
|
36
|
+
outTop: 'out-top',
|
|
37
|
+
connections: 'connections',
|
|
38
|
+
};
|
|
39
|
+
/** The members as they are now, by id (see the module's note). */
|
|
40
|
+
export const Members = createContext({});
|
|
41
|
+
/** The connections being called now, by node id: read by their nodes, as the members are. */
|
|
42
|
+
export const Busy = createContext({});
|
|
43
|
+
export const AT_PLACE = { dx: 0, dy: 0 };
|
|
44
|
+
/** An offset kept inside its bounds. */
|
|
45
|
+
export function clampOffset(offset, bounds) {
|
|
46
|
+
return {
|
|
47
|
+
dx: Math.min(bounds.maxDx, Math.max(bounds.minDx, offset.dx)),
|
|
48
|
+
dy: Math.min(bounds.maxDy, Math.max(bounds.minDy, offset.dy)),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** How far a press moves before it is a drag, not a click, in pixels. */
|
|
52
|
+
const DRAG_THRESHOLD = 4;
|
|
53
|
+
/** How far an arrow key moves a member; with Shift, more. */
|
|
54
|
+
const NUDGE = 10;
|
|
55
|
+
const NUDGE_FAR = 40;
|
|
56
|
+
/** The members' moves, read by their nodes. */
|
|
57
|
+
export const Moving = createContext({
|
|
58
|
+
zoom: 1,
|
|
59
|
+
offsetOf: () => AT_PLACE,
|
|
60
|
+
move: () => undefined,
|
|
61
|
+
reset: () => undefined,
|
|
62
|
+
});
|
|
63
|
+
/** A connection's node id: beside its member. */
|
|
64
|
+
export const connectionNodeId = (member, connection) => `${member}/${connection}`;
|
|
65
|
+
/** A member's accessible name: its name, and where it runs. */
|
|
66
|
+
export function memberLabel(member) {
|
|
67
|
+
return member.where ? `${member.name} (${member.where})` : member.name;
|
|
68
|
+
}
|
|
69
|
+
/** A connection's accessible name: `Odoo, Accounting's MCP connection`. */
|
|
70
|
+
export function connectionLabel(connection, member) {
|
|
71
|
+
return member
|
|
72
|
+
? `${connection.label}, ${member}’s MCP connection`
|
|
73
|
+
: `${connection.label}, an MCP connection`;
|
|
74
|
+
}
|
|
75
|
+
/** What a connection's details say: the server, who reaches it, and the call running now. */
|
|
76
|
+
export function connectionDetails(connection, member, calling) {
|
|
77
|
+
const tools = connection.tools.length;
|
|
78
|
+
return [
|
|
79
|
+
`${connection.name}, an MCP server${member ? ` ${member} reaches` : ''}.`,
|
|
80
|
+
tools ? `${tools} tool${tools === 1 ? '' : 's'}.` : '',
|
|
81
|
+
calling ? `Now: ${calling}.` : '',
|
|
82
|
+
]
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
.join(' ');
|
|
85
|
+
}
|
|
86
|
+
/** A member of the team, drawn in a look. */
|
|
87
|
+
export function MemberNodeBase({ data, look, }) {
|
|
88
|
+
const { id, side, size } = data;
|
|
89
|
+
const member = useContext(Members)[id];
|
|
90
|
+
const moving = useContext(Moving);
|
|
91
|
+
const offset = moving.offsetOf(id);
|
|
92
|
+
const stageRef = useRef(null);
|
|
93
|
+
// Its balloon's display, as its menu changes it.
|
|
94
|
+
const [display, setDisplay] = useState();
|
|
95
|
+
// Its balloon as a click on it left it: shown, hidden, or as the team
|
|
96
|
+
// says (`auto`: what it is doing, and on hover).
|
|
97
|
+
const [shown, setShown] = useState('auto');
|
|
98
|
+
// What it says or does now: something new reopens a hidden balloon.
|
|
99
|
+
const persona = member?.persona;
|
|
100
|
+
const news = [
|
|
101
|
+
persona?.saying,
|
|
102
|
+
persona?.tool ? toolLineText(persona.tool) : '',
|
|
103
|
+
persona?.notebook?.name,
|
|
104
|
+
].join('\u0000');
|
|
105
|
+
const seen = useRef(news);
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (news !== seen.current) {
|
|
108
|
+
seen.current = news;
|
|
109
|
+
setShown(current => (current === 'hidden' ? 'auto' : current));
|
|
110
|
+
}
|
|
111
|
+
}, [news]);
|
|
112
|
+
if (!member || !persona) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const shownDisplay = display ?? member.balloonDisplay ?? 'current';
|
|
116
|
+
// History: what it said and did, listed.
|
|
117
|
+
const listed = shownDisplay === 'history' && member.history?.length
|
|
118
|
+
? { history: member.history }
|
|
119
|
+
: {};
|
|
120
|
+
// A notebook it was given: read-only in its balloon, and its large visual
|
|
121
|
+
// the one that runs, editable — under the graph, or in a dialog.
|
|
122
|
+
const notebookTitle = member.notebookTitle ?? (persona.notebook?.name || 'The notebook');
|
|
123
|
+
const given = persona.notebook
|
|
124
|
+
? {
|
|
125
|
+
attachment: (_jsx(NotebookPreview, { notebook: persona.notebook.data, title: notebookTitle, maxHeight: NOTEBOOK_PREVIEW_HEIGHT, onOpen: member.onNotebookOpen, openLabel: member.expandTarget ? 'Open it below to run it' : undefined })),
|
|
126
|
+
visual: notebookBalloonVisual(persona.notebook, notebookTitle),
|
|
127
|
+
}
|
|
128
|
+
: {};
|
|
129
|
+
const balloonNow = persona.tool
|
|
130
|
+
? {
|
|
131
|
+
text: toolLineText(persona.tool),
|
|
132
|
+
tool: persona.tool,
|
|
133
|
+
busy: persona.state !== 'idle',
|
|
134
|
+
...given,
|
|
135
|
+
...listed,
|
|
136
|
+
}
|
|
137
|
+
: persona.saying
|
|
138
|
+
? {
|
|
139
|
+
text: persona.saying,
|
|
140
|
+
...(persona.full ? { fullText: persona.full } : {}),
|
|
141
|
+
more: persona.saying.endsWith('…'),
|
|
142
|
+
speaking: persona.state === 'speaking',
|
|
143
|
+
busy: persona.state === 'thinking' ||
|
|
144
|
+
persona.state === 'working' ||
|
|
145
|
+
persona.state === 'speaking',
|
|
146
|
+
...given,
|
|
147
|
+
...listed,
|
|
148
|
+
}
|
|
149
|
+
: 'history' in listed
|
|
150
|
+
? { text: '', ...listed }
|
|
151
|
+
: undefined;
|
|
152
|
+
return (_jsxs(Box
|
|
153
|
+
// React Flow lets the pointer through a node that is neither dragged
|
|
154
|
+
// nor selected (`pointer-events: none` on its wrapper): the figure,
|
|
155
|
+
// its balloon and its menu take it back, and are left to themselves —
|
|
156
|
+
// no drag, no pan, no wheel.
|
|
157
|
+
, {
|
|
158
|
+
// React Flow lets the pointer through a node that is neither dragged
|
|
159
|
+
// nor selected (`pointer-events: none` on its wrapper): the figure,
|
|
160
|
+
// its balloon and its menu take it back, and are left to themselves —
|
|
161
|
+
// no drag, no pan, no wheel.
|
|
162
|
+
className: "nodrag nopan nowheel", width: look.width, display: "flex", flexDirection: "column", alignItems: "center", gap: 1, cursor: "default", pointerEvents: "all", "data-team-member": member.id, "data-member-balloon": shown, "data-member-moved": offset.dx || offset.dy
|
|
163
|
+
? `${Math.round(offset.dx)},${Math.round(offset.dy)}`
|
|
164
|
+
: undefined, "data-member-state": persona.state, children: [_jsx(Handle, { id: MEMBER_HANDLES.inLeft, type: "target", position: Position.Left, style: HANDLE(size), isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.inRight, type: "target", position: Position.Right, style: HANDLE(size), isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.inTop, type: "target", position: Position.Top, style: { ...HANDLE(size), top: 0 }, isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.inBottom, type: "target", position: Position.Bottom, style: { ...HANDLE(size), top: size }, isConnectable: false }), _jsx(Box, { width: size, height: size, position: "relative", children: persona.away ? (_jsxs(Button, { size: "small", sx: { position: 'absolute', top: size / 2 - 16, left: -24 }, onClick: () => member.onAway?.(false), children: ["Call ", member.name, " back"] })) : (_jsx(AssistantStage, { character: look.figure(member), state: persona.state, size: size,
|
|
165
|
+
// In the node, not over the page; the balloon opens toward the
|
|
166
|
+
// middle of the graph: rightward from the left member.
|
|
167
|
+
place: side === 'left'
|
|
168
|
+
? { position: 'absolute', left: 'auto' }
|
|
169
|
+
: { position: 'absolute', right: 'auto' }, stageRef: stageRef,
|
|
170
|
+
// Dragged within the graph's box: a press that moves more than
|
|
171
|
+
// a few pixels is a drag; one that does not is a click, and the
|
|
172
|
+
// stage ignores the click that ends a drag.
|
|
173
|
+
onDragStart: event => {
|
|
174
|
+
if (event.button !== 0) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const start = { x: event.clientX, y: event.clientY };
|
|
178
|
+
const from = offset;
|
|
179
|
+
let dragging = false;
|
|
180
|
+
const onMove = (next) => {
|
|
181
|
+
const dx = next.clientX - start.x;
|
|
182
|
+
const dy = next.clientY - start.y;
|
|
183
|
+
if (!dragging && Math.hypot(dx, dy) <= DRAG_THRESHOLD) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
dragging = true;
|
|
187
|
+
moving.move(id, {
|
|
188
|
+
dx: from.dx + dx / moving.zoom,
|
|
189
|
+
dy: from.dy + dy / moving.zoom,
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
const onUp = () => {
|
|
193
|
+
window.removeEventListener('pointermove', onMove);
|
|
194
|
+
window.removeEventListener('pointerup', onUp);
|
|
195
|
+
window.removeEventListener('pointercancel', onUp);
|
|
196
|
+
};
|
|
197
|
+
window.addEventListener('pointermove', onMove);
|
|
198
|
+
window.addEventListener('pointerup', onUp);
|
|
199
|
+
window.addEventListener('pointercancel', onUp);
|
|
200
|
+
}, onCharacterKeyDown: event => {
|
|
201
|
+
const step = event.shiftKey ? NUDGE_FAR : NUDGE;
|
|
202
|
+
const by = {
|
|
203
|
+
ArrowLeft: { dx: -step, dy: 0 },
|
|
204
|
+
ArrowRight: { dx: step, dy: 0 },
|
|
205
|
+
ArrowUp: { dx: 0, dy: -step },
|
|
206
|
+
ArrowDown: { dx: 0, dy: step },
|
|
207
|
+
};
|
|
208
|
+
const nudge = by[event.key];
|
|
209
|
+
if (nudge) {
|
|
210
|
+
event.preventDefault();
|
|
211
|
+
moving.move(id, {
|
|
212
|
+
dx: offset.dx + nudge.dx,
|
|
213
|
+
dy: offset.dy + nudge.dy,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}, onResetPosition: offset.dx || offset.dy ? () => moving.reset(id) : undefined, open: false, onToggle: member.onToggle ?? (() => undefined),
|
|
217
|
+
// A click shows and hides its balloon; the menu's *Ask Sales*
|
|
218
|
+
// still takes the reader to the composer.
|
|
219
|
+
onCharacterClick: () => setShown(current => current === 'shown' ||
|
|
220
|
+
(current === 'auto' &&
|
|
221
|
+
(persona.insist || display === 'history'))
|
|
222
|
+
? 'hidden'
|
|
223
|
+
: 'shown'), balloonDisplay: shownDisplay, onBalloonDisplayChange: next => {
|
|
224
|
+
setDisplay(next);
|
|
225
|
+
member.onBalloonDisplayChange?.(next);
|
|
226
|
+
}, conversationLabel: member.onToggle ? (member.conversationLabel ?? undefined) : false, balloon: shown === 'hidden'
|
|
227
|
+
? undefined
|
|
228
|
+
: (balloonNow ??
|
|
229
|
+
(shown === 'shown'
|
|
230
|
+
? {
|
|
231
|
+
text: `${member.name}, ${member.where ?? ''}`.replace(/, $/, ''),
|
|
232
|
+
}
|
|
233
|
+
: undefined)), expandTarget: member.expandTarget, expandOnArrival: !!member.expandTarget,
|
|
234
|
+
// History, chosen in its menu: shown until Current is.
|
|
235
|
+
insist: shown === 'shown' ||
|
|
236
|
+
(shown === 'auto' && (persona.insist || display === 'history')), onDismiss: away => member.onAway?.(away !== 'none'), suggestions: member.suggestions, onSuggestion: member.onSuggestion, inspector: member.inspector, inspectAgent: member.name, onStop: member.onStop, about: member.about, sandbox: member.sandbox, contextMenu: member.contextMenu,
|
|
237
|
+
// A member of the graph: what is clicked around it is the graph.
|
|
238
|
+
stayPut: true,
|
|
239
|
+
// Named as the member, not its character: `Sales (in your
|
|
240
|
+
// browser)`; the character describes it.
|
|
241
|
+
agentName: member.name, label: memberLabel(member) })) }), _jsxs(Text, { sx: look.name, "data-member-name": "", children: [member.emoji ? `${member.emoji} ` : '', member.name] }), look.where !== false && (_jsx(Text, { sx: look.where, "data-member-where": "", children: member.where })), _jsx(Handle, { id: MEMBER_HANDLES.outRight, type: "source", position: Position.Right, style: HANDLE(size), isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.outLeft, type: "source", position: Position.Left, style: HANDLE(size), isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.outBottom, type: "source", position: Position.Bottom, style: { ...HANDLE(size), top: size }, isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.outTop, type: "source", position: Position.Top, style: { ...HANDLE(size), top: 0 }, isConnectable: false }), member.connections?.length ? (_jsx(Handle, { id: MEMBER_HANDLES.connections, type: "source", position: Position.Right, style: { ...HANDLE(size), left: 'auto', right: 0 }, isConnectable: false })) : null] }));
|
|
242
|
+
}
|
|
243
|
+
/** A connection of a member's: the server's mark, what it reaches, and how. */
|
|
244
|
+
export function ConnectionNodeBase({ id, data, look, }) {
|
|
245
|
+
const { member, connection: connectionId, size } = data;
|
|
246
|
+
const owner = useContext(Members)[member];
|
|
247
|
+
const connection = owner?.connections?.find(known => known.id === connectionId);
|
|
248
|
+
const tool = useContext(Busy)[id];
|
|
249
|
+
// Its details, shown and hidden by a click or Enter on its mark.
|
|
250
|
+
const [told, setTold] = useState(false);
|
|
251
|
+
const detailsId = useId();
|
|
252
|
+
if (!connection) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
return (_jsxs(Box, { className: "nodrag nopan nowheel", width: look.width, display: "flex", flexDirection: "column", alignItems: "center", gap: "2px", cursor: "default", pointerEvents: "all", title: connection.name, "data-team-connection": connection.id, "data-connection-busy": tool ? 'true' : 'false', children: [_jsx(Handle, { type: "target", position: Position.Left, style: { ...HANDLE(size), left: (look.width - size) / 2 }, isConnectable: false }), _jsx(Box, { as: "button", type: "button", className: tool ? 'a2a-team-connection-busy' : undefined, "aria-label": connectionLabel(connection, owner?.name), "aria-expanded": told, "aria-controls": detailsId, "data-connection-figure": "", onClick: () => setTold(shown => !shown), onKeyDown: (event) => {
|
|
256
|
+
if (event.key === 'Escape' && told) {
|
|
257
|
+
event.stopPropagation();
|
|
258
|
+
setTold(false);
|
|
259
|
+
}
|
|
260
|
+
}, width: size, height: size, p: 0, display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer", sx: look.mark(Boolean(tool)), focusVisible: {
|
|
261
|
+
outline: '2px solid',
|
|
262
|
+
outlineColor: 'var(--focus-outlineColor, var(--fgColor-accent))',
|
|
263
|
+
outlineOffset: 2,
|
|
264
|
+
}, children: _jsx(SpecMark, { icon: connection.icon, emoji: connection.emoji, size: Math.round(size * look.markScale) }) }), _jsx(Text, { sx: look.label, children: connection.label }), connection.via && (_jsx(Text, { sx: look.via, "data-connection-via": "", children: connection.via })), _jsx(Box, { id: detailsId, hidden: !told, "data-connection-details": "", textAlign: "center", maxWidth: look.width + 40, sx: look.details, children: connectionDetails(connection, owner?.name, tool) })] }));
|
|
265
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Edge, type EdgeProps, type Node, type NodeProps } from '@xyflow/react';
|
|
2
|
+
import type { CallData, ConnectionData, LinkData, MemberData, SceneBackdrop, SceneBackdropLayout } from './types';
|
|
3
|
+
/** The plain layout: as the studio's, a little tighter. */
|
|
4
|
+
export declare const PLAIN_LAYOUT: SceneBackdropLayout;
|
|
5
|
+
/** A member: a box with its face, its name and where it runs. */
|
|
6
|
+
export declare const PlainMemberNode: import("react").NamedExoticComponent<NodeProps<Node<MemberData>>>;
|
|
7
|
+
/** A system: a smaller box with its mark. */
|
|
8
|
+
export declare const PlainConnectionNode: import("react").NamedExoticComponent<NodeProps<Node<ConnectionData>>>;
|
|
9
|
+
/** The A2A link: an arrow toward the peer, toward the entry while the peer answers, its state in words. */
|
|
10
|
+
export declare const PlainLinkEdge: import("react").NamedExoticComponent<EdgeProps<Edge<LinkData>>>;
|
|
11
|
+
/** A member's link to a system: the same arrow, the call's words on it. */
|
|
12
|
+
export declare const PlainCallEdge: import("react").NamedExoticComponent<EdgeProps<Edge<CallData>>>;
|
|
13
|
+
/** The `plain` backdrop. */
|
|
14
|
+
export declare const PLAIN_BACKDROP: SceneBackdrop;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo } from 'react';
|
|
3
|
+
import { BaseEdge, EdgeLabelRenderer, } from '@xyflow/react';
|
|
4
|
+
import { Label } from '@primer/react';
|
|
5
|
+
import { Box } from '@datalayer/primer-addons';
|
|
6
|
+
import { ConnectionNodeBase, MemberNodeBase, } from './nodes.js';
|
|
7
|
+
import { arrowPath, midpoint, straightPath } from './edges.js';
|
|
8
|
+
import { faceCharacter } from './faces.js';
|
|
9
|
+
import { EdgeHalo } from './halo.js';
|
|
10
|
+
import { sceneBackdropChoice } from './catalogue.js';
|
|
11
|
+
const NODE_WIDTH = 180;
|
|
12
|
+
const CONNECTION_WIDTH = 90;
|
|
13
|
+
/** The plain layout: as the studio's, a little tighter. */
|
|
14
|
+
export const PLAIN_LAYOUT = {
|
|
15
|
+
nodeWidth: NODE_WIDTH,
|
|
16
|
+
gap: 200,
|
|
17
|
+
rowGap: 40,
|
|
18
|
+
memberHeight: size => size + 56,
|
|
19
|
+
connectionScale: 0.5,
|
|
20
|
+
connectionWidth: CONNECTION_WIDTH,
|
|
21
|
+
connectionGap: 130,
|
|
22
|
+
connectionsApart: 16,
|
|
23
|
+
connectionHeight: size => size + 58,
|
|
24
|
+
connectionsTop: size => size / 4,
|
|
25
|
+
balloonRoom: 144,
|
|
26
|
+
};
|
|
27
|
+
/** The faces drawn so far, by member: the same figure for the same member. */
|
|
28
|
+
const FACES = new Map();
|
|
29
|
+
/** The member's face in a box: made once per member. */
|
|
30
|
+
function plainFace(member) {
|
|
31
|
+
const key = `${member.id}\u0000${member.emoji ?? ''}\u0000${typeof member.character === 'string' ? member.character : member.character.url}`;
|
|
32
|
+
let face = FACES.get(key);
|
|
33
|
+
if (!face) {
|
|
34
|
+
face = faceCharacter(member, {
|
|
35
|
+
id: `plain-face-${member.id}`,
|
|
36
|
+
ring: {
|
|
37
|
+
borderRadius: 6,
|
|
38
|
+
border: '1px solid var(--borderColor-default)',
|
|
39
|
+
background: 'var(--bgColor-default)',
|
|
40
|
+
color: 'var(--fgColor-default)',
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
FACES.set(key, face);
|
|
44
|
+
}
|
|
45
|
+
return face;
|
|
46
|
+
}
|
|
47
|
+
const MEMBER_LOOK = {
|
|
48
|
+
width: NODE_WIDTH,
|
|
49
|
+
figure: plainFace,
|
|
50
|
+
name: { fontWeight: 600, textAlign: 'center' },
|
|
51
|
+
where: { fontSize: 0, color: 'fg.muted', textAlign: 'center' },
|
|
52
|
+
};
|
|
53
|
+
const CONNECTION_LOOK = {
|
|
54
|
+
width: CONNECTION_WIDTH,
|
|
55
|
+
mark: busy => ({
|
|
56
|
+
borderRadius: 1,
|
|
57
|
+
border: '1px solid',
|
|
58
|
+
borderColor: busy ? 'fg.default' : 'border.default',
|
|
59
|
+
bg: 'canvas.default',
|
|
60
|
+
}),
|
|
61
|
+
markScale: 0.6,
|
|
62
|
+
label: { fontWeight: 600, fontSize: 1, textAlign: 'center' },
|
|
63
|
+
via: { fontSize: '10px', color: 'fg.muted', textAlign: 'center' },
|
|
64
|
+
details: { fontSize: '10px', color: 'fg.muted' },
|
|
65
|
+
};
|
|
66
|
+
/** A member: a box with its face, its name and where it runs. */
|
|
67
|
+
export const PlainMemberNode = memo(function PlainMemberNode(props) {
|
|
68
|
+
return _jsx(MemberNodeBase, { ...props, look: MEMBER_LOOK });
|
|
69
|
+
});
|
|
70
|
+
/** A system: a smaller box with its mark. */
|
|
71
|
+
export const PlainConnectionNode = memo(function PlainConnectionNode(props) {
|
|
72
|
+
return _jsx(ConnectionNodeBase, { ...props, look: CONNECTION_LOOK });
|
|
73
|
+
});
|
|
74
|
+
/** A straight line with an arrowhead at the end the message goes to. */
|
|
75
|
+
function Arrow({ id, source, target, forward, flowing, selected, }) {
|
|
76
|
+
const stroke = flowing || selected
|
|
77
|
+
? 'var(--fgColor-default)'
|
|
78
|
+
: 'var(--borderColor-emphasis, var(--borderColor-default))';
|
|
79
|
+
return (_jsxs(_Fragment, { children: [selected && (_jsx(EdgeHalo, { path: straightPath(source, target), color: "var(--bgColor-neutral-emphasis)" })), _jsx(BaseEdge, { id: id, path: straightPath(source, target), style: { stroke, strokeWidth: selected ? 2.5 : 1.5 } }), _jsx("path", { d: arrowPath(source, target, forward), fill: stroke, "data-scene-arrow": forward ? 'target' : 'source' })] }));
|
|
80
|
+
}
|
|
81
|
+
/** The words on the edge: the state, in words, in a chip. */
|
|
82
|
+
function Words({ at, flowing, children, ...rest }) {
|
|
83
|
+
return (_jsx(EdgeLabelRenderer, { children: _jsx(Box, { className: "nodrag nopan", position: "absolute", transform: `translate(-50%, 12px) translate(${at.x}px, ${at.y}px)`, pointerEvents: "none", textAlign: "center", whiteSpace: "nowrap", ...rest, children: _jsx(Label, { variant: flowing ? 'primary' : 'secondary', children: children }) }) }));
|
|
84
|
+
}
|
|
85
|
+
/** The A2A link: an arrow toward the peer, toward the entry while the peer answers, its state in words. */
|
|
86
|
+
export const PlainLinkEdge = memo(function PlainLinkEdge({ id, sourceX, sourceY, targetX, targetY, data, }) {
|
|
87
|
+
const flow = data?.flow ?? 'still';
|
|
88
|
+
const source = { x: sourceX, y: sourceY };
|
|
89
|
+
const target = { x: targetX, y: targetY };
|
|
90
|
+
return (_jsxs(_Fragment, { children: [_jsx("g", { "data-a2a-edge-flow": flow, children: _jsx(Arrow, { id: id, source: source, target: target, forward: flow !== 'answering', flowing: flow !== 'still', selected: Boolean(data?.selected) }) }), _jsx(Words, { at: midpoint(source, target), flowing: flow !== 'still', children: data?.label })] }));
|
|
91
|
+
});
|
|
92
|
+
/** A member's link to a system: the same arrow, the call's words on it. */
|
|
93
|
+
export const PlainCallEdge = memo(function PlainCallEdge({ id, sourceX, sourceY, targetX, targetY, data, }) {
|
|
94
|
+
const flow = data?.flow ?? 'still';
|
|
95
|
+
const source = { x: sourceX, y: sourceY };
|
|
96
|
+
const target = { x: targetX, y: targetY };
|
|
97
|
+
return (_jsxs(_Fragment, { children: [_jsx("g", { "data-mcp-edge-flow": flow, ...(flow !== 'still' ? { 'data-mcp-edge-call': data?.calling } : {}), children: _jsx(Arrow, { id: id, source: source, target: target, forward: flow !== 'answering', flowing: flow !== 'still', selected: Boolean(data?.selected) }) }), _jsx(Words, { at: midpoint(source, target), flowing: flow !== 'still', "data-mcp-edge-label": "", children: data?.label })] }));
|
|
98
|
+
});
|
|
99
|
+
const NODE_TYPES = { member: PlainMemberNode, connection: PlainConnectionNode };
|
|
100
|
+
const EDGE_TYPES = { a2a: PlainLinkEdge, mcp: PlainCallEdge };
|
|
101
|
+
/** The `plain` backdrop. */
|
|
102
|
+
export const PLAIN_BACKDROP = {
|
|
103
|
+
...sceneBackdropChoice('plain'),
|
|
104
|
+
figure: plainFace,
|
|
105
|
+
nodeTypes: NODE_TYPES,
|
|
106
|
+
edgeTypes: EDGE_TYPES,
|
|
107
|
+
theme: {
|
|
108
|
+
light: {},
|
|
109
|
+
dark: {},
|
|
110
|
+
// On one page, when printed (`break-inside` is newer than the sx types).
|
|
111
|
+
sx: {
|
|
112
|
+
'@media print': { breakInside: 'avoid', pageBreakInside: 'avoid' },
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
layout: PLAIN_LAYOUT,
|
|
116
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Edge, type EdgeProps, type Node, type NodeProps } from '@xyflow/react';
|
|
2
|
+
import type { CallData, ConnectionData, LinkData, MemberData, SceneBackdrop, SceneBackdropLayout } from './types';
|
|
3
|
+
/** The studio's layout: the entry and a peer 220 apart, a connection half a member's size to its right. */
|
|
4
|
+
export declare const STUDIO_LAYOUT: SceneBackdropLayout;
|
|
5
|
+
/** A member: its character, its name and where it runs. */
|
|
6
|
+
export declare const MemberNode: import("react").NamedExoticComponent<NodeProps<Node<MemberData>>>;
|
|
7
|
+
/** A connection of a member's: the server's mark, what it reaches, and how. */
|
|
8
|
+
export declare const ConnectionNode: import("react").NamedExoticComponent<NodeProps<Node<ConnectionData>>>;
|
|
9
|
+
/** The A2A link between two members: still, or flowing the way the message goes. */
|
|
10
|
+
export declare const LinkEdge: import("react").NamedExoticComponent<EdgeProps<Edge<LinkData>>>;
|
|
11
|
+
/**
|
|
12
|
+
* A member's link to a connection, drawn as the A2A link is: still, flowing
|
|
13
|
+
* toward the connection while a call runs, and back with its result.
|
|
14
|
+
*/
|
|
15
|
+
export declare const CallEdge: import("react").NamedExoticComponent<EdgeProps<Edge<CallData>>>;
|
|
16
|
+
/** The `studio` backdrop. */
|
|
17
|
+
export declare const STUDIO_BACKDROP: SceneBackdrop;
|