@eventcatalog/core 5.0.0-beta.11 → 5.0.0-beta.13
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 +1 -1
- 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 +4 -4
- package/dist/catalog-runtime.cjs +1 -1
- package/dist/{chunk-MFEMKQZ6.js → chunk-BO2HHVZ3.js} +1 -1
- package/dist/{chunk-4YFM63DS.js → chunk-JPT3QC5V.js} +1 -1
- package/dist/{chunk-76DAAJLV.js → chunk-PLTLW45Q.js} +5 -5
- package/dist/{chunk-WM4QYMIA.js → chunk-T75DOVFP.js} +1 -1
- package/dist/{chunk-LQZ7IG7B.js → chunk-XA5W6DXX.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +2 -2
- package/dist/eventcatalog.js +14 -14
- package/dist/federation/federate.js +3 -3
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/runtime-dependencies/@eventcatalog__visualiser__studio-fonts.css.mjs +3 -0
- package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.css.mjs +3 -0
- package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.mjs +3 -0
- package/dist/runtime-dependencies/manifest.json +3 -0
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +2 -0
- package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +42 -3
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +5 -1
- package/eventcatalog/src/components/NavigationProgress.astro +7 -0
- package/eventcatalog/src/features/mcp/apps/architecture-diagram/node-chat.tsx +6 -45
- package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +3 -3
- package/eventcatalog/src/features/mcp/apps/canvas/styles.css +3 -1
- package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +7 -5
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +132 -70
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +71 -71
- package/eventcatalog/src/features/studio/agent-choreography.ts +13 -10
- package/eventcatalog/src/features/studio/agents.ts +28 -0
- package/eventcatalog/src/features/studio/api/canvases-api.ts +64 -15
- package/eventcatalog/src/features/studio/api/client.ts +6 -1
- package/eventcatalog/src/features/studio/canvas-actions.ts +160 -34
- package/eventcatalog/src/features/studio/canvas-preview.ts +1 -2
- package/eventcatalog/src/features/studio/catalog.ts +16 -14
- package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +263 -115
- package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +15 -14
- package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +272 -119
- package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +1 -2
- package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +28 -44
- package/eventcatalog/src/features/studio/components/Dialog.tsx +60 -0
- package/eventcatalog/src/features/studio/components/JoinForm.tsx +101 -26
- package/eventcatalog/src/features/studio/components/LeftPanel.tsx +119 -230
- package/eventcatalog/src/features/studio/components/NodeDetails.tsx +183 -0
- package/eventcatalog/src/features/studio/components/OpenInStudioDialog.tsx +79 -0
- package/eventcatalog/src/features/studio/components/Presence.tsx +52 -45
- package/eventcatalog/src/features/studio/components/ShareDialog.tsx +277 -141
- package/eventcatalog/src/features/studio/components/ShortcutsDialog.tsx +41 -0
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +1286 -411
- package/eventcatalog/src/features/studio/document-name.ts +7 -0
- package/eventcatalog/src/features/studio/from-visualiser.ts +138 -0
- package/eventcatalog/src/features/studio/hooks/presence-sender.ts +19 -2
- package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +9 -3
- package/eventcatalog/src/features/studio/hooks/use-comments.ts +2 -2
- package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +8 -3
- package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +223 -30
- package/eventcatalog/src/features/studio/identity.ts +23 -1
- package/eventcatalog/src/features/studio/limits.ts +2 -0
- package/eventcatalog/src/features/studio/pages/[id].astro +2 -1
- package/eventcatalog/src/features/studio/pages/index.astro +2 -2
- package/eventcatalog/src/features/studio/server/canvas-mcp.ts +8 -23
- package/eventcatalog/src/features/studio/server/runtime.ts +16 -9
- package/eventcatalog/src/utils/node-graphs/domain-levels-node-graph.ts +3 -3
- package/eventcatalog/src/utils/node-graphs/node-resource.ts +67 -0
- package/eventcatalog/src/utils/node-graphs/systems-node-graph.ts +1 -1
- package/package.json +3 -3
- package/eventcatalog/src/features/studio/canvas-doc.ts +0 -414
- package/eventcatalog/src/features/studio/clipboard.ts +0 -97
- package/eventcatalog/src/features/studio/components/CanvasControls.tsx +0 -154
- package/eventcatalog/src/features/studio/components/Comments.tsx +0 -770
- package/eventcatalog/src/features/studio/components/ContainerChooser.tsx +0 -145
- package/eventcatalog/src/features/studio/components/DragGhost.tsx +0 -115
- package/eventcatalog/src/features/studio/components/Picture.tsx +0 -21
- package/eventcatalog/src/features/studio/components/PropertiesPanel.tsx +0 -212
- package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +0 -255
- package/eventcatalog/src/features/studio/components/icons.ts +0 -53
- package/eventcatalog/src/features/studio/components/status.ts +0 -53
- package/eventcatalog/src/features/studio/edges.ts +0 -56
- package/eventcatalog/src/features/studio/flow-state.ts +0 -92
- package/eventcatalog/src/features/studio/grouping.ts +0 -226
- package/eventcatalog/src/features/studio/layout.ts +0 -42
- package/eventcatalog/src/features/studio/levels.ts +0 -282
- package/eventcatalog/src/features/studio/node-types.ts +0 -205
- package/eventcatalog/src/features/studio/placement.ts +0 -197
- package/eventcatalog/src/features/studio/studio.css +0 -273
- package/dist/{chunk-KHACCTW2.js → chunk-ZEY2XUJN.js} +4 -4
|
@@ -1,26 +1,58 @@
|
|
|
1
1
|
import { memo, useEffect, useRef, useState, type ReactNode } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
CircleHelp,
|
|
4
|
+
Download,
|
|
5
|
+
FolderOpen,
|
|
6
|
+
ImageDown,
|
|
7
|
+
Link2,
|
|
8
|
+
MoreVertical,
|
|
9
|
+
Pencil,
|
|
10
|
+
Plug,
|
|
11
|
+
Plus,
|
|
12
|
+
Share2,
|
|
13
|
+
Trash2,
|
|
14
|
+
Users,
|
|
15
|
+
} from 'lucide-react';
|
|
16
|
+
import {
|
|
17
|
+
DIAGRAM_MENU,
|
|
18
|
+
DIAGRAM_MENU_BUTTON_ICON,
|
|
19
|
+
DIAGRAM_MENU_ITEM,
|
|
20
|
+
DIAGRAM_MENU_ITEM_ICON,
|
|
21
|
+
DIAGRAM_MENU_SEPARATOR,
|
|
22
|
+
DIAGRAM_MENU_TITLE,
|
|
23
|
+
} from '@eventcatalog/visualiser';
|
|
3
24
|
import { usePeople, type Peer, type PresenceStore } from '../hooks/presence-store';
|
|
25
|
+
import { MAX_NAME_LENGTH, PERSON_COLORS } from '../identity';
|
|
4
26
|
import type { WebMcpStatus } from '../hooks/use-canvas-webmcp';
|
|
5
27
|
import type { Status, Transport } from '../hooks/use-studio-flow';
|
|
6
|
-
import type
|
|
28
|
+
import { type CanvasStatus, type StatusChange, STATUS, Picture, AgentMark } from '@eventcatalog/visualiser/studio';
|
|
7
29
|
import CanvasStatusMenu from './CanvasStatusMenu';
|
|
8
|
-
|
|
9
|
-
|
|
30
|
+
|
|
31
|
+
/** The dot showing whether the canvas is connected */
|
|
32
|
+
const CONNECTION_DOT: Record<Status, string> = {
|
|
33
|
+
connected: STATUS.success.dot,
|
|
34
|
+
connecting: STATUS.warning.dot,
|
|
35
|
+
disconnected: STATUS.danger.dot,
|
|
36
|
+
};
|
|
10
37
|
|
|
11
38
|
/**
|
|
12
|
-
* The canvas's header,
|
|
13
|
-
*
|
|
39
|
+
* The canvas's header, as Studio on the web's top bar: on the left the canvas (its title, its status and a menu), on
|
|
40
|
+
* the right who's here, connecting an agent and sharing, each a small card over the canvas. Its menus look like the
|
|
41
|
+
* visualiser's (`DIAGRAM_MENU_*`).
|
|
14
42
|
*/
|
|
15
43
|
|
|
16
44
|
const barClass =
|
|
17
|
-
'pointer-events-auto flex h-
|
|
45
|
+
'pointer-events-auto flex h-[38px] items-center rounded-[9px] border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] shadow-sm';
|
|
18
46
|
// The header lets the pointer through to the canvas around its bars, so its menus take it back
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const menuItemClass = `${menuItemLayout} hover:bg-[rgb(var(--ec-page-border)/0.5)]`;
|
|
47
|
+
const popoverClass = 'pointer-events-auto absolute top-full mt-2';
|
|
48
|
+
const menuClass = `${popoverClass} ${DIAGRAM_MENU}`;
|
|
49
|
+
const menuItemClass = `${DIAGRAM_MENU_ITEM} w-full text-left`;
|
|
23
50
|
const MAX_AVATARS = 4;
|
|
51
|
+
const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform);
|
|
52
|
+
const MOD = isMac ? 'Cmd' : 'Ctrl';
|
|
53
|
+
const itemClass =
|
|
54
|
+
'flex h-[34px] w-full cursor-pointer items-center gap-3 rounded-[7px] px-2 text-left text-sm outline-none hover:bg-[rgb(var(--ec-content-hover))]';
|
|
55
|
+
const itemIconClass = 'flex flex-none [&_svg]:h-[17px] [&_svg]:w-[17px] [&_svg]:stroke-[1.75]';
|
|
24
56
|
|
|
25
57
|
/** Closes a popover on outside clicks and Escape */
|
|
26
58
|
function usePopover() {
|
|
@@ -48,7 +80,7 @@ function Avatar({ peer, size = 28, children }: { peer: Peer; size?: number; chil
|
|
|
48
80
|
className="relative flex items-center justify-center rounded-full text-[11px] font-semibold text-white ring-2 ring-[rgb(var(--ec-card-bg))]"
|
|
49
81
|
style={{ backgroundColor: peer.color, width: size, height: size }}
|
|
50
82
|
>
|
|
51
|
-
{children ?? (peer.agent ? <
|
|
83
|
+
{children ?? (peer.agent ? <AgentMark name={peer.name} size={14} /> : peer.name.slice(0, 2).toUpperCase())}
|
|
52
84
|
{!children && <Picture src={peer.picture} />}
|
|
53
85
|
</span>
|
|
54
86
|
);
|
|
@@ -61,15 +93,33 @@ function CanvasMenu({
|
|
|
61
93
|
onRetitle,
|
|
62
94
|
shareUrl,
|
|
63
95
|
newCanvasUrl,
|
|
96
|
+
onConnectAgent,
|
|
97
|
+
webMcp,
|
|
98
|
+
agentsHere,
|
|
64
99
|
onDelete,
|
|
100
|
+
onOpenFile,
|
|
101
|
+
onSaveToFile,
|
|
102
|
+
onExportImage,
|
|
103
|
+
onLiveCollaboration,
|
|
104
|
+
onHelp,
|
|
65
105
|
children,
|
|
66
106
|
}: {
|
|
107
|
+
/** Opens a .eventcatalog file as a new canvas (not offered when not given, e.g. inside a chat) */
|
|
108
|
+
onOpenFile?: () => void;
|
|
109
|
+
onSaveToFile: () => void;
|
|
110
|
+
onExportImage: () => void;
|
|
111
|
+
onLiveCollaboration: () => void;
|
|
112
|
+
onHelp: () => void;
|
|
67
113
|
title?: string;
|
|
68
114
|
status: Status;
|
|
69
115
|
transport: Transport;
|
|
70
116
|
onRetitle: (title: string) => void;
|
|
71
117
|
shareUrl?: string;
|
|
72
118
|
newCanvasUrl?: string;
|
|
119
|
+
onConnectAgent: () => void;
|
|
120
|
+
webMcp: WebMcpStatus;
|
|
121
|
+
/** An AI agent is on the canvas */
|
|
122
|
+
agentsHere: boolean;
|
|
73
123
|
/** Asks to delete the canvas (not offered when not given, e.g. inside a chat) */
|
|
74
124
|
onDelete?: () => void;
|
|
75
125
|
/** Shown after the title (the canvas's status) */
|
|
@@ -84,10 +134,29 @@ function CanvasMenu({
|
|
|
84
134
|
disconnected: 'Offline, reconnecting…',
|
|
85
135
|
}[status];
|
|
86
136
|
const statusColor = CONNECTION_DOT[status];
|
|
137
|
+
/** A menu item, as Studio on the web's: its icon, what it does, and its shortcut (or something else on the right) */
|
|
138
|
+
const item = (icon: ReactNode, label: string, onSelect: () => void, shortcut?: ReactNode, className = '') => (
|
|
139
|
+
<button
|
|
140
|
+
type="button"
|
|
141
|
+
className={`${itemClass} ${className}`}
|
|
142
|
+
onClick={() => {
|
|
143
|
+
menu.setOpen(false);
|
|
144
|
+
onSelect();
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
<span className={itemIconClass}>{icon}</span>
|
|
148
|
+
<span className="min-w-0 flex-1 truncate">{label}</span>
|
|
149
|
+
{typeof shortcut === 'string' ? (
|
|
150
|
+
<span className="flex-none text-xs font-normal text-[rgb(var(--ec-page-text-muted))]">{shortcut}</span>
|
|
151
|
+
) : (
|
|
152
|
+
shortcut
|
|
153
|
+
)}
|
|
154
|
+
</button>
|
|
155
|
+
);
|
|
87
156
|
|
|
88
157
|
return (
|
|
89
158
|
<div ref={menu.ref} className="relative">
|
|
90
|
-
<div className={`${barClass} gap-1 pl-3 pr-1`}>
|
|
159
|
+
<div className={`${barClass} gap-1.5 pl-3 pr-1`}>
|
|
91
160
|
<span title={statusLabel} className={`h-2 w-2 shrink-0 rounded-full ${statusColor}`} />
|
|
92
161
|
<input
|
|
93
162
|
ref={titleInput}
|
|
@@ -101,59 +170,57 @@ function CanvasMenu({
|
|
|
101
170
|
}}
|
|
102
171
|
onKeyDown={(e) => e.key === 'Enter' && e.currentTarget.blur()}
|
|
103
172
|
size={Math.max(12, (draft ?? title ?? 'Untitled canvas').length)}
|
|
104
|
-
className=
|
|
173
|
+
className={`${DIAGRAM_MENU_TITLE} max-w-64 truncate rounded-md bg-transparent px-1.5 py-1 placeholder:text-[rgb(var(--ec-page-text-muted))] hover:bg-[rgb(var(--ec-page-border)/0.4)] focus:bg-[rgb(var(--ec-input-bg))] focus:outline-none`}
|
|
105
174
|
/>
|
|
106
175
|
{children}
|
|
107
176
|
<button
|
|
108
177
|
aria-label="Canvas menu"
|
|
109
178
|
onClick={() => menu.setOpen((open) => !open)}
|
|
110
|
-
className="rounded-md p-1
|
|
179
|
+
className="group rounded-md p-1 hover:bg-[rgb(var(--ec-accent-subtle))]"
|
|
111
180
|
>
|
|
112
|
-
<
|
|
181
|
+
<MoreVertical className={DIAGRAM_MENU_BUTTON_ICON} />
|
|
113
182
|
</button>
|
|
114
183
|
</div>
|
|
115
184
|
{menu.open && (
|
|
116
|
-
<div
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
<
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
185
|
+
<div
|
|
186
|
+
className={`${popoverClass} left-0 z-50 w-72 rounded-xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] p-1.5 text-[rgb(var(--ec-page-text))] shadow-xl`}
|
|
187
|
+
>
|
|
188
|
+
{/* As Studio on the web's menu: files and images, people and agents, help */}
|
|
189
|
+
{onOpenFile && item(<FolderOpen />, 'Open…', onOpenFile, `${MOD}+O`)}
|
|
190
|
+
{item(<Download />, 'Save to file…', onSaveToFile, `${MOD}+S`)}
|
|
191
|
+
{item(<ImageDown />, 'Export image…', onExportImage, `${MOD}+Shift+E`)}
|
|
192
|
+
{item(<Users />, 'Live collaboration…', onLiveCollaboration)}
|
|
193
|
+
{item(
|
|
194
|
+
<Plug />,
|
|
195
|
+
agentsHere ? 'AI agent connected' : 'Connect your AI agent…',
|
|
196
|
+
onConnectAgent,
|
|
197
|
+
webMcp.state === 'on' || agentsHere ? (
|
|
198
|
+
<span className={`h-1.5 w-1.5 rounded-full ${STATUS.success.dot}`} />
|
|
199
|
+
) : undefined
|
|
200
|
+
)}
|
|
201
|
+
{item(<CircleHelp />, 'Help', onHelp, '?')}
|
|
202
|
+
<div className="my-1.5 h-px bg-[rgb(var(--ec-page-border))]" />
|
|
203
|
+
{item(<Pencil />, 'Rename canvas', () => {
|
|
204
|
+
titleInput.current?.focus();
|
|
205
|
+
titleInput.current?.select();
|
|
206
|
+
})}
|
|
207
|
+
{item(<Link2 />, 'Copy link', () => void navigator.clipboard.writeText(shareUrl ?? window.location.href))}
|
|
136
208
|
{newCanvasUrl && (
|
|
137
|
-
<a className={
|
|
138
|
-
<
|
|
209
|
+
<a className={itemClass} href={newCanvasUrl}>
|
|
210
|
+
<span className={itemIconClass}>
|
|
211
|
+
<Plus />
|
|
212
|
+
</span>
|
|
213
|
+
<span className="min-w-0 flex-1 truncate">New canvas</span>
|
|
139
214
|
</a>
|
|
140
215
|
)}
|
|
141
216
|
{onDelete && (
|
|
142
217
|
<>
|
|
143
|
-
<div className="my-1 h-px bg-[rgb(var(--ec-page-border))]" />
|
|
144
|
-
<
|
|
145
|
-
className={`${menuItemLayout} ${STATUS.danger.text} ${STATUS.danger.hover}`}
|
|
146
|
-
onClick={() => {
|
|
147
|
-
menu.setOpen(false);
|
|
148
|
-
onDelete();
|
|
149
|
-
}}
|
|
150
|
-
>
|
|
151
|
-
<Trash2 size={14} /> Delete canvas
|
|
152
|
-
</button>
|
|
218
|
+
<div className="my-1.5 h-px bg-[rgb(var(--ec-page-border))]" />
|
|
219
|
+
{item(<Trash2 />, 'Delete canvas', onDelete, undefined, `${STATUS.danger.text} ${STATUS.danger.hover}`)}
|
|
153
220
|
</>
|
|
154
221
|
)}
|
|
155
|
-
<div className="my-1 h-px bg-[rgb(var(--ec-page-border))]" />
|
|
156
|
-
<p className="flex items-center gap-2 px-2
|
|
222
|
+
<div className="my-1.5 h-px bg-[rgb(var(--ec-page-border))]" />
|
|
223
|
+
<p className="flex items-center gap-2 px-2 py-1.5 text-[11px] text-[rgb(var(--ec-page-text-muted))]">
|
|
157
224
|
<span className={`h-1.5 w-1.5 rounded-full ${statusColor}`} />
|
|
158
225
|
{statusLabel}
|
|
159
226
|
</p>
|
|
@@ -163,48 +230,106 @@ function CanvasMenu({
|
|
|
163
230
|
);
|
|
164
231
|
}
|
|
165
232
|
|
|
166
|
-
|
|
167
|
-
|
|
233
|
+
const initialsOf = (name: string) => {
|
|
234
|
+
const words = name.trim().split(/\s+/).filter(Boolean);
|
|
235
|
+
return (words.length > 1 ? words.map((word) => word[0]).join('') : name.trim()).slice(0, 2).toUpperCase();
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Who you are here, top right (as Studio on the web's): your avatar, opening your name and colour to change. They're
|
|
240
|
+
* on your pointer and comments. Signed in, your name and picture are the ones you signed in with.
|
|
241
|
+
*/
|
|
242
|
+
function ProfileMenu({
|
|
243
|
+
me,
|
|
244
|
+
onRename,
|
|
245
|
+
onRecolor,
|
|
246
|
+
}: {
|
|
247
|
+
me: { name: string; color: string; picture?: string; agent?: boolean };
|
|
248
|
+
/** Not given when you're signed in */
|
|
249
|
+
onRename?: (name: string) => void;
|
|
250
|
+
onRecolor: (color: string) => void;
|
|
251
|
+
}) {
|
|
168
252
|
const popover = usePopover();
|
|
169
|
-
const [draft, setDraft] = useState(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
253
|
+
const [draft, setDraft] = useState(me.name);
|
|
254
|
+
useEffect(() => setDraft(me.name), [me.name]);
|
|
255
|
+
const save = () => {
|
|
256
|
+
const next = draft.trim();
|
|
257
|
+
if (!next) return setDraft(me.name);
|
|
258
|
+
if (next !== me.name) onRename?.(next);
|
|
174
259
|
};
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
260
|
+
const avatar = (size: string, text: string) => (
|
|
261
|
+
<span
|
|
262
|
+
className={`relative flex ${size} flex-none items-center justify-center rounded-full text-[10px] font-semibold text-white`}
|
|
263
|
+
style={{ background: me.color }}
|
|
264
|
+
>
|
|
265
|
+
{initialsOf(text)}
|
|
266
|
+
<Picture src={me.picture} />
|
|
267
|
+
</span>
|
|
268
|
+
);
|
|
269
|
+
// Here as an AI agent (e.g. a chat's view, as the chat's agent): who you are is its logo, not yours to change
|
|
270
|
+
if (me.agent)
|
|
271
|
+
return (
|
|
272
|
+
<span title={`You're here as ${me.name} (AI agent)`} className={`${barClass} pointer-events-auto w-[38px] justify-center`}>
|
|
273
|
+
<span className="flex h-6 w-6 items-center justify-center rounded-full text-white" style={{ background: me.color }}>
|
|
274
|
+
<AgentMark name={me.name} size={14} />
|
|
275
|
+
</span>
|
|
276
|
+
</span>
|
|
277
|
+
);
|
|
178
278
|
return (
|
|
179
|
-
<div ref={popover.ref} className="relative">
|
|
279
|
+
<div ref={popover.ref} className="pointer-events-auto relative">
|
|
180
280
|
<button
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}}
|
|
186
|
-
className="block rounded-full transition-transform hover:scale-110"
|
|
281
|
+
type="button"
|
|
282
|
+
title={onRename ? `${me.name}: change your name and colour` : `${me.name}: change your colour`}
|
|
283
|
+
onClick={() => popover.setOpen((open) => !open)}
|
|
284
|
+
className={`${barClass} w-[38px] justify-center hover:bg-[rgb(var(--ec-content-hover))]`}
|
|
187
285
|
>
|
|
188
|
-
|
|
286
|
+
{avatar('h-6 w-6', me.name)}
|
|
189
287
|
</button>
|
|
190
288
|
{popover.open && (
|
|
191
|
-
<div
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
289
|
+
<div
|
|
290
|
+
className={`${popoverClass} right-0 z-[70] w-72 rounded-xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] p-4 text-[rgb(var(--ec-page-text))] shadow-xl`}
|
|
291
|
+
>
|
|
292
|
+
<div className="flex items-center gap-3">
|
|
293
|
+
{avatar('h-9 w-9 text-xs', draft || me.name)}
|
|
294
|
+
<div className="min-w-0">
|
|
295
|
+
<p className="truncate text-sm font-semibold">{draft || me.name}</p>
|
|
296
|
+
<p className="text-xs text-[rgb(var(--ec-page-text-muted))]">On your pointer and comments</p>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
{onRename && (
|
|
300
|
+
<label className="mt-4 block text-xs font-medium text-[rgb(var(--ec-page-text-muted))]">
|
|
301
|
+
Your name
|
|
302
|
+
<input
|
|
303
|
+
value={draft}
|
|
304
|
+
maxLength={MAX_NAME_LENGTH}
|
|
305
|
+
onChange={(event) => setDraft(event.target.value)}
|
|
306
|
+
onBlur={save}
|
|
307
|
+
onKeyDown={(event) => event.key === 'Enter' && event.currentTarget.blur()}
|
|
308
|
+
className="mt-1.5 block h-9 w-full rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-page-bg))] px-3 text-sm text-[rgb(var(--ec-page-text))] outline-none focus:border-[rgb(var(--ec-accent))]"
|
|
309
|
+
/>
|
|
310
|
+
</label>
|
|
311
|
+
)}
|
|
312
|
+
<div className="mt-3 text-xs font-medium text-[rgb(var(--ec-page-text-muted))]">
|
|
313
|
+
Your colour
|
|
314
|
+
<div role="radiogroup" aria-label="Your colour" className="mt-2 flex gap-2">
|
|
315
|
+
{PERSON_COLORS.map((color) => (
|
|
316
|
+
<button
|
|
317
|
+
key={color}
|
|
318
|
+
type="button"
|
|
319
|
+
role="radio"
|
|
320
|
+
aria-checked={color === me.color}
|
|
321
|
+
aria-label={color}
|
|
322
|
+
onClick={() => onRecolor(color)}
|
|
323
|
+
className={`h-6 w-6 rounded-full transition-transform hover:scale-110 ${
|
|
324
|
+
color === me.color
|
|
325
|
+
? 'ring-2 ring-[rgb(var(--ec-page-text))] ring-offset-2 ring-offset-[rgb(var(--ec-card-bg))]'
|
|
326
|
+
: ''
|
|
327
|
+
}`}
|
|
328
|
+
style={{ background: color }}
|
|
329
|
+
/>
|
|
330
|
+
))}
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
208
333
|
</div>
|
|
209
334
|
)}
|
|
210
335
|
</div>
|
|
@@ -212,20 +337,34 @@ function YouAvatar({ peer, onRename }: { peer: Peer; onRename?: (name: string) =
|
|
|
212
337
|
}
|
|
213
338
|
|
|
214
339
|
/** Who's here: click someone to jump to them; everyone past the first few is in a list */
|
|
340
|
+
/**
|
|
341
|
+
* Who else is here. An agent is shown once, however many places it's in (e.g. its MCP tools and its chat's canvas
|
|
342
|
+
* view), and not at all when it's you.
|
|
343
|
+
*/
|
|
344
|
+
const othersHere = (peers: Peer[], clientId: number | null, me: { name: string; agent?: boolean }) => {
|
|
345
|
+
const agentsSeen = new Set(me.agent ? [me.name] : []);
|
|
346
|
+
return peers.filter((peer) => {
|
|
347
|
+
if (peer.clientId === clientId) return false;
|
|
348
|
+
if (!peer.agent) return true;
|
|
349
|
+
if (agentsSeen.has(peer.name)) return false;
|
|
350
|
+
agentsSeen.add(peer.name);
|
|
351
|
+
return true;
|
|
352
|
+
});
|
|
353
|
+
};
|
|
354
|
+
|
|
215
355
|
function People({
|
|
216
356
|
peers,
|
|
217
357
|
clientId,
|
|
358
|
+
me,
|
|
218
359
|
onJumpTo,
|
|
219
|
-
onRename,
|
|
220
360
|
}: {
|
|
221
361
|
peers: Peer[];
|
|
222
362
|
clientId: number | null;
|
|
363
|
+
me: { name: string; agent?: boolean };
|
|
223
364
|
onJumpTo: (peer: Peer) => void;
|
|
224
|
-
onRename?: (name: string) => void;
|
|
225
365
|
}) {
|
|
226
366
|
const overflow = usePopover();
|
|
227
|
-
const
|
|
228
|
-
const others = peers.filter((peer) => peer.clientId !== clientId);
|
|
367
|
+
const others = othersHere(peers, clientId, me);
|
|
229
368
|
const shown = others.slice(0, MAX_AVATARS);
|
|
230
369
|
const hidden = others.slice(MAX_AVATARS);
|
|
231
370
|
const describe = (peer: Peer) => `${peer.name}${peer.agent ? ` (AI agent)${peer.activity ? `: ${peer.activity}` : ''}` : ''}`;
|
|
@@ -272,7 +411,6 @@ function People({
|
|
|
272
411
|
</div>
|
|
273
412
|
)}
|
|
274
413
|
</div>
|
|
275
|
-
{you && <YouAvatar peer={you} onRename={onRename} />}
|
|
276
414
|
</div>
|
|
277
415
|
);
|
|
278
416
|
}
|
|
@@ -285,12 +423,19 @@ export default memo(function CanvasHeader({
|
|
|
285
423
|
presence,
|
|
286
424
|
clientId,
|
|
287
425
|
onJumpTo,
|
|
426
|
+
me,
|
|
288
427
|
onRename,
|
|
428
|
+
onRecolor,
|
|
289
429
|
onConnectAgent,
|
|
290
430
|
webMcp,
|
|
291
431
|
shareUrl,
|
|
292
432
|
newCanvasUrl,
|
|
293
433
|
onShare,
|
|
434
|
+
onOpenFile,
|
|
435
|
+
onSaveToFile,
|
|
436
|
+
onExportImage,
|
|
437
|
+
onLiveCollaboration,
|
|
438
|
+
onHelp,
|
|
294
439
|
canvasStatus,
|
|
295
440
|
statusHistory,
|
|
296
441
|
onStatusChange,
|
|
@@ -306,19 +451,30 @@ export default memo(function CanvasHeader({
|
|
|
306
451
|
presence: PresenceStore | null;
|
|
307
452
|
clientId: number | null;
|
|
308
453
|
onJumpTo: (peer: Peer) => void;
|
|
454
|
+
/** You: your name, colour and (signed in) picture */
|
|
455
|
+
me: { name: string; color: string; picture?: string; agent?: boolean };
|
|
309
456
|
/** Not given when you're signed in */
|
|
310
457
|
onRename?: (name: string) => void;
|
|
458
|
+
onRecolor: (color: string) => void;
|
|
311
459
|
onConnectAgent: () => void;
|
|
312
460
|
webMcp: WebMcpStatus;
|
|
313
461
|
shareUrl?: string;
|
|
314
462
|
newCanvasUrl?: string;
|
|
315
463
|
/** Open the share dialog */
|
|
316
464
|
onShare: () => void;
|
|
465
|
+
/** Opens a .eventcatalog file as a new canvas (not given where it can't, e.g. inside a chat) */
|
|
466
|
+
onOpenFile?: () => void;
|
|
467
|
+
onSaveToFile: () => void;
|
|
468
|
+
onExportImage: () => void;
|
|
469
|
+
onLiveCollaboration: () => void;
|
|
470
|
+
onHelp: () => void;
|
|
317
471
|
/** Ask to delete the canvas (not given where it can't be, e.g. inside a chat) */
|
|
318
472
|
onDelete?: () => void;
|
|
319
473
|
}) {
|
|
320
474
|
const peers = usePeople(presence);
|
|
321
475
|
const agentsHere = peers.some((peer) => peer.agent);
|
|
476
|
+
// Who else is here (you're your own avatar, next to it)
|
|
477
|
+
const showPeople = othersHere(peers, clientId, me).length > 0;
|
|
322
478
|
|
|
323
479
|
return (
|
|
324
480
|
<div className="pointer-events-none absolute left-3 right-3 top-3 z-10 flex items-start justify-between gap-3">
|
|
@@ -329,39 +485,31 @@ export default memo(function CanvasHeader({
|
|
|
329
485
|
onRetitle={onRetitle}
|
|
330
486
|
shareUrl={shareUrl}
|
|
331
487
|
newCanvasUrl={newCanvasUrl}
|
|
488
|
+
onConnectAgent={onConnectAgent}
|
|
489
|
+
webMcp={webMcp}
|
|
490
|
+
agentsHere={agentsHere}
|
|
332
491
|
onDelete={onDelete}
|
|
492
|
+
onOpenFile={onOpenFile}
|
|
493
|
+
onSaveToFile={onSaveToFile}
|
|
494
|
+
onExportImage={onExportImage}
|
|
495
|
+
onLiveCollaboration={onLiveCollaboration}
|
|
496
|
+
onHelp={onHelp}
|
|
333
497
|
>
|
|
334
498
|
<CanvasStatusMenu status={canvasStatus} history={statusHistory} onChange={onStatusChange} />
|
|
335
499
|
</CanvasMenu>
|
|
336
500
|
|
|
337
|
-
<div className=
|
|
338
|
-
|
|
339
|
-
<
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
title={
|
|
345
|
-
webMcp.state === 'on'
|
|
346
|
-
? `Connect an AI agent to this canvas. WebMCP is on in this tab${webMcp.lastCall ? ` (last: ${webMcp.lastCall})` : ''}`
|
|
347
|
-
: 'Connect an AI agent to this canvas'
|
|
348
|
-
}
|
|
349
|
-
// A button of its own next to Share (the two ways to bring someone in): people, or an AI agent
|
|
350
|
-
className="relative flex h-7 items-center gap-1.5 rounded-lg border px-2.5 text-xs font-semibold transition-colors border-[rgb(var(--ec-accent)/0.35)] bg-[rgb(var(--ec-accent-subtle))] text-[rgb(var(--ec-accent))] hover:border-[rgb(var(--ec-accent)/0.7)] hover:bg-[rgb(var(--ec-accent)/0.15)]"
|
|
351
|
-
>
|
|
352
|
-
<Bot size={15} />
|
|
353
|
-
{agentsHere ? 'Agent connected' : 'Connect agent'}
|
|
354
|
-
{(webMcp.state === 'on' || agentsHere) && (
|
|
355
|
-
<span
|
|
356
|
-
className={`absolute -right-1 -top-1 h-2.5 w-2.5 rounded-full ring-2 ring-[rgb(var(--ec-card-bg))] ${STATUS.success.dot}`}
|
|
357
|
-
/>
|
|
358
|
-
)}
|
|
359
|
-
</button>
|
|
501
|
+
<div className="flex items-center gap-2">
|
|
502
|
+
{showPeople && (
|
|
503
|
+
<div className={`${barClass} px-1.5`}>
|
|
504
|
+
<People peers={peers} clientId={clientId} me={me} onJumpTo={onJumpTo} />
|
|
505
|
+
</div>
|
|
506
|
+
)}
|
|
507
|
+
<ProfileMenu me={me} onRename={onRename} onRecolor={onRecolor} />
|
|
360
508
|
<button
|
|
361
509
|
onClick={onShare}
|
|
362
|
-
className="flex h-
|
|
510
|
+
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)]"
|
|
363
511
|
>
|
|
364
|
-
<
|
|
512
|
+
<Share2 size={16} aria-hidden />
|
|
365
513
|
Share
|
|
366
514
|
</button>
|
|
367
515
|
</div>
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { memo, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { Check, ChevronDown } from 'lucide-react';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
3
|
+
import { DIAGRAM_MENU, DIAGRAM_MENU_ITEM, DIAGRAM_MENU_SEPARATOR } from '@eventcatalog/visualiser';
|
|
4
|
+
import {
|
|
5
|
+
CANVAS_STATUSES,
|
|
6
|
+
type CanvasStatus,
|
|
7
|
+
type StatusChange,
|
|
8
|
+
timeAgo,
|
|
9
|
+
CANVAS_STATUS_LOOK,
|
|
10
|
+
} from '@eventcatalog/visualiser/studio';
|
|
6
11
|
|
|
7
12
|
/** Status changes shown in the menu, newest first */
|
|
8
13
|
const HISTORY_SHOWN = 8;
|
|
@@ -58,18 +63,14 @@ export default memo(function CanvasStatusMenu({
|
|
|
58
63
|
<ChevronDown size={12} />
|
|
59
64
|
</button>
|
|
60
65
|
{open && (
|
|
61
|
-
<div className=
|
|
62
|
-
<p className="px-
|
|
66
|
+
<div className={`absolute left-0 top-full mt-2 w-72 ${DIAGRAM_MENU}`}>
|
|
67
|
+
<p className="px-3 pb-1 pt-1.5 text-[11px] font-semibold uppercase tracking-wide text-[rgb(var(--ec-page-text-muted))]">
|
|
63
68
|
Status
|
|
64
69
|
</p>
|
|
65
70
|
{CANVAS_STATUSES.map((value) => {
|
|
66
71
|
const option = CANVAS_STATUS_LOOK[value];
|
|
67
72
|
return (
|
|
68
|
-
<button
|
|
69
|
-
key={value}
|
|
70
|
-
onClick={() => choose(value)}
|
|
71
|
-
className="flex w-full items-center gap-2.5 rounded-lg px-2.5 py-1.5 text-left hover:bg-[rgb(var(--ec-page-border)/0.5)]"
|
|
72
|
-
>
|
|
73
|
+
<button key={value} onClick={() => choose(value)} className={`${DIAGRAM_MENU_ITEM} w-full text-left`}>
|
|
73
74
|
<span className={`h-2 w-2 shrink-0 rounded-full ${option.dot}`} />
|
|
74
75
|
<span className="min-w-0 flex-1">
|
|
75
76
|
<span className="block text-xs font-medium">{option.label}</span>
|
|
@@ -79,7 +80,7 @@ export default memo(function CanvasStatusMenu({
|
|
|
79
80
|
</button>
|
|
80
81
|
);
|
|
81
82
|
})}
|
|
82
|
-
<div className="px-
|
|
83
|
+
<div className="px-3 py-2">
|
|
83
84
|
<input
|
|
84
85
|
value={note}
|
|
85
86
|
onChange={(event) => setNote(event.target.value)}
|
|
@@ -90,11 +91,11 @@ export default memo(function CanvasStatusMenu({
|
|
|
90
91
|
</div>
|
|
91
92
|
{recent.length > 0 && (
|
|
92
93
|
<>
|
|
93
|
-
<div className=
|
|
94
|
-
<p className="px-
|
|
94
|
+
<div className={DIAGRAM_MENU_SEPARATOR} />
|
|
95
|
+
<p className="px-3 pb-1 pt-1.5 text-[11px] font-semibold uppercase tracking-wide text-[rgb(var(--ec-page-text-muted))]">
|
|
95
96
|
History
|
|
96
97
|
</p>
|
|
97
|
-
<ul className="max-h-48 space-y-1.5 overflow-y-auto px-
|
|
98
|
+
<ul className="max-h-48 space-y-1.5 overflow-y-auto px-3 pb-2">
|
|
98
99
|
{recent.map((change) => (
|
|
99
100
|
<li key={`${change.at}-${change.status}`} className="text-[11px]">
|
|
100
101
|
<span className="flex items-center gap-1.5">
|