@eventcatalog/core 5.0.0-beta.12 → 5.0.0-beta.14
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/LICENSE +13 -12
- 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/catalog-runtime.cjs +1 -1
- package/dist/{chunk-TSY5CC27.js → chunk-4KE3EUDJ.js} +1 -1
- package/dist/{chunk-FY6MSP6J.js → chunk-I3XDOICA.js} +1 -1
- package/dist/{chunk-QB7X34OD.js → chunk-OFK7U7BX.js} +1 -1
- package/dist/{chunk-CX2VB7AI.js → chunk-PI5YNZZZ.js} +1 -1
- package/dist/{chunk-BWC4YKW3.js → chunk-WY6M3RPT.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +11 -6
- package/dist/eventcatalog.js +14 -9
- 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/astro.config.mjs +13 -1
- package/eventcatalog/integrations/skip-startup-reload.mjs +31 -0
- package/eventcatalog/integrations/stable-dependency-order.mjs +29 -0
- package/eventcatalog/src/components/CatalogGraph/CatalogForceGraph.tsx +39 -0
- package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +11 -3
- package/eventcatalog/src/components/Search/SearchDataLoader.astro +16 -28
- package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +37 -76
- package/eventcatalog/src/components/SideNav/NestedSideBar/storage.ts +2 -0
- package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +122 -1
- package/eventcatalog/src/components/SideNav/SideNav.astro +9 -1
- 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 +133 -71
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +66 -66
- package/eventcatalog/src/features/studio/agent-choreography.ts +5 -4
- package/eventcatalog/src/features/studio/agents.ts +28 -0
- package/eventcatalog/src/features/studio/api/canvases-api.ts +1 -1
- package/eventcatalog/src/features/studio/canvas-actions.ts +22 -14
- package/eventcatalog/src/features/studio/canvas-preview.ts +1 -2
- package/eventcatalog/src/features/studio/catalog.ts +1 -4
- package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +246 -115
- package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +7 -3
- 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 +1 -1
- package/eventcatalog/src/features/studio/components/JoinForm.tsx +101 -26
- package/eventcatalog/src/features/studio/components/LeftPanel.tsx +102 -241
- package/eventcatalog/src/features/studio/components/NodeDetails.tsx +183 -0
- package/eventcatalog/src/features/studio/components/OpenInStudioDialog.tsx +1 -1
- package/eventcatalog/src/features/studio/components/Presence.tsx +6 -8
- 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 +1005 -354
- package/eventcatalog/src/features/studio/document-name.ts +7 -0
- package/eventcatalog/src/features/studio/from-visualiser.ts +1 -1
- 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-studio-flow.ts +162 -23
- package/eventcatalog/src/features/studio/identity.ts +23 -1
- package/eventcatalog/src/features/studio/pages/[id].astro +2 -1
- package/eventcatalog/src/features/studio/pages/index.astro +1 -2
- package/eventcatalog/src/features/studio/server/canvas-mcp.ts +7 -22
- package/eventcatalog/src/features/studio/server/runtime.ts +4 -5
- package/package.json +3 -3
- package/eventcatalog/src/features/studio/canvas-doc.ts +0 -569
- package/eventcatalog/src/features/studio/clipboard.ts +0 -97
- package/eventcatalog/src/features/studio/components/CanvasControls.tsx +0 -119
- package/eventcatalog/src/features/studio/components/Comments.tsx +0 -777
- 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 -271
- 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 -121
- package/eventcatalog/src/features/studio/grouping.ts +0 -230
- package/eventcatalog/src/features/studio/hooks/use-counter-scale.ts +0 -20
- package/eventcatalog/src/features/studio/layout.ts +0 -56
- package/eventcatalog/src/features/studio/level-layout.ts +0 -145
- package/eventcatalog/src/features/studio/levels.ts +0 -337
- package/eventcatalog/src/features/studio/node-types.ts +0 -245
- package/eventcatalog/src/features/studio/placement.ts +0 -197
- package/eventcatalog/src/features/studio/studio.css +0 -273
|
@@ -14,7 +14,11 @@ import {
|
|
|
14
14
|
setMeta,
|
|
15
15
|
type Author,
|
|
16
16
|
type CommentAnchor,
|
|
17
|
-
|
|
17
|
+
getAbsolutePosition,
|
|
18
|
+
sizeOf,
|
|
19
|
+
type LayoutResult,
|
|
20
|
+
getNodeName,
|
|
21
|
+
} from '@eventcatalog/visualiser/studio';
|
|
18
22
|
import {
|
|
19
23
|
describeNode,
|
|
20
24
|
nodeCenter,
|
|
@@ -28,9 +32,6 @@ import {
|
|
|
28
32
|
type NodeUpdate,
|
|
29
33
|
} from './canvas-actions';
|
|
30
34
|
import { getRelatedEdges } from './catalog';
|
|
31
|
-
import { getAbsolutePosition, sizeOf } from './grouping';
|
|
32
|
-
import type { LayoutResult } from './layout';
|
|
33
|
-
import { getNodeName } from './node-types';
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* Agents work on a canvas like a person would: their cursor travels to where they're going, they drop one node,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who an agent is, named after its client when it doesn't say, in its brand's colour: on the server (MCP tools) and
|
|
3
|
+
* in a chat's canvas view (which joins as the chat's agent). Their logos are in the visualiser's ClientLogos.
|
|
4
|
+
*/
|
|
5
|
+
export type AgentIdentity = { name: string; color: string };
|
|
6
|
+
|
|
7
|
+
const AGENT_COLORS: Record<string, string> = {
|
|
8
|
+
ChatGPT: '#10a37f',
|
|
9
|
+
Codex: '#10a37f',
|
|
10
|
+
Claude: '#d97757',
|
|
11
|
+
'Claude Code': '#d97757',
|
|
12
|
+
Cursor: '#111827',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Agents' clients, by what's in their user agent or host name (most specific first) */
|
|
16
|
+
const CLIENTS: [RegExp, string][] = [
|
|
17
|
+
[/claude[-_ ]?code/i, 'Claude Code'],
|
|
18
|
+
[/claude|anthropic/i, 'Claude'],
|
|
19
|
+
[/codex/i, 'Codex'],
|
|
20
|
+
[/openai|chatgpt/i, 'ChatGPT'],
|
|
21
|
+
[/cursor/i, 'Cursor'],
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/** Who the agent is, from the name it gives, else its client's user agent (or a chat's host name) */
|
|
25
|
+
export const getAgent = (agentName: string | undefined, client = ''): AgentIdentity => {
|
|
26
|
+
const name = agentName?.trim() || CLIENTS.find(([pattern]) => pattern.test(client))?.[1] || 'AI agent';
|
|
27
|
+
return { name, color: AGENT_COLORS[name] ?? '#8b5cf6' };
|
|
28
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Hono, type Context } from 'hono';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { paginate } from '@features/tools/catalog-tools';
|
|
4
|
-
import type
|
|
4
|
+
import { type CanvasContent, type CanvasStatus } from '@eventcatalog/visualiser/studio';
|
|
5
5
|
import { MAX_TITLE_LENGTH } from '../limits';
|
|
6
6
|
import { diagramLevelsSchema, indexCatalog, newCanvasEdgesSchema, nodeSpecsSchema, planCanvas } from '../canvas-actions';
|
|
7
7
|
import { getCatalogResources } from '../catalog-resources';
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
import type { Edge, Node, XYPosition } from '@xyflow/react';
|
|
2
2
|
import type * as Y from 'yjs';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { buildNode, getCanvasMaps, getCanvasStatus, readCanvas, updateNodeData, type CanvasContent } from './canvas-doc';
|
|
5
|
-
import type { CatalogRelation, CatalogResource } from './catalog-resources';
|
|
6
|
-
import {
|
|
7
|
-
buildContainer,
|
|
8
|
-
canBeContainer,
|
|
9
|
-
catalogNodeData,
|
|
10
|
-
containerTypeFor,
|
|
11
|
-
getCatalogLink,
|
|
12
|
-
getRelatedEdges,
|
|
13
|
-
indexRelations,
|
|
14
|
-
} from './catalog';
|
|
15
|
-
import { containerFor, getAbsolutePosition, sizeOf } from './grouping';
|
|
16
|
-
import { placeNodes, type Link } from './placement';
|
|
17
4
|
import {
|
|
5
|
+
buildNode,
|
|
6
|
+
getCanvasMaps,
|
|
7
|
+
getCanvasStatus,
|
|
8
|
+
readCanvas,
|
|
9
|
+
updateNodeData,
|
|
10
|
+
type CanvasContent,
|
|
11
|
+
containerFor,
|
|
12
|
+
getAbsolutePosition,
|
|
13
|
+
sizeOf,
|
|
14
|
+
placeNodes,
|
|
15
|
+
type Link,
|
|
18
16
|
canGoInContainer,
|
|
19
17
|
CATALOG_COLLECTIONS,
|
|
20
18
|
COMPONENT_TYPES,
|
|
@@ -28,7 +26,17 @@ import {
|
|
|
28
26
|
isVersioned,
|
|
29
27
|
isWrittenOnCanvas,
|
|
30
28
|
updateResource,
|
|
31
|
-
} from '
|
|
29
|
+
} from '@eventcatalog/visualiser/studio';
|
|
30
|
+
import type { CatalogRelation, CatalogResource } from './catalog-resources';
|
|
31
|
+
import {
|
|
32
|
+
buildContainer,
|
|
33
|
+
canBeContainer,
|
|
34
|
+
catalogNodeData,
|
|
35
|
+
containerTypeFor,
|
|
36
|
+
getCatalogLink,
|
|
37
|
+
getRelatedEdges,
|
|
38
|
+
indexRelations,
|
|
39
|
+
} from './catalog';
|
|
32
40
|
|
|
33
41
|
/**
|
|
34
42
|
* What agents can do on a canvas, shared by the MCP tools (on the server) and the WebMCP tools (in the
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Edge, Node } from '@xyflow/react';
|
|
2
|
-
import { getAbsolutePosition, sizeOf } from '
|
|
3
|
-
import { isGroupType } from './node-types';
|
|
2
|
+
import { getAbsolutePosition, sizeOf, isGroupType } from '@eventcatalog/visualiser/studio';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* A canvas drawn small, for lists of canvases: each node as a box where it is on the canvas (containers as outlines
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import type { Edge, Node } from '@xyflow/react';
|
|
2
2
|
import type { CatalogRelation, CatalogResource } from './catalog-resources';
|
|
3
|
-
import { buildNode } from '
|
|
4
|
-
import { createEdge } from './edges';
|
|
5
|
-
import { layoutGraph, withGroupSizes } from './layout';
|
|
6
|
-
import { GROUP_TYPES, isGroupType } from './node-types';
|
|
3
|
+
import { buildNode, createEdge, layoutGraph, withGroupSizes, GROUP_TYPES, isGroupType } from '@eventcatalog/visualiser/studio';
|
|
7
4
|
|
|
8
5
|
/**
|
|
9
6
|
* Catalog resources on a canvas: the node for a resource, its connections to related resources already there,
|
|
@@ -1,5 +1,18 @@
|
|
|
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';
|
|
3
16
|
import {
|
|
4
17
|
DIAGRAM_MENU,
|
|
5
18
|
DIAGRAM_MENU_BUTTON_ICON,
|
|
@@ -9,26 +22,37 @@ import {
|
|
|
9
22
|
DIAGRAM_MENU_TITLE,
|
|
10
23
|
} from '@eventcatalog/visualiser';
|
|
11
24
|
import { usePeople, type Peer, type PresenceStore } from '../hooks/presence-store';
|
|
25
|
+
import { MAX_NAME_LENGTH, PERSON_COLORS } from '../identity';
|
|
12
26
|
import type { WebMcpStatus } from '../hooks/use-canvas-webmcp';
|
|
13
27
|
import type { Status, Transport } from '../hooks/use-studio-flow';
|
|
14
|
-
import type
|
|
28
|
+
import { type CanvasStatus, type StatusChange, STATUS, Picture, AgentMark } from '@eventcatalog/visualiser/studio';
|
|
15
29
|
import CanvasStatusMenu from './CanvasStatusMenu';
|
|
16
|
-
|
|
17
|
-
|
|
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
|
+
};
|
|
18
37
|
|
|
19
38
|
/**
|
|
20
|
-
* The canvas's header
|
|
21
|
-
* who's here, connecting an agent and sharing
|
|
22
|
-
* (
|
|
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_*`).
|
|
23
42
|
*/
|
|
24
43
|
|
|
25
44
|
const barClass =
|
|
26
|
-
'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';
|
|
27
46
|
// The header lets the pointer through to the canvas around its bars, so its menus take it back
|
|
28
47
|
const popoverClass = 'pointer-events-auto absolute top-full mt-2';
|
|
29
48
|
const menuClass = `${popoverClass} ${DIAGRAM_MENU}`;
|
|
30
49
|
const menuItemClass = `${DIAGRAM_MENU_ITEM} w-full text-left`;
|
|
31
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]';
|
|
32
56
|
|
|
33
57
|
/** Closes a popover on outside clicks and Escape */
|
|
34
58
|
function usePopover() {
|
|
@@ -56,7 +80,7 @@ function Avatar({ peer, size = 28, children }: { peer: Peer; size?: number; chil
|
|
|
56
80
|
className="relative flex items-center justify-center rounded-full text-[11px] font-semibold text-white ring-2 ring-[rgb(var(--ec-card-bg))]"
|
|
57
81
|
style={{ backgroundColor: peer.color, width: size, height: size }}
|
|
58
82
|
>
|
|
59
|
-
{children ?? (peer.agent ? <
|
|
83
|
+
{children ?? (peer.agent ? <AgentMark name={peer.name} size={14} /> : peer.name.slice(0, 2).toUpperCase())}
|
|
60
84
|
{!children && <Picture src={peer.picture} />}
|
|
61
85
|
</span>
|
|
62
86
|
);
|
|
@@ -69,15 +93,33 @@ function CanvasMenu({
|
|
|
69
93
|
onRetitle,
|
|
70
94
|
shareUrl,
|
|
71
95
|
newCanvasUrl,
|
|
96
|
+
onConnectAgent,
|
|
97
|
+
webMcp,
|
|
98
|
+
agentsHere,
|
|
72
99
|
onDelete,
|
|
100
|
+
onOpenFile,
|
|
101
|
+
onSaveToFile,
|
|
102
|
+
onExportImage,
|
|
103
|
+
onLiveCollaboration,
|
|
104
|
+
onHelp,
|
|
73
105
|
children,
|
|
74
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;
|
|
75
113
|
title?: string;
|
|
76
114
|
status: Status;
|
|
77
115
|
transport: Transport;
|
|
78
116
|
onRetitle: (title: string) => void;
|
|
79
117
|
shareUrl?: string;
|
|
80
118
|
newCanvasUrl?: string;
|
|
119
|
+
onConnectAgent: () => void;
|
|
120
|
+
webMcp: WebMcpStatus;
|
|
121
|
+
/** An AI agent is on the canvas */
|
|
122
|
+
agentsHere: boolean;
|
|
81
123
|
/** Asks to delete the canvas (not offered when not given, e.g. inside a chat) */
|
|
82
124
|
onDelete?: () => void;
|
|
83
125
|
/** Shown after the title (the canvas's status) */
|
|
@@ -92,10 +134,29 @@ function CanvasMenu({
|
|
|
92
134
|
disconnected: 'Offline, reconnecting…',
|
|
93
135
|
}[status];
|
|
94
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
|
+
);
|
|
95
156
|
|
|
96
157
|
return (
|
|
97
158
|
<div ref={menu.ref} className="relative">
|
|
98
|
-
<div className={`${barClass} gap-1.5 pl-
|
|
159
|
+
<div className={`${barClass} gap-1.5 pl-3 pr-1`}>
|
|
99
160
|
<span title={statusLabel} className={`h-2 w-2 shrink-0 rounded-full ${statusColor}`} />
|
|
100
161
|
<input
|
|
101
162
|
ref={titleInput}
|
|
@@ -121,48 +182,45 @@ function CanvasMenu({
|
|
|
121
182
|
</button>
|
|
122
183
|
</div>
|
|
123
184
|
{menu.open && (
|
|
124
|
-
<div
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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))}
|
|
144
208
|
{newCanvasUrl && (
|
|
145
|
-
<a className={
|
|
146
|
-
<
|
|
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>
|
|
147
214
|
</a>
|
|
148
215
|
)}
|
|
149
216
|
{onDelete && (
|
|
150
217
|
<>
|
|
151
|
-
<div className=
|
|
152
|
-
<
|
|
153
|
-
// Laid out like the other items, in the theme's danger colour
|
|
154
|
-
className={`flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-left text-xs transition-colors ${STATUS.danger.text} ${STATUS.danger.hover}`}
|
|
155
|
-
onClick={() => {
|
|
156
|
-
menu.setOpen(false);
|
|
157
|
-
onDelete();
|
|
158
|
-
}}
|
|
159
|
-
>
|
|
160
|
-
<Trash2 className="h-3.5 w-3.5 shrink-0" /> Delete canvas
|
|
161
|
-
</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}`)}
|
|
162
220
|
</>
|
|
163
221
|
)}
|
|
164
|
-
<div className=
|
|
165
|
-
<p className="flex items-center gap-2 px-
|
|
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))]">
|
|
166
224
|
<span className={`h-1.5 w-1.5 rounded-full ${statusColor}`} />
|
|
167
225
|
{statusLabel}
|
|
168
226
|
</p>
|
|
@@ -172,50 +230,106 @@ function CanvasMenu({
|
|
|
172
230
|
);
|
|
173
231
|
}
|
|
174
232
|
|
|
175
|
-
|
|
176
|
-
|
|
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
|
+
}) {
|
|
177
252
|
const popover = usePopover();
|
|
178
|
-
const [draft, setDraft] = useState(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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);
|
|
183
259
|
};
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
+
);
|
|
187
278
|
return (
|
|
188
|
-
<div ref={popover.ref} className="relative">
|
|
279
|
+
<div ref={popover.ref} className="pointer-events-auto relative">
|
|
189
280
|
<button
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}}
|
|
195
|
-
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))]`}
|
|
196
285
|
>
|
|
197
|
-
|
|
286
|
+
{avatar('h-6 w-6', me.name)}
|
|
198
287
|
</button>
|
|
199
288
|
{popover.open && (
|
|
200
289
|
<div
|
|
201
|
-
className={`${popoverClass} right-0 z-
|
|
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`}
|
|
202
291
|
>
|
|
203
|
-
<
|
|
204
|
-
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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>
|
|
219
333
|
</div>
|
|
220
334
|
)}
|
|
221
335
|
</div>
|
|
@@ -223,20 +337,34 @@ function YouAvatar({ peer, onRename }: { peer: Peer; onRename?: (name: string) =
|
|
|
223
337
|
}
|
|
224
338
|
|
|
225
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
|
+
|
|
226
355
|
function People({
|
|
227
356
|
peers,
|
|
228
357
|
clientId,
|
|
358
|
+
me,
|
|
229
359
|
onJumpTo,
|
|
230
|
-
onRename,
|
|
231
360
|
}: {
|
|
232
361
|
peers: Peer[];
|
|
233
362
|
clientId: number | null;
|
|
363
|
+
me: { name: string; agent?: boolean };
|
|
234
364
|
onJumpTo: (peer: Peer) => void;
|
|
235
|
-
onRename?: (name: string) => void;
|
|
236
365
|
}) {
|
|
237
366
|
const overflow = usePopover();
|
|
238
|
-
const
|
|
239
|
-
const others = peers.filter((peer) => peer.clientId !== clientId);
|
|
367
|
+
const others = othersHere(peers, clientId, me);
|
|
240
368
|
const shown = others.slice(0, MAX_AVATARS);
|
|
241
369
|
const hidden = others.slice(MAX_AVATARS);
|
|
242
370
|
const describe = (peer: Peer) => `${peer.name}${peer.agent ? ` (AI agent)${peer.activity ? `: ${peer.activity}` : ''}` : ''}`;
|
|
@@ -283,7 +411,6 @@ function People({
|
|
|
283
411
|
</div>
|
|
284
412
|
)}
|
|
285
413
|
</div>
|
|
286
|
-
{you && <YouAvatar peer={you} onRename={onRename} />}
|
|
287
414
|
</div>
|
|
288
415
|
);
|
|
289
416
|
}
|
|
@@ -296,12 +423,19 @@ export default memo(function CanvasHeader({
|
|
|
296
423
|
presence,
|
|
297
424
|
clientId,
|
|
298
425
|
onJumpTo,
|
|
426
|
+
me,
|
|
299
427
|
onRename,
|
|
428
|
+
onRecolor,
|
|
300
429
|
onConnectAgent,
|
|
301
430
|
webMcp,
|
|
302
431
|
shareUrl,
|
|
303
432
|
newCanvasUrl,
|
|
304
433
|
onShare,
|
|
434
|
+
onOpenFile,
|
|
435
|
+
onSaveToFile,
|
|
436
|
+
onExportImage,
|
|
437
|
+
onLiveCollaboration,
|
|
438
|
+
onHelp,
|
|
305
439
|
canvasStatus,
|
|
306
440
|
statusHistory,
|
|
307
441
|
onStatusChange,
|
|
@@ -317,24 +451,33 @@ export default memo(function CanvasHeader({
|
|
|
317
451
|
presence: PresenceStore | null;
|
|
318
452
|
clientId: number | null;
|
|
319
453
|
onJumpTo: (peer: Peer) => void;
|
|
454
|
+
/** You: your name, colour and (signed in) picture */
|
|
455
|
+
me: { name: string; color: string; picture?: string; agent?: boolean };
|
|
320
456
|
/** Not given when you're signed in */
|
|
321
457
|
onRename?: (name: string) => void;
|
|
458
|
+
onRecolor: (color: string) => void;
|
|
322
459
|
onConnectAgent: () => void;
|
|
323
460
|
webMcp: WebMcpStatus;
|
|
324
461
|
shareUrl?: string;
|
|
325
462
|
newCanvasUrl?: string;
|
|
326
463
|
/** Open the share dialog */
|
|
327
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;
|
|
328
471
|
/** Ask to delete the canvas (not given where it can't be, e.g. inside a chat) */
|
|
329
472
|
onDelete?: () => void;
|
|
330
473
|
}) {
|
|
331
474
|
const peers = usePeople(presence);
|
|
332
475
|
const agentsHere = peers.some((peer) => peer.agent);
|
|
333
|
-
//
|
|
334
|
-
const showPeople =
|
|
476
|
+
// Who else is here (you're your own avatar, next to it)
|
|
477
|
+
const showPeople = othersHere(peers, clientId, me).length > 0;
|
|
335
478
|
|
|
336
479
|
return (
|
|
337
|
-
<div className="pointer-events-none absolute left-
|
|
480
|
+
<div className="pointer-events-none absolute left-3 right-3 top-3 z-10 flex items-start justify-between gap-3">
|
|
338
481
|
<CanvasMenu
|
|
339
482
|
title={title}
|
|
340
483
|
status={status}
|
|
@@ -342,43 +485,31 @@ export default memo(function CanvasHeader({
|
|
|
342
485
|
onRetitle={onRetitle}
|
|
343
486
|
shareUrl={shareUrl}
|
|
344
487
|
newCanvasUrl={newCanvasUrl}
|
|
488
|
+
onConnectAgent={onConnectAgent}
|
|
489
|
+
webMcp={webMcp}
|
|
490
|
+
agentsHere={agentsHere}
|
|
345
491
|
onDelete={onDelete}
|
|
492
|
+
onOpenFile={onOpenFile}
|
|
493
|
+
onSaveToFile={onSaveToFile}
|
|
494
|
+
onExportImage={onExportImage}
|
|
495
|
+
onLiveCollaboration={onLiveCollaboration}
|
|
496
|
+
onHelp={onHelp}
|
|
346
497
|
>
|
|
347
498
|
<CanvasStatusMenu status={canvasStatus} history={statusHistory} onChange={onStatusChange} />
|
|
348
499
|
</CanvasMenu>
|
|
349
500
|
|
|
350
|
-
<div className=
|
|
501
|
+
<div className="flex items-center gap-2">
|
|
351
502
|
{showPeople && (
|
|
352
|
-
|
|
353
|
-
<
|
|
354
|
-
|
|
355
|
-
</div>
|
|
356
|
-
<div className="h-5 w-px bg-[rgb(var(--ec-page-border))]" />
|
|
357
|
-
</>
|
|
503
|
+
<div className={`${barClass} px-1.5`}>
|
|
504
|
+
<People peers={peers} clientId={clientId} me={me} onJumpTo={onJumpTo} />
|
|
505
|
+
</div>
|
|
358
506
|
)}
|
|
359
|
-
<
|
|
360
|
-
onClick={onConnectAgent}
|
|
361
|
-
title={
|
|
362
|
-
webMcp.state === 'on'
|
|
363
|
-
? `Connect an AI agent to this canvas. WebMCP is on in this tab${webMcp.lastCall ? ` (last: ${webMcp.lastCall})` : ''}`
|
|
364
|
-
: 'Connect an AI agent to this canvas'
|
|
365
|
-
}
|
|
366
|
-
// A button of its own next to Share (the two ways to bring someone in): people, or an AI agent
|
|
367
|
-
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)]"
|
|
368
|
-
>
|
|
369
|
-
<Bot size={15} />
|
|
370
|
-
{agentsHere ? 'Agent connected' : 'Connect agent'}
|
|
371
|
-
{(webMcp.state === 'on' || agentsHere) && (
|
|
372
|
-
<span
|
|
373
|
-
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}`}
|
|
374
|
-
/>
|
|
375
|
-
)}
|
|
376
|
-
</button>
|
|
507
|
+
<ProfileMenu me={me} onRename={onRename} onRecolor={onRecolor} />
|
|
377
508
|
<button
|
|
378
509
|
onClick={onShare}
|
|
379
|
-
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)]"
|
|
380
511
|
>
|
|
381
|
-
<
|
|
512
|
+
<Share2 size={16} aria-hidden />
|
|
382
513
|
Share
|
|
383
514
|
</button>
|
|
384
515
|
</div>
|