vargai 0.3.1 → 0.4.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -38
- package/biome.json +6 -1
- package/docs/index.html +1130 -0
- package/docs/prompting.md +326 -0
- package/docs/react.md +834 -0
- package/package.json +11 -6
- package/src/ai-sdk/index.ts +2 -21
- package/src/cli/commands/index.ts +1 -4
- package/src/cli/commands/render.tsx +71 -0
- package/src/cli/index.ts +2 -0
- package/src/react/cli.ts +52 -0
- package/src/react/elements.ts +146 -0
- package/src/react/examples/branching.tsx +66 -0
- package/src/react/examples/captions-demo.tsx +37 -0
- package/src/react/examples/character-video.tsx +84 -0
- package/src/react/examples/grid.tsx +53 -0
- package/src/react/examples/layouts-demo.tsx +57 -0
- package/src/react/examples/madi.tsx +60 -0
- package/src/react/examples/music-test.tsx +35 -0
- package/src/react/examples/onlyfans-1m/workflow.tsx +88 -0
- package/src/react/examples/orange-portrait.tsx +41 -0
- package/src/react/examples/split-element-demo.tsx +60 -0
- package/src/react/examples/split-layout-demo.tsx +60 -0
- package/src/react/examples/split.tsx +41 -0
- package/src/react/examples/video-grid.tsx +46 -0
- package/src/react/index.ts +43 -0
- package/src/react/layouts/grid.tsx +28 -0
- package/src/react/layouts/index.ts +2 -0
- package/src/react/layouts/split.tsx +20 -0
- package/src/react/react.test.ts +309 -0
- package/src/react/render.ts +21 -0
- package/src/react/renderers/animate.ts +59 -0
- package/src/react/renderers/captions.ts +297 -0
- package/src/react/renderers/clip.ts +248 -0
- package/src/react/renderers/context.ts +17 -0
- package/src/react/renderers/image.ts +109 -0
- package/src/react/renderers/index.ts +22 -0
- package/src/react/renderers/music.ts +60 -0
- package/src/react/renderers/packshot.ts +84 -0
- package/src/react/renderers/progress.ts +173 -0
- package/src/react/renderers/render.ts +243 -0
- package/src/react/renderers/slider.ts +69 -0
- package/src/react/renderers/speech.ts +53 -0
- package/src/react/renderers/split.ts +91 -0
- package/src/react/renderers/subtitle.ts +16 -0
- package/src/react/renderers/swipe.ts +75 -0
- package/src/react/renderers/title.ts +17 -0
- package/src/react/renderers/utils.ts +124 -0
- package/src/react/renderers/video.ts +127 -0
- package/src/react/runtime/jsx-dev-runtime.ts +43 -0
- package/src/react/runtime/jsx-runtime.ts +35 -0
- package/src/react/types.ts +232 -0
- package/src/studio/index.ts +26 -0
- package/src/studio/scanner.ts +102 -0
- package/src/studio/server.ts +554 -0
- package/src/studio/stages.ts +251 -0
- package/src/studio/step-renderer.ts +279 -0
- package/src/studio/types.ts +60 -0
- package/src/studio/ui/cache.html +303 -0
- package/src/studio/ui/index.html +1820 -0
- package/tsconfig.cli.json +8 -0
- package/tsconfig.json +3 -1
- package/bun.lock +0 -1255
- package/docs/plan.md +0 -66
- package/docs/todo.md +0 -14
- package/src/ai-sdk/middleware/index.ts +0 -25
- package/src/ai-sdk/middleware/placeholder.ts +0 -111
- package/src/ai-sdk/middleware/wrap-image-model.ts +0 -86
- package/src/ai-sdk/middleware/wrap-music-model.ts +0 -108
- package/src/ai-sdk/middleware/wrap-video-model.ts +0 -115
- /package/docs/{varg-sdk.md → sdk.md} +0 -0
- /package/src/ai-sdk/providers/{elevenlabs.ts → elevenlabs-provider.ts} +0 -0
- /package/src/ai-sdk/providers/{fal.ts → fal-provider.ts} +0 -0
|
@@ -0,0 +1,1820 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>varg studio</title>
|
|
7
|
+
<link rel="stylesheet" href="https://unpkg.com/drawflow@0.0.60/dist/drawflow.min.css" />
|
|
8
|
+
<style>
|
|
9
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #0a0a0a;
|
|
12
|
+
--bg-secondary: #141414;
|
|
13
|
+
--border: #222;
|
|
14
|
+
--border-hover: #444;
|
|
15
|
+
--text: #fafafa;
|
|
16
|
+
--text-muted: #888;
|
|
17
|
+
--accent: #3b82f6;
|
|
18
|
+
--accent-hover: #2563eb;
|
|
19
|
+
--success: #22c55e;
|
|
20
|
+
--error: #ef4444;
|
|
21
|
+
}
|
|
22
|
+
body {
|
|
23
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
24
|
+
background: var(--bg);
|
|
25
|
+
color: var(--text);
|
|
26
|
+
height: 100vh;
|
|
27
|
+
overflow: hidden;
|
|
28
|
+
}
|
|
29
|
+
header {
|
|
30
|
+
height: 48px;
|
|
31
|
+
padding: 0 1rem;
|
|
32
|
+
border-bottom: 1px solid var(--border);
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
justify-content: space-between;
|
|
36
|
+
gap: 1rem;
|
|
37
|
+
}
|
|
38
|
+
.logo {
|
|
39
|
+
font-weight: 600;
|
|
40
|
+
font-size: 0.875rem;
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: 0.5rem;
|
|
44
|
+
}
|
|
45
|
+
.nav {
|
|
46
|
+
display: flex;
|
|
47
|
+
gap: 0.25rem;
|
|
48
|
+
}
|
|
49
|
+
.nav-btn {
|
|
50
|
+
padding: 0.5rem 0.75rem;
|
|
51
|
+
border-radius: 0.375rem;
|
|
52
|
+
border: none;
|
|
53
|
+
background: transparent;
|
|
54
|
+
color: var(--text-muted);
|
|
55
|
+
font-size: 0.8rem;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
transition: all 0.15s;
|
|
58
|
+
}
|
|
59
|
+
.nav-btn:hover { color: var(--text); background: var(--bg-secondary); }
|
|
60
|
+
.nav-btn.active { color: var(--text); background: var(--bg-secondary); }
|
|
61
|
+
.header-right {
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
gap: 0.5rem;
|
|
65
|
+
}
|
|
66
|
+
select {
|
|
67
|
+
padding: 0.375rem 0.5rem;
|
|
68
|
+
border-radius: 0.375rem;
|
|
69
|
+
border: 1px solid var(--border);
|
|
70
|
+
background: var(--bg-secondary);
|
|
71
|
+
color: var(--text);
|
|
72
|
+
font-size: 0.8rem;
|
|
73
|
+
cursor: pointer;
|
|
74
|
+
}
|
|
75
|
+
select:hover { border-color: var(--border-hover); }
|
|
76
|
+
.main {
|
|
77
|
+
display: flex;
|
|
78
|
+
height: calc(100vh - 48px);
|
|
79
|
+
}
|
|
80
|
+
.editor-pane {
|
|
81
|
+
flex: 1;
|
|
82
|
+
border-right: 1px solid var(--border);
|
|
83
|
+
display: flex;
|
|
84
|
+
flex-direction: column;
|
|
85
|
+
}
|
|
86
|
+
.preview-pane {
|
|
87
|
+
width: 480px;
|
|
88
|
+
display: flex;
|
|
89
|
+
flex-direction: column;
|
|
90
|
+
overflow: hidden;
|
|
91
|
+
}
|
|
92
|
+
#editor-container {
|
|
93
|
+
flex: 1;
|
|
94
|
+
overflow: hidden;
|
|
95
|
+
}
|
|
96
|
+
.preview-header {
|
|
97
|
+
padding: 0.75rem 1rem;
|
|
98
|
+
border-bottom: 1px solid var(--border);
|
|
99
|
+
font-size: 0.75rem;
|
|
100
|
+
color: var(--text-muted);
|
|
101
|
+
text-transform: uppercase;
|
|
102
|
+
letter-spacing: 0.05em;
|
|
103
|
+
flex-shrink: 0;
|
|
104
|
+
}
|
|
105
|
+
.preview-content {
|
|
106
|
+
flex: 1;
|
|
107
|
+
background: #000;
|
|
108
|
+
position: relative;
|
|
109
|
+
overflow: hidden;
|
|
110
|
+
min-height: 200px;
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
justify-content: center;
|
|
114
|
+
}
|
|
115
|
+
.preview-video {
|
|
116
|
+
max-width: 100%;
|
|
117
|
+
max-height: 100%;
|
|
118
|
+
object-fit: contain;
|
|
119
|
+
}
|
|
120
|
+
.preview-placeholder {
|
|
121
|
+
color: var(--text-muted);
|
|
122
|
+
font-size: 0.875rem;
|
|
123
|
+
text-align: center;
|
|
124
|
+
position: absolute;
|
|
125
|
+
top: 50%;
|
|
126
|
+
left: 50%;
|
|
127
|
+
transform: translate(-50%, -50%);
|
|
128
|
+
}
|
|
129
|
+
.preview-placeholder-icon {
|
|
130
|
+
font-size: 2rem;
|
|
131
|
+
margin-bottom: 0.5rem;
|
|
132
|
+
opacity: 0.5;
|
|
133
|
+
}
|
|
134
|
+
.progress-section {
|
|
135
|
+
padding: 1rem;
|
|
136
|
+
border-top: 1px solid var(--border);
|
|
137
|
+
flex-shrink: 0;
|
|
138
|
+
}
|
|
139
|
+
.progress-bar-container {
|
|
140
|
+
height: 4px;
|
|
141
|
+
background: var(--bg-secondary);
|
|
142
|
+
border-radius: 2px;
|
|
143
|
+
overflow: hidden;
|
|
144
|
+
margin-bottom: 0.5rem;
|
|
145
|
+
}
|
|
146
|
+
.progress-bar {
|
|
147
|
+
height: 100%;
|
|
148
|
+
background: var(--accent);
|
|
149
|
+
width: 0%;
|
|
150
|
+
transition: width 0.3s ease;
|
|
151
|
+
}
|
|
152
|
+
.progress-bar.complete { background: var(--success); }
|
|
153
|
+
.progress-bar.error { background: var(--error); }
|
|
154
|
+
.progress-text {
|
|
155
|
+
font-size: 0.75rem;
|
|
156
|
+
color: var(--text-muted);
|
|
157
|
+
}
|
|
158
|
+
.controls {
|
|
159
|
+
padding: 1rem;
|
|
160
|
+
border-top: 1px solid var(--border);
|
|
161
|
+
display: flex;
|
|
162
|
+
gap: 0.5rem;
|
|
163
|
+
flex-shrink: 0;
|
|
164
|
+
}
|
|
165
|
+
.btn {
|
|
166
|
+
padding: 0.5rem 1rem;
|
|
167
|
+
border-radius: 0.375rem;
|
|
168
|
+
border: 1px solid var(--border);
|
|
169
|
+
background: var(--bg-secondary);
|
|
170
|
+
color: var(--text);
|
|
171
|
+
font-size: 0.8rem;
|
|
172
|
+
cursor: pointer;
|
|
173
|
+
transition: all 0.15s;
|
|
174
|
+
display: flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
gap: 0.375rem;
|
|
177
|
+
}
|
|
178
|
+
.btn:hover { border-color: var(--border-hover); }
|
|
179
|
+
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
180
|
+
.btn-primary {
|
|
181
|
+
background: var(--accent);
|
|
182
|
+
border-color: var(--accent);
|
|
183
|
+
}
|
|
184
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
185
|
+
.btn-danger {
|
|
186
|
+
background: var(--error);
|
|
187
|
+
border-color: var(--error);
|
|
188
|
+
}
|
|
189
|
+
.btn-danger:hover { background: #dc2626; border-color: #dc2626; }
|
|
190
|
+
.btn-toggle {
|
|
191
|
+
position: relative;
|
|
192
|
+
}
|
|
193
|
+
.btn-toggle.active {
|
|
194
|
+
background: var(--success);
|
|
195
|
+
border-color: var(--success);
|
|
196
|
+
}
|
|
197
|
+
.history-section {
|
|
198
|
+
padding: 1rem;
|
|
199
|
+
border-top: 1px solid var(--border);
|
|
200
|
+
flex-shrink: 0;
|
|
201
|
+
}
|
|
202
|
+
.history-title {
|
|
203
|
+
font-size: 0.75rem;
|
|
204
|
+
color: var(--text-muted);
|
|
205
|
+
text-transform: uppercase;
|
|
206
|
+
letter-spacing: 0.05em;
|
|
207
|
+
margin-bottom: 0.75rem;
|
|
208
|
+
}
|
|
209
|
+
.history-grid {
|
|
210
|
+
display: flex;
|
|
211
|
+
gap: 0.5rem;
|
|
212
|
+
overflow-x: auto;
|
|
213
|
+
padding-bottom: 0.5rem;
|
|
214
|
+
}
|
|
215
|
+
.history-item {
|
|
216
|
+
width: 80px;
|
|
217
|
+
flex-shrink: 0;
|
|
218
|
+
cursor: pointer;
|
|
219
|
+
border-radius: 0.375rem;
|
|
220
|
+
overflow: hidden;
|
|
221
|
+
border: 2px solid transparent;
|
|
222
|
+
transition: border-color 0.15s;
|
|
223
|
+
}
|
|
224
|
+
.history-item:hover { border-color: var(--border-hover); }
|
|
225
|
+
.history-item.active { border-color: var(--accent); }
|
|
226
|
+
.history-thumb {
|
|
227
|
+
width: 100%;
|
|
228
|
+
aspect-ratio: 9/16;
|
|
229
|
+
background: var(--bg-secondary);
|
|
230
|
+
object-fit: cover;
|
|
231
|
+
}
|
|
232
|
+
.history-meta {
|
|
233
|
+
padding: 0.25rem;
|
|
234
|
+
font-size: 0.625rem;
|
|
235
|
+
color: var(--text-muted);
|
|
236
|
+
text-align: center;
|
|
237
|
+
}
|
|
238
|
+
.spinner {
|
|
239
|
+
width: 24px;
|
|
240
|
+
height: 24px;
|
|
241
|
+
border: 2px solid var(--border);
|
|
242
|
+
border-top-color: var(--accent);
|
|
243
|
+
border-radius: 50%;
|
|
244
|
+
animation: spin 0.8s linear infinite;
|
|
245
|
+
}
|
|
246
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
247
|
+
|
|
248
|
+
/* Step mode timeline */
|
|
249
|
+
.stage-timeline {
|
|
250
|
+
padding: 1rem;
|
|
251
|
+
border-top: 1px solid var(--border);
|
|
252
|
+
flex-shrink: 0;
|
|
253
|
+
display: none;
|
|
254
|
+
}
|
|
255
|
+
.stage-timeline.active {
|
|
256
|
+
display: block;
|
|
257
|
+
}
|
|
258
|
+
.stage-timeline-header {
|
|
259
|
+
display: flex;
|
|
260
|
+
align-items: center;
|
|
261
|
+
justify-content: space-between;
|
|
262
|
+
margin-bottom: 0.75rem;
|
|
263
|
+
}
|
|
264
|
+
.stage-timeline-title {
|
|
265
|
+
font-size: 0.75rem;
|
|
266
|
+
color: var(--text-muted);
|
|
267
|
+
text-transform: uppercase;
|
|
268
|
+
letter-spacing: 0.05em;
|
|
269
|
+
}
|
|
270
|
+
.stage-timeline-hint {
|
|
271
|
+
font-size: 0.625rem;
|
|
272
|
+
color: var(--text-muted);
|
|
273
|
+
opacity: 0.7;
|
|
274
|
+
}
|
|
275
|
+
.stage-timeline-hint kbd {
|
|
276
|
+
background: var(--bg-secondary);
|
|
277
|
+
border: 1px solid var(--border);
|
|
278
|
+
border-radius: 3px;
|
|
279
|
+
padding: 0.125rem 0.375rem;
|
|
280
|
+
font-family: inherit;
|
|
281
|
+
font-size: 0.625rem;
|
|
282
|
+
}
|
|
283
|
+
.stage-track {
|
|
284
|
+
display: flex;
|
|
285
|
+
gap: 0.5rem;
|
|
286
|
+
overflow-x: auto;
|
|
287
|
+
padding-bottom: 0.5rem;
|
|
288
|
+
scrollbar-width: thin;
|
|
289
|
+
scrollbar-color: var(--border) transparent;
|
|
290
|
+
}
|
|
291
|
+
.stage-track::-webkit-scrollbar {
|
|
292
|
+
height: 4px;
|
|
293
|
+
}
|
|
294
|
+
.stage-track::-webkit-scrollbar-track {
|
|
295
|
+
background: transparent;
|
|
296
|
+
}
|
|
297
|
+
.stage-track::-webkit-scrollbar-thumb {
|
|
298
|
+
background: var(--border);
|
|
299
|
+
border-radius: 2px;
|
|
300
|
+
}
|
|
301
|
+
.stage-item {
|
|
302
|
+
flex-shrink: 0;
|
|
303
|
+
width: 100px;
|
|
304
|
+
background: var(--bg-secondary);
|
|
305
|
+
border: 1px solid var(--border);
|
|
306
|
+
border-radius: 0.5rem;
|
|
307
|
+
padding: 0.625rem;
|
|
308
|
+
cursor: default;
|
|
309
|
+
transition: all 0.2s ease;
|
|
310
|
+
position: relative;
|
|
311
|
+
overflow: hidden;
|
|
312
|
+
}
|
|
313
|
+
.stage-item::before {
|
|
314
|
+
content: '';
|
|
315
|
+
position: absolute;
|
|
316
|
+
top: 0;
|
|
317
|
+
left: 0;
|
|
318
|
+
right: 0;
|
|
319
|
+
height: 2px;
|
|
320
|
+
background: transparent;
|
|
321
|
+
transition: background 0.2s ease;
|
|
322
|
+
}
|
|
323
|
+
.stage-item.pending {
|
|
324
|
+
opacity: 0.5;
|
|
325
|
+
}
|
|
326
|
+
.stage-item.running {
|
|
327
|
+
border-color: var(--accent);
|
|
328
|
+
box-shadow: 0 0 12px rgba(59, 130, 246, 0.25);
|
|
329
|
+
}
|
|
330
|
+
.stage-item.running::before {
|
|
331
|
+
background: var(--accent);
|
|
332
|
+
animation: stage-pulse 1.5s ease-in-out infinite;
|
|
333
|
+
}
|
|
334
|
+
@keyframes stage-pulse {
|
|
335
|
+
0%, 100% { opacity: 1; }
|
|
336
|
+
50% { opacity: 0.4; }
|
|
337
|
+
}
|
|
338
|
+
.stage-item.complete {
|
|
339
|
+
cursor: pointer;
|
|
340
|
+
border-color: var(--success);
|
|
341
|
+
}
|
|
342
|
+
.stage-item.complete::before {
|
|
343
|
+
background: var(--success);
|
|
344
|
+
}
|
|
345
|
+
.stage-item.complete:hover {
|
|
346
|
+
border-color: var(--success);
|
|
347
|
+
background: rgba(34, 197, 94, 0.1);
|
|
348
|
+
}
|
|
349
|
+
.stage-item.error {
|
|
350
|
+
border-color: var(--error);
|
|
351
|
+
}
|
|
352
|
+
.stage-item.error::before {
|
|
353
|
+
background: var(--error);
|
|
354
|
+
}
|
|
355
|
+
.stage-item.selected {
|
|
356
|
+
border-color: var(--accent);
|
|
357
|
+
background: rgba(59, 130, 246, 0.1);
|
|
358
|
+
}
|
|
359
|
+
.stage-icon {
|
|
360
|
+
font-size: 1.25rem;
|
|
361
|
+
margin-bottom: 0.375rem;
|
|
362
|
+
}
|
|
363
|
+
.stage-label {
|
|
364
|
+
font-size: 0.7rem;
|
|
365
|
+
color: var(--text);
|
|
366
|
+
white-space: nowrap;
|
|
367
|
+
overflow: hidden;
|
|
368
|
+
text-overflow: ellipsis;
|
|
369
|
+
margin-bottom: 0.25rem;
|
|
370
|
+
}
|
|
371
|
+
.stage-status {
|
|
372
|
+
font-size: 0.625rem;
|
|
373
|
+
color: var(--text-muted);
|
|
374
|
+
text-transform: uppercase;
|
|
375
|
+
letter-spacing: 0.02em;
|
|
376
|
+
}
|
|
377
|
+
.stage-item.running .stage-status {
|
|
378
|
+
color: var(--accent);
|
|
379
|
+
}
|
|
380
|
+
.stage-item.complete .stage-status {
|
|
381
|
+
color: var(--success);
|
|
382
|
+
}
|
|
383
|
+
.stage-item.error .stage-status {
|
|
384
|
+
color: var(--error);
|
|
385
|
+
}
|
|
386
|
+
.stage-connector {
|
|
387
|
+
flex-shrink: 0;
|
|
388
|
+
width: 24px;
|
|
389
|
+
display: flex;
|
|
390
|
+
align-items: center;
|
|
391
|
+
justify-content: center;
|
|
392
|
+
}
|
|
393
|
+
.stage-connector-line {
|
|
394
|
+
width: 100%;
|
|
395
|
+
height: 2px;
|
|
396
|
+
background: var(--border);
|
|
397
|
+
position: relative;
|
|
398
|
+
}
|
|
399
|
+
.stage-connector-line.complete {
|
|
400
|
+
background: var(--success);
|
|
401
|
+
}
|
|
402
|
+
.stage-connector-line::after {
|
|
403
|
+
content: '';
|
|
404
|
+
position: absolute;
|
|
405
|
+
right: -4px;
|
|
406
|
+
top: -3px;
|
|
407
|
+
border: 4px solid transparent;
|
|
408
|
+
border-left-color: var(--border);
|
|
409
|
+
}
|
|
410
|
+
.stage-connector-line.complete::after {
|
|
411
|
+
border-left-color: var(--success);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/* Mode toggle */
|
|
415
|
+
.mode-toggle {
|
|
416
|
+
display: flex;
|
|
417
|
+
background: var(--bg-secondary);
|
|
418
|
+
border: 1px solid var(--border);
|
|
419
|
+
border-radius: 0.375rem;
|
|
420
|
+
overflow: hidden;
|
|
421
|
+
}
|
|
422
|
+
.mode-toggle-btn {
|
|
423
|
+
padding: 0.375rem 0.625rem;
|
|
424
|
+
border: none;
|
|
425
|
+
background: transparent;
|
|
426
|
+
color: var(--text-muted);
|
|
427
|
+
font-size: 0.7rem;
|
|
428
|
+
cursor: pointer;
|
|
429
|
+
transition: all 0.15s;
|
|
430
|
+
display: flex;
|
|
431
|
+
align-items: center;
|
|
432
|
+
gap: 0.25rem;
|
|
433
|
+
}
|
|
434
|
+
.mode-toggle-btn:hover {
|
|
435
|
+
color: var(--text);
|
|
436
|
+
}
|
|
437
|
+
.mode-toggle-btn.active {
|
|
438
|
+
background: var(--accent);
|
|
439
|
+
color: var(--text);
|
|
440
|
+
}
|
|
441
|
+
.mode-toggle-btn:first-child {
|
|
442
|
+
border-right: 1px solid var(--border);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/* Next step button */
|
|
446
|
+
.btn-next {
|
|
447
|
+
background: linear-gradient(135deg, var(--accent), #6366f1);
|
|
448
|
+
border-color: transparent;
|
|
449
|
+
position: relative;
|
|
450
|
+
overflow: hidden;
|
|
451
|
+
}
|
|
452
|
+
.btn-next::before {
|
|
453
|
+
content: '';
|
|
454
|
+
position: absolute;
|
|
455
|
+
top: 0;
|
|
456
|
+
left: -100%;
|
|
457
|
+
width: 100%;
|
|
458
|
+
height: 100%;
|
|
459
|
+
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
|
|
460
|
+
transition: left 0.5s;
|
|
461
|
+
}
|
|
462
|
+
.btn-next:hover::before {
|
|
463
|
+
left: 100%;
|
|
464
|
+
}
|
|
465
|
+
.btn-next:hover {
|
|
466
|
+
background: linear-gradient(135deg, var(--accent-hover), #4f46e5);
|
|
467
|
+
}
|
|
468
|
+
.btn-next:disabled {
|
|
469
|
+
background: var(--bg-secondary);
|
|
470
|
+
border-color: var(--border);
|
|
471
|
+
}
|
|
472
|
+
.btn-next:disabled::before {
|
|
473
|
+
display: none;
|
|
474
|
+
}
|
|
475
|
+
.error-banner {
|
|
476
|
+
background: rgba(239, 68, 68, 0.1);
|
|
477
|
+
border: 1px solid var(--error);
|
|
478
|
+
border-radius: 0.375rem;
|
|
479
|
+
padding: 0.75rem 1rem;
|
|
480
|
+
margin: 1rem;
|
|
481
|
+
font-size: 0.8rem;
|
|
482
|
+
color: var(--error);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/* Editor pane header with view toggle */
|
|
486
|
+
.editor-pane-header {
|
|
487
|
+
height: 36px;
|
|
488
|
+
padding: 0 0.75rem;
|
|
489
|
+
border-bottom: 1px solid var(--border);
|
|
490
|
+
display: flex;
|
|
491
|
+
align-items: center;
|
|
492
|
+
justify-content: space-between;
|
|
493
|
+
flex-shrink: 0;
|
|
494
|
+
}
|
|
495
|
+
.view-toggle {
|
|
496
|
+
display: flex;
|
|
497
|
+
background: var(--bg-secondary);
|
|
498
|
+
border: 1px solid var(--border);
|
|
499
|
+
border-radius: 0.375rem;
|
|
500
|
+
overflow: hidden;
|
|
501
|
+
}
|
|
502
|
+
.view-toggle-btn {
|
|
503
|
+
padding: 0.25rem 0.625rem;
|
|
504
|
+
border: none;
|
|
505
|
+
background: transparent;
|
|
506
|
+
color: var(--text-muted);
|
|
507
|
+
font-size: 0.7rem;
|
|
508
|
+
cursor: pointer;
|
|
509
|
+
transition: all 0.15s;
|
|
510
|
+
display: flex;
|
|
511
|
+
align-items: center;
|
|
512
|
+
gap: 0.25rem;
|
|
513
|
+
}
|
|
514
|
+
.view-toggle-btn:hover { color: var(--text); }
|
|
515
|
+
.view-toggle-btn.active {
|
|
516
|
+
background: var(--accent);
|
|
517
|
+
color: var(--text);
|
|
518
|
+
}
|
|
519
|
+
.view-toggle-btn:first-child {
|
|
520
|
+
border-right: 1px solid var(--border);
|
|
521
|
+
}
|
|
522
|
+
.editor-pane-content {
|
|
523
|
+
flex: 1;
|
|
524
|
+
position: relative;
|
|
525
|
+
overflow: hidden;
|
|
526
|
+
}
|
|
527
|
+
#editor-container, #nodes-container {
|
|
528
|
+
position: absolute;
|
|
529
|
+
top: 0;
|
|
530
|
+
left: 0;
|
|
531
|
+
right: 0;
|
|
532
|
+
bottom: 0;
|
|
533
|
+
}
|
|
534
|
+
#nodes-container {
|
|
535
|
+
display: none;
|
|
536
|
+
background: var(--bg);
|
|
537
|
+
}
|
|
538
|
+
#nodes-container.active { display: block; }
|
|
539
|
+
#editor-container.hidden { display: none; }
|
|
540
|
+
|
|
541
|
+
/* Drawflow dark theme */
|
|
542
|
+
#drawflow {
|
|
543
|
+
width: 100%;
|
|
544
|
+
height: 100%;
|
|
545
|
+
background: var(--bg);
|
|
546
|
+
background-size: 20px 20px;
|
|
547
|
+
background-image:
|
|
548
|
+
linear-gradient(to right, #1a1a1a 1px, transparent 1px),
|
|
549
|
+
linear-gradient(to bottom, #1a1a1a 1px, transparent 1px);
|
|
550
|
+
}
|
|
551
|
+
.drawflow .drawflow-node {
|
|
552
|
+
background: var(--bg-secondary);
|
|
553
|
+
border: 1px solid var(--border);
|
|
554
|
+
border-radius: 8px;
|
|
555
|
+
min-width: 160px;
|
|
556
|
+
color: var(--text);
|
|
557
|
+
font-size: 0.8rem;
|
|
558
|
+
}
|
|
559
|
+
.drawflow .drawflow-node.selected {
|
|
560
|
+
border-color: var(--accent);
|
|
561
|
+
box-shadow: 0 0 12px rgba(59, 130, 246, 0.3);
|
|
562
|
+
}
|
|
563
|
+
.drawflow .drawflow-node .inputs, .drawflow .drawflow-node .outputs {
|
|
564
|
+
display: flex;
|
|
565
|
+
flex-direction: column;
|
|
566
|
+
gap: 4px;
|
|
567
|
+
}
|
|
568
|
+
.drawflow .drawflow-node .input, .drawflow .drawflow-node .output {
|
|
569
|
+
width: 12px;
|
|
570
|
+
height: 12px;
|
|
571
|
+
border-radius: 50%;
|
|
572
|
+
border: 2px solid var(--border);
|
|
573
|
+
background: var(--bg);
|
|
574
|
+
}
|
|
575
|
+
.drawflow .drawflow-node .input:hover, .drawflow .drawflow-node .output:hover {
|
|
576
|
+
background: var(--accent);
|
|
577
|
+
border-color: var(--accent);
|
|
578
|
+
}
|
|
579
|
+
.drawflow .connection .main-path {
|
|
580
|
+
stroke: var(--accent);
|
|
581
|
+
stroke-width: 2px;
|
|
582
|
+
}
|
|
583
|
+
.drawflow .connection .main-path:hover {
|
|
584
|
+
stroke-width: 3px;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/* Custom node styles */
|
|
588
|
+
.node-title {
|
|
589
|
+
padding: 8px 12px;
|
|
590
|
+
border-bottom: 1px solid var(--border);
|
|
591
|
+
font-weight: 500;
|
|
592
|
+
display: flex;
|
|
593
|
+
align-items: center;
|
|
594
|
+
gap: 6px;
|
|
595
|
+
background: rgba(255,255,255,0.02);
|
|
596
|
+
border-radius: 7px 7px 0 0;
|
|
597
|
+
}
|
|
598
|
+
.node-title-icon { font-size: 1rem; }
|
|
599
|
+
.node-content {
|
|
600
|
+
padding: 8px;
|
|
601
|
+
}
|
|
602
|
+
.node-preview {
|
|
603
|
+
width: 100%;
|
|
604
|
+
max-height: 120px;
|
|
605
|
+
object-fit: contain;
|
|
606
|
+
border-radius: 4px;
|
|
607
|
+
background: #000;
|
|
608
|
+
}
|
|
609
|
+
.node-preview-placeholder {
|
|
610
|
+
height: 80px;
|
|
611
|
+
display: flex;
|
|
612
|
+
align-items: center;
|
|
613
|
+
justify-content: center;
|
|
614
|
+
background: rgba(0,0,0,0.3);
|
|
615
|
+
border-radius: 4px;
|
|
616
|
+
color: var(--text-muted);
|
|
617
|
+
font-size: 0.7rem;
|
|
618
|
+
}
|
|
619
|
+
.node-label {
|
|
620
|
+
font-size: 0.7rem;
|
|
621
|
+
color: var(--text-muted);
|
|
622
|
+
margin-top: 6px;
|
|
623
|
+
white-space: nowrap;
|
|
624
|
+
overflow: hidden;
|
|
625
|
+
text-overflow: ellipsis;
|
|
626
|
+
}
|
|
627
|
+
.node-status {
|
|
628
|
+
font-size: 0.625rem;
|
|
629
|
+
text-transform: uppercase;
|
|
630
|
+
margin-top: 4px;
|
|
631
|
+
letter-spacing: 0.03em;
|
|
632
|
+
}
|
|
633
|
+
.node-status.pending { color: var(--text-muted); }
|
|
634
|
+
.node-status.running { color: var(--accent); }
|
|
635
|
+
.node-status.complete { color: var(--success); }
|
|
636
|
+
.node-status.error { color: var(--error); }
|
|
637
|
+
.node-btn {
|
|
638
|
+
margin-top: 8px;
|
|
639
|
+
padding: 4px 8px;
|
|
640
|
+
border: 1px solid var(--border);
|
|
641
|
+
border-radius: 4px;
|
|
642
|
+
background: var(--bg);
|
|
643
|
+
color: var(--text);
|
|
644
|
+
font-size: 0.65rem;
|
|
645
|
+
cursor: pointer;
|
|
646
|
+
width: 100%;
|
|
647
|
+
}
|
|
648
|
+
.node-btn:hover {
|
|
649
|
+
border-color: var(--accent);
|
|
650
|
+
background: rgba(59, 130, 246, 0.1);
|
|
651
|
+
}
|
|
652
|
+
.node-btn:disabled {
|
|
653
|
+
opacity: 0.5;
|
|
654
|
+
cursor: not-allowed;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.drawflow-node.complete { border-color: var(--success); }
|
|
658
|
+
.drawflow-node.running {
|
|
659
|
+
border-color: var(--accent);
|
|
660
|
+
box-shadow: 0 0 12px rgba(59, 130, 246, 0.25);
|
|
661
|
+
}
|
|
662
|
+
.drawflow-node.error { border-color: var(--error); }
|
|
663
|
+
</style>
|
|
664
|
+
</head>
|
|
665
|
+
<body>
|
|
666
|
+
<header>
|
|
667
|
+
<div class="logo">
|
|
668
|
+
<span>varg studio</span>
|
|
669
|
+
</div>
|
|
670
|
+
<nav class="nav">
|
|
671
|
+
<a href="/editor" class="nav-btn active">editor</a>
|
|
672
|
+
<a href="/cache" class="nav-btn">cache</a>
|
|
673
|
+
</nav>
|
|
674
|
+
<div class="header-right">
|
|
675
|
+
<select id="template-select">
|
|
676
|
+
<option value="">select template...</option>
|
|
677
|
+
</select>
|
|
678
|
+
</div>
|
|
679
|
+
</header>
|
|
680
|
+
|
|
681
|
+
<main class="main">
|
|
682
|
+
<div class="editor-pane">
|
|
683
|
+
<div class="editor-pane-header">
|
|
684
|
+
<div class="view-toggle">
|
|
685
|
+
<button class="view-toggle-btn active" id="view-code-btn" title="Code view">
|
|
686
|
+
<span>{ }</span> code
|
|
687
|
+
</button>
|
|
688
|
+
<button class="view-toggle-btn" id="view-nodes-btn" title="Node view">
|
|
689
|
+
<span>◎</span> nodes
|
|
690
|
+
</button>
|
|
691
|
+
</div>
|
|
692
|
+
<div style="font-size: 0.7rem; color: var(--text-muted);">
|
|
693
|
+
<span id="view-hint">cmd+enter to run</span>
|
|
694
|
+
</div>
|
|
695
|
+
</div>
|
|
696
|
+
<div class="editor-pane-content">
|
|
697
|
+
<div id="editor-container"></div>
|
|
698
|
+
<div id="nodes-container">
|
|
699
|
+
<div id="drawflow"></div>
|
|
700
|
+
</div>
|
|
701
|
+
</div>
|
|
702
|
+
</div>
|
|
703
|
+
|
|
704
|
+
<div class="preview-pane">
|
|
705
|
+
<div class="preview-header">preview</div>
|
|
706
|
+
|
|
707
|
+
<div class="preview-content" id="preview-content">
|
|
708
|
+
<div class="preview-placeholder">
|
|
709
|
+
<div class="preview-placeholder-icon">▶</div>
|
|
710
|
+
<div>click run to generate</div>
|
|
711
|
+
</div>
|
|
712
|
+
</div>
|
|
713
|
+
|
|
714
|
+
<div class="progress-section" id="progress-section" style="display: none;">
|
|
715
|
+
<div class="progress-bar-container">
|
|
716
|
+
<div class="progress-bar" id="progress-bar"></div>
|
|
717
|
+
</div>
|
|
718
|
+
<div class="progress-text" id="progress-text">ready</div>
|
|
719
|
+
</div>
|
|
720
|
+
|
|
721
|
+
<div class="controls">
|
|
722
|
+
<div class="mode-toggle">
|
|
723
|
+
<button class="mode-toggle-btn active" id="mode-all-btn" title="Run all stages at once">
|
|
724
|
+
<span>⚡</span> all
|
|
725
|
+
</button>
|
|
726
|
+
<button class="mode-toggle-btn" id="mode-step-btn" title="Run stages one by one">
|
|
727
|
+
<span>👣</span> step
|
|
728
|
+
</button>
|
|
729
|
+
</div>
|
|
730
|
+
<button class="btn btn-primary" id="run-btn">
|
|
731
|
+
<span>▶</span> run
|
|
732
|
+
</button>
|
|
733
|
+
<button class="btn btn-next" id="next-btn" style="display: none;">
|
|
734
|
+
<span>→</span> next
|
|
735
|
+
</button>
|
|
736
|
+
<button class="btn btn-toggle" id="auto-btn">
|
|
737
|
+
<span>⟳</span> auto
|
|
738
|
+
</button>
|
|
739
|
+
<button class="btn btn-danger" id="stop-btn" disabled>
|
|
740
|
+
<span>⬜</span> stop
|
|
741
|
+
</button>
|
|
742
|
+
<button class="btn" id="share-btn" style="margin-left: auto;">
|
|
743
|
+
<span>🔗</span> share
|
|
744
|
+
</button>
|
|
745
|
+
</div>
|
|
746
|
+
|
|
747
|
+
<div class="stage-timeline" id="stage-timeline">
|
|
748
|
+
<div class="stage-timeline-header">
|
|
749
|
+
<div class="stage-timeline-title">stages</div>
|
|
750
|
+
<div class="stage-timeline-hint"><kbd>Space</kbd> next step</div>
|
|
751
|
+
</div>
|
|
752
|
+
<div class="stage-track" id="stage-track"></div>
|
|
753
|
+
</div>
|
|
754
|
+
|
|
755
|
+
<div class="history-section">
|
|
756
|
+
<div class="history-title">history</div>
|
|
757
|
+
<div class="history-grid" id="history-grid">
|
|
758
|
+
</div>
|
|
759
|
+
</div>
|
|
760
|
+
</div>
|
|
761
|
+
</main>
|
|
762
|
+
|
|
763
|
+
<script src="https://unpkg.com/drawflow@0.0.60/dist/drawflow.min.js"></script>
|
|
764
|
+
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script>
|
|
765
|
+
<script>
|
|
766
|
+
const DEFAULT_CODE = `import { fal } from "../fal-provider";
|
|
767
|
+
import { Clip, Image, Render, Video } from "../react";
|
|
768
|
+
|
|
769
|
+
export default (
|
|
770
|
+
<Render width={1080} height={1920}>
|
|
771
|
+
<Clip duration={5}>
|
|
772
|
+
<Video
|
|
773
|
+
prompt={{
|
|
774
|
+
text: "A person walking through a neon-lit city at night",
|
|
775
|
+
images: [
|
|
776
|
+
Image({
|
|
777
|
+
prompt: "cyberpunk street scene, neon lights, rain",
|
|
778
|
+
model: fal.imageModel("flux-schnell"),
|
|
779
|
+
}),
|
|
780
|
+
],
|
|
781
|
+
}}
|
|
782
|
+
model={fal.videoModel("kling-v2.5")}
|
|
783
|
+
duration={5}
|
|
784
|
+
/>
|
|
785
|
+
</Clip>
|
|
786
|
+
</Render>
|
|
787
|
+
);`;
|
|
788
|
+
|
|
789
|
+
let editor;
|
|
790
|
+
let isRendering = false;
|
|
791
|
+
let autoMode = false;
|
|
792
|
+
let currentRenderId = null;
|
|
793
|
+
let debounceTimer = null;
|
|
794
|
+
let history = JSON.parse(localStorage.getItem('varg-history') || '[]');
|
|
795
|
+
|
|
796
|
+
// Step mode state
|
|
797
|
+
let stepMode = false;
|
|
798
|
+
let stepSession = null;
|
|
799
|
+
let stages = [];
|
|
800
|
+
let currentStageIndex = -1;
|
|
801
|
+
let selectedStageId = null;
|
|
802
|
+
|
|
803
|
+
// View mode state
|
|
804
|
+
let currentView = 'code';
|
|
805
|
+
let drawflowEditor = null;
|
|
806
|
+
let renderingFinal = false;
|
|
807
|
+
|
|
808
|
+
// Initialize Drawflow
|
|
809
|
+
function initDrawflow() {
|
|
810
|
+
const container = document.getElementById('drawflow');
|
|
811
|
+
drawflowEditor = new Drawflow(container);
|
|
812
|
+
drawflowEditor.reroute = true;
|
|
813
|
+
drawflowEditor.start();
|
|
814
|
+
|
|
815
|
+
drawflowEditor.on('nodeSelected', (nodeId) => {
|
|
816
|
+
const stage = stages.find(s => s.nodeId === nodeId);
|
|
817
|
+
if (stage && stage.status === 'complete') {
|
|
818
|
+
selectedStageId = stage.id;
|
|
819
|
+
renderStageTimeline();
|
|
820
|
+
if (stage.result) {
|
|
821
|
+
showStagePreview({ result: stage.result });
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
async function setView(view) {
|
|
828
|
+
currentView = view;
|
|
829
|
+
document.getElementById('view-code-btn').classList.toggle('active', view === 'code');
|
|
830
|
+
document.getElementById('view-nodes-btn').classList.toggle('active', view === 'nodes');
|
|
831
|
+
document.getElementById('editor-container').classList.toggle('hidden', view === 'nodes');
|
|
832
|
+
document.getElementById('nodes-container').classList.toggle('active', view === 'nodes');
|
|
833
|
+
|
|
834
|
+
if (view === 'nodes') {
|
|
835
|
+
document.getElementById('view-hint').textContent = 'click node to preview';
|
|
836
|
+
if (stages.length > 0) {
|
|
837
|
+
renderNodeGraph();
|
|
838
|
+
} else {
|
|
839
|
+
// Auto-parse stages when switching to nodes view
|
|
840
|
+
await parseStagesForNodeView();
|
|
841
|
+
}
|
|
842
|
+
} else {
|
|
843
|
+
document.getElementById('view-hint').textContent = 'cmd+enter to run';
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
async function parseStagesForNodeView() {
|
|
848
|
+
if (!drawflowEditor) {
|
|
849
|
+
showEmptyNodeGraph();
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const code = editor.getValue();
|
|
854
|
+
try {
|
|
855
|
+
const res = await fetch('/api/step/session', {
|
|
856
|
+
method: 'POST',
|
|
857
|
+
headers: { 'Content-Type': 'application/json' },
|
|
858
|
+
body: JSON.stringify({ code }),
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
if (!res.ok) {
|
|
862
|
+
const err = await res.json();
|
|
863
|
+
throw new Error(err.error || 'Failed to parse stages');
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const data = await res.json();
|
|
867
|
+
stepSession = data;
|
|
868
|
+
stages = data.stages.map(s => ({ ...s, status: 'pending' }));
|
|
869
|
+
currentStageIndex = -1;
|
|
870
|
+
selectedStageId = null;
|
|
871
|
+
|
|
872
|
+
renderNodeGraph();
|
|
873
|
+
renderStageTimeline();
|
|
874
|
+
|
|
875
|
+
} catch (err) {
|
|
876
|
+
console.error('Failed to parse stages:', err);
|
|
877
|
+
showParseErrorNode(err.message);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function showParseErrorNode(message) {
|
|
882
|
+
if (!drawflowEditor) return;
|
|
883
|
+
drawflowEditor.clear();
|
|
884
|
+
const html = `
|
|
885
|
+
<div class="node-title" style="background: rgba(239,68,68,0.1);">
|
|
886
|
+
<span class="node-title-icon">⚠️</span>
|
|
887
|
+
<span>parse error</span>
|
|
888
|
+
</div>
|
|
889
|
+
<div class="node-content">
|
|
890
|
+
<div class="node-label" style="color: var(--error); white-space: normal;">${message}</div>
|
|
891
|
+
<button class="node-btn" onclick="setView('code')">← back to code</button>
|
|
892
|
+
</div>
|
|
893
|
+
`;
|
|
894
|
+
drawflowEditor.addNode('error', 0, 0, 200, 150, 'error', {}, html);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function showEmptyNodeGraph() {
|
|
898
|
+
if (!drawflowEditor) return;
|
|
899
|
+
drawflowEditor.clear();
|
|
900
|
+
const html = `
|
|
901
|
+
<div class="node-title">
|
|
902
|
+
<span class="node-title-icon">💡</span>
|
|
903
|
+
<span>no stages</span>
|
|
904
|
+
</div>
|
|
905
|
+
<div class="node-content">
|
|
906
|
+
<div class="node-preview-placeholder">run in step mode first</div>
|
|
907
|
+
</div>
|
|
908
|
+
`;
|
|
909
|
+
drawflowEditor.addNode('empty', 0, 0, 200, 150, 'empty-node', {}, html);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function renderNodeGraph() {
|
|
913
|
+
if (!drawflowEditor || stages.length === 0) return;
|
|
914
|
+
|
|
915
|
+
drawflowEditor.clear();
|
|
916
|
+
|
|
917
|
+
const nodeWidth = 180;
|
|
918
|
+
const nodeHeight = 180;
|
|
919
|
+
const gapX = 100;
|
|
920
|
+
const gapY = 50;
|
|
921
|
+
const startX = 50;
|
|
922
|
+
const startY = 50;
|
|
923
|
+
|
|
924
|
+
// Build dependency graph and calculate levels
|
|
925
|
+
const stageMap = new Map(stages.map(s => [s.id, s]));
|
|
926
|
+
const levels = new Map(); // stageId -> level (depth)
|
|
927
|
+
const children = new Map(); // stageId -> stages that depend on it
|
|
928
|
+
|
|
929
|
+
// Initialize children map
|
|
930
|
+
stages.forEach(s => { children.set(s.id, []); });
|
|
931
|
+
|
|
932
|
+
// Build reverse dependency (who depends on me)
|
|
933
|
+
stages.forEach(s => {
|
|
934
|
+
(s.dependsOn || []).forEach(depId => {
|
|
935
|
+
if (children.has(depId)) {
|
|
936
|
+
children.get(depId).push(s.id);
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
// Calculate levels using BFS from roots (nodes with no dependencies)
|
|
942
|
+
function calculateLevels() {
|
|
943
|
+
const queue = [];
|
|
944
|
+
|
|
945
|
+
// Find root nodes (no dependencies)
|
|
946
|
+
stages.forEach(s => {
|
|
947
|
+
if (!s.dependsOn || s.dependsOn.length === 0) {
|
|
948
|
+
levels.set(s.id, 0);
|
|
949
|
+
queue.push(s.id);
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
// BFS to assign levels
|
|
954
|
+
while (queue.length > 0) {
|
|
955
|
+
const currentId = queue.shift();
|
|
956
|
+
const _currentLevel = levels.get(currentId);
|
|
957
|
+
|
|
958
|
+
children.get(currentId).forEach(childId => {
|
|
959
|
+
const childStage = stageMap.get(childId);
|
|
960
|
+
if (!childStage) return;
|
|
961
|
+
|
|
962
|
+
// Child's level is max of all its dependencies + 1
|
|
963
|
+
const depLevels = (childStage.dependsOn || [])
|
|
964
|
+
.map(d => levels.get(d) ?? -1)
|
|
965
|
+
.filter(l => l >= 0);
|
|
966
|
+
|
|
967
|
+
if (depLevels.length === (childStage.dependsOn || []).length) {
|
|
968
|
+
// All dependencies have levels assigned
|
|
969
|
+
const newLevel = Math.max(...depLevels) + 1;
|
|
970
|
+
if (!levels.has(childId) || levels.get(childId) < newLevel) {
|
|
971
|
+
levels.set(childId, newLevel);
|
|
972
|
+
queue.push(childId);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Handle any unassigned (shouldn't happen with valid DAG)
|
|
979
|
+
stages.forEach(s => {
|
|
980
|
+
if (!levels.has(s.id)) {
|
|
981
|
+
levels.set(s.id, 0);
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
calculateLevels();
|
|
987
|
+
|
|
988
|
+
// Group stages by level
|
|
989
|
+
const levelGroups = new Map();
|
|
990
|
+
stages.forEach(s => {
|
|
991
|
+
const level = levels.get(s.id);
|
|
992
|
+
if (!levelGroups.has(level)) {
|
|
993
|
+
levelGroups.set(level, []);
|
|
994
|
+
}
|
|
995
|
+
levelGroups.get(level).push(s);
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
const maxLevel = Math.max(...levels.values());
|
|
999
|
+
const stageNodeIds = new Map(); // stageId -> drawflow nodeId
|
|
1000
|
+
|
|
1001
|
+
// Render nodes level by level (left to right)
|
|
1002
|
+
for (let level = 0; level <= maxLevel; level++) {
|
|
1003
|
+
const group = levelGroups.get(level) || [];
|
|
1004
|
+
const x = startX + level * (nodeWidth + gapX);
|
|
1005
|
+
|
|
1006
|
+
group.forEach((stage, idx) => {
|
|
1007
|
+
// Center vertically based on group size
|
|
1008
|
+
const totalHeight = group.length * (nodeHeight + gapY) - gapY;
|
|
1009
|
+
const offsetY = (500 - totalHeight) / 2; // Center in ~500px viewport
|
|
1010
|
+
const y = startY + Math.max(0, offsetY) + idx * (nodeHeight + gapY);
|
|
1011
|
+
|
|
1012
|
+
const icon = getStageIcon(stage.type);
|
|
1013
|
+
const statusClass = stage.status;
|
|
1014
|
+
const hasPreview = stage.result?.previewUrl;
|
|
1015
|
+
|
|
1016
|
+
let previewHtml = '';
|
|
1017
|
+
if (hasPreview) {
|
|
1018
|
+
const url = stage.result.previewUrl;
|
|
1019
|
+
if (stage.result.mimeType?.startsWith('image/') || stage.type === 'image') {
|
|
1020
|
+
previewHtml = `<img class="node-preview" src="${url}" alt="preview">`;
|
|
1021
|
+
} else if (stage.result.mimeType?.startsWith('video/') || stage.type === 'video' || stage.type === 'animate') {
|
|
1022
|
+
previewHtml = `<video class="node-preview" src="${url}" muted loop></video>`;
|
|
1023
|
+
} else if (stage.result.mimeType?.startsWith('audio/') || stage.type === 'speech' || stage.type === 'music') {
|
|
1024
|
+
previewHtml = `<div class="node-preview-placeholder">🎵 audio</div>`;
|
|
1025
|
+
}
|
|
1026
|
+
} else {
|
|
1027
|
+
previewHtml = `<div class="node-preview-placeholder">click to generate</div>`;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const btnHtml = stage.status === 'pending'
|
|
1031
|
+
? `<button class="node-btn" onclick="event.stopPropagation(); generateNodeStage('${stage.id}')">▶ generate</button>`
|
|
1032
|
+
: '';
|
|
1033
|
+
|
|
1034
|
+
const html = `
|
|
1035
|
+
<div class="node-title">
|
|
1036
|
+
<span class="node-title-icon">${icon}</span>
|
|
1037
|
+
<span>${stage.type}</span>
|
|
1038
|
+
</div>
|
|
1039
|
+
<div class="node-content">
|
|
1040
|
+
${previewHtml}
|
|
1041
|
+
<div class="node-label" title="${stage.label}">${stage.label}</div>
|
|
1042
|
+
<div class="node-status ${statusClass}">${stage.status}</div>
|
|
1043
|
+
${btnHtml}
|
|
1044
|
+
</div>
|
|
1045
|
+
`;
|
|
1046
|
+
|
|
1047
|
+
// Input count = number of dependencies, output = 1 (for children or render node)
|
|
1048
|
+
const inputCount = (stage.dependsOn || []).length || 0;
|
|
1049
|
+
const _hasChildren = children.get(stage.id)?.length > 0;
|
|
1050
|
+
const outputCount = 1; // Always 1 output (to children or render)
|
|
1051
|
+
|
|
1052
|
+
const nodeId = drawflowEditor.addNode(
|
|
1053
|
+
stage.type,
|
|
1054
|
+
inputCount > 0 ? inputCount : 0,
|
|
1055
|
+
outputCount,
|
|
1056
|
+
x,
|
|
1057
|
+
y,
|
|
1058
|
+
statusClass,
|
|
1059
|
+
{ stageId: stage.id },
|
|
1060
|
+
html
|
|
1061
|
+
);
|
|
1062
|
+
|
|
1063
|
+
stage.nodeId = nodeId;
|
|
1064
|
+
stageNodeIds.set(stage.id, nodeId);
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// Add connections based on actual dependencies
|
|
1069
|
+
stages.forEach(stage => {
|
|
1070
|
+
const targetNodeId = stageNodeIds.get(stage.id);
|
|
1071
|
+
(stage.dependsOn || []).forEach((depId, depIndex) => {
|
|
1072
|
+
const sourceNodeId = stageNodeIds.get(depId);
|
|
1073
|
+
if (sourceNodeId && targetNodeId) {
|
|
1074
|
+
// Connect source output to target input
|
|
1075
|
+
const inputNum = depIndex + 1;
|
|
1076
|
+
drawflowEditor.addConnection(sourceNodeId, targetNodeId, 'output_1', `input_${inputNum}`);
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
// Find leaf nodes (no children) to connect to render node
|
|
1082
|
+
const leafStages = stages.filter(s => {
|
|
1083
|
+
const childList = children.get(s.id) || [];
|
|
1084
|
+
return childList.length === 0;
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
// Add final "Render" root node
|
|
1088
|
+
const allComplete = stages.every(s => s.status === 'complete');
|
|
1089
|
+
const renderX = startX + (maxLevel + 1) * (nodeWidth + gapX);
|
|
1090
|
+
const renderY = startY + (500 - nodeHeight) / 4; // Roughly centered
|
|
1091
|
+
|
|
1092
|
+
let renderPreviewHtml = '';
|
|
1093
|
+
const hasRenderResult = currentVideoUrl || stepSession?.renderResult?.previewUrl;
|
|
1094
|
+
|
|
1095
|
+
if (renderingFinal) {
|
|
1096
|
+
renderPreviewHtml = `<div class="node-preview-placeholder"><div class="spinner"></div></div>`;
|
|
1097
|
+
} else if (hasRenderResult) {
|
|
1098
|
+
const url = currentVideoUrl || stepSession.renderResult.previewUrl;
|
|
1099
|
+
renderPreviewHtml = `<video class="node-preview" src="${url}" muted loop></video>`;
|
|
1100
|
+
} else if (allComplete) {
|
|
1101
|
+
renderPreviewHtml = `<div class="node-preview-placeholder">ready to render</div>`;
|
|
1102
|
+
} else {
|
|
1103
|
+
renderPreviewHtml = `<div class="node-preview-placeholder">waiting for stages...</div>`;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
const renderBtnHtml = (allComplete && !hasRenderResult && !renderingFinal)
|
|
1107
|
+
? `<button class="node-btn" onclick="event.stopPropagation(); runFinalRender()" style="background: var(--accent); border-color: var(--accent);">▶ render</button>`
|
|
1108
|
+
: '';
|
|
1109
|
+
|
|
1110
|
+
const renderStatusText = renderingFinal ? 'rendering...' : (hasRenderResult ? 'complete' : (allComplete ? 'ready' : 'pending'));
|
|
1111
|
+
const renderStatusClass = renderingFinal ? 'running' : (hasRenderResult ? 'complete' : 'pending');
|
|
1112
|
+
|
|
1113
|
+
const renderHtml = `
|
|
1114
|
+
<div class="node-title" style="background: rgba(59,130,246,0.15);">
|
|
1115
|
+
<span class="node-title-icon">🎬</span>
|
|
1116
|
+
<span>render</span>
|
|
1117
|
+
</div>
|
|
1118
|
+
<div class="node-content">
|
|
1119
|
+
${renderPreviewHtml}
|
|
1120
|
+
<div class="node-label">final output</div>
|
|
1121
|
+
<div class="node-status ${renderStatusClass}">${renderStatusText}</div>
|
|
1122
|
+
${renderBtnHtml}
|
|
1123
|
+
</div>
|
|
1124
|
+
`;
|
|
1125
|
+
|
|
1126
|
+
const renderNodeId = drawflowEditor.addNode(
|
|
1127
|
+
'render',
|
|
1128
|
+
leafStages.length || 1,
|
|
1129
|
+
0,
|
|
1130
|
+
renderX,
|
|
1131
|
+
renderY,
|
|
1132
|
+
renderingFinal ? 'running' : (hasRenderResult ? 'complete' : 'pending'),
|
|
1133
|
+
{ type: 'render' },
|
|
1134
|
+
renderHtml
|
|
1135
|
+
);
|
|
1136
|
+
|
|
1137
|
+
// Connect all leaf nodes to render node
|
|
1138
|
+
leafStages.forEach((stage, idx) => {
|
|
1139
|
+
const sourceNodeId = stageNodeIds.get(stage.id);
|
|
1140
|
+
if (sourceNodeId) {
|
|
1141
|
+
drawflowEditor.addConnection(sourceNodeId, renderNodeId, 'output_1', `input_${idx + 1}`);
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
// Play videos on hover
|
|
1146
|
+
document.querySelectorAll('.node-preview').forEach(el => {
|
|
1147
|
+
if (el.tagName === 'VIDEO') {
|
|
1148
|
+
el.addEventListener('mouseenter', () => el.play());
|
|
1149
|
+
el.addEventListener('mouseleave', () => { el.pause(); el.currentTime = 0; });
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
async function _generateNodeStage(stageId) {
|
|
1155
|
+
if (!stepSession || isRendering) return;
|
|
1156
|
+
|
|
1157
|
+
const stage = stages.find(s => s.id === stageId);
|
|
1158
|
+
if (!stage) return;
|
|
1159
|
+
|
|
1160
|
+
// Check if all dependencies are complete
|
|
1161
|
+
const incompleteDeps = (stage.dependsOn || [])
|
|
1162
|
+
.map(depId => stages.find(s => s.id === depId))
|
|
1163
|
+
.filter(dep => dep && dep.status !== 'complete');
|
|
1164
|
+
|
|
1165
|
+
if (incompleteDeps.length > 0) {
|
|
1166
|
+
const depNames = incompleteDeps.map(d => d.label).join(', ');
|
|
1167
|
+
alert(`Complete dependencies first: ${depNames}`);
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
const stageIndex = stages.findIndex(s => s.id === stageId);
|
|
1172
|
+
|
|
1173
|
+
isRendering = true;
|
|
1174
|
+
stages[stageIndex].status = 'running';
|
|
1175
|
+
renderNodeGraph();
|
|
1176
|
+
renderStageTimeline();
|
|
1177
|
+
|
|
1178
|
+
try {
|
|
1179
|
+
const res = await fetch('/api/step/run', {
|
|
1180
|
+
method: 'POST',
|
|
1181
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1182
|
+
body: JSON.stringify({ sessionId: stepSession.sessionId, stageId }),
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
if (!res.ok) {
|
|
1186
|
+
const err = await res.json();
|
|
1187
|
+
throw new Error(err.message || 'Stage execution failed');
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
const data = await res.json();
|
|
1191
|
+
stages[stageIndex].status = 'complete';
|
|
1192
|
+
stages[stageIndex].result = data.result;
|
|
1193
|
+
currentStageIndex = Math.max(currentStageIndex, stageIndex);
|
|
1194
|
+
|
|
1195
|
+
selectedStageId = stageId;
|
|
1196
|
+
renderNodeGraph();
|
|
1197
|
+
renderStageTimeline();
|
|
1198
|
+
showStagePreview(data);
|
|
1199
|
+
|
|
1200
|
+
} catch (err) {
|
|
1201
|
+
stages[stageIndex].status = 'error';
|
|
1202
|
+
renderNodeGraph();
|
|
1203
|
+
renderStageTimeline();
|
|
1204
|
+
showError(err.message);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
isRendering = false;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
async function _runFinalRender() {
|
|
1211
|
+
if (!stepSession || isRendering) return;
|
|
1212
|
+
|
|
1213
|
+
const allComplete = stages.every(s => s.status === 'complete');
|
|
1214
|
+
if (!allComplete) {
|
|
1215
|
+
alert('Complete all stages first');
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
isRendering = true;
|
|
1220
|
+
renderingFinal = true;
|
|
1221
|
+
renderNodeGraph();
|
|
1222
|
+
updateProgress(0.5, 'rendering final video...');
|
|
1223
|
+
document.getElementById('progress-section').style.display = 'block';
|
|
1224
|
+
|
|
1225
|
+
try {
|
|
1226
|
+
const res = await fetch('/api/step/render', {
|
|
1227
|
+
method: 'POST',
|
|
1228
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1229
|
+
body: JSON.stringify({ sessionId: stepSession.sessionId }),
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
if (!res.ok) {
|
|
1233
|
+
const err = await res.json();
|
|
1234
|
+
throw new Error(err.error || err.message || 'Final render failed');
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
const data = await res.json();
|
|
1238
|
+
currentVideoUrl = data.videoUrl;
|
|
1239
|
+
|
|
1240
|
+
renderingFinal = false;
|
|
1241
|
+
renderNodeGraph();
|
|
1242
|
+
showVideo(data.videoUrl);
|
|
1243
|
+
addToHistory(editor.getValue(), data.videoUrl);
|
|
1244
|
+
updateProgress(1, 'render complete');
|
|
1245
|
+
document.getElementById('progress-bar').className = 'progress-bar complete';
|
|
1246
|
+
|
|
1247
|
+
} catch (err) {
|
|
1248
|
+
renderingFinal = false;
|
|
1249
|
+
renderNodeGraph();
|
|
1250
|
+
showError(err.message);
|
|
1251
|
+
updateProgress(0, err.message);
|
|
1252
|
+
document.getElementById('progress-bar').className = 'progress-bar error';
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
isRendering = false;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
document.getElementById('view-code-btn').addEventListener('click', () => setView('code'));
|
|
1259
|
+
document.getElementById('view-nodes-btn').addEventListener('click', () => setView('nodes'));
|
|
1260
|
+
|
|
1261
|
+
require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' } });
|
|
1262
|
+
|
|
1263
|
+
require(['vs/editor/editor.main'], () => {
|
|
1264
|
+
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
|
|
1265
|
+
noSemanticValidation: true,
|
|
1266
|
+
noSyntaxValidation: true,
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
|
1270
|
+
jsx: monaco.languages.typescript.JsxEmit.React,
|
|
1271
|
+
jsxFactory: 'createElement',
|
|
1272
|
+
target: monaco.languages.typescript.ScriptTarget.ESNext,
|
|
1273
|
+
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
|
1274
|
+
allowNonTsExtensions: true,
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
monaco.editor.defineTheme('varg-dark', {
|
|
1278
|
+
base: 'vs-dark',
|
|
1279
|
+
inherit: true,
|
|
1280
|
+
rules: [],
|
|
1281
|
+
colors: {
|
|
1282
|
+
'editor.background': '#0a0a0a',
|
|
1283
|
+
'editor.lineHighlightBackground': '#141414',
|
|
1284
|
+
'editorLineNumber.foreground': '#444',
|
|
1285
|
+
'editorLineNumber.activeForeground': '#888',
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
editor = monaco.editor.create(document.getElementById('editor-container'), {
|
|
1290
|
+
value: localStorage.getItem('varg-code') || DEFAULT_CODE,
|
|
1291
|
+
language: 'typescript',
|
|
1292
|
+
theme: 'varg-dark',
|
|
1293
|
+
fontSize: 13,
|
|
1294
|
+
fontFamily: 'SF Mono, Menlo, Monaco, monospace',
|
|
1295
|
+
lineNumbers: 'on',
|
|
1296
|
+
minimap: { enabled: false },
|
|
1297
|
+
scrollBeyondLastLine: false,
|
|
1298
|
+
padding: { top: 16, bottom: 16 },
|
|
1299
|
+
automaticLayout: true,
|
|
1300
|
+
tabSize: 2,
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
editor.onDidChangeModelContent(() => {
|
|
1304
|
+
localStorage.setItem('varg-code', editor.getValue());
|
|
1305
|
+
if (autoMode && !isRendering) {
|
|
1306
|
+
clearTimeout(debounceTimer);
|
|
1307
|
+
debounceTimer = setTimeout(() => startRender(), 2000);
|
|
1308
|
+
}
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
loadInitialCode();
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
async function loadTemplates() {
|
|
1315
|
+
const res = await fetch('/api/templates');
|
|
1316
|
+
const templates = await res.json();
|
|
1317
|
+
const select = document.getElementById('template-select');
|
|
1318
|
+
templates.forEach(t => {
|
|
1319
|
+
const opt = document.createElement('option');
|
|
1320
|
+
opt.value = t.id;
|
|
1321
|
+
opt.textContent = t.name;
|
|
1322
|
+
select.appendChild(opt);
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
async function loadInitialCode() {
|
|
1327
|
+
const res = await fetch('/api/initial-code');
|
|
1328
|
+
const data = await res.json();
|
|
1329
|
+
if (data.code) {
|
|
1330
|
+
editor.setValue(data.code);
|
|
1331
|
+
localStorage.setItem('varg-code', data.code);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
document.getElementById('template-select').addEventListener('change', async (e) => {
|
|
1336
|
+
if (!e.target.value) return;
|
|
1337
|
+
const res = await fetch(`/api/templates/${e.target.value}`);
|
|
1338
|
+
const { code } = await res.json();
|
|
1339
|
+
editor.setValue(code);
|
|
1340
|
+
e.target.value = '';
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
document.getElementById('run-btn').addEventListener('click', () => {
|
|
1344
|
+
if (stepMode) {
|
|
1345
|
+
startStepSession();
|
|
1346
|
+
} else {
|
|
1347
|
+
startRender();
|
|
1348
|
+
}
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
document.getElementById('next-btn').addEventListener('click', executeNextStep);
|
|
1352
|
+
|
|
1353
|
+
document.getElementById('mode-all-btn').addEventListener('click', () => {
|
|
1354
|
+
setStepMode(false);
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
document.getElementById('mode-step-btn').addEventListener('click', () => {
|
|
1358
|
+
setStepMode(true);
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
document.getElementById('auto-btn').addEventListener('click', () => {
|
|
1362
|
+
autoMode = !autoMode;
|
|
1363
|
+
document.getElementById('auto-btn').classList.toggle('active', autoMode);
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
document.getElementById('stop-btn').addEventListener('click', stopRender);
|
|
1367
|
+
|
|
1368
|
+
document.addEventListener('keydown', (e) => {
|
|
1369
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
1370
|
+
e.preventDefault();
|
|
1371
|
+
if (stepMode) {
|
|
1372
|
+
startStepSession();
|
|
1373
|
+
} else {
|
|
1374
|
+
startRender();
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
if (e.key === 'Escape' && isRendering) {
|
|
1378
|
+
stopRender();
|
|
1379
|
+
}
|
|
1380
|
+
// Space to execute next step in step mode
|
|
1381
|
+
if (e.code === 'Space' && stepMode && stepSession && !isRendering) {
|
|
1382
|
+
// Don't trigger if focused on editor or input
|
|
1383
|
+
if (document.activeElement.tagName !== 'INPUT' &&
|
|
1384
|
+
document.activeElement.tagName !== 'TEXTAREA' &&
|
|
1385
|
+
!document.activeElement.closest('#editor-container')) {
|
|
1386
|
+
e.preventDefault();
|
|
1387
|
+
executeNextStep();
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
function setStepMode(enabled) {
|
|
1393
|
+
stepMode = enabled;
|
|
1394
|
+
document.getElementById('mode-all-btn').classList.toggle('active', !enabled);
|
|
1395
|
+
document.getElementById('mode-step-btn').classList.toggle('active', enabled);
|
|
1396
|
+
document.getElementById('stage-timeline').classList.toggle('active', enabled && stages.length > 0);
|
|
1397
|
+
document.getElementById('next-btn').style.display = enabled && stepSession ? 'flex' : 'none';
|
|
1398
|
+
document.getElementById('auto-btn').style.display = enabled ? 'none' : 'flex';
|
|
1399
|
+
|
|
1400
|
+
if (!enabled) {
|
|
1401
|
+
// Reset step state when switching to run-all mode
|
|
1402
|
+
stepSession = null;
|
|
1403
|
+
stages = [];
|
|
1404
|
+
currentStageIndex = -1;
|
|
1405
|
+
selectedStageId = null;
|
|
1406
|
+
renderStageTimeline();
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function getStageIcon(type) {
|
|
1411
|
+
const icons = {
|
|
1412
|
+
image: '🖼️',
|
|
1413
|
+
video: '🎬',
|
|
1414
|
+
animate: '🎬',
|
|
1415
|
+
speech: '🎤',
|
|
1416
|
+
music: '🎵',
|
|
1417
|
+
default: '⚙️'
|
|
1418
|
+
};
|
|
1419
|
+
return icons[type] || icons.default;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function renderStageTimeline() {
|
|
1423
|
+
const track = document.getElementById('stage-track');
|
|
1424
|
+
const timeline = document.getElementById('stage-timeline');
|
|
1425
|
+
|
|
1426
|
+
if (stages.length === 0) {
|
|
1427
|
+
timeline.classList.remove('active');
|
|
1428
|
+
track.innerHTML = '';
|
|
1429
|
+
if (currentView === 'nodes') showEmptyNodeGraph();
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
if (stepMode) {
|
|
1434
|
+
timeline.classList.add('active');
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
if (currentView === 'nodes') {
|
|
1438
|
+
renderNodeGraph();
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
let html = '';
|
|
1442
|
+
stages.forEach((stage, index) => {
|
|
1443
|
+
const isSelected = stage.id === selectedStageId;
|
|
1444
|
+
const statusClass = stage.status + (isSelected ? ' selected' : '');
|
|
1445
|
+
|
|
1446
|
+
// Add connector before each stage except the first
|
|
1447
|
+
if (index > 0) {
|
|
1448
|
+
const prevComplete = stages[index - 1].status === 'complete';
|
|
1449
|
+
html += `
|
|
1450
|
+
<div class="stage-connector">
|
|
1451
|
+
<div class="stage-connector-line ${prevComplete ? 'complete' : ''}"></div>
|
|
1452
|
+
</div>
|
|
1453
|
+
`;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
html += `
|
|
1457
|
+
<div class="stage-item ${statusClass}"
|
|
1458
|
+
data-stage-id="${stage.id}"
|
|
1459
|
+
data-stage-index="${index}"
|
|
1460
|
+
onclick="selectStage('${stage.id}')">
|
|
1461
|
+
<div class="stage-icon">${getStageIcon(stage.type)}</div>
|
|
1462
|
+
<div class="stage-label" title="${stage.label}">${stage.label}</div>
|
|
1463
|
+
<div class="stage-status">${stage.status}</div>
|
|
1464
|
+
</div>
|
|
1465
|
+
`;
|
|
1466
|
+
});
|
|
1467
|
+
|
|
1468
|
+
track.innerHTML = html;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
async function _selectStage(stageId) {
|
|
1472
|
+
const stage = stages.find(s => s.id === stageId);
|
|
1473
|
+
if (!stage || stage.status !== 'complete') return;
|
|
1474
|
+
|
|
1475
|
+
selectedStageId = stageId;
|
|
1476
|
+
renderStageTimeline();
|
|
1477
|
+
|
|
1478
|
+
// Show stored result directly (preview URL already available)
|
|
1479
|
+
if (stage.result) {
|
|
1480
|
+
showStagePreview({ result: stage.result });
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
function showStagePreview(data) {
|
|
1485
|
+
const container = document.getElementById('preview-content');
|
|
1486
|
+
console.log('[preview] showStagePreview called with:', data);
|
|
1487
|
+
|
|
1488
|
+
if (!data || !data.result) {
|
|
1489
|
+
container.innerHTML = `<div class="preview-placeholder">
|
|
1490
|
+
<div class="preview-placeholder-icon">📭</div>
|
|
1491
|
+
<div>no preview available</div>
|
|
1492
|
+
</div>`;
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
const { type, previewUrl, mimeType } = data.result;
|
|
1497
|
+
console.log('[preview] type:', type, 'previewUrl:', previewUrl, 'mimeType:', mimeType);
|
|
1498
|
+
|
|
1499
|
+
if (mimeType?.startsWith('video/') || type === 'video' || type === 'animate') {
|
|
1500
|
+
container.innerHTML = `<video class="preview-video" src="${previewUrl}" controls autoplay loop></video>`;
|
|
1501
|
+
} else if (mimeType?.startsWith('image/') || type === 'image') {
|
|
1502
|
+
container.innerHTML = `<img class="preview-video" src="${previewUrl}" alt="Stage preview" style="object-fit: contain; max-width: 100%; max-height: 100%;">`;
|
|
1503
|
+
} else if (mimeType?.startsWith('audio/') || type === 'speech' || type === 'music') {
|
|
1504
|
+
container.innerHTML = `
|
|
1505
|
+
<div style="display: flex; flex-direction: column; align-items: center; gap: 1rem;">
|
|
1506
|
+
<div style="font-size: 3rem;">${type === 'music' ? '🎵' : '🎤'}</div>
|
|
1507
|
+
<audio controls src="${previewUrl}" style="width: 80%;"></audio>
|
|
1508
|
+
</div>
|
|
1509
|
+
`;
|
|
1510
|
+
} else {
|
|
1511
|
+
container.innerHTML = `<div class="preview-placeholder">
|
|
1512
|
+
<div class="preview-placeholder-icon">✓</div>
|
|
1513
|
+
<div>stage complete</div>
|
|
1514
|
+
</div>`;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
async function startStepSession() {
|
|
1519
|
+
if (isRendering) return;
|
|
1520
|
+
|
|
1521
|
+
isRendering = true;
|
|
1522
|
+
updateUI('rendering');
|
|
1523
|
+
|
|
1524
|
+
const code = editor.getValue();
|
|
1525
|
+
|
|
1526
|
+
try {
|
|
1527
|
+
const res = await fetch('/api/step/session', {
|
|
1528
|
+
method: 'POST',
|
|
1529
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1530
|
+
body: JSON.stringify({ code }),
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1533
|
+
if (!res.ok) {
|
|
1534
|
+
const err = await res.json();
|
|
1535
|
+
throw new Error(err.message || 'Failed to create step session');
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
const data = await res.json();
|
|
1539
|
+
stepSession = data;
|
|
1540
|
+
stages = data.stages.map(s => ({ ...s, status: 'pending' }));
|
|
1541
|
+
currentStageIndex = -1;
|
|
1542
|
+
selectedStageId = null;
|
|
1543
|
+
|
|
1544
|
+
renderStageTimeline();
|
|
1545
|
+
document.getElementById('next-btn').style.display = 'flex';
|
|
1546
|
+
document.getElementById('next-btn').disabled = false;
|
|
1547
|
+
|
|
1548
|
+
updateProgress(0, 'session ready — click next to start');
|
|
1549
|
+
updateUI('idle');
|
|
1550
|
+
|
|
1551
|
+
// Show placeholder for step mode
|
|
1552
|
+
const container = document.getElementById('preview-content');
|
|
1553
|
+
container.innerHTML = `<div class="preview-placeholder">
|
|
1554
|
+
<div class="preview-placeholder-icon">👣</div>
|
|
1555
|
+
<div>step through ${stages.length} stage${stages.length > 1 ? 's' : ''}</div>
|
|
1556
|
+
</div>`;
|
|
1557
|
+
|
|
1558
|
+
} catch (err) {
|
|
1559
|
+
showError(err.message);
|
|
1560
|
+
updateUI('error');
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
isRendering = false;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
async function executeNextStep() {
|
|
1567
|
+
if (!stepSession || isRendering) return;
|
|
1568
|
+
|
|
1569
|
+
const nextIndex = currentStageIndex + 1;
|
|
1570
|
+
if (nextIndex >= stages.length) {
|
|
1571
|
+
// All stages complete
|
|
1572
|
+
document.getElementById('next-btn').disabled = true;
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
isRendering = true;
|
|
1577
|
+
currentStageIndex = nextIndex;
|
|
1578
|
+
stages[nextIndex].status = 'running';
|
|
1579
|
+
renderStageTimeline();
|
|
1580
|
+
|
|
1581
|
+
document.getElementById('next-btn').disabled = true;
|
|
1582
|
+
updateProgress((nextIndex / stages.length), `executing: ${stages[nextIndex].label}`);
|
|
1583
|
+
document.getElementById('progress-section').style.display = 'block';
|
|
1584
|
+
|
|
1585
|
+
try {
|
|
1586
|
+
const res = await fetch('/api/step/next', {
|
|
1587
|
+
method: 'POST',
|
|
1588
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1589
|
+
body: JSON.stringify({ sessionId: stepSession.sessionId }),
|
|
1590
|
+
});
|
|
1591
|
+
|
|
1592
|
+
if (!res.ok) {
|
|
1593
|
+
const err = await res.json();
|
|
1594
|
+
throw new Error(err.message || 'Step execution failed');
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
const data = await res.json();
|
|
1598
|
+
stages[nextIndex].status = 'complete';
|
|
1599
|
+
stages[nextIndex].result = data.result;
|
|
1600
|
+
|
|
1601
|
+
// Auto-select and show the completed stage
|
|
1602
|
+
selectedStageId = stages[nextIndex].id;
|
|
1603
|
+
renderStageTimeline();
|
|
1604
|
+
showStagePreview(data);
|
|
1605
|
+
|
|
1606
|
+
const progress = (nextIndex + 1) / stages.length;
|
|
1607
|
+
if (nextIndex + 1 >= stages.length) {
|
|
1608
|
+
updateProgress(1, 'all stages complete');
|
|
1609
|
+
document.getElementById('progress-bar').className = 'progress-bar complete';
|
|
1610
|
+
document.getElementById('next-btn').disabled = true;
|
|
1611
|
+
|
|
1612
|
+
// Add to history if final result is a video
|
|
1613
|
+
if (data.result?.type === 'video' && data.result?.previewUrl) {
|
|
1614
|
+
addToHistory(editor.getValue(), data.result.previewUrl);
|
|
1615
|
+
}
|
|
1616
|
+
} else {
|
|
1617
|
+
updateProgress(progress, `${nextIndex + 1}/${stages.length} complete — ready for next`);
|
|
1618
|
+
document.getElementById('next-btn').disabled = false;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
} catch (err) {
|
|
1622
|
+
stages[nextIndex].status = 'error';
|
|
1623
|
+
renderStageTimeline();
|
|
1624
|
+
showError(err.message);
|
|
1625
|
+
updateUI('error');
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
isRendering = false;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
async function startRender() {
|
|
1632
|
+
if (isRendering) return;
|
|
1633
|
+
|
|
1634
|
+
isRendering = true;
|
|
1635
|
+
updateUI('rendering');
|
|
1636
|
+
|
|
1637
|
+
const code = editor.getValue();
|
|
1638
|
+
|
|
1639
|
+
try {
|
|
1640
|
+
const res = await fetch('/api/render', {
|
|
1641
|
+
method: 'POST',
|
|
1642
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1643
|
+
body: JSON.stringify({ code }),
|
|
1644
|
+
});
|
|
1645
|
+
|
|
1646
|
+
const reader = res.body.getReader();
|
|
1647
|
+
const decoder = new TextDecoder();
|
|
1648
|
+
let buffer = '';
|
|
1649
|
+
|
|
1650
|
+
while (true) {
|
|
1651
|
+
const { done, value } = await reader.read();
|
|
1652
|
+
if (done) break;
|
|
1653
|
+
|
|
1654
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1655
|
+
const lines = buffer.split('\n\n');
|
|
1656
|
+
buffer = lines.pop() || '';
|
|
1657
|
+
|
|
1658
|
+
for (const line of lines) {
|
|
1659
|
+
if (!line.trim()) continue;
|
|
1660
|
+
const eventMatch = line.match(/event: (\w+)/);
|
|
1661
|
+
const dataMatch = line.match(/data: (.+)/);
|
|
1662
|
+
if (!eventMatch || !dataMatch) continue;
|
|
1663
|
+
|
|
1664
|
+
const event = eventMatch[1];
|
|
1665
|
+
const data = JSON.parse(dataMatch[1]);
|
|
1666
|
+
|
|
1667
|
+
if (event === 'start') {
|
|
1668
|
+
currentRenderId = data.renderId;
|
|
1669
|
+
} else if (event === 'progress') {
|
|
1670
|
+
updateProgress(data.progress, data.message);
|
|
1671
|
+
} else if (event === 'complete') {
|
|
1672
|
+
showVideo(data.videoUrl);
|
|
1673
|
+
addToHistory(code, data.videoUrl);
|
|
1674
|
+
updateUI('complete');
|
|
1675
|
+
} else if (event === 'error') {
|
|
1676
|
+
showError(data.message);
|
|
1677
|
+
updateUI('error');
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
} catch (err) {
|
|
1682
|
+
showError(err.message);
|
|
1683
|
+
updateUI('error');
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
isRendering = false;
|
|
1687
|
+
currentRenderId = null;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
async function stopRender() {
|
|
1691
|
+
if (!currentRenderId) return;
|
|
1692
|
+
await fetch(`/api/render/${currentRenderId}`, { method: 'DELETE' });
|
|
1693
|
+
isRendering = false;
|
|
1694
|
+
currentRenderId = null;
|
|
1695
|
+
updateUI('idle');
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
function updateUI(state) {
|
|
1699
|
+
const runBtn = document.getElementById('run-btn');
|
|
1700
|
+
const stopBtn = document.getElementById('stop-btn');
|
|
1701
|
+
const nextBtn = document.getElementById('next-btn');
|
|
1702
|
+
const progressSection = document.getElementById('progress-section');
|
|
1703
|
+
const progressBar = document.getElementById('progress-bar');
|
|
1704
|
+
|
|
1705
|
+
if (state === 'rendering') {
|
|
1706
|
+
runBtn.disabled = true;
|
|
1707
|
+
stopBtn.disabled = false;
|
|
1708
|
+
if (stepMode) {
|
|
1709
|
+
nextBtn.disabled = true;
|
|
1710
|
+
}
|
|
1711
|
+
progressSection.style.display = 'block';
|
|
1712
|
+
progressBar.className = 'progress-bar';
|
|
1713
|
+
} else if (state === 'complete') {
|
|
1714
|
+
runBtn.disabled = false;
|
|
1715
|
+
stopBtn.disabled = true;
|
|
1716
|
+
progressBar.className = 'progress-bar complete';
|
|
1717
|
+
} else if (state === 'error') {
|
|
1718
|
+
runBtn.disabled = false;
|
|
1719
|
+
stopBtn.disabled = true;
|
|
1720
|
+
progressBar.className = 'progress-bar error';
|
|
1721
|
+
} else {
|
|
1722
|
+
runBtn.disabled = false;
|
|
1723
|
+
stopBtn.disabled = true;
|
|
1724
|
+
if (!stepMode) {
|
|
1725
|
+
progressSection.style.display = 'none';
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
function updateProgress(progress, message) {
|
|
1731
|
+
document.getElementById('progress-bar').style.width = `${progress * 100}%`;
|
|
1732
|
+
document.getElementById('progress-text').textContent = message;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
function showVideo(url) {
|
|
1736
|
+
const container = document.getElementById('preview-content');
|
|
1737
|
+
container.innerHTML = `<video class="preview-video" src="${url}" controls autoplay loop></video>`;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
function showError(message) {
|
|
1741
|
+
const container = document.getElementById('preview-content');
|
|
1742
|
+
container.innerHTML = `<div class="error-banner">${message}</div>`;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
function addToHistory(code, videoUrl) {
|
|
1746
|
+
const item = {
|
|
1747
|
+
id: Date.now().toString(),
|
|
1748
|
+
code,
|
|
1749
|
+
videoUrl,
|
|
1750
|
+
createdAt: new Date().toISOString(),
|
|
1751
|
+
};
|
|
1752
|
+
history.unshift(item);
|
|
1753
|
+
if (history.length > 10) history = history.slice(0, 10);
|
|
1754
|
+
localStorage.setItem('varg-history', JSON.stringify(history));
|
|
1755
|
+
renderHistory();
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
function renderHistory() {
|
|
1759
|
+
const grid = document.getElementById('history-grid');
|
|
1760
|
+
grid.innerHTML = history.map(item => `
|
|
1761
|
+
<div class="history-item" onclick="loadHistory('${item.id}')">
|
|
1762
|
+
<video class="history-thumb" src="${item.videoUrl}" muted></video>
|
|
1763
|
+
<div class="history-meta">${new Date(item.createdAt).toLocaleTimeString()}</div>
|
|
1764
|
+
</div>
|
|
1765
|
+
`).join('');
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
function _loadHistory(id) {
|
|
1769
|
+
const item = history.find(h => h.id === id);
|
|
1770
|
+
if (!item) return;
|
|
1771
|
+
editor.setValue(item.code);
|
|
1772
|
+
showVideo(item.videoUrl);
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
let currentVideoUrl = null;
|
|
1776
|
+
|
|
1777
|
+
document.getElementById('share-btn').addEventListener('click', async () => {
|
|
1778
|
+
const code = editor.getValue();
|
|
1779
|
+
const res = await fetch('/api/share', {
|
|
1780
|
+
method: 'POST',
|
|
1781
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1782
|
+
body: JSON.stringify({ code, videoUrl: currentVideoUrl }),
|
|
1783
|
+
});
|
|
1784
|
+
const { url } = await res.json();
|
|
1785
|
+
const fullUrl = window.location.origin + url;
|
|
1786
|
+
await navigator.clipboard.writeText(fullUrl);
|
|
1787
|
+
alert(`Link copied to clipboard: ${fullUrl}`);
|
|
1788
|
+
});
|
|
1789
|
+
|
|
1790
|
+
async function loadSharedCode() {
|
|
1791
|
+
const path = window.location.pathname;
|
|
1792
|
+
if (!path.startsWith('/s/')) return;
|
|
1793
|
+
const shareId = path.replace('/s/', '');
|
|
1794
|
+
try {
|
|
1795
|
+
const res = await fetch(`/api/share/${shareId}`);
|
|
1796
|
+
if (!res.ok) return;
|
|
1797
|
+
const data = await res.json();
|
|
1798
|
+
if (data.code && editor) {
|
|
1799
|
+
editor.setValue(data.code);
|
|
1800
|
+
}
|
|
1801
|
+
if (data.videoUrl) {
|
|
1802
|
+
showVideo(data.videoUrl);
|
|
1803
|
+
currentVideoUrl = data.videoUrl;
|
|
1804
|
+
}
|
|
1805
|
+
} catch {}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
loadTemplates();
|
|
1809
|
+
renderHistory();
|
|
1810
|
+
setTimeout(loadSharedCode, 500);
|
|
1811
|
+
|
|
1812
|
+
// Init drawflow after DOM is ready
|
|
1813
|
+
if (typeof Drawflow !== 'undefined') {
|
|
1814
|
+
initDrawflow();
|
|
1815
|
+
} else {
|
|
1816
|
+
console.error('Drawflow not available');
|
|
1817
|
+
}
|
|
1818
|
+
</script>
|
|
1819
|
+
</body>
|
|
1820
|
+
</html>
|