project-graph-mcp 2.3.0 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -3
- package/project-graph-mcp-2.3.0.tgz +0 -0
- package/src/network/web-server.js +1 -1
- package/vendor/symbiote-node/engine/AgentUICommands.js +100 -0
- package/vendor/symbiote-node/engine/Executor.js +371 -0
- package/vendor/symbiote-node/engine/Graph.js +314 -0
- package/vendor/symbiote-node/engine/GraphServer.js +353 -0
- package/vendor/symbiote-node/engine/HandlerLoader.js +145 -0
- package/vendor/symbiote-node/engine/History.js +83 -0
- package/vendor/symbiote-node/engine/Lifecycle.js +118 -0
- package/vendor/symbiote-node/engine/Persistence.js +84 -0
- package/vendor/symbiote-node/engine/Registry.js +264 -0
- package/vendor/symbiote-node/engine/SocketTypes.js +79 -0
- package/vendor/symbiote-node/engine/cli.js +404 -0
- package/vendor/symbiote-node/engine/index.js +56 -0
- package/vendor/symbiote-node/engine/nanoid.js +28 -0
- package/vendor/symbiote-node/engine/package.json +26 -0
- package/vendor/symbiote-node/engine/packs/ai/beat-detect.handler.js +215 -0
- package/vendor/symbiote-node/engine/packs/ai/content-adapt.handler.js +238 -0
- package/vendor/symbiote-node/engine/packs/ai/face-detect.handler.js +287 -0
- package/vendor/symbiote-node/engine/packs/ai/grok-generate.handler.js +565 -0
- package/vendor/symbiote-node/engine/packs/ai/kling-lipsync.handler.js +414 -0
- package/vendor/symbiote-node/engine/packs/ai/lesson-generate.handler.js +343 -0
- package/vendor/symbiote-node/engine/packs/ai/opencode.handler.js +164 -0
- package/vendor/symbiote-node/engine/packs/ai/replicate-lipsync.handler.js +341 -0
- package/vendor/symbiote-node/engine/packs/ai/tts.handler.js +241 -0
- package/vendor/symbiote-node/engine/packs/ai/whisper.handler.js +191 -0
- package/vendor/symbiote-node/engine/packs/data/db-query.handler.js +67 -0
- package/vendor/symbiote-node/engine/packs/data/news-accumulate.handler.js +281 -0
- package/vendor/symbiote-node/engine/packs/data/personas.handler.js +160 -0
- package/vendor/symbiote-node/engine/packs/data/prompt-loader.handler.js +193 -0
- package/vendor/symbiote-node/engine/packs/data/roles.handler.js +216 -0
- package/vendor/symbiote-node/engine/packs/data/rss-feed.handler.js +244 -0
- package/vendor/symbiote-node/engine/packs/debug/inject.handler.js +52 -0
- package/vendor/symbiote-node/engine/packs/flow/agent.handler.js +73 -0
- package/vendor/symbiote-node/engine/packs/flow/if.handler.js +107 -0
- package/vendor/symbiote-node/engine/packs/flow/loop.handler.js +58 -0
- package/vendor/symbiote-node/engine/packs/flow/merge.handler.js +60 -0
- package/vendor/symbiote-node/engine/packs/flow/retry.handler.js +65 -0
- package/vendor/symbiote-node/engine/packs/flow/switch.handler.js +64 -0
- package/vendor/symbiote-node/engine/packs/flow/wait-all.handler.js +39 -0
- package/vendor/symbiote-node/engine/packs/io/http-request.handler.js +82 -0
- package/vendor/symbiote-node/engine/packs/io/read-file.handler.js +60 -0
- package/vendor/symbiote-node/engine/packs/io/write-file.handler.js +63 -0
- package/vendor/symbiote-node/engine/packs/transform/anchor-match.handler.js +494 -0
- package/vendor/symbiote-node/engine/packs/transform/effects-skeleton.handler.js +417 -0
- package/vendor/symbiote-node/engine/packs/transform/json-parse.handler.js +43 -0
- package/vendor/symbiote-node/engine/packs/transform/lipsync-select.handler.js +339 -0
- package/vendor/symbiote-node/engine/packs/transform/riopla-adapt.handler.js +432 -0
- package/vendor/symbiote-node/engine/packs/transform/set.handler.js +57 -0
- package/vendor/symbiote-node/engine/packs/transform/template-builder.handler.js +134 -0
- package/vendor/symbiote-node/engine/packs/transform/template.handler.js +79 -0
- package/vendor/symbiote-node/engine/packs/transform/timeline-build.handler.js +399 -0
- package/vendor/symbiote-node/engine/packs/util/delay.handler.js +39 -0
- package/vendor/symbiote-node/engine/packs/util/log.handler.js +44 -0
- package/vendor/symbiote-node/engine/packs/video-pack.js +323 -0
- package/vendor/symbiote-node/package.json +2 -2
- package/web/app.js +6 -3
- package/web/components/canvas-graph.js +50 -11
- package/web/components/code-block.js +1 -1
- package/web/components/event-feed/MiniGraphWidget.js +105 -15
- package/web/components/follow-ribbon.js +134 -0
- package/web/follow-controller.js +241 -0
- package/web/panels/code-viewer.js +1 -1
- package/web/panels/dep-graph.js +21 -42
- package/web/style.css +6 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* video-pack.js - Video domain pack for agi-graph
|
|
3
|
+
*
|
|
4
|
+
* Registers video-specific node types and socket types.
|
|
5
|
+
* Extracted from symbiote-video/src/graph/NodeTypes.js + NodeProcessors.js.
|
|
6
|
+
*
|
|
7
|
+
* @module agi-graph/packs/video-pack
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { registerPack } from '../index.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Video-domain socket types
|
|
14
|
+
*/
|
|
15
|
+
const socketTypes = {
|
|
16
|
+
image: { color: '#C79650', label: 'Image', compatible: ['image'] },
|
|
17
|
+
audio: { color: '#C1990E', label: 'Audio', compatible: ['audio'] },
|
|
18
|
+
timeline: { color: '#5090C7', label: 'Timeline', compatible: ['timeline'] },
|
|
19
|
+
skeleton: { color: '#90C750', label: 'Skeleton', compatible: ['skeleton'] },
|
|
20
|
+
effect: { color: '#C750C7', label: 'Effect', compatible: ['effect'] },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Video node type definitions
|
|
25
|
+
*/
|
|
26
|
+
const nodes = [
|
|
27
|
+
// ─── Source ─────────────────────────────────────────────────────────
|
|
28
|
+
{
|
|
29
|
+
type: 'source/audio',
|
|
30
|
+
category: 'source',
|
|
31
|
+
icon: 'music_note',
|
|
32
|
+
driver: {
|
|
33
|
+
description: 'Audio source file',
|
|
34
|
+
capabilities: ['source', 'audio'],
|
|
35
|
+
inputs: [],
|
|
36
|
+
outputs: [
|
|
37
|
+
{ name: 'audio', type: 'audio' },
|
|
38
|
+
{ name: 'duration', type: 'float' },
|
|
39
|
+
],
|
|
40
|
+
params: {
|
|
41
|
+
src: { type: 'string', required: true, description: 'Audio file path or URL' },
|
|
42
|
+
volume: { type: 'float', default: 1.0, min: 0, max: 2 },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
process: (_inputs, params) => ({
|
|
46
|
+
audio: { src: params.src, volume: params.volume },
|
|
47
|
+
duration: params.duration || 0,
|
|
48
|
+
}),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
type: 'source/webp-sequence',
|
|
52
|
+
category: 'source',
|
|
53
|
+
icon: 'movie',
|
|
54
|
+
driver: {
|
|
55
|
+
description: 'WebP image sequence as video frames',
|
|
56
|
+
capabilities: ['source', 'video'],
|
|
57
|
+
inputs: [],
|
|
58
|
+
outputs: [
|
|
59
|
+
{ name: 'frames', type: 'image' },
|
|
60
|
+
{ name: 'frameCount', type: 'int' },
|
|
61
|
+
],
|
|
62
|
+
params: {
|
|
63
|
+
directory: { type: 'string', required: true },
|
|
64
|
+
pattern: { type: 'string', default: '*.webp' },
|
|
65
|
+
fps: { type: 'int', default: 30, min: 1, max: 120 },
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
process: (_inputs, params) => ({
|
|
69
|
+
frames: { directory: params.directory, pattern: params.pattern, fps: params.fps },
|
|
70
|
+
frameCount: params.frameCount || 0,
|
|
71
|
+
}),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
type: 'source/image',
|
|
75
|
+
category: 'source',
|
|
76
|
+
icon: 'image',
|
|
77
|
+
driver: {
|
|
78
|
+
description: 'Static image source',
|
|
79
|
+
capabilities: ['source', 'image'],
|
|
80
|
+
inputs: [],
|
|
81
|
+
outputs: [{ name: 'image', type: 'image' }],
|
|
82
|
+
params: {
|
|
83
|
+
src: { type: 'string', required: true },
|
|
84
|
+
fit: { type: 'string', default: 'cover', enum: ['cover', 'contain', 'fill', 'none'] },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
process: (_inputs, params) => ({
|
|
88
|
+
image: { src: params.src, fit: params.fit },
|
|
89
|
+
}),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
type: 'source/text',
|
|
93
|
+
category: 'source',
|
|
94
|
+
icon: 'text_fields',
|
|
95
|
+
driver: {
|
|
96
|
+
description: 'Text source for overlays and subtitles',
|
|
97
|
+
capabilities: ['source', 'text'],
|
|
98
|
+
inputs: [],
|
|
99
|
+
outputs: [{ name: 'text', type: 'string' }],
|
|
100
|
+
params: {
|
|
101
|
+
content: { type: 'string', default: '' },
|
|
102
|
+
style: { type: 'string', default: 'default' },
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
process: (_inputs, params) => ({
|
|
106
|
+
text: { content: params.content, style: params.style },
|
|
107
|
+
}),
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
// ─── Analysis ──────────────────────────────────────────────────────
|
|
111
|
+
{
|
|
112
|
+
type: 'analysis/beat-analyzer',
|
|
113
|
+
category: 'analysis',
|
|
114
|
+
icon: 'graphic_eq',
|
|
115
|
+
driver: {
|
|
116
|
+
description: 'Analyzes audio for beats, energy, and generates effect skeleton',
|
|
117
|
+
capabilities: ['analysis', 'audio', 'ai'],
|
|
118
|
+
inputs: [{ name: 'audio', type: 'audio', required: true }],
|
|
119
|
+
outputs: [
|
|
120
|
+
{ name: 'skeleton', type: 'skeleton' },
|
|
121
|
+
{ name: 'beats', type: 'float' },
|
|
122
|
+
{ name: 'energy', type: 'float' },
|
|
123
|
+
],
|
|
124
|
+
params: {
|
|
125
|
+
energyPerSecond: { type: 'int', default: 10, min: 1, max: 100 },
|
|
126
|
+
strongThreshold: { type: 'float', default: 1.3, min: 0.5, max: 3 },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
process: (inputs, params) => ({
|
|
130
|
+
skeleton: {
|
|
131
|
+
intensityZones: [],
|
|
132
|
+
fadeZones: [],
|
|
133
|
+
dropPoints: [],
|
|
134
|
+
beatMarkers: [],
|
|
135
|
+
transitionAnchors: [],
|
|
136
|
+
},
|
|
137
|
+
beats: [],
|
|
138
|
+
energy: [],
|
|
139
|
+
}),
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
type: 'analysis/ai-director',
|
|
143
|
+
category: 'analysis',
|
|
144
|
+
icon: 'smart_toy',
|
|
145
|
+
driver: {
|
|
146
|
+
description: 'AI-driven timeline composition from skeleton and prompt',
|
|
147
|
+
capabilities: ['analysis', 'ai', 'llm'],
|
|
148
|
+
inputs: [
|
|
149
|
+
{ name: 'skeleton', type: 'skeleton', required: true },
|
|
150
|
+
{ name: 'prompt', type: 'string' },
|
|
151
|
+
],
|
|
152
|
+
outputs: [{ name: 'timeline', type: 'timeline' }],
|
|
153
|
+
params: {
|
|
154
|
+
model: { type: 'string', default: 'auto' },
|
|
155
|
+
temperature: { type: 'float', default: 0.7, min: 0, max: 2 },
|
|
156
|
+
},
|
|
157
|
+
constraints: { requiresSecret: 'OPENAI_API_KEY' },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
// ─── Processing ────────────────────────────────────────────────────
|
|
162
|
+
{
|
|
163
|
+
type: 'processing/physics-vfx',
|
|
164
|
+
category: 'processing',
|
|
165
|
+
icon: 'bolt',
|
|
166
|
+
driver: {
|
|
167
|
+
description: 'Physics-based visual effects synced to beats',
|
|
168
|
+
capabilities: ['effects', 'animation'],
|
|
169
|
+
inputs: [
|
|
170
|
+
{ name: 'input', type: 'any' },
|
|
171
|
+
{ name: 'beats', type: 'float' },
|
|
172
|
+
],
|
|
173
|
+
outputs: [{ name: 'output', type: 'effect' }],
|
|
174
|
+
params: {
|
|
175
|
+
preset: { type: 'string', default: 'bounceIn', enum: ['bounceIn', 'dropImpact', 'glitch', 'shake', 'zoom', 'rubberBand', 'pulse'] },
|
|
176
|
+
beatSync: { type: 'boolean', default: true },
|
|
177
|
+
intensity: { type: 'float', default: 1.0, min: 0, max: 3 },
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
type: 'processing/color-correction',
|
|
183
|
+
category: 'processing',
|
|
184
|
+
icon: 'palette',
|
|
185
|
+
driver: {
|
|
186
|
+
description: 'Color grading and correction filters',
|
|
187
|
+
capabilities: ['effects', 'color'],
|
|
188
|
+
inputs: [{ name: 'input', type: 'image', required: true }],
|
|
189
|
+
outputs: [{ name: 'output', type: 'image' }],
|
|
190
|
+
params: {
|
|
191
|
+
brightness: { type: 'float', default: 0, min: -100, max: 100 },
|
|
192
|
+
contrast: { type: 'float', default: 0, min: -100, max: 100 },
|
|
193
|
+
saturation: { type: 'float', default: 0, min: -100, max: 100 },
|
|
194
|
+
hueRotate: { type: 'float', default: 0, min: 0, max: 360 },
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
type: 'processing/transition',
|
|
200
|
+
category: 'processing',
|
|
201
|
+
icon: 'shuffle',
|
|
202
|
+
driver: {
|
|
203
|
+
description: 'Transition effect between two sources',
|
|
204
|
+
capabilities: ['effects', 'transition'],
|
|
205
|
+
inputs: [
|
|
206
|
+
{ name: 'from', type: 'image', required: true },
|
|
207
|
+
{ name: 'to', type: 'image', required: true },
|
|
208
|
+
],
|
|
209
|
+
outputs: [{ name: 'output', type: 'image' }],
|
|
210
|
+
params: {
|
|
211
|
+
type: { type: 'string', default: 'fade', enum: ['fade', 'slide', 'wipe', 'zoom', 'dissolve'] },
|
|
212
|
+
duration: { type: 'int', default: 30, min: 1 },
|
|
213
|
+
direction: { type: 'string', default: 'left', enum: ['left', 'right', 'up', 'down'] },
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
// ─── Composition ───────────────────────────────────────────────────
|
|
219
|
+
{
|
|
220
|
+
type: 'composition/layout',
|
|
221
|
+
category: 'composition',
|
|
222
|
+
icon: 'grid_view',
|
|
223
|
+
driver: {
|
|
224
|
+
description: 'Positions content in the viewport',
|
|
225
|
+
capabilities: ['composition', 'layout'],
|
|
226
|
+
inputs: [{ name: 'content', type: 'any', required: true }],
|
|
227
|
+
outputs: [{ name: 'output', type: 'image' }],
|
|
228
|
+
params: {
|
|
229
|
+
anchor: { type: 'string', default: 'center' },
|
|
230
|
+
x: { type: 'string', default: '50%' },
|
|
231
|
+
y: { type: 'string', default: '50%' },
|
|
232
|
+
width: { type: 'string', default: '100%' },
|
|
233
|
+
height: { type: 'string', default: '100%' },
|
|
234
|
+
rotation: { type: 'float', default: 0 },
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
type: 'composition/blend',
|
|
240
|
+
category: 'composition',
|
|
241
|
+
icon: 'theaters',
|
|
242
|
+
driver: {
|
|
243
|
+
description: 'Blends/composites two image layers',
|
|
244
|
+
capabilities: ['composition', 'blend'],
|
|
245
|
+
inputs: [
|
|
246
|
+
{ name: 'base', type: 'image', required: true },
|
|
247
|
+
{ name: 'overlay', type: 'image', required: true },
|
|
248
|
+
],
|
|
249
|
+
outputs: [{ name: 'output', type: 'image' }],
|
|
250
|
+
params: {
|
|
251
|
+
mode: { type: 'string', default: 'normal', enum: ['normal', 'multiply', 'screen', 'overlay', 'add'] },
|
|
252
|
+
opacity: { type: 'float', default: 1.0, min: 0, max: 1 },
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
type: 'composition/timeline',
|
|
258
|
+
category: 'composition',
|
|
259
|
+
icon: 'timer',
|
|
260
|
+
driver: {
|
|
261
|
+
description: 'Assembles layers into a timeline for rendering',
|
|
262
|
+
capabilities: ['composition', 'timeline'],
|
|
263
|
+
inputs: [
|
|
264
|
+
{ name: 'layers', type: 'any', required: true },
|
|
265
|
+
{ name: 'dynamics', type: 'skeleton' },
|
|
266
|
+
],
|
|
267
|
+
outputs: [{ name: 'timeline', type: 'timeline' }],
|
|
268
|
+
params: {
|
|
269
|
+
fps: { type: 'int', default: 30, min: 1, max: 120 },
|
|
270
|
+
duration: { type: 'string', default: 'auto' },
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
// ─── Output ────────────────────────────────────────────────────────
|
|
276
|
+
{
|
|
277
|
+
type: 'output/viewport',
|
|
278
|
+
category: 'output',
|
|
279
|
+
icon: 'desktop_windows',
|
|
280
|
+
driver: {
|
|
281
|
+
description: 'Preview viewport for real-time monitoring',
|
|
282
|
+
capabilities: ['output', 'preview'],
|
|
283
|
+
inputs: [{ name: 'timeline', type: 'timeline', required: true }],
|
|
284
|
+
outputs: [],
|
|
285
|
+
params: {
|
|
286
|
+
width: { type: 'int', default: 1080, min: 1 },
|
|
287
|
+
height: { type: 'int', default: 1920, min: 1 },
|
|
288
|
+
background: { type: 'string', default: '#000000' },
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
type: 'output/render',
|
|
294
|
+
category: 'output',
|
|
295
|
+
icon: 'videocam',
|
|
296
|
+
driver: {
|
|
297
|
+
description: 'Renders timeline to video file',
|
|
298
|
+
capabilities: ['output', 'render'],
|
|
299
|
+
inputs: [{ name: 'timeline', type: 'timeline', required: true }],
|
|
300
|
+
outputs: [],
|
|
301
|
+
params: {
|
|
302
|
+
format: { type: 'string', default: 'mp4', enum: ['mp4', 'webm', 'mov'] },
|
|
303
|
+
codec: { type: 'string', default: 'h264', enum: ['h264', 'h265', 'vp9', 'av1'] },
|
|
304
|
+
quality: { type: 'string', default: 'high', enum: ['low', 'medium', 'high', 'lossless'] },
|
|
305
|
+
preset: { type: 'string', default: 'vertical', enum: ['vertical', 'horizontal', 'square'] },
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
];
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Register the video pack
|
|
313
|
+
*/
|
|
314
|
+
export function registerVideoPack() {
|
|
315
|
+
registerPack({
|
|
316
|
+
name: 'video',
|
|
317
|
+
socketTypes,
|
|
318
|
+
nodes,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Auto-register when imported
|
|
323
|
+
registerVideoPack();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symbiote-node",
|
|
3
|
-
"version": "0.3.0-alpha.
|
|
3
|
+
"version": "0.3.0-alpha.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Visual Node Graph Editor + Execution Engine — extensible, themeable graph editor and runtime built on Symbiote.js",
|
|
6
6
|
"main": "index.js",
|
|
@@ -56,4 +56,4 @@
|
|
|
56
56
|
"engines": {
|
|
57
57
|
"node": ">=18.0.0"
|
|
58
58
|
}
|
|
59
|
-
}
|
|
59
|
+
}
|
package/web/app.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// @ctx .context/web/app.ctx
|
|
2
2
|
import{Layout as e,LayoutTree as t,applyTheme as n,CARBON as o}from"symbiote-node";
|
|
3
|
+
import{followController}from"./follow-controller.js";
|
|
4
|
+
import"./components/follow-ribbon.js";
|
|
3
5
|
import{state as a,subscribe as s,onEvent as i,call as r,connect as c}from"./state.js";
|
|
4
6
|
import"./panels/file-tree.js";
|
|
5
7
|
import"./panels/code-viewer.js";
|
|
@@ -16,9 +18,10 @@ export const baseUrl=new URL(".",import.meta.url).href;const l=baseUrl;
|
|
|
16
18
|
export async function api(e,t={}){if(a.connected&&e.startsWith("/api/")){const n=await async function(e,t){const n={"/api/skeleton":{name:"get_skeleton",args:e=>({path:e.path})},"/api/file":{name:"compact",args:e=>({action:"compact_file",path:e.path,beautify:!0})},"/api/analysis":{name:"analyze",args:e=>({action:"full_analysis",path:e.path})},"/api/analysis-summary":{name:"analyze",args:e=>({action:"analysis_summary",path:e.path})},"/api/deps":{name:"navigate",args:e=>({action:"deps",symbol:e.symbol})},"/api/usages":{name:"navigate",args:e=>({action:"usages",symbol:e.symbol})},"/api/expand":{name:"navigate",args:e=>({action:"expand",symbol:e.symbol})},"/api/chain":{name:"navigate",args:e=>({action:"call_chain",from:e.from,to:e.to})}}[e];return n?r(n.name,n.args(t)):null}(e,t);if(null!==n)return n}const n=new URLSearchParams(t).toString(),o=e.replace(/^\//, ""),s=n?`${l}${o}?${n}`:`${l}${o}`,i=await fetch(s);if(!i.ok)throw new Error(`API error: ${i.status}`);return i.json()}
|
|
17
19
|
export const events=new EventTarget;
|
|
18
20
|
export function emit(e,t={}){events.dispatchEvent(new CustomEvent(e,{detail:t}))}
|
|
19
|
-
const p={"file-tree":{title:"Files",icon:"folder",component:"pg-file-tree"},"code-viewer":{title:"Code",icon:"code",component:"pg-code-viewer"},"ctx-panel":{title:"Documentation",icon:"description",component:"pg-ctx-panel"},"dep-graph":{title:"Dependencies",icon:"account_tree",component:"pg-dep-graph"},health:{title:"Health",icon:"analytics",component:"pg-health-panel"},monitor:{title:"Live Monitor",icon:"monitor_heart",component:"pg-live-monitor"},settings:{title:"Settings",icon:"settings",component:"pg-settings-panel"}},m=[{id:"explorer",icon:"folder_open",label:"Explorer"},{id:"graph",icon:"developer_board",label:"Graph"},{id:"analysis",icon:"analytics",label:"Analysis"},{id:"monitor",icon:"monitor_heart",label:"Monitor"},{id:"settings",icon:"settings",label:"Settings"}],d={explorer:()=>t.createSplit("horizontal",t.createPanel("file-tree"),t.createSplit("horizontal",t.createPanel("code-viewer"),t.createPanel("ctx-panel"),.65),.2),graph:()=>t.createSplit("horizontal",t.createPanel("file-tree"),t.createPanel("dep-graph"),.18),analysis:()=>t.createPanel("health"),monitor:()=>t.createPanel("monitor"),settings:()=>t.createPanel("settings")};
|
|
21
|
+
const p={"file-tree":{title:"Files",icon:"folder",component:"pg-file-tree"},"code-viewer":{title:"Code",icon:"code",component:"pg-code-viewer"},"ctx-panel":{title:"Documentation",icon:"description",component:"pg-ctx-panel"},"dep-graph":{title:"Dependencies",icon:"account_tree",component:"pg-dep-graph"},health:{title:"Health",icon:"analytics",component:"pg-health-panel"},monitor:{title:"Live Monitor",icon:"monitor_heart",component:"pg-live-monitor"},settings:{title:"Settings",icon:"settings",component:"pg-settings-panel"}},m=[{id:"explorer",icon:"folder_open",label:"Explorer"},{id:"graph",icon:"developer_board",label:"Graph"},{id:"follow",icon:"smart_toy",label:"Follow"},{id:"analysis",icon:"analytics",label:"Analysis"},{id:"monitor",icon:"monitor_heart",label:"Monitor"},{id:"settings",icon:"settings",label:"Settings"}],d={explorer:()=>t.createSplit("horizontal",t.createPanel("file-tree"),t.createSplit("horizontal",t.createPanel("code-viewer"),t.createPanel("ctx-panel"),.65),.2),graph:()=>t.createSplit("horizontal",t.createPanel("file-tree"),t.createPanel("dep-graph"),.18),follow:()=>t.createSplit("horizontal",t.createPanel("file-tree"),t.createSplit("vertical",t.createSplit("horizontal",t.createPanel("dep-graph"),t.createPanel("code-viewer"),.65),t.createPanel("monitor"),.72),.12),analysis:()=>t.createPanel("health"),monitor:()=>t.createPanel("monitor"),settings:()=>t.createPanel("settings")};
|
|
20
22
|
async function u(){(function(){n(document.documentElement,o);const e=document.querySelector(".app-workspace"),t=document.createElement("layout-sidebar");e.prepend(t);const a=e.querySelector(".app-content"),s=document.createElement("panel-layout");s.setAttribute("storage-key","pg-explorer-layout"),s.setAttribute("min-panel-size","150"),s.id="main-layout",a.appendChild(s),requestAnimationFrame(()=>{for(const[e,t]of Object.entries(p))s.registerPanelType(e,t);let lastSection="";function e(){const e=location.hash.replace("#","")||"explorer",t=e.indexOf("?"),n=t>=0?e.substring(0,t):e,o=n.indexOf("/"),a=o>=0?n.substring(0,o):n,i=o>=0?n.substring(o+1):"";if(d[a]&&a!==lastSection){lastSection=a;s.setLayout(d[a]())}"explorer"===a&&i&&requestAnimationFrame(()=>{state.activeFile=i,emit("file-selected",{path:i,fromRoute:!0})})}t.setSections(m),window.addEventListener("hashchange",e),events.addEventListener("file-selected",e=>{if(e.detail.fromRoute)return;if(e.detail.source==="canvas")return;const t=e.detail.path;const _sec=(location.hash.replace("#","").split("?")[0].split("/")[0])||"explorer";if(t&&_sec==="explorer")history.replaceState(null,"",`#explorer/${t}`)}),localStorage.getItem("pg-explorer-layout")||s.setLayout(d.explorer()),location.hash&&"#"!==location.hash?e():location.hash="explorer"})})(),s("project",e=>{e&&(document.title=`${e.name} — Project Graph`,document.getElementById("project-name").textContent=e.name,document.documentElement.style.setProperty("--project-accent",e.color),g(e.agents))}),events.addEventListener("skeleton-loaded",e=>{const t=e.detail;if(!t)return;state.skeleton=t;const n=new Set;for(const e of Object.values(t.n||{}))e.f&&n.add(e.f);for(const e of Object.keys(t.X||{}))n.add(e);for(const[e,o]of Object.entries(t.f||{}))for(const t of o)n.add("./"===e?t:`${e}${t}`);for(const[e,o]of Object.entries(t.a||{}))for(const t of o)n.add("./"===e?t:`${e}${t}`);const o=document.getElementById("project-files");o&&(o.textContent=`${n.size} files`)}),s("skeleton",e=>{if(!e)return;state.skeleton=e;emit("skeleton-loaded",e)}),s("connected",e=>{const t=document.getElementById("status-indicator");t&&(t.className=e?"status connected":"status disconnected")}),i(e=>{if("agent_connect"===e.type||"agent_disconnect"===e.type)return g(e.agents),void emit("agent-event",e);state.monitorEvents.push(e),state.monitorEvents.length>500&&state.monitorEvents.shift(),emit("tool-event",e)}),c()}
|
|
21
23
|
function g(e){let t=document.getElementById("agent-badge");if(!t){const e=document.querySelector(".app-topbar");if(!e)return;t=document.createElement("span"),t.id="agent-badge",t.className="agent-badge",e.appendChild(t)}t.textContent=e>0?`● ${e} agent${1!==e?"s":""}`:"",t.style.display=e>0?"":"none"}
|
|
22
24
|
function f(){document.querySelector("pg-quick-open")||document.body.appendChild(document.createElement("pg-quick-open"))}
|
|
23
|
-
function h(){const btn=document.getElementById("follow-btn");if(!btn)return;let active=false;btn.addEventListener("click",()=>{active=!active;if(active){btn.setAttribute("data-active","");btn.classList.add("active")}else{btn.removeAttribute("data-active");btn.classList.remove("active")}events.dispatchEvent(new CustomEvent("follow-mode-changed",{detail:{enabled:active}}))})}
|
|
24
|
-
"
|
|
25
|
+
function h(){const btn=document.getElementById("follow-btn");if(!btn)return;let active=false;btn.addEventListener("click",()=>{active=!active;if(active){btn.setAttribute("data-active","");btn.classList.add("active");followController.enable();location.hash="follow"}else{btn.removeAttribute("data-active");btn.classList.remove("active");const prev=followController.getPreviousHash();followController.disable();if(prev&&prev!=="#follow")location.hash=prev.replace(/^#/,"")}events.dispatchEvent(new CustomEvent("follow-mode-changed",{detail:{enabled:active}}))});events.addEventListener("follow-state-changed",e=>{const en=e.detail?.enabled;if(en&&!active){active=true;btn.setAttribute("data-active","");btn.classList.add("active")}else if(!en&&active){active=false;btn.removeAttribute("data-active");btn.classList.remove("active")}});window.addEventListener("hashchange",()=>{const sec=(location.hash.replace("#","").split("?")[0].split("/")[0])||"explorer";if(sec==="follow"&&!active){active=true;btn.setAttribute("data-active","");btn.classList.add("active");followController.enable()}else if(sec!=="follow"&&active){active=false;btn.removeAttribute("data-active");btn.classList.remove("active");followController.disable()}})}
|
|
26
|
+
function _initRibbon(){if(!document.querySelector("follow-ribbon"))document.body.appendChild(document.createElement("follow-ribbon"))}
|
|
27
|
+
"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{u(),f(),followController.init(events,emit),h(),_initRibbon()}):(u(),f(),followController.init(events,emit),h(),_initRibbon());
|
|
@@ -16,12 +16,18 @@ const HIT_RADIUS = 14;
|
|
|
16
16
|
function getNodeRadius(node, conns, opts = {}) {
|
|
17
17
|
const hubScale = 1 + Math.min(conns, 8) * 0.1;
|
|
18
18
|
const aScale = opts.scale ?? (node.aScale || 1);
|
|
19
|
-
let
|
|
19
|
+
let baseR = DOT_RADIUS * hubScale * aScale;
|
|
20
20
|
if (node.isGroup) {
|
|
21
|
-
const childCount = node.children?.length ||
|
|
22
|
-
|
|
21
|
+
const childCount = Math.max(2, Math.min(12, node.children?.length || 3));
|
|
22
|
+
const innerR = baseR * Math.max(0.1, 0.18 - (childCount - 3) * 0.008);
|
|
23
|
+
const spacing = innerR * 2.5;
|
|
24
|
+
const orbitR = spacing / (2 * Math.sin(Math.PI / childCount));
|
|
25
|
+
// r = orbitR + innerR + ringW + padding
|
|
26
|
+
// ringW = r * 0.12
|
|
27
|
+
// r = (orbitR + innerR + 2) / 0.88
|
|
28
|
+
return (orbitR + innerR + 2) / 0.88;
|
|
23
29
|
}
|
|
24
|
-
return
|
|
30
|
+
return baseR;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
const NODE_TYPES = ['data', 'action', 'output', 'config', 'external', 'style', 'docs', 'asset'];
|
|
@@ -280,6 +286,17 @@ export class CanvasGraph extends Symbiote {
|
|
|
280
286
|
this._wakeLoop();
|
|
281
287
|
}
|
|
282
288
|
|
|
289
|
+
pulseNode(nodeId, durationMs = 1500) {
|
|
290
|
+
this._pulses = this._pulses || [];
|
|
291
|
+
this._pulses.push({
|
|
292
|
+
id: nodeId,
|
|
293
|
+
startTime: performance.now(),
|
|
294
|
+
duration: durationMs
|
|
295
|
+
});
|
|
296
|
+
this.needsDraw = true;
|
|
297
|
+
this._wakeLoop();
|
|
298
|
+
}
|
|
299
|
+
|
|
283
300
|
flyToNode(nodeId, options = {}) {
|
|
284
301
|
const node = this.graphDB?.nodes.get(nodeId);
|
|
285
302
|
if (node && node.parentId) {
|
|
@@ -617,8 +634,8 @@ export class CanvasGraph extends Symbiote {
|
|
|
617
634
|
nodeWidth: this.renderMode === 'dots' ? DOT_RADIUS * 2 : 160,
|
|
618
635
|
nodeHeight: this.renderMode === 'dots' ? DOT_RADIUS * 2 : 40,
|
|
619
636
|
mode: 'continuous',
|
|
620
|
-
activeGroupId:
|
|
621
|
-
boundaryRadius:
|
|
637
|
+
activeGroupId: this.currentGroupId,
|
|
638
|
+
boundaryRadius: this.currentGroupId ? this.graphDB.nodes.get(this.currentGroupId).w / 2 : null,
|
|
622
639
|
attractors: null,
|
|
623
640
|
};
|
|
624
641
|
|
|
@@ -1063,15 +1080,13 @@ export class CanvasGraph extends Symbiote {
|
|
|
1063
1080
|
currentCtx.fillStyle = this.blendBg(tc[0], tc[1], tc[2], layerOpacity);
|
|
1064
1081
|
currentCtx.fill();
|
|
1065
1082
|
|
|
1083
|
+
let baseR = DOT_RADIUS * hubScale * (node.aScale || 1);
|
|
1066
1084
|
const childCount = Math.max(2, Math.min(12, node.children?.length || 3));
|
|
1067
|
-
const innerR =
|
|
1085
|
+
const innerR = baseR * Math.max(0.1, 0.18 - (childCount - 3) * 0.008);
|
|
1068
1086
|
|
|
1069
1087
|
// Calculate perfect orbit radius to maintain consistent spacing between dots
|
|
1070
1088
|
const spacing = innerR * 2.5; // Gap between dots
|
|
1071
|
-
const
|
|
1072
|
-
// Ensure they never spill out of the golden ring
|
|
1073
|
-
const maxOrbitR = Math.max(0, r - ringW - innerR - 2);
|
|
1074
|
-
const orbitR = Math.min(idealOrbitR, maxOrbitR);
|
|
1089
|
+
const orbitR = spacing / (2 * Math.sin(Math.PI / childCount));
|
|
1075
1090
|
const isHovered = this.hoverNode && this.hoverNode.id === node.id;
|
|
1076
1091
|
node.aRotSpeed = node.aRotSpeed || 0;
|
|
1077
1092
|
const targetRotSpeed = (isActive || isHovered) ? 0.025 : 0;
|
|
@@ -1130,6 +1145,29 @@ export class CanvasGraph extends Symbiote {
|
|
|
1130
1145
|
mainCtx.setTransform(dpr * this.zoom, 0, 0, dpr * this.zoom, dpr * this.panX, dpr * this.panY);
|
|
1131
1146
|
}
|
|
1132
1147
|
drawDepth(0, mainCtx);
|
|
1148
|
+
|
|
1149
|
+
if (this._pulses && this._pulses.length > 0) {
|
|
1150
|
+
const now = performance.now();
|
|
1151
|
+
this._pulses = this._pulses.filter(p => {
|
|
1152
|
+
const elapsed = now - p.startTime;
|
|
1153
|
+
if (elapsed > p.duration) return false;
|
|
1154
|
+
const pos = this.getSmooth(p.id) || this.nodePositions.get(p.id);
|
|
1155
|
+
if (!pos) return false;
|
|
1156
|
+
const progress = elapsed / p.duration;
|
|
1157
|
+
const pulsePhase = (progress * 3) % 1;
|
|
1158
|
+
const r = 20 + (pulsePhase * 80);
|
|
1159
|
+
const opacity = 1 - pulsePhase;
|
|
1160
|
+
mainCtx.beginPath();
|
|
1161
|
+
mainCtx.arc(pos.x, pos.y, r, 0, Math.PI * 2);
|
|
1162
|
+
mainCtx.fillStyle = `rgba(76, 139, 245, ${opacity * 0.4})`;
|
|
1163
|
+
mainCtx.fill();
|
|
1164
|
+
mainCtx.lineWidth = 2;
|
|
1165
|
+
mainCtx.strokeStyle = `rgba(76, 139, 245, ${opacity * 0.8})`;
|
|
1166
|
+
mainCtx.stroke();
|
|
1167
|
+
this.needsDraw = true;
|
|
1168
|
+
return true;
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1133
1171
|
}
|
|
1134
1172
|
|
|
1135
1173
|
const showMenu = this.activeNode && !this.dragNode && !this.deactivating;
|
|
@@ -1557,6 +1595,7 @@ export class CanvasGraph extends Symbiote {
|
|
|
1557
1595
|
});
|
|
1558
1596
|
|
|
1559
1597
|
this.canvas.addEventListener('pointerup', (e) => {
|
|
1598
|
+
const draggedNode = this.dragNode;
|
|
1560
1599
|
if (this.dragNode) {
|
|
1561
1600
|
this.worker.postMessage({ type: 'unpin', id: this.dragNode.id });
|
|
1562
1601
|
this.dragNode = null;
|
|
@@ -29,6 +29,6 @@ this.$.highlighted=hl;
|
|
|
29
29
|
const e=o.split("\n").length,t=[];for(let o=1;o<=e;o++)t.push(o);this.$.lineNums=t.join("\n");
|
|
30
30
|
}
|
|
31
31
|
}),this.sub("isMarkdown",v=>{this.toggleAttribute("mode-markdown",v)}),this.sub("isImage",v=>{this.toggleAttribute("mode-image",v)})
|
|
32
|
-
}setBasePath(p){this._basePath=p}}
|
|
32
|
+
}setBasePath(p){this._basePath=p}scrollToLine(line){const pre=this.querySelector('.cb-pre');const scroll=this.querySelector('.cb-scroll');if(!pre||!scroll)return;const lineHeight=parseFloat(window.getComputedStyle(pre).lineHeight)||19.2;scroll.scrollTo({top:Math.max(0,(line-1)*lineHeight-scroll.clientHeight/2+lineHeight/2),behavior:'smooth'})}}
|
|
33
33
|
CodeBlock.template='\n <div class="cb-scroll">\n <pre class="cb-gutter" bind="textContent: lineNums"></pre>\n <pre class="cb-pre"><code bind="innerHTML: highlighted"></code></pre>\n <div class="cb-md" bind="innerHTML: highlighted"></div>\n <div class="cb-img-wrap"><img class="cb-img" bind="src: imageSrc"></div>\n </div>\n';
|
|
34
34
|
CodeBlock.rootStyles="\n code-block {\n display: block;\n height: 100%;\n overflow: hidden;\n }\n code-block .cb-scroll {\n display: flex;\n height: 100%;\n overflow: auto;\n align-items: stretch;\n }\n code-block .cb-gutter {\n position: sticky;\n left: 0;\n z-index: 1;\n margin: 0;\n padding: 12px 8px 12px 12px;\n font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;\n font-size: 12px;\n line-height: 1.6;\n text-align: right;\n color: var(--sn-text-dim, hsl(30, 10%, 55%));\n opacity: 0.45;\n background: var(--sn-bg, hsl(37, 30%, 96%));\n border-right: 1px solid var(--sn-node-border, hsl(35, 18%, 88%));\n user-select: none;\n white-space: pre;\n min-width: 32px;\n flex-shrink: 0;\n }\n code-block .cb-pre {\n margin: 0;\n padding: 12px;\n flex: 1;\n min-width: 0;\n font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;\n font-size: 12px;\n line-height: 1.6;\n color: var(--sn-text, hsl(30, 15%, 18%));\n tab-size: 2;\n white-space: pre;\n box-sizing: border-box;\n }\n /* Markdown container — hidden by default */\n code-block .cb-md {\n display: none;\n padding: 20px 28px;\n flex: 1;\n min-width: 0;\n overflow-wrap: break-word;\n word-wrap: break-word;\n line-height: 1.7;\n color: var(--sn-text, hsl(30, 15%, 18%));\n font-family: var(--sn-font, 'Inter', -apple-system, sans-serif);\n font-size: 14px;\n }\n /* Image container — hidden by default */\n code-block .cb-img-wrap {\n display: none;\n flex: 1;\n padding: 20px;\n justify-content: center;\n align-items: center;\n background: repeating-conic-gradient(hsl(30, 10%, 88%) 0% 25%, hsl(30, 10%, 94%) 0% 50%) 0 0 / 16px 16px;\n }\n code-block .cb-img {\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n border-radius: 4px;\n box-shadow: 0 2px 12px rgba(0,0,0,0.12);\n }\n /* In markdown mode: hide code, show md */\n code-block[mode-markdown] .cb-gutter { display: none; }\n code-block[mode-markdown] .cb-pre { display: none; }\n code-block[mode-markdown] .cb-md { display: block; }\n /* In image mode: hide code, show image */\n code-block[mode-image] .cb-gutter { display: none; }\n code-block[mode-image] .cb-pre { display: none; }\n code-block[mode-image] .cb-img-wrap { display: flex; }\n\n /* Markdown styles */\n code-block .md-h { margin: 20px 0 8px; color: var(--sn-text, #222); font-weight: 700; }\n code-block h1.md-h { font-size: 24px; border-bottom: 2px solid var(--sn-node-border, #ddd); padding-bottom: 8px; }\n code-block h2.md-h { font-size: 20px; border-bottom: 1px solid var(--sn-node-border, #ddd); padding-bottom: 6px; }\n code-block h3.md-h { font-size: 16px; }\n code-block h4.md-h { font-size: 14px; }\n code-block .md-p { margin: 8px 0; }\n code-block .md-quote {\n margin: 8px 0;\n padding: 8px 16px;\n border-left: 4px solid var(--sn-cat-server, hsl(210, 45%, 55%));\n background: hsla(210, 40%, 55%, 0.08);\n border-radius: 0 4px 4px 0;\n font-style: italic;\n }\n code-block .md-list {\n margin: 8px 0;\n padding-left: 24px;\n }\n code-block .md-list li {\n margin: 3px 0;\n }\n code-block .md-code-block {\n margin: 12px 0;\n padding: 12px 16px;\n background: var(--sn-bg, hsl(37, 30%, 94%));\n border: 1px solid var(--sn-node-border, hsl(35, 18%, 85%));\n border-radius: 6px;\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 12px;\n line-height: 1.6;\n overflow-x: auto;\n white-space: pre;\n }\n code-block .md-inline-code {\n padding: 1px 5px;\n background: hsla(30, 20%, 50%, 0.12);\n border-radius: 3px;\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 0.9em;\n }\n code-block .md-link {\n color: var(--sn-cat-server, hsl(210, 55%, 50%));\n text-decoration: underline;\n text-decoration-style: dotted;\n }\n code-block .md-link:hover { text-decoration-style: solid; }\n code-block .md-img {\n max-width: 100%;\n height: auto;\n border-radius: 6px;\n margin: 8px 0;\n border: 1px solid var(--sn-node-border, hsl(35, 18%, 85%));\n box-shadow: 0 2px 8px rgba(0,0,0,0.08);\n }\n code-block .md-hr {\n border: none;\n border-top: 1px solid var(--sn-node-border, hsl(35, 18%, 85%));\n margin: 16px 0;\n }\n code-block .md-table {\n width: 100%;\n border-collapse: collapse;\n margin: 12px 0;\n font-size: 13px;\n }\n code-block .md-table th,\n code-block .md-table td {\n padding: 6px 12px;\n border: 1px solid var(--sn-node-border, hsl(35, 18%, 85%));\n text-align: left;\n }\n code-block .md-table th {\n background: hsla(30, 15%, 50%, 0.08);\n font-weight: 600;\n }\n code-block .md-table tr:hover td {\n background: hsla(30, 15%, 50%, 0.04);\n }\n /* Token colors */\n code-block .t-kw { color: rgb(254, 165, 176); }\n code-block .t-str { color: rgb(251, 182, 79); }\n code-block .t-cm { color: rgb(149, 149, 149); font-style: italic; }\n code-block .t-fn { color: rgb(180, 243, 255); }\n code-block .t-num { color: rgb(251, 182, 79); }\n code-block .t-bi { color: rgb(180, 243, 255); }\n code-block .t-prop { color: rgb(238, 131, 252); }\n code-block .t-lit { color: rgb(254, 165, 176); }\n /* JSDoc */\n code-block .t-jd { color: rgb(130, 155, 130); font-style: italic; }\n code-block .t-jd-tag { color: rgb(180, 220, 140); font-style: normal; font-weight: 500; }\n code-block .t-jd-type { color: rgb(130, 210, 240); font-style: normal; }\n",CodeBlock.reg("code-block");
|
|
@@ -20,36 +20,126 @@ export class MiniGraphWidget extends Symbiote {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
_renderSVG(data) {
|
|
23
|
-
// Basic SVG renderer for mini graphs to avoid WebGL context explosion
|
|
24
|
-
// Extracts nodes and links if present in the data
|
|
25
23
|
const nodes = data.nodes || (data.n ? Object.keys(data.n).map(id => ({ id, ...data.n[id] })) : []);
|
|
26
|
-
const
|
|
24
|
+
const rawLinks = data.links || data.e || [];
|
|
25
|
+
|
|
26
|
+
// Some formats use {from, to}, some use {source, target}
|
|
27
|
+
const links = rawLinks.map(l => ({
|
|
28
|
+
from: l.from || l.source,
|
|
29
|
+
to: l.to || l.target
|
|
30
|
+
}));
|
|
27
31
|
|
|
28
32
|
if (!nodes.length) {
|
|
29
33
|
this.$.svgContent = '<text x="10" y="20" fill="var(--sn-text-dim)">No graph data</text>';
|
|
30
34
|
return;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
// Simple circle layout for demonstration
|
|
34
37
|
const width = 300;
|
|
35
38
|
const height = 150;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
|
|
40
|
+
// Initial deterministic positions (circle)
|
|
41
|
+
nodes.forEach((n, i) => {
|
|
42
|
+
const angle = (i / nodes.length) * Math.PI * 2;
|
|
43
|
+
n.x = width / 2 + Math.cos(angle) * (Math.min(width, height) / 4);
|
|
44
|
+
n.y = height / 2 + Math.sin(angle) * (Math.min(width, height) / 4);
|
|
45
|
+
n.vx = 0; n.vy = 0;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const K = 0.05; // Spring
|
|
49
|
+
const L = 40; // Ideal length
|
|
50
|
+
const REP = 300; // Repulsion
|
|
51
|
+
const DAMP = 0.8;
|
|
39
52
|
|
|
53
|
+
// Run 100 iterations
|
|
54
|
+
for (let i = 0; i < 100; i++) {
|
|
55
|
+
for (let j = 0; j < nodes.length; j++) {
|
|
56
|
+
for (let k = j + 1; k < nodes.length; k++) {
|
|
57
|
+
const a = nodes[j], b = nodes[k];
|
|
58
|
+
let dx = a.x - b.x, dy = a.y - b.y;
|
|
59
|
+
let dist = Math.sqrt(dx*dx + dy*dy) || 0.1;
|
|
60
|
+
let f = REP / (dist * dist);
|
|
61
|
+
const fx = (dx / dist) * f;
|
|
62
|
+
const fy = (dy / dist) * f;
|
|
63
|
+
a.vx += fx; a.vy += fy;
|
|
64
|
+
b.vx -= fx; b.vy -= fy;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
links.forEach(link => {
|
|
68
|
+
const a = nodes.find(n => n.id === link.from);
|
|
69
|
+
const b = nodes.find(n => n.id === link.to);
|
|
70
|
+
if (!a || !b) return;
|
|
71
|
+
let dx = b.x - a.x, dy = b.y - a.y;
|
|
72
|
+
let dist = Math.sqrt(dx*dx + dy*dy) || 0.1;
|
|
73
|
+
let f = (dist - L) * K;
|
|
74
|
+
const fx = (dx / dist) * f;
|
|
75
|
+
const fy = (dy / dist) * f;
|
|
76
|
+
a.vx += fx; a.vy += fy;
|
|
77
|
+
b.vx -= fx; b.vy -= fy;
|
|
78
|
+
});
|
|
79
|
+
nodes.forEach(n => {
|
|
80
|
+
n.vx += (width/2 - n.x) * 0.015;
|
|
81
|
+
n.vy += (height/2 - n.y) * 0.015;
|
|
82
|
+
n.vx *= DAMP; n.vy *= DAMP;
|
|
83
|
+
n.x += n.vx; n.y += n.vy;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Now build SVG
|
|
40
88
|
let svg = '';
|
|
41
89
|
|
|
42
|
-
//
|
|
43
|
-
|
|
90
|
+
// Bounds and scaling to fit within 300x150
|
|
91
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
92
|
+
nodes.forEach(n => {
|
|
93
|
+
minX = Math.min(minX, n.x);
|
|
94
|
+
minY = Math.min(minY, n.y);
|
|
95
|
+
maxX = Math.max(maxX, n.x);
|
|
96
|
+
maxY = Math.max(maxY, n.y);
|
|
97
|
+
});
|
|
44
98
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
99
|
+
const pad = 20;
|
|
100
|
+
const gW = Math.max(1, maxX - minX);
|
|
101
|
+
const gH = Math.max(1, maxY - minY);
|
|
102
|
+
const scale = Math.min((width - pad*2) / gW, (height - pad*2) / gH, 1.2);
|
|
103
|
+
const cx = (minX + maxX) / 2;
|
|
104
|
+
const cy = (minY + maxY) / 2;
|
|
105
|
+
const offX = width / 2 - cx * scale;
|
|
106
|
+
const offY = height / 2 - cy * scale;
|
|
107
|
+
|
|
108
|
+
// Draw Links
|
|
109
|
+
svg += '<g stroke="var(--sn-edge, #666)" stroke-width="1.5" opacity="0.5">';
|
|
110
|
+
links.forEach(link => {
|
|
111
|
+
const a = nodes.find(n => n.id === link.from);
|
|
112
|
+
const b = nodes.find(n => n.id === link.to);
|
|
113
|
+
if (!a || !b) return;
|
|
114
|
+
const x1 = a.x * scale + offX;
|
|
115
|
+
const y1 = a.y * scale + offY;
|
|
116
|
+
const x2 = b.x * scale + offX;
|
|
117
|
+
const y2 = b.y * scale + offY;
|
|
118
|
+
|
|
119
|
+
// Curved paths
|
|
120
|
+
const dx = x2 - x1, dy = y2 - y1;
|
|
121
|
+
const cx1 = x1 + dx * 0.3 - dy * 0.1;
|
|
122
|
+
const cy1 = y1 + dy * 0.3 + dx * 0.1;
|
|
123
|
+
const cx2 = x1 + dx * 0.7 - dy * 0.1;
|
|
124
|
+
const cy2 = y1 + dy * 0.7 + dx * 0.1;
|
|
125
|
+
|
|
126
|
+
svg += `<path d="M${x1},${y1} C${cx1},${cy1} ${cx2},${cy2} ${x2},${y2}" fill="none" />`;
|
|
127
|
+
});
|
|
128
|
+
svg += '</g>';
|
|
129
|
+
|
|
130
|
+
// Draw Nodes
|
|
131
|
+
svg += '<g>';
|
|
132
|
+
nodes.forEach(n => {
|
|
133
|
+
const x = n.x * scale + offX;
|
|
134
|
+
const y = n.y * scale + offY;
|
|
135
|
+
const tc = n.type === 'action' ? '#ff968c' :
|
|
136
|
+
n.type === 'output' ? '#78d2aa' :
|
|
137
|
+
n.type === 'config' ? '#ffc878' : '#78b4ff';
|
|
138
|
+
|
|
139
|
+
svg += `<circle cx="${x}" cy="${y}" r="4" fill="${tc}"></circle>`;
|
|
51
140
|
svg += `<text x="${x + 6}" y="${y + 3}" fill="var(--sn-text)" font-size="10">${this._esc(n.id || n.name || 'node')}</text>`;
|
|
52
141
|
});
|
|
142
|
+
svg += '</g>';
|
|
53
143
|
|
|
54
144
|
this.$.svgContent = svg;
|
|
55
145
|
}
|