pmx-canvas 0.1.0
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/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/Readme.md +865 -0
- package/dist/canvas/global.css +3173 -0
- package/dist/canvas/index.js +183 -0
- package/dist/json-render/index.css +2 -0
- package/dist/json-render/index.js +389 -0
- package/dist/types/cli/agent.d.ts +13 -0
- package/dist/types/cli/index.d.ts +2 -0
- package/dist/types/cli/watch.d.ts +5 -0
- package/dist/types/client/App.d.ts +1 -0
- package/dist/types/client/canvas/AttentionHistory.d.ts +1 -0
- package/dist/types/client/canvas/AttentionToast.d.ts +1 -0
- package/dist/types/client/canvas/CanvasNode.d.ts +8 -0
- package/dist/types/client/canvas/CanvasViewport.d.ts +8 -0
- package/dist/types/client/canvas/CommandPalette.d.ts +4 -0
- package/dist/types/client/canvas/ContextMenu.d.ts +24 -0
- package/dist/types/client/canvas/ContextPinBar.d.ts +1 -0
- package/dist/types/client/canvas/ContextPinHud.d.ts +1 -0
- package/dist/types/client/canvas/DockedNode.d.ts +4 -0
- package/dist/types/client/canvas/EdgeLayer.d.ts +8 -0
- package/dist/types/client/canvas/ExpandedNodeOverlay.d.ts +1 -0
- package/dist/types/client/canvas/FocusFieldLayer.d.ts +1 -0
- package/dist/types/client/canvas/Minimap.d.ts +23 -0
- package/dist/types/client/canvas/SelectionBar.d.ts +1 -0
- package/dist/types/client/canvas/ShortcutOverlay.d.ts +3 -0
- package/dist/types/client/canvas/SnapshotPanel.d.ts +7 -0
- package/dist/types/client/canvas/snap-guides.d.ts +23 -0
- package/dist/types/client/canvas/use-node-drag.d.ts +15 -0
- package/dist/types/client/canvas/use-node-resize.d.ts +15 -0
- package/dist/types/client/canvas/use-pan-zoom.d.ts +16 -0
- package/dist/types/client/ext-app/bridge.d.ts +161 -0
- package/dist/types/client/icons.d.ts +70 -0
- package/dist/types/client/index.d.ts +1 -0
- package/dist/types/client/nodes/ContextNode.d.ts +34 -0
- package/dist/types/client/nodes/ExtAppFrame.d.ts +18 -0
- package/dist/types/client/nodes/FileNode.d.ts +5 -0
- package/dist/types/client/nodes/GroupNode.d.ts +6 -0
- package/dist/types/client/nodes/ImageNode.d.ts +10 -0
- package/dist/types/client/nodes/InlineFormatBar.d.ts +7 -0
- package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +14 -0
- package/dist/types/client/nodes/LedgerNode.d.ts +4 -0
- package/dist/types/client/nodes/MarkdownNode.d.ts +6 -0
- package/dist/types/client/nodes/McpAppNode.d.ts +4 -0
- package/dist/types/client/nodes/MdFormatBar.d.ts +8 -0
- package/dist/types/client/nodes/PromptNode.d.ts +5 -0
- package/dist/types/client/nodes/ResponseNode.d.ts +5 -0
- package/dist/types/client/nodes/StatusNode.d.ts +4 -0
- package/dist/types/client/nodes/StatusSummary.d.ts +4 -0
- package/dist/types/client/nodes/TraceNode.d.ts +4 -0
- package/dist/types/client/nodes/WebpageNode.d.ts +5 -0
- package/dist/types/client/nodes/image-warnings.d.ts +6 -0
- package/dist/types/client/nodes/inline-editor-commands.d.ts +11 -0
- package/dist/types/client/nodes/md-format.d.ts +25 -0
- package/dist/types/client/state/attention-bridge.d.ts +3 -0
- package/dist/types/client/state/attention-store.d.ts +25 -0
- package/dist/types/client/state/canvas-store.d.ts +74 -0
- package/dist/types/client/state/intent-bridge.d.ts +158 -0
- package/dist/types/client/state/sse-bridge.d.ts +5 -0
- package/dist/types/client/theme/tokens.d.ts +27 -0
- package/dist/types/client/types.d.ts +40 -0
- package/dist/types/client/utils/ext-app-tool-result.d.ts +1 -0
- package/dist/types/client/utils/placement.d.ts +1 -0
- package/dist/types/client/utils/platform.d.ts +2 -0
- package/dist/types/json-render/catalog.d.ts +815 -0
- package/dist/types/json-render/charts/components.d.ts +54 -0
- package/dist/types/json-render/charts/definitions.d.ts +103 -0
- package/dist/types/json-render/charts/extra-components.d.ts +58 -0
- package/dist/types/json-render/charts/extra-definitions.d.ts +181 -0
- package/dist/types/json-render/renderer/index.d.ts +16 -0
- package/dist/types/json-render/schema.d.ts +46 -0
- package/dist/types/json-render/server.d.ts +55 -0
- package/dist/types/mcp/server.d.ts +22 -0
- package/dist/types/server/agent-context.d.ts +21 -0
- package/dist/types/server/artifact-paths.d.ts +3 -0
- package/dist/types/server/canvas-operations.d.ts +154 -0
- package/dist/types/server/canvas-provenance.d.ts +13 -0
- package/dist/types/server/canvas-schema.d.ts +49 -0
- package/dist/types/server/canvas-serialization.d.ts +25 -0
- package/dist/types/server/canvas-state.d.ts +174 -0
- package/dist/types/server/canvas-validation.d.ts +33 -0
- package/dist/types/server/chart-template.d.ts +29 -0
- package/dist/types/server/code-graph.d.ts +67 -0
- package/dist/types/server/context-cards.d.ts +24 -0
- package/dist/types/server/diagram-presets.d.ts +28 -0
- package/dist/types/server/ext-app-call-registry.d.ts +16 -0
- package/dist/types/server/ext-app-tool-result.d.ts +1 -0
- package/dist/types/server/file-watcher.d.ts +16 -0
- package/dist/types/server/index.d.ts +243 -0
- package/dist/types/server/mcp-app-candidate.d.ts +25 -0
- package/dist/types/server/mcp-app-host.d.ts +65 -0
- package/dist/types/server/mcp-app-runtime.d.ts +47 -0
- package/dist/types/server/mutation-history.d.ts +105 -0
- package/dist/types/server/placement.d.ts +37 -0
- package/dist/types/server/server.d.ts +103 -0
- package/dist/types/server/spatial-analysis.d.ts +87 -0
- package/dist/types/server/trace-manager.d.ts +48 -0
- package/dist/types/server/web-artifacts.d.ts +50 -0
- package/dist/types/server/webpage-node.d.ts +25 -0
- package/dist/types/shared/auto-arrange.d.ts +29 -0
- package/dist/types/shared/ext-app-tool-result.d.ts +9 -0
- package/dist/types/shared/placement.d.ts +26 -0
- package/dist/types/shared/semantic-attention.d.ts +97 -0
- package/package.json +109 -0
- package/skills/data-analysis/SKILL.md +324 -0
- package/skills/doc-coauthoring/SKILL.md +375 -0
- package/skills/frontend-design/SKILL.md +45 -0
- package/skills/json-render-codegen/SKILL.md +112 -0
- package/skills/json-render-core/SKILL.md +265 -0
- package/skills/json-render-ink/SKILL.md +273 -0
- package/skills/json-render-mcp/SKILL.md +132 -0
- package/skills/json-render-react/SKILL.md +264 -0
- package/skills/json-render-shadcn/SKILL.md +159 -0
- package/skills/playwright-cli/SKILL.md +67 -0
- package/skills/pmx-canvas/SKILL.md +668 -0
- package/skills/pmx-canvas/evals/evals.json +186 -0
- package/skills/pmx-canvas-testing/SKILL.md +78 -0
- package/skills/published-consumer-e2e/SKILL.md +43 -0
- package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +241 -0
- package/skills/web-artifacts-builder/SKILL.md +80 -0
- package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +167 -0
- package/skills/web-artifacts-builder/scripts/init-artifact.sh +425 -0
- package/skills/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
- package/skills/web-design-guidelines/SKILL.md +39 -0
- package/src/cli/agent.ts +2144 -0
- package/src/cli/index.ts +622 -0
- package/src/cli/watch.ts +88 -0
- package/src/client/App.tsx +507 -0
- package/src/client/canvas/AttentionHistory.tsx +81 -0
- package/src/client/canvas/AttentionToast.tsx +19 -0
- package/src/client/canvas/CanvasNode.tsx +363 -0
- package/src/client/canvas/CanvasViewport.tsx +590 -0
- package/src/client/canvas/CommandPalette.tsx +302 -0
- package/src/client/canvas/ContextMenu.tsx +601 -0
- package/src/client/canvas/ContextPinBar.tsx +25 -0
- package/src/client/canvas/ContextPinHud.tsx +22 -0
- package/src/client/canvas/DockedNode.tsx +66 -0
- package/src/client/canvas/EdgeLayer.tsx +280 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +260 -0
- package/src/client/canvas/FocusFieldLayer.tsx +107 -0
- package/src/client/canvas/Minimap.tsx +301 -0
- package/src/client/canvas/SelectionBar.tsx +69 -0
- package/src/client/canvas/ShortcutOverlay.tsx +69 -0
- package/src/client/canvas/SnapshotPanel.tsx +236 -0
- package/src/client/canvas/snap-guides.ts +170 -0
- package/src/client/canvas/use-node-drag.ts +51 -0
- package/src/client/canvas/use-node-resize.ts +59 -0
- package/src/client/canvas/use-pan-zoom.ts +191 -0
- package/src/client/ext-app/bridge.ts +542 -0
- package/src/client/icons.tsx +424 -0
- package/src/client/index.tsx +7 -0
- package/src/client/nodes/ContextNode.tsx +412 -0
- package/src/client/nodes/ExtAppFrame.tsx +509 -0
- package/src/client/nodes/FileNode.tsx +256 -0
- package/src/client/nodes/GroupNode.tsx +39 -0
- package/src/client/nodes/ImageNode.tsx +160 -0
- package/src/client/nodes/InlineFormatBar.tsx +169 -0
- package/src/client/nodes/InlineMarkdownEditor.tsx +123 -0
- package/src/client/nodes/LedgerNode.tsx +37 -0
- package/src/client/nodes/MarkdownNode.tsx +359 -0
- package/src/client/nodes/McpAppNode.tsx +85 -0
- package/src/client/nodes/MdFormatBar.tsx +109 -0
- package/src/client/nodes/PromptNode.tsx +597 -0
- package/src/client/nodes/ResponseNode.tsx +153 -0
- package/src/client/nodes/StatusNode.tsx +84 -0
- package/src/client/nodes/StatusSummary.tsx +38 -0
- package/src/client/nodes/TraceNode.tsx +120 -0
- package/src/client/nodes/WebpageNode.tsx +288 -0
- package/src/client/nodes/image-warnings.ts +95 -0
- package/src/client/nodes/inline-editor-commands.ts +37 -0
- package/src/client/nodes/md-format.ts +206 -0
- package/src/client/state/attention-bridge.ts +328 -0
- package/src/client/state/attention-store.ts +73 -0
- package/src/client/state/canvas-store.ts +631 -0
- package/src/client/state/intent-bridge.ts +315 -0
- package/src/client/state/sse-bridge.ts +965 -0
- package/src/client/theme/global.css +3173 -0
- package/src/client/theme/tokens.ts +72 -0
- package/src/client/types-shims.d.ts +5 -0
- package/src/client/types.ts +81 -0
- package/src/client/utils/ext-app-tool-result.ts +4 -0
- package/src/client/utils/placement.ts +4 -0
- package/src/client/utils/platform.ts +2 -0
- package/src/json-render/catalog.ts +256 -0
- package/src/json-render/charts/components.tsx +198 -0
- package/src/json-render/charts/definitions.ts +81 -0
- package/src/json-render/charts/extra-components.tsx +267 -0
- package/src/json-render/charts/extra-definitions.ts +145 -0
- package/src/json-render/renderer/index.css +174 -0
- package/src/json-render/renderer/index.tsx +86 -0
- package/src/json-render/schema.ts +62 -0
- package/src/json-render/server.ts +597 -0
- package/src/mcp/server.ts +1377 -0
- package/src/server/agent-context.ts +242 -0
- package/src/server/artifact-paths.ts +17 -0
- package/src/server/canvas-operations.ts +1279 -0
- package/src/server/canvas-provenance.ts +243 -0
- package/src/server/canvas-schema.ts +432 -0
- package/src/server/canvas-serialization.ts +95 -0
- package/src/server/canvas-state.ts +1134 -0
- package/src/server/canvas-validation.ts +114 -0
- package/src/server/chart-template.ts +449 -0
- package/src/server/code-graph.ts +370 -0
- package/src/server/context-cards.ts +31 -0
- package/src/server/diagram-presets.ts +71 -0
- package/src/server/ext-app-call-registry.ts +77 -0
- package/src/server/ext-app-tool-result.ts +4 -0
- package/src/server/file-watcher.ts +121 -0
- package/src/server/index.ts +647 -0
- package/src/server/mcp-app-candidate.ts +174 -0
- package/src/server/mcp-app-host.ts +814 -0
- package/src/server/mcp-app-runtime.ts +459 -0
- package/src/server/mutation-history.ts +350 -0
- package/src/server/placement.ts +125 -0
- package/src/server/server.ts +3846 -0
- package/src/server/spatial-analysis.ts +356 -0
- package/src/server/trace-manager.ts +333 -0
- package/src/server/web-artifacts/scripts/bundle-artifact.sh +167 -0
- package/src/server/web-artifacts/scripts/init-artifact.sh +426 -0
- package/src/server/web-artifacts/scripts/shadcn-components.tar.gz +0 -0
- package/src/server/web-artifacts.ts +442 -0
- package/src/server/webpage-node.ts +328 -0
- package/src/shared/auto-arrange.ts +439 -0
- package/src/shared/ext-app-tool-result.ts +76 -0
- package/src/shared/placement.ts +81 -0
- package/src/shared/semantic-attention.ts +598 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import type { Signal } from '@preact/signals';
|
|
2
|
+
import { activeNodeId, draggingEdge, searchHighlightIds } from '../state/canvas-store';
|
|
3
|
+
import type { CanvasEdge, CanvasNodeState } from '../types';
|
|
4
|
+
|
|
5
|
+
// ── Edge type visual styles ──────────────────────────────────
|
|
6
|
+
const EDGE_COLORS: Record<CanvasEdge['type'], string> = {
|
|
7
|
+
relation: 'var(--c-muted)',
|
|
8
|
+
'depends-on': 'var(--c-warn)',
|
|
9
|
+
flow: 'var(--c-accent)',
|
|
10
|
+
references: 'var(--c-dim)',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const DIRECTED_TYPES = new Set<CanvasEdge['type']>(['depends-on', 'flow']);
|
|
14
|
+
|
|
15
|
+
function dashArray(edge: CanvasEdge): string | undefined {
|
|
16
|
+
if (edge.style === 'dashed') return '8 4';
|
|
17
|
+
if (edge.style === 'dotted') return '3 3';
|
|
18
|
+
if (edge.type === 'references' && !edge.style) return '8 4';
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── Anchor computation ───────────────────────────────────────
|
|
23
|
+
interface Anchor {
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function computeAnchor(node: CanvasNodeState, target: CanvasNodeState): Anchor {
|
|
29
|
+
const cx = node.position.x + node.size.width / 2;
|
|
30
|
+
const cy = node.position.y + node.size.height / 2;
|
|
31
|
+
const tx = target.position.x + target.size.width / 2;
|
|
32
|
+
const ty = target.position.y + target.size.height / 2;
|
|
33
|
+
|
|
34
|
+
const dx = tx - cx;
|
|
35
|
+
const dy = ty - cy;
|
|
36
|
+
|
|
37
|
+
const hw = node.size.width / 2;
|
|
38
|
+
const hh = node.size.height / 2;
|
|
39
|
+
|
|
40
|
+
// Determine which side the edge exits from
|
|
41
|
+
const tanAngle = Math.abs(dy / (dx || 0.001));
|
|
42
|
+
const boxRatio = hh / (hw || 0.001);
|
|
43
|
+
|
|
44
|
+
if (tanAngle > boxRatio) {
|
|
45
|
+
// Top or bottom
|
|
46
|
+
const sign = dy > 0 ? 1 : -1;
|
|
47
|
+
return {
|
|
48
|
+
x: cx + (hh / tanAngle) * (dx > 0 ? 1 : -1),
|
|
49
|
+
y: cy + hh * sign,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Left or right
|
|
54
|
+
const sign = dx > 0 ? 1 : -1;
|
|
55
|
+
return {
|
|
56
|
+
x: cx + hw * sign,
|
|
57
|
+
y: cy + tanAngle * hw * (dy > 0 ? 1 : -1),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Bezier midpoint at t=0.5 ─────────────────────────────────
|
|
62
|
+
function bezierMidpoint(
|
|
63
|
+
x1: number,
|
|
64
|
+
y1: number,
|
|
65
|
+
cx1: number,
|
|
66
|
+
cy1: number,
|
|
67
|
+
cx2: number,
|
|
68
|
+
cy2: number,
|
|
69
|
+
x2: number,
|
|
70
|
+
y2: number,
|
|
71
|
+
): { x: number; y: number } {
|
|
72
|
+
const t = 0.5;
|
|
73
|
+
const mt = 1 - t;
|
|
74
|
+
return {
|
|
75
|
+
x: mt * mt * mt * x1 + 3 * mt * mt * t * cx1 + 3 * mt * t * t * cx2 + t * t * t * x2,
|
|
76
|
+
y: mt * mt * mt * y1 + 3 * mt * mt * t * cy1 + 3 * mt * t * t * cy2 + t * t * t * y2,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── EdgePath component ───────────────────────────────────────
|
|
81
|
+
interface EdgePathProps {
|
|
82
|
+
edge: CanvasEdge;
|
|
83
|
+
fromNode: CanvasNodeState;
|
|
84
|
+
toNode: CanvasNodeState;
|
|
85
|
+
focused: boolean; // connected to the active node
|
|
86
|
+
dimmed: boolean; // active node exists but this edge is NOT connected
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function EdgePath({ edge, fromNode, toNode, focused, dimmed }: EdgePathProps) {
|
|
90
|
+
const start = computeAnchor(fromNode, toNode);
|
|
91
|
+
const end = computeAnchor(toNode, fromNode);
|
|
92
|
+
|
|
93
|
+
const dx = end.x - start.x;
|
|
94
|
+
const dy = end.y - start.y;
|
|
95
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
96
|
+
const curvature = Math.min(dist * 0.25, 80);
|
|
97
|
+
|
|
98
|
+
// Control points: offset perpendicular to direct line
|
|
99
|
+
const nx = dx / (dist || 1);
|
|
100
|
+
const ny = dy / (dist || 1);
|
|
101
|
+
const cx1 = start.x + nx * curvature;
|
|
102
|
+
const cy1 = start.y + ny * curvature;
|
|
103
|
+
const cx2 = end.x - nx * curvature;
|
|
104
|
+
const cy2 = end.y - ny * curvature;
|
|
105
|
+
|
|
106
|
+
const d = `M ${start.x} ${start.y} C ${cx1} ${cy1}, ${cx2} ${cy2}, ${end.x} ${end.y}`;
|
|
107
|
+
const color = EDGE_COLORS[edge.type];
|
|
108
|
+
const directed = DIRECTED_TYPES.has(edge.type);
|
|
109
|
+
const dash = dashArray(edge);
|
|
110
|
+
|
|
111
|
+
const mid = edge.label
|
|
112
|
+
? bezierMidpoint(start.x, start.y, cx1, cy1, cx2, cy2, end.x, end.y)
|
|
113
|
+
: null;
|
|
114
|
+
|
|
115
|
+
const pathId = `edge-path-${edge.id}`;
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<g>
|
|
119
|
+
{/* Invisible wide hitbox for hover/click */}
|
|
120
|
+
<path
|
|
121
|
+
d={d}
|
|
122
|
+
fill="none"
|
|
123
|
+
stroke="transparent"
|
|
124
|
+
stroke-width="12"
|
|
125
|
+
style={{ cursor: 'pointer' }}
|
|
126
|
+
/>
|
|
127
|
+
|
|
128
|
+
{/* Glow layer for focused edges */}
|
|
129
|
+
{focused && (
|
|
130
|
+
<path
|
|
131
|
+
d={d}
|
|
132
|
+
fill="none"
|
|
133
|
+
stroke={color}
|
|
134
|
+
stroke-width="6"
|
|
135
|
+
stroke-dasharray={dash}
|
|
136
|
+
opacity="0.15"
|
|
137
|
+
style={{ filter: 'blur(3px)' }}
|
|
138
|
+
/>
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
{/* Visible edge */}
|
|
142
|
+
<path
|
|
143
|
+
id={pathId}
|
|
144
|
+
d={d}
|
|
145
|
+
fill="none"
|
|
146
|
+
stroke={color}
|
|
147
|
+
stroke-width={focused ? 2.5 : 1.5}
|
|
148
|
+
stroke-dasharray={dash}
|
|
149
|
+
marker-end={directed ? 'url(#edge-arrow)' : undefined}
|
|
150
|
+
opacity={dimmed ? 0.2 : focused ? 1 : 0.75}
|
|
151
|
+
style={{ transition: 'opacity 0.2s, stroke-width 0.2s' }}
|
|
152
|
+
/>
|
|
153
|
+
|
|
154
|
+
{/* Animated pulse dot */}
|
|
155
|
+
{edge.animated && (
|
|
156
|
+
<circle r="3" fill={color} opacity="0.9">
|
|
157
|
+
<animateMotion dur="2s" repeatCount="indefinite">
|
|
158
|
+
<mpath href={`#${pathId}`} />
|
|
159
|
+
</animateMotion>
|
|
160
|
+
</circle>
|
|
161
|
+
)}
|
|
162
|
+
|
|
163
|
+
{/* Label at midpoint */}
|
|
164
|
+
{mid && edge.label && (
|
|
165
|
+
<g transform={`translate(${mid.x}, ${mid.y})`}>
|
|
166
|
+
<rect
|
|
167
|
+
class="edge-label-bg"
|
|
168
|
+
x={-(edge.label.length * 3.5 + 8)}
|
|
169
|
+
y="-10"
|
|
170
|
+
width={edge.label.length * 7 + 16}
|
|
171
|
+
height="20"
|
|
172
|
+
rx="4"
|
|
173
|
+
/>
|
|
174
|
+
<text
|
|
175
|
+
class="edge-label"
|
|
176
|
+
text-anchor="middle"
|
|
177
|
+
dominant-baseline="central"
|
|
178
|
+
fill="var(--c-text)"
|
|
179
|
+
font-size="11"
|
|
180
|
+
>
|
|
181
|
+
{edge.label}
|
|
182
|
+
</text>
|
|
183
|
+
</g>
|
|
184
|
+
)}
|
|
185
|
+
</g>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── EdgeLayer ────────────────────────────────────────────────
|
|
190
|
+
interface EdgeLayerProps {
|
|
191
|
+
nodes: Signal<Map<string, CanvasNodeState>>;
|
|
192
|
+
edges: Signal<Map<string, CanvasEdge>>;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function EdgeLayer({ nodes, edges }: EdgeLayerProps) {
|
|
196
|
+
const nodeMap = nodes.value;
|
|
197
|
+
const edgeList = Array.from(edges.value.values());
|
|
198
|
+
const focusId = activeNodeId.value;
|
|
199
|
+
const hasFocus = focusId !== null;
|
|
200
|
+
const searchSet = searchHighlightIds.value;
|
|
201
|
+
const hasSearch = searchSet !== null;
|
|
202
|
+
|
|
203
|
+
if (edgeList.length === 0) return null;
|
|
204
|
+
|
|
205
|
+
const PAD = 96;
|
|
206
|
+
const worldNodes = Array.from(nodeMap.values());
|
|
207
|
+
const minX = Math.min(...worldNodes.map((node) => node.position.x)) - PAD;
|
|
208
|
+
const minY = Math.min(...worldNodes.map((node) => node.position.y)) - PAD;
|
|
209
|
+
const maxX = Math.max(...worldNodes.map((node) => node.position.x + node.size.width)) + PAD;
|
|
210
|
+
const maxY = Math.max(...worldNodes.map((node) => node.position.y + node.size.height)) + PAD;
|
|
211
|
+
const width = Math.max(1, maxX - minX);
|
|
212
|
+
const height = Math.max(1, maxY - minY);
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<svg
|
|
216
|
+
aria-label="Canvas connections"
|
|
217
|
+
role="img"
|
|
218
|
+
viewBox={`${minX} ${minY} ${width} ${height}`}
|
|
219
|
+
width={width}
|
|
220
|
+
height={height}
|
|
221
|
+
style={{
|
|
222
|
+
position: 'absolute',
|
|
223
|
+
top: `${minY}px`,
|
|
224
|
+
left: `${minX}px`,
|
|
225
|
+
pointerEvents: 'none',
|
|
226
|
+
overflow: 'visible',
|
|
227
|
+
}}
|
|
228
|
+
>
|
|
229
|
+
<title>Canvas connections</title>
|
|
230
|
+
<defs>
|
|
231
|
+
<marker
|
|
232
|
+
id="edge-arrow"
|
|
233
|
+
viewBox="0 0 10 10"
|
|
234
|
+
refX="9"
|
|
235
|
+
refY="5"
|
|
236
|
+
markerWidth="8"
|
|
237
|
+
markerHeight="8"
|
|
238
|
+
orient="auto-start-reverse"
|
|
239
|
+
>
|
|
240
|
+
<path d="M 0 1 L 10 5 L 0 9 z" fill="currentColor" opacity="0.75" />
|
|
241
|
+
</marker>
|
|
242
|
+
</defs>
|
|
243
|
+
{edgeList.map((edge) => {
|
|
244
|
+
const fromNode = nodeMap.get(edge.from);
|
|
245
|
+
const toNode = nodeMap.get(edge.to);
|
|
246
|
+
if (!fromNode || !toNode) return null;
|
|
247
|
+
const isConnected = hasFocus && (edge.from === focusId || edge.to === focusId);
|
|
248
|
+
const searchDimmed = hasSearch && !(searchSet.has(edge.from) || searchSet.has(edge.to));
|
|
249
|
+
return (
|
|
250
|
+
<EdgePath
|
|
251
|
+
key={edge.id}
|
|
252
|
+
edge={edge}
|
|
253
|
+
fromNode={fromNode}
|
|
254
|
+
toNode={toNode}
|
|
255
|
+
focused={isConnected}
|
|
256
|
+
dimmed={(hasFocus && !isConnected) || searchDimmed}
|
|
257
|
+
/>
|
|
258
|
+
);
|
|
259
|
+
})}
|
|
260
|
+
{/* Live preview edge while drag-connecting */}
|
|
261
|
+
{draggingEdge.value && (() => {
|
|
262
|
+
const de = draggingEdge.value;
|
|
263
|
+
const dx = de.cursorX - de.fromX;
|
|
264
|
+
const dy = de.cursorY - de.fromY;
|
|
265
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
266
|
+
const curve = Math.min(dist * 0.25, 80);
|
|
267
|
+
const nx = dx / (dist || 1);
|
|
268
|
+
const ny = dy / (dist || 1);
|
|
269
|
+
const previewD = `M ${de.fromX} ${de.fromY} C ${de.fromX + nx * curve} ${de.fromY + ny * curve}, ${de.cursorX - nx * curve} ${de.cursorY - ny * curve}, ${de.cursorX} ${de.cursorY}`;
|
|
270
|
+
return (
|
|
271
|
+
<g>
|
|
272
|
+
<path d={previewD} fill="none" stroke="var(--c-accent)" stroke-width="6" opacity="0.1" style={{ filter: 'blur(3px)' }} />
|
|
273
|
+
<path d={previewD} fill="none" stroke="var(--c-accent)" stroke-width="2" stroke-dasharray="6 4" opacity="0.8" />
|
|
274
|
+
<circle cx={de.cursorX} cy={de.cursorY} r="5" fill="var(--c-accent)" opacity="0.5" />
|
|
275
|
+
</g>
|
|
276
|
+
);
|
|
277
|
+
})()}
|
|
278
|
+
</svg>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { useCallback, useState } from 'preact/hooks';
|
|
2
|
+
import { ContextNode } from '../nodes/ContextNode';
|
|
3
|
+
import { FileNode } from '../nodes/FileNode';
|
|
4
|
+
import { LedgerNode } from '../nodes/LedgerNode';
|
|
5
|
+
import { MarkdownNode } from '../nodes/MarkdownNode';
|
|
6
|
+
import { McpAppNode } from '../nodes/McpAppNode';
|
|
7
|
+
import { StatusNode } from '../nodes/StatusNode';
|
|
8
|
+
import { ImageNode } from '../nodes/ImageNode';
|
|
9
|
+
import { WebpageNode } from '../nodes/WebpageNode';
|
|
10
|
+
import { PromptNode } from '../nodes/PromptNode';
|
|
11
|
+
import { ResponseNode } from '../nodes/ResponseNode';
|
|
12
|
+
import { TraceNode } from '../nodes/TraceNode';
|
|
13
|
+
import {
|
|
14
|
+
collapseExpandedNode,
|
|
15
|
+
contextPinnedNodeIds,
|
|
16
|
+
expandedNodeId,
|
|
17
|
+
nodes,
|
|
18
|
+
toggleContextPin,
|
|
19
|
+
} from '../state/canvas-store';
|
|
20
|
+
import { TYPE_LABELS } from '../types';
|
|
21
|
+
import type { CanvasNodeState } from '../types';
|
|
22
|
+
|
|
23
|
+
function renderContent(node: CanvasNodeState, expanded: boolean) {
|
|
24
|
+
switch (node.type) {
|
|
25
|
+
case 'markdown':
|
|
26
|
+
return <MarkdownNode node={node} expanded={expanded} />;
|
|
27
|
+
case 'mcp-app':
|
|
28
|
+
return <McpAppNode node={node} />;
|
|
29
|
+
case 'webpage':
|
|
30
|
+
return <WebpageNode node={node} expanded={expanded} />;
|
|
31
|
+
case 'json-render':
|
|
32
|
+
return <McpAppNode node={node} />;
|
|
33
|
+
case 'graph':
|
|
34
|
+
return <McpAppNode node={node} />;
|
|
35
|
+
case 'prompt':
|
|
36
|
+
return <PromptNode node={node} />;
|
|
37
|
+
case 'response':
|
|
38
|
+
return <ResponseNode node={node} expanded={expanded} />;
|
|
39
|
+
case 'status':
|
|
40
|
+
return <StatusNode node={node} />;
|
|
41
|
+
case 'context':
|
|
42
|
+
return <ContextNode node={node} expanded={expanded} />;
|
|
43
|
+
case 'ledger':
|
|
44
|
+
return <LedgerNode node={node} />;
|
|
45
|
+
case 'trace':
|
|
46
|
+
return <TraceNode node={node} />;
|
|
47
|
+
case 'file':
|
|
48
|
+
return <FileNode node={node} expanded={expanded} />;
|
|
49
|
+
case 'image':
|
|
50
|
+
return <ImageNode node={node} expanded={expanded} />;
|
|
51
|
+
default:
|
|
52
|
+
return <div>Unknown node type</div>;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Extract plain text content from a node for word count / copy. */
|
|
57
|
+
function getNodeTextContent(node: CanvasNodeState): string {
|
|
58
|
+
switch (node.type) {
|
|
59
|
+
case 'markdown':
|
|
60
|
+
return (node.data.content as string) || '';
|
|
61
|
+
case 'file':
|
|
62
|
+
return (node.data.fileContent as string) || '';
|
|
63
|
+
case 'webpage':
|
|
64
|
+
return (node.data.content as string) || '';
|
|
65
|
+
case 'json-render':
|
|
66
|
+
case 'graph':
|
|
67
|
+
return JSON.stringify(node.data.spec ?? node.data.graphConfig ?? {}, null, 2);
|
|
68
|
+
default:
|
|
69
|
+
return '';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function wordCount(text: string): number {
|
|
74
|
+
if (!text) return 0;
|
|
75
|
+
return text.split(/\s+/).filter(Boolean).length;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function ExpandedNodeOverlay() {
|
|
79
|
+
const nodeId = expandedNodeId.value;
|
|
80
|
+
const node = nodeId ? nodes.value.get(nodeId) : undefined;
|
|
81
|
+
const [copied, setCopied] = useState(false);
|
|
82
|
+
|
|
83
|
+
const handleClose = useCallback(() => {
|
|
84
|
+
collapseExpandedNode();
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
const handleBackdropPointerDown = useCallback((e: PointerEvent) => {
|
|
88
|
+
if ((e.target as HTMLElement).classList.contains('expanded-overlay-backdrop')) {
|
|
89
|
+
collapseExpandedNode();
|
|
90
|
+
}
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
const handleCopy = useCallback(() => {
|
|
94
|
+
if (!node) return;
|
|
95
|
+
const text = getNodeTextContent(node);
|
|
96
|
+
if (!text) return;
|
|
97
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
98
|
+
setCopied(true);
|
|
99
|
+
setTimeout(() => setCopied(false), 1500);
|
|
100
|
+
});
|
|
101
|
+
}, [node]);
|
|
102
|
+
|
|
103
|
+
const handleToggleCtxPin = useCallback(() => {
|
|
104
|
+
if (!nodeId) return;
|
|
105
|
+
toggleContextPin(nodeId);
|
|
106
|
+
}, [nodeId]);
|
|
107
|
+
|
|
108
|
+
if (!node) return null;
|
|
109
|
+
|
|
110
|
+
const title =
|
|
111
|
+
(node.data.title as string) ||
|
|
112
|
+
(node.data.path as string)?.split('/').pop() ||
|
|
113
|
+
TYPE_LABELS[node.type];
|
|
114
|
+
const textContent = getNodeTextContent(node);
|
|
115
|
+
const words = wordCount(textContent);
|
|
116
|
+
const isCtxPinned = nodeId ? contextPinnedNodeIds.value.has(nodeId) : false;
|
|
117
|
+
const hasText = textContent.length > 0;
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div
|
|
121
|
+
class="expanded-overlay-backdrop"
|
|
122
|
+
onPointerDown={handleBackdropPointerDown}
|
|
123
|
+
style={{
|
|
124
|
+
position: 'fixed',
|
|
125
|
+
inset: 0,
|
|
126
|
+
zIndex: 10001,
|
|
127
|
+
background: 'rgba(10,14,30,0.85)',
|
|
128
|
+
backdropFilter: 'blur(8px)',
|
|
129
|
+
display: 'flex',
|
|
130
|
+
alignItems: 'stretch',
|
|
131
|
+
justifyContent: 'center',
|
|
132
|
+
padding: '32px',
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
<div
|
|
136
|
+
class="expanded-overlay-panel"
|
|
137
|
+
style={{
|
|
138
|
+
flex: 1,
|
|
139
|
+
maxWidth: '1200px',
|
|
140
|
+
display: 'flex',
|
|
141
|
+
flexDirection: 'column',
|
|
142
|
+
background: 'var(--c-panel)',
|
|
143
|
+
border: `1px solid ${isCtxPinned ? 'var(--c-warn)' : 'var(--c-accent)'}`,
|
|
144
|
+
borderRadius: 'var(--radius)',
|
|
145
|
+
boxShadow: `0 0 0 1px ${isCtxPinned ? 'var(--c-warn)' : 'var(--c-accent)'}, 0 24px 80px rgba(0,0,0,0.6)`,
|
|
146
|
+
overflow: 'hidden',
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
{/* Title bar */}
|
|
150
|
+
<div
|
|
151
|
+
style={{
|
|
152
|
+
display: 'flex',
|
|
153
|
+
alignItems: 'center',
|
|
154
|
+
gap: '10px',
|
|
155
|
+
padding: '10px 16px',
|
|
156
|
+
background: 'var(--c-panel-glass)',
|
|
157
|
+
borderBottom: '1px solid var(--c-line)',
|
|
158
|
+
flexShrink: 0,
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
<span
|
|
162
|
+
style={{
|
|
163
|
+
fontSize: '10px',
|
|
164
|
+
padding: '1px 6px',
|
|
165
|
+
borderRadius: '4px',
|
|
166
|
+
background: 'var(--c-accent-12)',
|
|
167
|
+
color: 'var(--c-accent)',
|
|
168
|
+
textTransform: 'uppercase',
|
|
169
|
+
letterSpacing: '0.04em',
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
{TYPE_LABELS[node.type]}
|
|
173
|
+
</span>
|
|
174
|
+
<span
|
|
175
|
+
style={{
|
|
176
|
+
flex: 1,
|
|
177
|
+
fontSize: '13px',
|
|
178
|
+
fontWeight: 600,
|
|
179
|
+
color: 'var(--c-text)',
|
|
180
|
+
whiteSpace: 'nowrap',
|
|
181
|
+
overflow: 'hidden',
|
|
182
|
+
textOverflow: 'ellipsis',
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
{title}
|
|
186
|
+
</span>
|
|
187
|
+
|
|
188
|
+
{/* Action buttons */}
|
|
189
|
+
<div class="expanded-actions">
|
|
190
|
+
{/* Context pin toggle */}
|
|
191
|
+
<button
|
|
192
|
+
type="button"
|
|
193
|
+
class={`expanded-action-btn ${isCtxPinned ? 'expanded-action-active' : ''}`}
|
|
194
|
+
onClick={handleToggleCtxPin}
|
|
195
|
+
title={isCtxPinned ? 'Remove from context' : 'Pin as context'}
|
|
196
|
+
>
|
|
197
|
+
{isCtxPinned ? '\u2726 In context' : '\u2726 Pin as context'}
|
|
198
|
+
</button>
|
|
199
|
+
|
|
200
|
+
{/* Copy content */}
|
|
201
|
+
{hasText && (
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
class="expanded-action-btn"
|
|
205
|
+
onClick={handleCopy}
|
|
206
|
+
title="Copy content to clipboard"
|
|
207
|
+
>
|
|
208
|
+
{copied ? 'Copied!' : 'Copy'}
|
|
209
|
+
</button>
|
|
210
|
+
)}
|
|
211
|
+
|
|
212
|
+
{/* Word count */}
|
|
213
|
+
{words > 0 && (
|
|
214
|
+
<span class="expanded-meta">
|
|
215
|
+
{words.toLocaleString()} word{words !== 1 ? 's' : ''}
|
|
216
|
+
</span>
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<span style={{ fontSize: '10px', color: 'var(--c-muted)' }}>Esc to close</span>
|
|
221
|
+
<button
|
|
222
|
+
type="button"
|
|
223
|
+
onClick={handleClose}
|
|
224
|
+
style={{
|
|
225
|
+
background: 'none',
|
|
226
|
+
border: 'none',
|
|
227
|
+
color: 'var(--c-muted)',
|
|
228
|
+
cursor: 'pointer',
|
|
229
|
+
padding: '2px 6px',
|
|
230
|
+
fontSize: '16px',
|
|
231
|
+
lineHeight: 1,
|
|
232
|
+
borderRadius: '4px',
|
|
233
|
+
}}
|
|
234
|
+
onMouseEnter={(e) => {
|
|
235
|
+
(e.target as HTMLElement).style.color = 'var(--c-text)';
|
|
236
|
+
}}
|
|
237
|
+
onMouseLeave={(e) => {
|
|
238
|
+
(e.target as HTMLElement).style.color = 'var(--c-muted)';
|
|
239
|
+
}}
|
|
240
|
+
title="Close (Esc)"
|
|
241
|
+
>
|
|
242
|
+
×
|
|
243
|
+
</button>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
{/* Content area — full height */}
|
|
247
|
+
<div
|
|
248
|
+
style={{
|
|
249
|
+
flex: 1,
|
|
250
|
+
overflow: 'auto',
|
|
251
|
+
padding: '16px',
|
|
252
|
+
minHeight: 0,
|
|
253
|
+
}}
|
|
254
|
+
>
|
|
255
|
+
{renderContent(node, true)}
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { attentionPrimaryNodeIds, attentionRegions, attentionSecondaryNodeIds } from '../state/attention-store';
|
|
2
|
+
import { nodes } from '../state/canvas-store';
|
|
3
|
+
|
|
4
|
+
interface Rect {
|
|
5
|
+
left: number;
|
|
6
|
+
top: number;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getNodeRect(nodeId: string): Rect | null {
|
|
12
|
+
const node = nodes.value.get(nodeId);
|
|
13
|
+
if (!node || node.dockPosition !== null) return null;
|
|
14
|
+
return {
|
|
15
|
+
left: node.position.x,
|
|
16
|
+
top: node.position.y,
|
|
17
|
+
width: node.size.width,
|
|
18
|
+
height: node.size.height,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getRegionRect(nodeIds: string[]): Rect | null {
|
|
23
|
+
const rects = nodeIds
|
|
24
|
+
.map((nodeId) => getNodeRect(nodeId))
|
|
25
|
+
.filter((rect): rect is Rect => rect !== null);
|
|
26
|
+
if (rects.length === 0) return null;
|
|
27
|
+
|
|
28
|
+
const minLeft = Math.min(...rects.map((rect) => rect.left));
|
|
29
|
+
const minTop = Math.min(...rects.map((rect) => rect.top));
|
|
30
|
+
const maxRight = Math.max(...rects.map((rect) => rect.left + rect.width));
|
|
31
|
+
const maxBottom = Math.max(...rects.map((rect) => rect.top + rect.height));
|
|
32
|
+
const padX = 54;
|
|
33
|
+
const padY = 46;
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
left: minLeft - padX,
|
|
37
|
+
top: minTop - padY,
|
|
38
|
+
width: maxRight - minLeft + padX * 2,
|
|
39
|
+
height: maxBottom - minTop + padY * 2,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function rectStyle(rect: Rect, radius: number): Record<string, string> {
|
|
44
|
+
return {
|
|
45
|
+
left: `${rect.left}px`,
|
|
46
|
+
top: `${rect.top}px`,
|
|
47
|
+
width: `${rect.width}px`,
|
|
48
|
+
height: `${rect.height}px`,
|
|
49
|
+
borderRadius: `${radius}px`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function FocusFieldLayer() {
|
|
54
|
+
const primaryNodeIds = Array.from(attentionPrimaryNodeIds.value);
|
|
55
|
+
const secondaryNodeIds = Array.from(attentionSecondaryNodeIds.value);
|
|
56
|
+
const regions = attentionRegions.value;
|
|
57
|
+
|
|
58
|
+
if (primaryNodeIds.length === 0 && secondaryNodeIds.length === 0) return null;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div class="attention-field-layer" aria-hidden="true">
|
|
62
|
+
{regions.map((region) => {
|
|
63
|
+
const rect = getRegionRect(region.nodeIds);
|
|
64
|
+
if (!rect) return null;
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
key={region.id}
|
|
68
|
+
class="attention-field-region"
|
|
69
|
+
style={rectStyle(rect, 42)}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
})}
|
|
73
|
+
{secondaryNodeIds.map((nodeId) => {
|
|
74
|
+
const rect = getNodeRect(nodeId);
|
|
75
|
+
if (!rect) return null;
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
key={`secondary-${nodeId}`}
|
|
79
|
+
class="attention-field-node attention-field-secondary"
|
|
80
|
+
style={rectStyle({
|
|
81
|
+
left: rect.left - 18,
|
|
82
|
+
top: rect.top - 18,
|
|
83
|
+
width: rect.width + 36,
|
|
84
|
+
height: rect.height + 36,
|
|
85
|
+
}, 28)}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
})}
|
|
89
|
+
{primaryNodeIds.map((nodeId) => {
|
|
90
|
+
const rect = getNodeRect(nodeId);
|
|
91
|
+
if (!rect) return null;
|
|
92
|
+
return (
|
|
93
|
+
<div
|
|
94
|
+
key={`primary-${nodeId}`}
|
|
95
|
+
class="attention-field-node attention-field-primary"
|
|
96
|
+
style={rectStyle({
|
|
97
|
+
left: rect.left - 24,
|
|
98
|
+
top: rect.top - 24,
|
|
99
|
+
width: rect.width + 48,
|
|
100
|
+
height: rect.height + 48,
|
|
101
|
+
}, 30)}
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
})}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|