@eventcatalog/core 5.7.0 → 5.7.2
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/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-FMWQ2YYN.js → chunk-B3FUBDJN.js} +1 -1
- package/dist/{chunk-4CV7VC4K.js → chunk-EPI5BHEU.js} +1 -1
- package/dist/{chunk-MOPKQMQP.js → chunk-NNTWQNQ5.js} +1 -1
- package/dist/{chunk-BE5PTWNC.js → chunk-OR4VBY3F.js} +1 -1
- package/dist/{chunk-22KZ4TJF.js → chunk-TYZENGGJ.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +5 -5
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +702 -128
- package/eventcatalog/src/components/ChatPanel/ChatPanelButton.tsx +3 -2
- package/eventcatalog/src/components/ChatPanel/assistant-events.ts +37 -0
- package/eventcatalog/src/components/ChatPanel/panel-width.ts +28 -0
- package/eventcatalog/src/components/MDX/ResourceRef/ResourceRef.astro +2 -34
- package/eventcatalog/src/features/ai/chat-api.ts +24 -5
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +72 -71
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +1 -1
- package/eventcatalog/src/features/studio/agent-choreography.ts +84 -16
- package/eventcatalog/src/features/studio/api/canvases-api.ts +3 -0
- package/eventcatalog/src/features/studio/canvas-actions.ts +330 -15
- package/eventcatalog/src/features/studio/catalog.ts +5 -3
- package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +14 -0
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +116 -13
- package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +1 -1
- package/eventcatalog/src/features/studio/pages/[id].astro +2 -0
- package/eventcatalog/src/features/studio/server/assistant-tools.ts +207 -0
- package/eventcatalog/src/features/studio/server/canvas-agent.ts +27 -0
- package/eventcatalog/src/features/studio/server/canvas-mcp.ts +3 -15
- package/eventcatalog/src/utils/resource-reference-icons.ts +37 -0
- package/package.json +4 -4
|
@@ -97,15 +97,17 @@ export const buildContainer = (resource: CatalogResource, center: { x: number; y
|
|
|
97
97
|
* A domain or system as a container with everything it holds in the catalog: its systems (as containers too)
|
|
98
98
|
* and services, and the messages and data stores of those services, so every level of detail (L1 to L3) has
|
|
99
99
|
* something to show. Laid out with the visualiser's layout and centred on a point, with the connections between
|
|
100
|
-
* them and to related resources already on the canvas. Resources already on the canvas aren't added again
|
|
100
|
+
* them and to related resources already on the canvas. Resources already on the canvas aren't added again, nor
|
|
101
|
+
* any in `alsoAdding` (by catalog key: added some other way at the same time, e.g. by an agent in the same call).
|
|
101
102
|
*/
|
|
102
103
|
export const buildContainerWithContents = async (
|
|
103
104
|
resource: CatalogResource,
|
|
104
105
|
center: { x: number; y: number },
|
|
105
106
|
catalog: { resourcesByKey: Map<string, CatalogResource>; relationsByKey: Map<string, CatalogRelation[]> },
|
|
106
|
-
existing: Node[]
|
|
107
|
+
existing: Node[],
|
|
108
|
+
alsoAdding: ReadonlySet<string> = new Set()
|
|
107
109
|
): Promise<{ nodes: Node[]; edges: Edge[] }> => {
|
|
108
|
-
const onCanvas = new Set(existing.map((node) => getCatalogLink(node)?.key).filter(Boolean));
|
|
110
|
+
const onCanvas = new Set([...existing.map((node) => getCatalogLink(node)?.key).filter(Boolean), ...alsoAdding]);
|
|
109
111
|
const root = buildContainer(resource, { x: 0, y: 0 });
|
|
110
112
|
const nodes: Node[] = [root];
|
|
111
113
|
const added = (key: string) => onCanvas.has(key) || nodes.some((node) => getCatalogLink(node)?.key === key);
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
Plug,
|
|
11
11
|
Plus,
|
|
12
12
|
Share2,
|
|
13
|
+
Sparkles,
|
|
13
14
|
Trash2,
|
|
14
15
|
Users,
|
|
15
16
|
} from 'lucide-react';
|
|
@@ -440,6 +441,7 @@ export default memo(function CanvasHeader({
|
|
|
440
441
|
statusHistory,
|
|
441
442
|
onStatusChange,
|
|
442
443
|
onDelete,
|
|
444
|
+
onDrawWithAi,
|
|
443
445
|
}: {
|
|
444
446
|
title?: string;
|
|
445
447
|
onRetitle: (title: string) => void;
|
|
@@ -470,6 +472,8 @@ export default memo(function CanvasHeader({
|
|
|
470
472
|
onHelp: () => void;
|
|
471
473
|
/** Ask to delete the canvas (not given where it can't be, e.g. inside a chat) */
|
|
472
474
|
onDelete?: () => void;
|
|
475
|
+
/** Opens the assistant to draw on the canvas (not given where there isn't one, e.g. inside a chat) */
|
|
476
|
+
onDrawWithAi?: () => void;
|
|
473
477
|
}) {
|
|
474
478
|
const peers = usePeople(presence);
|
|
475
479
|
const agentsHere = peers.some((peer) => peer.agent);
|
|
@@ -505,6 +509,16 @@ export default memo(function CanvasHeader({
|
|
|
505
509
|
</div>
|
|
506
510
|
)}
|
|
507
511
|
<ProfileMenu me={me} onRename={onRename} onRecolor={onRecolor} />
|
|
512
|
+
{onDrawWithAi && (
|
|
513
|
+
<button
|
|
514
|
+
onClick={onDrawWithAi}
|
|
515
|
+
title="Describe what to diagram and the assistant draws it here"
|
|
516
|
+
className={`${barClass} gap-2 px-3 text-sm font-medium text-[rgb(var(--ec-page-text))] transition-colors hover:bg-[rgb(var(--ec-accent-subtle))]`}
|
|
517
|
+
>
|
|
518
|
+
<Sparkles size={16} className="text-[rgb(var(--ec-accent))]" aria-hidden />
|
|
519
|
+
Draw with AI
|
|
520
|
+
</button>
|
|
521
|
+
)}
|
|
508
522
|
<button
|
|
509
523
|
onClick={onShare}
|
|
510
524
|
className="pointer-events-auto inline-flex h-[38px] items-center gap-2 rounded-[9px] bg-[rgb(var(--ec-page-text))] px-4 text-sm font-semibold text-[rgb(var(--ec-page-bg))] shadow-sm transition-colors hover:bg-[rgb(var(--ec-page-text)/0.85)]"
|
|
@@ -94,6 +94,7 @@ import {
|
|
|
94
94
|
isGroupType,
|
|
95
95
|
type Hotspot,
|
|
96
96
|
type HotspotDraft,
|
|
97
|
+
type HotspotPlace,
|
|
97
98
|
type HotspotFields,
|
|
98
99
|
type HotspotSeverity,
|
|
99
100
|
type HotspotSpot,
|
|
@@ -133,6 +134,12 @@ import ShareDialog from './ShareDialog';
|
|
|
133
134
|
import DeleteCanvasDialog from './DeleteCanvasDialog';
|
|
134
135
|
import { createCanvasThroughApi, deleteCanvasThroughApi } from '../api/client';
|
|
135
136
|
import ShortcutsDialog from './ShortcutsDialog';
|
|
137
|
+
import {
|
|
138
|
+
ASSISTANT_DRAWING_EVENT,
|
|
139
|
+
CANVAS_FOCUS_EVENT,
|
|
140
|
+
CANVAS_HOTSPOT_EVENT,
|
|
141
|
+
OPEN_CHAT_EVENT,
|
|
142
|
+
} from '../../../components/ChatPanel/assistant-events';
|
|
136
143
|
|
|
137
144
|
type CanvasProps = {
|
|
138
145
|
canvasId: string;
|
|
@@ -165,6 +172,8 @@ type CanvasProps = {
|
|
|
165
172
|
asAgent?: { name: string; color: string };
|
|
166
173
|
/** Signed in with the catalog's sign-in (SSO): you're shown by this name and picture, and can't change them */
|
|
167
174
|
signedInAs?: { name: string; picture?: string };
|
|
175
|
+
/** The catalog's assistant (Ask AI) is on the page: "Draw with AI" opens it to draw on the canvas */
|
|
176
|
+
assistant?: boolean;
|
|
168
177
|
resources: CatalogResource[];
|
|
169
178
|
relations: CatalogRelation[];
|
|
170
179
|
};
|
|
@@ -198,6 +207,7 @@ type Landing = {
|
|
|
198
207
|
};
|
|
199
208
|
const DELETE_KEYS = ['Backspace', 'Delete'];
|
|
200
209
|
const NO_THREADS: ReadonlySet<string> = new Set();
|
|
210
|
+
const NO_HOTSPOTS: ReadonlySet<string> = new Set();
|
|
201
211
|
// React Flow's warnings, except that a handle has no node: the left panel's drag previews draw nodes off the canvas
|
|
202
212
|
const onFlowError = (code: string, message: string) => {
|
|
203
213
|
if (code !== '010') console.warn(message);
|
|
@@ -339,6 +349,9 @@ export default function StudioDesigner({ asAgent, signedInAs, ...props }: Canvas
|
|
|
339
349
|
);
|
|
340
350
|
}
|
|
341
351
|
|
|
352
|
+
// The assistant's panel (ChatPanelButton, in the page header) opens on this; on a canvas it draws on it
|
|
353
|
+
const openAssistant = () => window.dispatchEvent(new Event(OPEN_CHAT_EVENT));
|
|
354
|
+
|
|
342
355
|
const toSocketUrl = (socketPath = DEFAULT_SOCKET_PATH) =>
|
|
343
356
|
`${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}${socketPath}`;
|
|
344
357
|
|
|
@@ -353,6 +366,7 @@ function Canvas({
|
|
|
353
366
|
mcpUrl = '/docs/mcp',
|
|
354
367
|
relayScriptPath,
|
|
355
368
|
keptInMemory = false,
|
|
369
|
+
assistant = false,
|
|
356
370
|
syncViaTools,
|
|
357
371
|
followChanges = false,
|
|
358
372
|
paletteCollapsed,
|
|
@@ -686,6 +700,8 @@ function Canvas({
|
|
|
686
700
|
const [lastCopied, setLastCopied] = useState<ClipboardContent | null>(null);
|
|
687
701
|
// Nodes to copy instead of the selection (copying from the context menu)
|
|
688
702
|
const copyOnly = useRef<string[] | null>(null);
|
|
703
|
+
// Whether the menu's copy got onto the clipboard (Cut only deletes once it has)
|
|
704
|
+
const copiedFromMenu = useRef(false);
|
|
689
705
|
|
|
690
706
|
/** Adds copies (as one change, so one undo takes them back) and selects them instead of what was selected */
|
|
691
707
|
const paste = useStableCallback((content: ClipboardContent, place: { at: XYPosition } | { offset: XYPosition }) => {
|
|
@@ -718,6 +734,7 @@ function Canvas({
|
|
|
718
734
|
event.clipboardData.setData('text/plain', content.nodes.map((node) => getNodeName(node.type, node.data)).join('\n'));
|
|
719
735
|
pastes.current = 0;
|
|
720
736
|
setLastCopied(content);
|
|
737
|
+
if (fromMenu) copiedFromMenu.current = true;
|
|
721
738
|
if (event.type === 'cut') flow.deleteNodes(ids);
|
|
722
739
|
};
|
|
723
740
|
const onPaste = (event: ClipboardEvent) => {
|
|
@@ -973,7 +990,13 @@ function Canvas({
|
|
|
973
990
|
// Comments that are part of the selection (selecting everything selects them too): only here, like what nodes
|
|
974
991
|
// are selected. While what's selected is dragged, the ones not on a node move with it by this much.
|
|
975
992
|
const [selectedThreads, setSelectedThreads] = useState<ReadonlySet<string>>(NO_THREADS);
|
|
976
|
-
|
|
993
|
+
// Hotspots too (selecting everything selects them, and deleting it deletes them)
|
|
994
|
+
const [selectedHotspots, setSelectedHotspots] = useState<ReadonlySet<string>>(NO_HOTSPOTS);
|
|
995
|
+
/** Comments and hotspots stop being part of the selection */
|
|
996
|
+
const clearSelectedPins = useCallback(() => {
|
|
997
|
+
setSelectedThreads((current) => (current.size ? NO_THREADS : current));
|
|
998
|
+
setSelectedHotspots((current) => (current.size ? NO_HOTSPOTS : current));
|
|
999
|
+
}, []);
|
|
977
1000
|
const [threadsOffset, setThreadsOffset] = useState<XYPosition | null>(null);
|
|
978
1001
|
const threadsShown = () => comments.threads.filter((thread) => showResolved || !thread.resolved);
|
|
979
1002
|
const [contextMenu, setContextMenu] = useState<ContextMenuState>(null);
|
|
@@ -1173,6 +1196,10 @@ function Canvas({
|
|
|
1173
1196
|
setHotspotDraft(null);
|
|
1174
1197
|
});
|
|
1175
1198
|
const cancelHotspot = useCallback(() => setHotspotDraft(null), []);
|
|
1199
|
+
// The hotspot being added, dragged somewhere else before it's added: onto a node, or a place on the level shown
|
|
1200
|
+
const moveHotspotDraft = useStableCallback((to: HotspotPlace) =>
|
|
1201
|
+
setHotspotDraft((draft) => draft && { ...(to.nodeId ? { nodeId: to.nodeId } : placeLevel()), position: to.position })
|
|
1202
|
+
);
|
|
1176
1203
|
const deleteHotspot = useStableCallback((id: string) => flow.deleteHotspot(id));
|
|
1177
1204
|
// Dragged onto a node (they move with it), or to a place; open where it lands if it was open
|
|
1178
1205
|
const moveHotspots = useStableCallback((ids: string[], to: { nodeId?: string; position: XYPosition }) => {
|
|
@@ -1194,8 +1221,8 @@ function Canvas({
|
|
|
1194
1221
|
const width = node ? (node.measured?.width ?? node.width ?? 0) : 0;
|
|
1195
1222
|
void setCenter(at.x + width / 2 + 120, at.y + 80, { zoom: Math.max(getZoom(), 0.8), duration: 300 });
|
|
1196
1223
|
setHotspotDraft(null);
|
|
1224
|
+
// (the list stays open, so they can go through them one by one)
|
|
1197
1225
|
setOpenHotspot(node ? `node:${node.id}` : `place:${hotspot.id}`);
|
|
1198
|
-
setListingHotspots(false);
|
|
1199
1226
|
});
|
|
1200
1227
|
const nameOfNode = useStableCallback((id: string) => {
|
|
1201
1228
|
const node = nodesRef.current.find((candidate) => candidate.id === id);
|
|
@@ -1328,6 +1355,57 @@ function Canvas({
|
|
|
1328
1355
|
|
|
1329
1356
|
const [following, setFollowing] = useState(followChanges);
|
|
1330
1357
|
const toggleFollowing = useCallback(() => setFollowing((on) => !on), []);
|
|
1358
|
+
// Asking the assistant (Ask AI) to draw on this canvas follows it while it works
|
|
1359
|
+
useEffect(() => {
|
|
1360
|
+
const followAssistant = (event: Event) => {
|
|
1361
|
+
if ((event as CustomEvent<{ canvasId?: string }>).detail?.canvasId === canvasId) setFollowing(true);
|
|
1362
|
+
};
|
|
1363
|
+
window.addEventListener(ASSISTANT_DRAWING_EVENT, followAssistant);
|
|
1364
|
+
return () => window.removeEventListener(ASSISTANT_DRAWING_EVENT, followAssistant);
|
|
1365
|
+
}, [canvasId]);
|
|
1366
|
+
|
|
1367
|
+
// The assistant taking you to part of the canvas (showOnCanvas, a walkthrough's steps): zoomed to, and selected. On
|
|
1368
|
+
// the canvas people edit (L3), where everything is.
|
|
1369
|
+
const focusOn = useStableCallback((ids: string[]) => {
|
|
1370
|
+
const nodes = lookupNodes();
|
|
1371
|
+
const found = ids.filter((id) => nodes.has(id));
|
|
1372
|
+
if (!found.length) return;
|
|
1373
|
+
const changingLevel = levelRef.current !== 3;
|
|
1374
|
+
if (changingLevel) changeLevel(3);
|
|
1375
|
+
// You're looking around now: following the agent would take the camera back to its pointer a moment later
|
|
1376
|
+
setFollowing(false);
|
|
1377
|
+
// Once the level's switched (and drawn), if it was
|
|
1378
|
+
setTimeout(
|
|
1379
|
+
() => {
|
|
1380
|
+
select(new Set(found));
|
|
1381
|
+
void fitView({ nodes: found.map((id) => ({ id })), duration: 600, padding: 0.35, maxZoom: 1.2 });
|
|
1382
|
+
},
|
|
1383
|
+
changingLevel ? 400 : 0
|
|
1384
|
+
);
|
|
1385
|
+
});
|
|
1386
|
+
useEffect(() => {
|
|
1387
|
+
const focus = (event: Event) => {
|
|
1388
|
+
const { canvasId: on, nodeIds } = (event as CustomEvent<{ canvasId?: string; nodeIds?: string[] }>).detail ?? {};
|
|
1389
|
+
if (on === canvasId && nodeIds?.length) focusOn(nodeIds);
|
|
1390
|
+
};
|
|
1391
|
+
window.addEventListener(CANVAS_FOCUS_EVENT, focus);
|
|
1392
|
+
return () => window.removeEventListener(CANVAS_FOCUS_EVENT, focus);
|
|
1393
|
+
}, [canvasId, focusOn]);
|
|
1394
|
+
// ...and to a hotspot it flagged, opened
|
|
1395
|
+
const showHotspot = useStableCallback((hotspotId: string) => {
|
|
1396
|
+
const hotspot = flow.hotspots.find((candidate) => candidate.id === hotspotId);
|
|
1397
|
+
if (!hotspot) return;
|
|
1398
|
+
setFollowing(false);
|
|
1399
|
+
goToHotspot(hotspot);
|
|
1400
|
+
});
|
|
1401
|
+
useEffect(() => {
|
|
1402
|
+
const show = (event: Event) => {
|
|
1403
|
+
const { canvasId: on, hotspotId } = (event as CustomEvent<{ canvasId?: string; hotspotId?: string }>).detail ?? {};
|
|
1404
|
+
if (on === canvasId && hotspotId) showHotspot(hotspotId);
|
|
1405
|
+
};
|
|
1406
|
+
window.addEventListener(CANVAS_HOTSPOT_EVENT, show);
|
|
1407
|
+
return () => window.removeEventListener(CANVAS_HOTSPOT_EVENT, show);
|
|
1408
|
+
}, [canvasId, showHotspot]);
|
|
1331
1409
|
|
|
1332
1410
|
const [shareOpen, setShareOpen] = useState<false | 'choose' | 'invite'>(false);
|
|
1333
1411
|
const openShare = useCallback(() => setShareOpen('choose'), []);
|
|
@@ -1458,6 +1536,8 @@ function Canvas({
|
|
|
1458
1536
|
flow.onEdgesChange(edgesRef.current.map((edge) => ({ type: 'select', id: edge.id, selected: true })));
|
|
1459
1537
|
// ...and the comments shown on it
|
|
1460
1538
|
setSelectedThreads(new Set(threadsShown().map((thread) => thread.id)));
|
|
1539
|
+
// ...and its hotspots (the ones on L1 and L2's own places are on those levels)
|
|
1540
|
+
setSelectedHotspots(new Set(flow.hotspots.filter((hotspot) => !hotspot.level).map((hotspot) => hotspot.id)));
|
|
1461
1541
|
},
|
|
1462
1542
|
{ preventDefault: true }
|
|
1463
1543
|
);
|
|
@@ -1492,7 +1572,7 @@ function Canvas({
|
|
|
1492
1572
|
setTool('select');
|
|
1493
1573
|
setDraft(null);
|
|
1494
1574
|
setOpenThreadId(null);
|
|
1495
|
-
|
|
1575
|
+
clearSelectedPins();
|
|
1496
1576
|
setEditingId(null);
|
|
1497
1577
|
flow.onNodesChange(
|
|
1498
1578
|
nodesRef.current.filter((node) => node.selected).map((node) => ({ type: 'select', id: node.id, selected: false }))
|
|
@@ -1501,11 +1581,16 @@ function Canvas({
|
|
|
1501
1581
|
edgesRef.current.filter((edge) => edge.selected).map((edge) => ({ type: 'select', id: edge.id, selected: false }))
|
|
1502
1582
|
);
|
|
1503
1583
|
});
|
|
1504
|
-
// Selected comments are deleted with what's selected (React Flow deletes the nodes and edges)
|
|
1584
|
+
// Selected comments and hotspots are deleted with what's selected (React Flow deletes the nodes and edges)
|
|
1505
1585
|
useHotkeys(DELETE_KEYS, () => {
|
|
1506
|
-
if (!onCanvas() || selectedThreads.size === 0) return;
|
|
1507
|
-
comments.deleteThreads([...selectedThreads]);
|
|
1508
|
-
|
|
1586
|
+
if (!onCanvas() || (selectedThreads.size === 0 && selectedHotspots.size === 0)) return;
|
|
1587
|
+
if (selectedThreads.size) comments.deleteThreads([...selectedThreads]);
|
|
1588
|
+
// (hotspots, like nodes, stay on a locked canvas)
|
|
1589
|
+
if (!locked) {
|
|
1590
|
+
selectedHotspots.forEach((id) => flow.deleteHotspot(id));
|
|
1591
|
+
if (selectedHotspots.size) setOpenHotspot(null);
|
|
1592
|
+
}
|
|
1593
|
+
clearSelectedPins();
|
|
1509
1594
|
});
|
|
1510
1595
|
/** A tool that puts something down: picked, or put back to selecting if it was already */
|
|
1511
1596
|
const pickTool = useStableCallback((next: 'text' | 'note' | 'comment' | 'hotspot') => {
|
|
@@ -1538,7 +1623,9 @@ function Canvas({
|
|
|
1538
1623
|
// A place on L1 or L2 is on that level (they're laid out on their own): what's put there says so
|
|
1539
1624
|
const placeLevel = () => (levelRef.current === 3 ? {} : { level: levelRef.current as 1 | 2 });
|
|
1540
1625
|
const onPaneClick = useStableCallback((event: ReactMouseEvent) => {
|
|
1541
|
-
|
|
1626
|
+
clearSelectedPins();
|
|
1627
|
+
// An open hotspot closes on a click anywhere else on the canvas
|
|
1628
|
+
setOpenHotspot(null);
|
|
1542
1629
|
if (tool === 'hand') return;
|
|
1543
1630
|
if (commentMode && editable) return startComment(anchorAt(flowPositionOf(event)));
|
|
1544
1631
|
// Flagging: a hotspot where it's clicked
|
|
@@ -1574,7 +1661,8 @@ function Canvas({
|
|
|
1574
1661
|
|
|
1575
1662
|
const onNodeClick = useStableCallback((event: ReactMouseEvent, node: Node) => {
|
|
1576
1663
|
// Clicking one node selects just it (with shift or ⌘, it's added to what's selected)
|
|
1577
|
-
if (!event.shiftKey && !event.metaKey)
|
|
1664
|
+
if (!event.shiftKey && !event.metaKey) clearSelectedPins();
|
|
1665
|
+
setOpenHotspot(null);
|
|
1578
1666
|
// Not on notes and text: they're written on, not what's being flagged or commented on
|
|
1579
1667
|
if (isWrittenOnCanvas(node.type)) return;
|
|
1580
1668
|
// A hotspot on it (it shows on the node, wherever it goes, on every level it's on)
|
|
@@ -1645,6 +1733,13 @@ function Canvas({
|
|
|
1645
1733
|
flow.deleteNodes(menuTargets());
|
|
1646
1734
|
setContextMenu(null);
|
|
1647
1735
|
});
|
|
1736
|
+
// Copies, then deletes, as ⌘X does: only on L3, where copying works (so it's not offered on L1 and L2)
|
|
1737
|
+
const cutFromMenu = useStableCallback(() => {
|
|
1738
|
+
copiedFromMenu.current = false;
|
|
1739
|
+
copyFromMenu();
|
|
1740
|
+
// Not copied (the browser refused, or there was nothing to copy): nothing's deleted, as with ⌘X
|
|
1741
|
+
if (copiedFromMenu.current) deleteFromMenu();
|
|
1742
|
+
});
|
|
1648
1743
|
|
|
1649
1744
|
const openThread = useStableCallback((thread: Thread) => {
|
|
1650
1745
|
setListingComments(false);
|
|
@@ -1761,7 +1856,7 @@ function Canvas({
|
|
|
1761
1856
|
ref={canvasRef}
|
|
1762
1857
|
className={`eventcatalog-visualizer studio-canvas relative flex-1 min-w-0 ${commentMode ? 'comment-mode' : ''} ${
|
|
1763
1858
|
transitioning ? 'is-layout-transition' : ''
|
|
1764
|
-
} ${placing || tool === 'comment' || tool === 'hotspot' ? 'is-placing' : ''} ${tool === 'hand' ? 'is-panning' : ''}`}
|
|
1859
|
+
} ${placing || tool === 'comment' || tool === 'hotspot' ? 'is-placing' : ''} ${tool === 'hotspot' ? 'is-placing-hotspot' : ''} ${tool === 'hand' ? 'is-panning' : ''}`}
|
|
1765
1860
|
onPointerMove={trackPointer}
|
|
1766
1861
|
onPointerLeave={clearPointer}
|
|
1767
1862
|
onDoubleClick={onCanvasDoubleClick}
|
|
@@ -1799,7 +1894,7 @@ function Canvas({
|
|
|
1799
1894
|
onNodeClick={onNodeClick}
|
|
1800
1895
|
onNodeDoubleClick={onNodeDoubleClick}
|
|
1801
1896
|
onError={onFlowError}
|
|
1802
|
-
onSelectionStart={
|
|
1897
|
+
onSelectionStart={clearSelectedPins}
|
|
1803
1898
|
onPaneContextMenu={onPaneContextMenu}
|
|
1804
1899
|
onNodeContextMenu={onNodeContextMenu}
|
|
1805
1900
|
connectionLineType={ConnectionLineType.SmoothStep}
|
|
@@ -1849,7 +1944,7 @@ function Canvas({
|
|
|
1849
1944
|
draft={hotspotDraft}
|
|
1850
1945
|
locked={locked}
|
|
1851
1946
|
movable={!locked}
|
|
1852
|
-
selectedIds={null}
|
|
1947
|
+
selectedIds={selectedHotspots.size ? selectedHotspots : null}
|
|
1853
1948
|
onMove={moveHotspots}
|
|
1854
1949
|
onDropTarget={setHotspotDropTarget}
|
|
1855
1950
|
onOpen={setOpenHotspot}
|
|
@@ -1858,6 +1953,7 @@ function Canvas({
|
|
|
1858
1953
|
onUpdate={flow.updateHotspot}
|
|
1859
1954
|
onDelete={deleteHotspot}
|
|
1860
1955
|
onGo={goToHotspot}
|
|
1956
|
+
onMoveDraft={moveHotspotDraft}
|
|
1861
1957
|
/>
|
|
1862
1958
|
</ReactFlow>
|
|
1863
1959
|
|
|
@@ -1971,7 +2067,12 @@ function Canvas({
|
|
|
1971
2067
|
</button>
|
|
1972
2068
|
</div>
|
|
1973
2069
|
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
1974
|
-
<HotspotList
|
|
2070
|
+
<HotspotList
|
|
2071
|
+
hotspots={flow.hotspots}
|
|
2072
|
+
nameOf={nameOfNode}
|
|
2073
|
+
onOpen={goToHotspot}
|
|
2074
|
+
goOnHover={!hotspotDraft}
|
|
2075
|
+
/>
|
|
1975
2076
|
</div>
|
|
1976
2077
|
</div>
|
|
1977
2078
|
)}
|
|
@@ -2001,6 +2102,7 @@ function Canvas({
|
|
|
2001
2102
|
onAddNote={noteFromMenu}
|
|
2002
2103
|
onEditDetails={editFromMenu}
|
|
2003
2104
|
onCopy={copyFromMenu}
|
|
2105
|
+
onCut={level === 3 ? cutFromMenu : undefined}
|
|
2004
2106
|
onDuplicate={duplicateFromMenu}
|
|
2005
2107
|
onPaste={lastCopied ? pasteFromMenu : undefined}
|
|
2006
2108
|
onReorder={reorderFromMenu}
|
|
@@ -2033,6 +2135,7 @@ function Canvas({
|
|
|
2033
2135
|
statusHistory={flow.canvasStatus.history}
|
|
2034
2136
|
onStatusChange={flow.changeStatus}
|
|
2035
2137
|
onDelete={canvasesApiUrl ? openDelete : undefined}
|
|
2138
|
+
onDrawWithAi={assistant ? openAssistant : undefined}
|
|
2036
2139
|
/>
|
|
2037
2140
|
|
|
2038
2141
|
{flow.deleted && !leaving && (
|
|
@@ -277,7 +277,7 @@ const createCanvasTools = (get: () => CanvasWebMcpOptions, getStage: () => Agent
|
|
|
277
277
|
defineTool({
|
|
278
278
|
name: 'remove_from_canvas',
|
|
279
279
|
title: 'Remove from the canvas',
|
|
280
|
-
description: 'Removes nodes (and their connections) or connections. Only remove what the user asked for.',
|
|
280
|
+
description: 'Removes nodes (and their connections and hotspots) or connections. Only remove what the user asked for.',
|
|
281
281
|
input: z.object({ nodeIds: z.array(z.string()).optional(), connectionIds: z.array(z.string()).optional() }),
|
|
282
282
|
annotations: { consequentialHint: true },
|
|
283
283
|
execute: async ({ nodeIds = [], connectionIds = [] }) => {
|
|
@@ -11,6 +11,7 @@ import { getCatalogResources } from '../catalog-resources';
|
|
|
11
11
|
import { startStudio, STUDIO_SOCKET_PATH } from '../server/runtime';
|
|
12
12
|
import { getSignedInUser, isSignedIn } from '../server/sign-in';
|
|
13
13
|
import { CANVASES_API_PATH } from '../api/canvases-api';
|
|
14
|
+
import { isEventCatalogChatVisible } from '@utils/feature';
|
|
14
15
|
|
|
15
16
|
export const prerender = false;
|
|
16
17
|
|
|
@@ -59,6 +60,7 @@ const signedInAs = getSignedInUser(Astro.locals);
|
|
|
59
60
|
mcpUrl={buildUrl('/docs/mcp', true)}
|
|
60
61
|
relayScriptPath={buildUrl('/webmcp-relay/embed.js', true)}
|
|
61
62
|
keptInMemory={keptInMemory}
|
|
63
|
+
assistant={isEventCatalogChatVisible()}
|
|
62
64
|
signedInAs={signedInAs}
|
|
63
65
|
resources={resources}
|
|
64
66
|
relations={relations}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getCanvasMaps, getLayoutPositions, readCanvas } from '@eventcatalog/visualiser/studio';
|
|
4
|
+
import {
|
|
5
|
+
ADD_TO_CANVAS_DESCRIPTION,
|
|
6
|
+
describeCanvas,
|
|
7
|
+
edgeSpecsSchema,
|
|
8
|
+
indexCatalog,
|
|
9
|
+
nodeSpecsSchema,
|
|
10
|
+
withNotesPlaced,
|
|
11
|
+
} from '../canvas-actions';
|
|
12
|
+
import { getCatalogResources } from '../catalog-resources';
|
|
13
|
+
import { playAddToCanvas, playConnectAll, playHotspot, playLayout, playRemove, playUpdateNode } from '../agent-choreography';
|
|
14
|
+
import { getAgent } from '../agents';
|
|
15
|
+
import { agentOnCanvas } from './canvas-agent';
|
|
16
|
+
import { findStudioRuntime } from './runtime';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The assistant (Ask AI) on a Studio canvas: the canvas it was opened on, drawn on with the model the catalog has
|
|
20
|
+
* configured. Its tools play changes out like the MCP canvas tools do (same choreography), so everyone on the canvas
|
|
21
|
+
* watches the assistant's pointer draw the diagram.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const ASSISTANT = getAgent('EventCatalog AI');
|
|
25
|
+
// Someone's waiting on the assistant in the chat: it draws about three times as fast as an MCP agent
|
|
26
|
+
const ASSISTANT_PACE = 0.3;
|
|
27
|
+
|
|
28
|
+
/** What the assistant is told when it's opened on a canvas, after its usual instructions */
|
|
29
|
+
const CANVAS_ASSISTANT_PROMPT = `
|
|
30
|
+
The user is on an EventCatalog Studio canvas: a live, collaborative diagram. They opened you there to draw on it with them.
|
|
31
|
+
When they ask you to diagram, design, map out, sketch or change something, draw it on the canvas with the canvas tools; don't answer with Mermaid or text diagrams.
|
|
32
|
+
|
|
33
|
+
If you need to ask the user something first (what to diagram, or a request too vague to draw), ask straight away, without calling any tools.
|
|
34
|
+
|
|
35
|
+
How to draw:
|
|
36
|
+
1. Read the canvas before you change it (getCanvas), so you build on what's there and don't add what's already on it.
|
|
37
|
+
2. Find what they asked about in the catalog (getResources, getResource, getMessagesProducedOrConsumedByResource, getArchitectureDiagramAsMermaid) and add those resources by collection and id. Catalog resources are connected to the related resources already on the canvas for you.
|
|
38
|
+
- A domain or system from the catalog: add it with contents: true, which brings everything in it at once. Don't look up and add its services and messages yourself.
|
|
39
|
+
- A service or message with what it connects to: add it with connections (e.g. ["sends", "receives"]), rather than finding each one.
|
|
40
|
+
3. Only add new components (type, name, summary) for what isn't in the catalog yet, e.g. a proposed service or event when the user is designing something new.
|
|
41
|
+
4. Add things in one or a few addToCanvas calls (with refs and edges), rather than one call per node. Use containers (domains, systems) when the diagram covers more than one of them.
|
|
42
|
+
5. Don't lay the canvas out yourself: when you've finished, the canvas is tidied for you if you added things or connections. Only call layoutCanvas when the user asks you to tidy up.
|
|
43
|
+
|
|
44
|
+
Explaining the diagram on the canvas:
|
|
45
|
+
- Hotspots flag something people should know about a node, or a place on the canvas: a risk, a problem or something worth knowing (addHotspot: critical, warning or info, with a short title and why it matters). Use them when the user asks to flag, highlight or review things, e.g. single points of failure.
|
|
46
|
+
- Sticky notes hold free text on the canvas: add a node with type "note" and its text in summary. Use them for notes, assumptions, open questions, suggestions and explanations. Keep each one very short and precise, like a sticky note on a whiteboard: one point in a few words (at most about 12), e.g. "Kafka: ordered, replayable order events". Add as many as are useful, one point per note, each beside the part of the diagram it's about: give it x/y about 300 to the right of, or 250 above, that node's centre (from getCanvas, after you've drawn), clear of other nodes. Add notes and text after the nodes they're about, so you know where those are. A note or text that would land on a node is moved to the nearest clear spot beside it, and notes stay beside their node when the canvas is tidied.
|
|
47
|
+
- Text is words written straight on the canvas, without a note around them: add a node with type "text", what it says in summary, and fontSize for how big (22 by default; 40 to 60 for a title or a heading over part of the diagram). Use it for a diagram's title (above everything: y about 200 above the top of what's on the canvas), headings over areas (just above the domain or group they name), or short labels. Never put text where a node is. Move it with updateCanvasNode (x, y), and make it bigger or smaller with fontSize.
|
|
48
|
+
- When asked to review or improve the design: flag each problem or risk with a hotspot on the node it's about, and write your suggestions on a few sticky notes (one per idea), then sum up what you found in a sentence or two. The hotspots you flag are listed under your reply for the user to go to, so don't list them again.
|
|
49
|
+
Only add hotspots and notes when the user asks for them, or the request is about reviewing or annotating the design.
|
|
50
|
+
|
|
51
|
+
Showing and explaining what's on the canvas:
|
|
52
|
+
- showOnCanvas takes the user to nodes: their view zooms to them and they're highlighted. Use it to show them what you're talking about.
|
|
53
|
+
- Whenever you mention something that's on the canvas in your reply, write it as a reference to it, which the user can hover or click to be taken there: [Order Service](#canvas-ref=service:<node id>). The type before the colon gives it its icon and colour: service, event, command, query, channel, domain, system, container (a data store), entity, agent, flow, user, team or data-product; for anything else, leave it out: [Customer](#canvas-ref=:<node id>). Use the node ids from getCanvas (or what addToCanvas created).
|
|
54
|
+
- To walk the user through how something works (e.g. "walk me through how checkout works"), read the canvas, then call walkThrough with the steps in order: each a short title, a sentence or two on what happens, and the nodes involved (ids from getCanvas). The user sees the steps in the chat, and each takes them to its nodes on the canvas, starting with the first: don't write the steps out again in your reply, just a sentence to introduce them. If what they ask about isn't on the canvas, draw it first.
|
|
55
|
+
|
|
56
|
+
Keep your reply short: say what you drew and anything you couldn't find in the catalog.
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
/** The assistant's tools and instructions for the canvas it was opened on, or undefined if there's no such canvas */
|
|
60
|
+
export const getCanvasAssistant = (canvasId: string) => {
|
|
61
|
+
const runtime = findStudioRuntime();
|
|
62
|
+
if (!runtime?.exists(canvasId)) return undefined;
|
|
63
|
+
const agent = () => agentOnCanvas(runtime, canvasId, ASSISTANT, { pace: ASSISTANT_PACE });
|
|
64
|
+
// Added things or connections since the canvas was last laid out: it's tidied once the assistant has finished
|
|
65
|
+
let drew = false;
|
|
66
|
+
|
|
67
|
+
const layout = async () => {
|
|
68
|
+
drew = false;
|
|
69
|
+
const { nodes, edges } = await runtime.withCanvas(canvasId, readCanvas);
|
|
70
|
+
// Notes and text stay beside what they're about
|
|
71
|
+
const positions = withNotesPlaced(nodes, await getLayoutPositions(nodes, edges));
|
|
72
|
+
await playLayout(agent().stage, positions);
|
|
73
|
+
return { laidOut: positions.nodes.size };
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const tools = {
|
|
77
|
+
getCanvas: tool({
|
|
78
|
+
description:
|
|
79
|
+
'Reads everything on the canvas the user has open: its nodes (with ids, names, positions (their centres), sizes and the catalog resource they stand for), the connections between them, its hotspots and the comment threads. Read it before changing the canvas, and again to see what people have changed.',
|
|
80
|
+
inputSchema: z.object({}),
|
|
81
|
+
execute: async () => runtime.withCanvas(canvasId, describeCanvas),
|
|
82
|
+
}),
|
|
83
|
+
addToCanvas: tool({
|
|
84
|
+
description: `${ADD_TO_CANVAS_DESCRIPTION} The user watches you add them one by one. Use getResources to find catalog resources first, and prefer them over new components.`,
|
|
85
|
+
inputSchema: z.object({ nodes: nodeSpecsSchema, edges: edgeSpecsSchema.optional() }),
|
|
86
|
+
execute: async ({ nodes, edges }) => {
|
|
87
|
+
const { resources, relations } = await getCatalogResources();
|
|
88
|
+
const result = await playAddToCanvas(agent().stage, { nodes, edges }, indexCatalog(resources, relations));
|
|
89
|
+
// Notes and text alone (a review) leave the layout as it is
|
|
90
|
+
if (result.connections.length || nodes.some((node) => node.type !== 'note' && node.type !== 'text')) drew = true;
|
|
91
|
+
return result;
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
updateCanvasNode: tool({
|
|
95
|
+
description:
|
|
96
|
+
'Renames, re-describes, re-versions or moves a node on the canvas (x/y: its new centre). Catalog resources keep their catalog name, summary and version. For notes and text, summary is what is written; fontSize makes text bigger or smaller.',
|
|
97
|
+
inputSchema: z.object({
|
|
98
|
+
nodeId: z.string(),
|
|
99
|
+
name: z.string().optional(),
|
|
100
|
+
summary: z.string().optional(),
|
|
101
|
+
version: z.string().optional().describe('e.g. 1.0.0'),
|
|
102
|
+
fontSize: z.number().min(8).max(200).optional().describe('For text: how big it is written, in pixels'),
|
|
103
|
+
x: z.number().optional(),
|
|
104
|
+
y: z.number().optional(),
|
|
105
|
+
}),
|
|
106
|
+
execute: async (update) => playUpdateNode(agent().stage, update),
|
|
107
|
+
}),
|
|
108
|
+
connectCanvasNodes: tool({
|
|
109
|
+
description:
|
|
110
|
+
'Draws connections between nodes already on the canvas, one at a time. Labels default to EventCatalog wording (e.g. "publishes event", "sent to").',
|
|
111
|
+
inputSchema: z.object({ edges: edgeSpecsSchema.min(1) }),
|
|
112
|
+
execute: async ({ edges }) => {
|
|
113
|
+
const result = await playConnectAll(agent().stage, edges);
|
|
114
|
+
if (result.connections.length) drew = true;
|
|
115
|
+
return result;
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
addHotspot: tool({
|
|
119
|
+
description:
|
|
120
|
+
'Flags a hotspot on a node (nodeId), or at a place on the canvas (x, y): a marker everyone sees, with how serious it is, a short title and, optionally, why it matters and what to do about it. For risks, problems and things worth knowing, e.g. a single point of failure.',
|
|
121
|
+
inputSchema: z.object({
|
|
122
|
+
severity: z.enum(['critical', 'warning', 'info']).describe('critical: a problem; warning: a risk; info: worth knowing'),
|
|
123
|
+
title: z.string().describe('What people should know, in a few words, e.g. "Single point of failure"'),
|
|
124
|
+
description: z.string().optional().describe('Why it matters, and what to do about it'),
|
|
125
|
+
nodeId: z.string().optional().describe('The node it is about (from getCanvas or addToCanvas)'),
|
|
126
|
+
x: z.number().optional(),
|
|
127
|
+
y: z.number().optional(),
|
|
128
|
+
}),
|
|
129
|
+
execute: async (hotspot) => {
|
|
130
|
+
const { stage, author } = agent();
|
|
131
|
+
return playHotspot(stage, hotspot, author);
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
showOnCanvas: tool({
|
|
135
|
+
description:
|
|
136
|
+
"Takes the user to nodes on the canvas: their view zooms to them and they're highlighted (only their view, not anyone else's). Use it to show them what you're talking about, e.g. the first step of a walkthrough.",
|
|
137
|
+
inputSchema: z.object({ nodeIds: z.array(z.string()).min(1).describe('The nodes to show (ids from getCanvas)') }),
|
|
138
|
+
execute: async ({ nodeIds }) => {
|
|
139
|
+
const onCanvas = await runtime.withCanvas(canvasId, (doc) => new Set(getCanvasMaps(doc).nodes.keys()));
|
|
140
|
+
const shown = nodeIds.filter((id) => onCanvas.has(id));
|
|
141
|
+
const missing = nodeIds.filter((id) => !onCanvas.has(id));
|
|
142
|
+
if (!shown.length)
|
|
143
|
+
return { error: `None of these are on the canvas: ${missing.join(', ')}. Use getCanvas for their ids.` };
|
|
144
|
+
return { nodeIds: shown, ...(missing.length && { notOnCanvas: missing }) };
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
walkThrough: tool({
|
|
148
|
+
description:
|
|
149
|
+
'Walks the user through how something on the canvas works, step by step: they see the steps in the chat, and each takes them to its nodes on the canvas (starting with the first). Use it when they ask how something works or flows, e.g. "walk me through checkout".',
|
|
150
|
+
inputSchema: z.object({
|
|
151
|
+
title: z.string().describe('What it walks through, e.g. "How checkout works"'),
|
|
152
|
+
steps: z
|
|
153
|
+
.array(
|
|
154
|
+
z.object({
|
|
155
|
+
title: z.string().describe('The step, in a few words, e.g. "Customer places an order"'),
|
|
156
|
+
description: z.string().describe('What happens in this step, in a sentence or two'),
|
|
157
|
+
nodeIds: z.array(z.string()).min(1).describe('The nodes involved in this step (ids from getCanvas)'),
|
|
158
|
+
})
|
|
159
|
+
)
|
|
160
|
+
.min(1)
|
|
161
|
+
.max(20),
|
|
162
|
+
}),
|
|
163
|
+
execute: async ({ title, steps }) => {
|
|
164
|
+
const onCanvas = await runtime.withCanvas(canvasId, (doc) => new Set(getCanvasMaps(doc).nodes.keys()));
|
|
165
|
+
const missing = [...new Set(steps.flatMap((step) => step.nodeIds).filter((id) => !onCanvas.has(id)))];
|
|
166
|
+
return {
|
|
167
|
+
title,
|
|
168
|
+
steps: steps.map((step) => ({ ...step, nodeIds: step.nodeIds.filter((id) => onCanvas.has(id)) })),
|
|
169
|
+
...(missing.length && { notOnCanvas: missing }),
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
}),
|
|
173
|
+
removeFromCanvas: tool({
|
|
174
|
+
description:
|
|
175
|
+
'Removes nodes (and their connections) or connections from the canvas. Only remove what the user asked for or what you added.',
|
|
176
|
+
inputSchema: z.object({ nodeIds: z.array(z.string()).optional(), connectionIds: z.array(z.string()).optional() }),
|
|
177
|
+
execute: async ({ nodeIds = [], connectionIds = [] }) => {
|
|
178
|
+
await playRemove(agent().stage, nodeIds, connectionIds);
|
|
179
|
+
return { removedNodes: nodeIds, removedConnections: connectionIds };
|
|
180
|
+
},
|
|
181
|
+
}),
|
|
182
|
+
layoutCanvas: tool({
|
|
183
|
+
description:
|
|
184
|
+
"Lays the whole canvas out again with EventCatalog's visualiser layout (left to right, connections routed around nodes). Only when the user asks you to tidy up: what you draw is laid out for you when you've finished.",
|
|
185
|
+
inputSchema: z.object({}),
|
|
186
|
+
execute: layout,
|
|
187
|
+
}),
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
tools,
|
|
192
|
+
instructions: CANVAS_ASSISTANT_PROMPT,
|
|
193
|
+
/**
|
|
194
|
+
* The model started writing a call to a tool: a big addToCanvas takes a while to write, so the canvas shows the
|
|
195
|
+
* assistant planning it meanwhile
|
|
196
|
+
*/
|
|
197
|
+
startedWriting: (toolName: string) => {
|
|
198
|
+
if (toolName !== 'addToCanvas') return;
|
|
199
|
+
const { stage } = agent();
|
|
200
|
+
stage.present({ pointer: stage.lastPointer ?? { x: 0, y: 0 }, selection: [], activity: 'Planning the diagram' });
|
|
201
|
+
},
|
|
202
|
+
/** The assistant has finished: what it drew is laid out, so the canvas is tidy (the model doesn't always remember to) */
|
|
203
|
+
finished: async () => {
|
|
204
|
+
if (drew) await layout();
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { reopeningIfEdited, type Author } from '@eventcatalog/visualiser/studio';
|
|
2
|
+
import type { AgentStage } from '../agent-choreography';
|
|
3
|
+
import type { AgentIdentity } from '../agents';
|
|
4
|
+
import type { StudioRuntime } from './runtime';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Work on a canvas as an agent: changes are played out like a person would make them (see agent-choreography),
|
|
8
|
+
* with the agent's pointer, selection and what it's doing shown to everyone on the canvas. Used by the MCP canvas
|
|
9
|
+
* tools and the assistant's.
|
|
10
|
+
*/
|
|
11
|
+
export const agentOnCanvas = (
|
|
12
|
+
runtime: StudioRuntime,
|
|
13
|
+
canvasId: string,
|
|
14
|
+
agent: AgentIdentity,
|
|
15
|
+
{ pace }: Pick<AgentStage, 'pace'> = {}
|
|
16
|
+
) => {
|
|
17
|
+
const author = { ...agent, agent: true } as Author;
|
|
18
|
+
const stage: AgentStage = {
|
|
19
|
+
// An agent changing an accepted (or rejected) canvas makes it a draft again, like a person doing it
|
|
20
|
+
change: (fn) => runtime.withCanvas(canvasId, (doc) => reopeningIfEdited(doc, author, () => fn(doc))),
|
|
21
|
+
present: (presence) => runtime.setAgentPresence(canvasId, agent, presence),
|
|
22
|
+
lastPointer: runtime.getAgentPointer(canvasId, agent),
|
|
23
|
+
sharesMoves: true,
|
|
24
|
+
pace,
|
|
25
|
+
};
|
|
26
|
+
return { stage, author };
|
|
27
|
+
};
|