ralphflow 0.5.0 → 0.5.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/README.md +2 -0
- package/dist/{chunk-TCCMQDVT.js → chunk-DOC64TD6.js} +32 -2
- package/dist/ralphflow.js +584 -24
- package/dist/{server-DOSLU36L.js → server-EX5MWYW4.js} +210 -10
- package/package.json +6 -2
- package/src/dashboard/ui/app.js +203 -0
- package/src/dashboard/ui/archives.js +167 -0
- package/src/dashboard/ui/index.html +2 -3210
- package/src/dashboard/ui/loop-detail.js +880 -0
- package/src/dashboard/ui/notifications.js +151 -0
- package/src/dashboard/ui/prompt-builder.js +362 -0
- package/src/dashboard/ui/sidebar.js +97 -0
- package/src/dashboard/ui/state.js +54 -0
- package/src/dashboard/ui/styles.css +2140 -0
- package/src/dashboard/ui/templates.js +1858 -0
- package/src/dashboard/ui/utils.js +115 -0
- package/src/templates/code-implementation/loops/00-story-loop/prompt.md +73 -11
- package/src/templates/code-implementation/loops/01-tasks-loop/prompt.md +51 -2
- package/src/templates/code-implementation/loops/02-delivery-loop/prompt.md +48 -4
- package/src/templates/research/loops/00-discovery-loop/prompt.md +58 -5
- package/src/templates/research/loops/01-research-loop/prompt.md +44 -2
- package/src/templates/research/loops/02-story-loop/prompt.md +42 -1
- package/src/templates/research/loops/03-document-loop/prompt.md +42 -1
|
@@ -4,1203 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>RalphFlow Dashboard</title>
|
|
7
|
-
<
|
|
8
|
-
:root {
|
|
9
|
-
--bg: #0d1117;
|
|
10
|
-
--bg-surface: #161b22;
|
|
11
|
-
--bg-hover: #1c2128;
|
|
12
|
-
--bg-active: #21262d;
|
|
13
|
-
--border: #30363d;
|
|
14
|
-
--text: #e6edf3;
|
|
15
|
-
--text-dim: #8b949e;
|
|
16
|
-
--text-muted: #484f58;
|
|
17
|
-
--accent: #58a6ff;
|
|
18
|
-
--green: #3fb950;
|
|
19
|
-
--blue: #58a6ff;
|
|
20
|
-
--yellow: #d29922;
|
|
21
|
-
--red: #f85149;
|
|
22
|
-
--purple: #bc8cff;
|
|
23
|
-
--mono: 'SF Mono', 'Cascadia Code', 'Fira Code', 'JetBrains Mono', monospace;
|
|
24
|
-
--sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
25
|
-
--radius: 6px;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
29
|
-
|
|
30
|
-
body {
|
|
31
|
-
font-family: var(--sans);
|
|
32
|
-
background: var(--bg);
|
|
33
|
-
color: var(--text);
|
|
34
|
-
height: 100vh;
|
|
35
|
-
display: flex;
|
|
36
|
-
flex-direction: column;
|
|
37
|
-
overflow: hidden;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/* Header */
|
|
41
|
-
.header {
|
|
42
|
-
display: flex;
|
|
43
|
-
align-items: center;
|
|
44
|
-
justify-content: space-between;
|
|
45
|
-
padding: 12px 20px;
|
|
46
|
-
border-bottom: 1px solid var(--border);
|
|
47
|
-
background: var(--bg-surface);
|
|
48
|
-
flex-shrink: 0;
|
|
49
|
-
}
|
|
50
|
-
.header h1 {
|
|
51
|
-
font-size: 15px;
|
|
52
|
-
font-weight: 600;
|
|
53
|
-
letter-spacing: -0.3px;
|
|
54
|
-
}
|
|
55
|
-
.header .host {
|
|
56
|
-
font-family: var(--mono);
|
|
57
|
-
font-size: 12px;
|
|
58
|
-
color: var(--text-dim);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/* Main layout */
|
|
62
|
-
.main {
|
|
63
|
-
display: flex;
|
|
64
|
-
flex: 1;
|
|
65
|
-
overflow: hidden;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/* Sidebar */
|
|
69
|
-
.sidebar {
|
|
70
|
-
width: 240px;
|
|
71
|
-
border-right: 1px solid var(--border);
|
|
72
|
-
background: var(--bg-surface);
|
|
73
|
-
overflow-y: auto;
|
|
74
|
-
flex-shrink: 0;
|
|
75
|
-
}
|
|
76
|
-
.sidebar-section {
|
|
77
|
-
padding: 12px 0;
|
|
78
|
-
}
|
|
79
|
-
.sidebar-label {
|
|
80
|
-
padding: 0 16px;
|
|
81
|
-
font-size: 11px;
|
|
82
|
-
font-weight: 600;
|
|
83
|
-
text-transform: uppercase;
|
|
84
|
-
letter-spacing: 0.5px;
|
|
85
|
-
color: var(--text-dim);
|
|
86
|
-
margin-bottom: 4px;
|
|
87
|
-
}
|
|
88
|
-
.sidebar-item {
|
|
89
|
-
display: block;
|
|
90
|
-
padding: 6px 16px;
|
|
91
|
-
font-size: 13px;
|
|
92
|
-
color: var(--text-dim);
|
|
93
|
-
cursor: pointer;
|
|
94
|
-
border-left: 2px solid transparent;
|
|
95
|
-
transition: background 0.1s;
|
|
96
|
-
}
|
|
97
|
-
.sidebar-item:hover { background: var(--bg-hover); color: var(--text); }
|
|
98
|
-
.sidebar-item.active {
|
|
99
|
-
background: var(--bg-active);
|
|
100
|
-
color: var(--text);
|
|
101
|
-
border-left-color: var(--accent);
|
|
102
|
-
}
|
|
103
|
-
.sidebar-item.app-item {
|
|
104
|
-
font-weight: 600;
|
|
105
|
-
color: var(--text);
|
|
106
|
-
padding: 8px 16px;
|
|
107
|
-
}
|
|
108
|
-
.sidebar-item.loop-item { padding-left: 32px; font-size: 12px; }
|
|
109
|
-
.sidebar-item .badge {
|
|
110
|
-
font-size: 10px;
|
|
111
|
-
font-family: var(--mono);
|
|
112
|
-
padding: 1px 6px;
|
|
113
|
-
border-radius: 10px;
|
|
114
|
-
margin-left: 6px;
|
|
115
|
-
background: var(--bg-hover);
|
|
116
|
-
color: var(--text-dim);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/* Content */
|
|
120
|
-
.content {
|
|
121
|
-
flex: 1;
|
|
122
|
-
overflow-y: auto;
|
|
123
|
-
padding: 24px 32px;
|
|
124
|
-
}
|
|
125
|
-
.content-empty {
|
|
126
|
-
display: flex;
|
|
127
|
-
align-items: center;
|
|
128
|
-
justify-content: center;
|
|
129
|
-
height: 100%;
|
|
130
|
-
color: var(--text-muted);
|
|
131
|
-
font-size: 14px;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/* App header */
|
|
135
|
-
.app-header { margin-bottom: 24px; }
|
|
136
|
-
.app-header h2 {
|
|
137
|
-
font-size: 20px;
|
|
138
|
-
font-weight: 600;
|
|
139
|
-
margin-bottom: 4px;
|
|
140
|
-
}
|
|
141
|
-
.app-type-badge {
|
|
142
|
-
display: inline-block;
|
|
143
|
-
font-family: var(--mono);
|
|
144
|
-
font-size: 11px;
|
|
145
|
-
color: var(--purple);
|
|
146
|
-
background: rgba(188, 140, 255, 0.1);
|
|
147
|
-
padding: 2px 8px;
|
|
148
|
-
border-radius: 4px;
|
|
149
|
-
margin-right: 8px;
|
|
150
|
-
}
|
|
151
|
-
.app-desc {
|
|
152
|
-
color: var(--text-dim);
|
|
153
|
-
font-size: 13px;
|
|
154
|
-
margin-top: 6px;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/* Pipeline view */
|
|
158
|
-
.pipeline {
|
|
159
|
-
display: flex;
|
|
160
|
-
align-items: center;
|
|
161
|
-
gap: 0;
|
|
162
|
-
margin-bottom: 28px;
|
|
163
|
-
padding: 16px 0;
|
|
164
|
-
}
|
|
165
|
-
.pipeline-node {
|
|
166
|
-
display: flex;
|
|
167
|
-
flex-direction: column;
|
|
168
|
-
align-items: center;
|
|
169
|
-
gap: 6px;
|
|
170
|
-
cursor: pointer;
|
|
171
|
-
padding: 12px 20px;
|
|
172
|
-
border-radius: var(--radius);
|
|
173
|
-
border: 1px solid var(--border);
|
|
174
|
-
background: var(--bg-surface);
|
|
175
|
-
transition: border-color 0.15s, background 0.15s;
|
|
176
|
-
min-width: 120px;
|
|
177
|
-
}
|
|
178
|
-
.pipeline-node:hover { border-color: var(--text-dim); }
|
|
179
|
-
.pipeline-node.selected { border-color: var(--accent); background: rgba(88, 166, 255, 0.05); }
|
|
180
|
-
.pipeline-node .node-name { font-size: 12px; font-weight: 600; }
|
|
181
|
-
.pipeline-node .node-status {
|
|
182
|
-
font-family: var(--mono);
|
|
183
|
-
font-size: 10px;
|
|
184
|
-
padding: 2px 8px;
|
|
185
|
-
border-radius: 10px;
|
|
186
|
-
}
|
|
187
|
-
.pipeline-node .node-status.complete { background: rgba(63,185,80,0.15); color: var(--green); }
|
|
188
|
-
.pipeline-node .node-status.running { background: rgba(88,166,255,0.15); color: var(--blue); }
|
|
189
|
-
.pipeline-node .node-status.pending { background: rgba(139,148,158,0.1); color: var(--text-muted); }
|
|
190
|
-
.pipeline-connector {
|
|
191
|
-
width: 32px;
|
|
192
|
-
height: 2px;
|
|
193
|
-
background: var(--border);
|
|
194
|
-
flex-shrink: 0;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/* Section */
|
|
198
|
-
.section {
|
|
199
|
-
margin-bottom: 24px;
|
|
200
|
-
}
|
|
201
|
-
.section-title {
|
|
202
|
-
font-size: 11px;
|
|
203
|
-
font-weight: 600;
|
|
204
|
-
text-transform: uppercase;
|
|
205
|
-
letter-spacing: 0.5px;
|
|
206
|
-
color: var(--text-dim);
|
|
207
|
-
margin-bottom: 12px;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/* Loop detail */
|
|
211
|
-
.loop-meta {
|
|
212
|
-
display: grid;
|
|
213
|
-
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
|
214
|
-
gap: 12px;
|
|
215
|
-
margin-bottom: 16px;
|
|
216
|
-
}
|
|
217
|
-
.meta-card {
|
|
218
|
-
padding: 12px;
|
|
219
|
-
background: var(--bg-surface);
|
|
220
|
-
border: 1px solid var(--border);
|
|
221
|
-
border-radius: var(--radius);
|
|
222
|
-
}
|
|
223
|
-
.meta-label {
|
|
224
|
-
font-size: 11px;
|
|
225
|
-
color: var(--text-dim);
|
|
226
|
-
margin-bottom: 4px;
|
|
227
|
-
}
|
|
228
|
-
.meta-value {
|
|
229
|
-
font-family: var(--mono);
|
|
230
|
-
font-size: 13px;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/* Progress bar */
|
|
234
|
-
.progress-bar {
|
|
235
|
-
height: 6px;
|
|
236
|
-
background: var(--bg-active);
|
|
237
|
-
border-radius: 3px;
|
|
238
|
-
overflow: hidden;
|
|
239
|
-
margin-top: 8px;
|
|
240
|
-
}
|
|
241
|
-
.progress-fill {
|
|
242
|
-
height: 100%;
|
|
243
|
-
background: var(--green);
|
|
244
|
-
border-radius: 3px;
|
|
245
|
-
transition: width 0.3s ease;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/* Agent table */
|
|
249
|
-
.agent-table {
|
|
250
|
-
width: 100%;
|
|
251
|
-
border-collapse: collapse;
|
|
252
|
-
font-size: 12px;
|
|
253
|
-
font-family: var(--mono);
|
|
254
|
-
}
|
|
255
|
-
.agent-table th {
|
|
256
|
-
text-align: left;
|
|
257
|
-
padding: 8px 12px;
|
|
258
|
-
border-bottom: 1px solid var(--border);
|
|
259
|
-
color: var(--text-dim);
|
|
260
|
-
font-weight: 500;
|
|
261
|
-
font-size: 11px;
|
|
262
|
-
}
|
|
263
|
-
.agent-table td {
|
|
264
|
-
padding: 8px 12px;
|
|
265
|
-
border-bottom: 1px solid var(--border);
|
|
266
|
-
color: var(--text);
|
|
267
|
-
}
|
|
268
|
-
.agent-table tr:hover td { background: var(--bg-hover); }
|
|
269
|
-
|
|
270
|
-
/* Editor */
|
|
271
|
-
.editor-wrap {
|
|
272
|
-
position: relative;
|
|
273
|
-
}
|
|
274
|
-
.editor {
|
|
275
|
-
width: 100%;
|
|
276
|
-
min-height: 300px;
|
|
277
|
-
background: var(--bg-surface);
|
|
278
|
-
border: 1px solid var(--border);
|
|
279
|
-
border-radius: var(--radius);
|
|
280
|
-
color: var(--text);
|
|
281
|
-
font-family: var(--mono);
|
|
282
|
-
font-size: 13px;
|
|
283
|
-
line-height: 1.6;
|
|
284
|
-
padding: 16px;
|
|
285
|
-
resize: vertical;
|
|
286
|
-
outline: none;
|
|
287
|
-
tab-size: 2;
|
|
288
|
-
}
|
|
289
|
-
.editor:focus { border-color: var(--accent); }
|
|
290
|
-
.editor-actions {
|
|
291
|
-
display: flex;
|
|
292
|
-
gap: 8px;
|
|
293
|
-
margin-top: 8px;
|
|
294
|
-
align-items: center;
|
|
295
|
-
}
|
|
296
|
-
.btn {
|
|
297
|
-
font-family: var(--sans);
|
|
298
|
-
font-size: 12px;
|
|
299
|
-
font-weight: 500;
|
|
300
|
-
padding: 6px 14px;
|
|
301
|
-
border-radius: var(--radius);
|
|
302
|
-
border: 1px solid var(--border);
|
|
303
|
-
cursor: pointer;
|
|
304
|
-
transition: background 0.1s, border-color 0.1s;
|
|
305
|
-
background: var(--bg-surface);
|
|
306
|
-
color: var(--text);
|
|
307
|
-
}
|
|
308
|
-
.btn:hover { background: var(--bg-hover); border-color: var(--text-dim); }
|
|
309
|
-
.btn-primary {
|
|
310
|
-
background: var(--accent);
|
|
311
|
-
color: #000;
|
|
312
|
-
border-color: var(--accent);
|
|
313
|
-
}
|
|
314
|
-
.btn-primary:hover { background: #79c0ff; }
|
|
315
|
-
.btn-danger {
|
|
316
|
-
background: transparent;
|
|
317
|
-
color: var(--red);
|
|
318
|
-
border-color: var(--red);
|
|
319
|
-
}
|
|
320
|
-
.btn-danger:hover { background: rgba(248, 81, 73, 0.15); }
|
|
321
|
-
.btn-muted {
|
|
322
|
-
background: transparent;
|
|
323
|
-
color: var(--text-dim);
|
|
324
|
-
border-color: var(--border);
|
|
325
|
-
}
|
|
326
|
-
.btn-muted:hover { background: var(--bg-hover); color: var(--text); }
|
|
327
|
-
.btn:disabled { opacity: 0.5; cursor: default; }
|
|
328
|
-
.dirty-indicator {
|
|
329
|
-
font-size: 11px;
|
|
330
|
-
color: var(--yellow);
|
|
331
|
-
}
|
|
332
|
-
.save-ok {
|
|
333
|
-
font-size: 11px;
|
|
334
|
-
color: var(--green);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/* Three-panel layout */
|
|
338
|
-
.panel-grid {
|
|
339
|
-
display: grid;
|
|
340
|
-
grid-template-columns: 40fr 60fr;
|
|
341
|
-
gap: 16px;
|
|
342
|
-
min-height: 0;
|
|
343
|
-
}
|
|
344
|
-
.panel-col-left {
|
|
345
|
-
display: flex;
|
|
346
|
-
flex-direction: column;
|
|
347
|
-
gap: 16px;
|
|
348
|
-
min-height: 0;
|
|
349
|
-
}
|
|
350
|
-
.panel {
|
|
351
|
-
background: var(--bg-surface);
|
|
352
|
-
border: 1px solid var(--border);
|
|
353
|
-
border-radius: var(--radius);
|
|
354
|
-
display: flex;
|
|
355
|
-
flex-direction: column;
|
|
356
|
-
}
|
|
357
|
-
.panel-header {
|
|
358
|
-
padding: 10px 16px;
|
|
359
|
-
font-size: 11px;
|
|
360
|
-
font-weight: 600;
|
|
361
|
-
text-transform: uppercase;
|
|
362
|
-
letter-spacing: 0.5px;
|
|
363
|
-
color: var(--text-dim);
|
|
364
|
-
border-bottom: 1px solid var(--border);
|
|
365
|
-
flex-shrink: 0;
|
|
366
|
-
}
|
|
367
|
-
.panel-body {
|
|
368
|
-
padding: 16px;
|
|
369
|
-
overflow-y: auto;
|
|
370
|
-
flex: 1;
|
|
371
|
-
min-height: 0;
|
|
372
|
-
}
|
|
373
|
-
.panel-interactive {
|
|
374
|
-
flex-shrink: 0;
|
|
375
|
-
}
|
|
376
|
-
.panel-interactive .panel-body {
|
|
377
|
-
display: flex;
|
|
378
|
-
align-items: center;
|
|
379
|
-
gap: 8px;
|
|
380
|
-
padding: 12px 16px;
|
|
381
|
-
color: var(--text-muted);
|
|
382
|
-
font-size: 12px;
|
|
383
|
-
}
|
|
384
|
-
.panel-interactive .bell-icon {
|
|
385
|
-
font-size: 14px;
|
|
386
|
-
opacity: 0.5;
|
|
387
|
-
}
|
|
388
|
-
.panel-progress {
|
|
389
|
-
flex: 1;
|
|
390
|
-
min-height: 0;
|
|
391
|
-
}
|
|
392
|
-
.panel-edit {
|
|
393
|
-
min-height: 0;
|
|
394
|
-
max-height: calc(100vh - 200px);
|
|
395
|
-
}
|
|
396
|
-
.panel-edit .panel-body {
|
|
397
|
-
display: flex;
|
|
398
|
-
flex-direction: column;
|
|
399
|
-
}
|
|
400
|
-
.panel-edit .editor {
|
|
401
|
-
flex: 1;
|
|
402
|
-
min-height: 60vh;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/* Edit tabs */
|
|
406
|
-
.edit-tabs {
|
|
407
|
-
display: flex;
|
|
408
|
-
border-bottom: 1px solid var(--border);
|
|
409
|
-
flex-shrink: 0;
|
|
410
|
-
}
|
|
411
|
-
.edit-tab {
|
|
412
|
-
padding: 10px 16px;
|
|
413
|
-
font-family: var(--sans);
|
|
414
|
-
font-size: 12px;
|
|
415
|
-
font-weight: 500;
|
|
416
|
-
color: var(--text-dim);
|
|
417
|
-
background: none;
|
|
418
|
-
border: none;
|
|
419
|
-
border-bottom: 2px solid transparent;
|
|
420
|
-
cursor: pointer;
|
|
421
|
-
transition: color 0.1s, border-color 0.1s;
|
|
422
|
-
}
|
|
423
|
-
.edit-tab:hover { color: var(--text); }
|
|
424
|
-
.edit-tab.active {
|
|
425
|
-
color: var(--text);
|
|
426
|
-
border-bottom-color: var(--accent);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
/* Model selector */
|
|
430
|
-
.model-selector-wrap {
|
|
431
|
-
display: flex;
|
|
432
|
-
align-items: center;
|
|
433
|
-
gap: 8px;
|
|
434
|
-
margin-left: auto;
|
|
435
|
-
padding: 4px 12px;
|
|
436
|
-
}
|
|
437
|
-
.model-selector-wrap label {
|
|
438
|
-
font-size: 11px;
|
|
439
|
-
color: var(--text-dim);
|
|
440
|
-
white-space: nowrap;
|
|
441
|
-
}
|
|
442
|
-
.model-selector {
|
|
443
|
-
font-family: var(--mono);
|
|
444
|
-
font-size: 11px;
|
|
445
|
-
color: var(--text);
|
|
446
|
-
background: var(--bg);
|
|
447
|
-
border: 1px solid var(--border);
|
|
448
|
-
border-radius: var(--radius);
|
|
449
|
-
padding: 4px 8px;
|
|
450
|
-
cursor: pointer;
|
|
451
|
-
outline: none;
|
|
452
|
-
}
|
|
453
|
-
.model-selector:hover { border-color: var(--text-dim); }
|
|
454
|
-
.model-selector:focus { border-color: var(--accent); }
|
|
455
|
-
.model-save-ok {
|
|
456
|
-
font-size: 11px;
|
|
457
|
-
color: var(--green);
|
|
458
|
-
opacity: 0;
|
|
459
|
-
transition: opacity 0.2s;
|
|
460
|
-
}
|
|
461
|
-
.model-save-ok.visible { opacity: 1; }
|
|
462
|
-
|
|
463
|
-
/* Prompt mode toggle */
|
|
464
|
-
.prompt-mode-toggle {
|
|
465
|
-
display: flex;
|
|
466
|
-
gap: 0;
|
|
467
|
-
margin-bottom: 12px;
|
|
468
|
-
border: 1px solid var(--border);
|
|
469
|
-
border-radius: var(--radius);
|
|
470
|
-
overflow: hidden;
|
|
471
|
-
width: fit-content;
|
|
472
|
-
}
|
|
473
|
-
.prompt-mode-btn {
|
|
474
|
-
padding: 6px 16px;
|
|
475
|
-
font-family: var(--sans);
|
|
476
|
-
font-size: 12px;
|
|
477
|
-
font-weight: 500;
|
|
478
|
-
color: var(--text-dim);
|
|
479
|
-
background: var(--bg);
|
|
480
|
-
border: none;
|
|
481
|
-
cursor: pointer;
|
|
482
|
-
transition: all 0.15s;
|
|
483
|
-
}
|
|
484
|
-
.prompt-mode-btn:not(:last-child) {
|
|
485
|
-
border-right: 1px solid var(--border);
|
|
486
|
-
}
|
|
487
|
-
.prompt-mode-btn:hover { color: var(--text); background: var(--bg-hover); }
|
|
488
|
-
.prompt-mode-btn.active {
|
|
489
|
-
color: var(--text);
|
|
490
|
-
background: var(--bg-active);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
/* Markdown preview */
|
|
494
|
-
.prompt-preview {
|
|
495
|
-
flex: 1;
|
|
496
|
-
min-height: 60vh;
|
|
497
|
-
background: var(--bg-surface);
|
|
498
|
-
border: 1px solid var(--border);
|
|
499
|
-
border-radius: var(--radius);
|
|
500
|
-
color: var(--text);
|
|
501
|
-
font-family: var(--sans);
|
|
502
|
-
font-size: 14px;
|
|
503
|
-
line-height: 1.7;
|
|
504
|
-
padding: 24px 28px;
|
|
505
|
-
overflow: auto;
|
|
506
|
-
}
|
|
507
|
-
.prompt-preview h1 { font-size: 22px; font-weight: 600; margin: 24px 0 12px; color: var(--text); border-bottom: 1px solid var(--border); padding-bottom: 8px; }
|
|
508
|
-
.prompt-preview h1:first-child { margin-top: 0; }
|
|
509
|
-
.prompt-preview h2 { font-size: 18px; font-weight: 600; margin: 20px 0 10px; color: var(--text); }
|
|
510
|
-
.prompt-preview h3 { font-size: 15px; font-weight: 600; margin: 16px 0 8px; color: var(--text); }
|
|
511
|
-
.prompt-preview h4 { font-size: 13px; font-weight: 600; margin: 14px 0 6px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.5px; }
|
|
512
|
-
.prompt-preview p { margin: 0 0 12px; }
|
|
513
|
-
.prompt-preview ul, .prompt-preview ol { margin: 0 0 12px; padding-left: 24px; }
|
|
514
|
-
.prompt-preview li { margin: 4px 0; }
|
|
515
|
-
.prompt-preview li.task-done { color: var(--text-dim); text-decoration: line-through; }
|
|
516
|
-
.prompt-preview code {
|
|
517
|
-
font-family: var(--mono);
|
|
518
|
-
font-size: 12px;
|
|
519
|
-
background: var(--bg);
|
|
520
|
-
border: 1px solid var(--border);
|
|
521
|
-
border-radius: 3px;
|
|
522
|
-
padding: 2px 6px;
|
|
523
|
-
}
|
|
524
|
-
.prompt-preview pre {
|
|
525
|
-
background: var(--bg);
|
|
526
|
-
border: 1px solid var(--border);
|
|
527
|
-
border-radius: var(--radius);
|
|
528
|
-
padding: 14px 16px;
|
|
529
|
-
margin: 0 0 12px;
|
|
530
|
-
overflow-x: auto;
|
|
531
|
-
}
|
|
532
|
-
.prompt-preview pre code {
|
|
533
|
-
background: none;
|
|
534
|
-
border: none;
|
|
535
|
-
padding: 0;
|
|
536
|
-
font-size: 13px;
|
|
537
|
-
line-height: 1.5;
|
|
538
|
-
}
|
|
539
|
-
.prompt-preview blockquote {
|
|
540
|
-
border-left: 3px solid var(--accent);
|
|
541
|
-
padding: 4px 16px;
|
|
542
|
-
margin: 0 0 12px;
|
|
543
|
-
color: var(--text-dim);
|
|
544
|
-
background: var(--bg);
|
|
545
|
-
border-radius: 0 var(--radius) var(--radius) 0;
|
|
546
|
-
}
|
|
547
|
-
.prompt-preview hr {
|
|
548
|
-
border: none;
|
|
549
|
-
border-top: 1px solid var(--border);
|
|
550
|
-
margin: 16px 0;
|
|
551
|
-
}
|
|
552
|
-
.prompt-preview strong { font-weight: 600; color: var(--text); }
|
|
553
|
-
.prompt-preview em { font-style: italic; }
|
|
554
|
-
.prompt-preview table {
|
|
555
|
-
width: 100%;
|
|
556
|
-
border-collapse: collapse;
|
|
557
|
-
margin: 0 0 12px;
|
|
558
|
-
font-size: 13px;
|
|
559
|
-
}
|
|
560
|
-
.prompt-preview th, .prompt-preview td {
|
|
561
|
-
border: 1px solid var(--border);
|
|
562
|
-
padding: 8px 12px;
|
|
563
|
-
text-align: left;
|
|
564
|
-
}
|
|
565
|
-
.prompt-preview th {
|
|
566
|
-
background: var(--bg);
|
|
567
|
-
font-weight: 600;
|
|
568
|
-
font-size: 12px;
|
|
569
|
-
text-transform: uppercase;
|
|
570
|
-
letter-spacing: 0.3px;
|
|
571
|
-
color: var(--text-dim);
|
|
572
|
-
}
|
|
573
|
-
.prompt-preview a { color: var(--accent); text-decoration: none; }
|
|
574
|
-
.prompt-preview a:hover { text-decoration: underline; }
|
|
575
|
-
|
|
576
|
-
/* Read-only code viewer */
|
|
577
|
-
.code-viewer {
|
|
578
|
-
width: 100%;
|
|
579
|
-
flex: 1;
|
|
580
|
-
min-height: 200px;
|
|
581
|
-
background: var(--bg);
|
|
582
|
-
border: 1px solid var(--border);
|
|
583
|
-
border-radius: var(--radius);
|
|
584
|
-
color: var(--text);
|
|
585
|
-
font-family: var(--mono);
|
|
586
|
-
font-size: 13px;
|
|
587
|
-
line-height: 1.6;
|
|
588
|
-
padding: 16px;
|
|
589
|
-
overflow: auto;
|
|
590
|
-
white-space: pre-wrap;
|
|
591
|
-
word-wrap: break-word;
|
|
592
|
-
margin: 0;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
/* Tracker viewer */
|
|
596
|
-
.tracker-viewer {
|
|
597
|
-
background: var(--bg-surface);
|
|
598
|
-
border: 1px solid var(--border);
|
|
599
|
-
border-radius: var(--radius);
|
|
600
|
-
padding: 16px;
|
|
601
|
-
font-family: var(--mono);
|
|
602
|
-
font-size: 13px;
|
|
603
|
-
line-height: 1.7;
|
|
604
|
-
max-height: 400px;
|
|
605
|
-
overflow-y: auto;
|
|
606
|
-
white-space: pre-wrap;
|
|
607
|
-
word-wrap: break-word;
|
|
608
|
-
}
|
|
609
|
-
.panel-progress .tracker-viewer {
|
|
610
|
-
border: none;
|
|
611
|
-
border-radius: 0;
|
|
612
|
-
max-height: none;
|
|
613
|
-
padding: 0;
|
|
614
|
-
margin-top: 16px;
|
|
615
|
-
}
|
|
616
|
-
.tracker-viewer h1, .tracker-viewer h2, .tracker-viewer h3 {
|
|
617
|
-
font-family: var(--sans);
|
|
618
|
-
margin: 12px 0 6px;
|
|
619
|
-
}
|
|
620
|
-
.tracker-viewer h1 { font-size: 16px; }
|
|
621
|
-
.tracker-viewer h2 { font-size: 14px; }
|
|
622
|
-
.tracker-viewer h3 { font-size: 13px; }
|
|
623
|
-
.tracker-viewer .cb-done { color: var(--green); }
|
|
624
|
-
.tracker-viewer .cb-todo { color: var(--text-muted); }
|
|
625
|
-
.tracker-viewer table {
|
|
626
|
-
border-collapse: collapse;
|
|
627
|
-
margin: 8px 0;
|
|
628
|
-
width: 100%;
|
|
629
|
-
}
|
|
630
|
-
.tracker-viewer th, .tracker-viewer td {
|
|
631
|
-
border: 1px solid var(--border);
|
|
632
|
-
padding: 4px 10px;
|
|
633
|
-
text-align: left;
|
|
634
|
-
font-size: 12px;
|
|
635
|
-
}
|
|
636
|
-
.tracker-viewer th { background: var(--bg-active); color: var(--text-dim); }
|
|
637
|
-
|
|
638
|
-
/* Status bar */
|
|
639
|
-
.statusbar {
|
|
640
|
-
display: flex;
|
|
641
|
-
align-items: center;
|
|
642
|
-
gap: 16px;
|
|
643
|
-
padding: 6px 20px;
|
|
644
|
-
border-top: 1px solid var(--border);
|
|
645
|
-
background: var(--bg-surface);
|
|
646
|
-
font-size: 11px;
|
|
647
|
-
color: var(--text-dim);
|
|
648
|
-
flex-shrink: 0;
|
|
649
|
-
}
|
|
650
|
-
.status-dot {
|
|
651
|
-
display: inline-block;
|
|
652
|
-
width: 7px;
|
|
653
|
-
height: 7px;
|
|
654
|
-
border-radius: 50%;
|
|
655
|
-
margin-right: 4px;
|
|
656
|
-
}
|
|
657
|
-
.status-dot.connected { background: var(--green); }
|
|
658
|
-
.status-dot.disconnected { background: var(--red); }
|
|
659
|
-
.status-dot.connecting { background: var(--yellow); }
|
|
660
|
-
|
|
661
|
-
/* New App button */
|
|
662
|
-
.new-app-btn {
|
|
663
|
-
display: flex;
|
|
664
|
-
align-items: center;
|
|
665
|
-
gap: 6px;
|
|
666
|
-
width: calc(100% - 24px);
|
|
667
|
-
margin: 8px 12px 4px;
|
|
668
|
-
padding: 6px 12px;
|
|
669
|
-
font-family: var(--sans);
|
|
670
|
-
font-size: 12px;
|
|
671
|
-
font-weight: 500;
|
|
672
|
-
color: var(--text-dim);
|
|
673
|
-
background: transparent;
|
|
674
|
-
border: 1px dashed var(--border);
|
|
675
|
-
border-radius: var(--radius);
|
|
676
|
-
cursor: pointer;
|
|
677
|
-
transition: background 0.1s, color 0.1s, border-color 0.1s;
|
|
678
|
-
}
|
|
679
|
-
.new-app-btn:hover {
|
|
680
|
-
background: var(--bg-hover);
|
|
681
|
-
color: var(--text);
|
|
682
|
-
border-color: var(--text-dim);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/* Modal overlay */
|
|
686
|
-
.modal-overlay {
|
|
687
|
-
position: fixed;
|
|
688
|
-
inset: 0;
|
|
689
|
-
background: rgba(0, 0, 0, 0.6);
|
|
690
|
-
display: flex;
|
|
691
|
-
align-items: center;
|
|
692
|
-
justify-content: center;
|
|
693
|
-
z-index: 1000;
|
|
694
|
-
}
|
|
695
|
-
.modal {
|
|
696
|
-
background: var(--bg-surface);
|
|
697
|
-
border: 1px solid var(--border);
|
|
698
|
-
border-radius: 8px;
|
|
699
|
-
width: 420px;
|
|
700
|
-
max-width: 90vw;
|
|
701
|
-
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4);
|
|
702
|
-
}
|
|
703
|
-
.modal-header {
|
|
704
|
-
display: flex;
|
|
705
|
-
align-items: center;
|
|
706
|
-
justify-content: space-between;
|
|
707
|
-
padding: 16px 20px;
|
|
708
|
-
border-bottom: 1px solid var(--border);
|
|
709
|
-
}
|
|
710
|
-
.modal-header h3 {
|
|
711
|
-
font-size: 14px;
|
|
712
|
-
font-weight: 600;
|
|
713
|
-
}
|
|
714
|
-
.modal-close {
|
|
715
|
-
background: none;
|
|
716
|
-
border: none;
|
|
717
|
-
color: var(--text-dim);
|
|
718
|
-
font-size: 18px;
|
|
719
|
-
cursor: pointer;
|
|
720
|
-
padding: 0 4px;
|
|
721
|
-
line-height: 1;
|
|
722
|
-
}
|
|
723
|
-
.modal-close:hover { color: var(--text); }
|
|
724
|
-
.modal-body {
|
|
725
|
-
padding: 20px;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
/* Form elements */
|
|
729
|
-
.form-group {
|
|
730
|
-
margin-bottom: 16px;
|
|
731
|
-
}
|
|
732
|
-
.form-label {
|
|
733
|
-
display: block;
|
|
734
|
-
font-size: 12px;
|
|
735
|
-
font-weight: 500;
|
|
736
|
-
color: var(--text-dim);
|
|
737
|
-
margin-bottom: 6px;
|
|
738
|
-
}
|
|
739
|
-
.form-select, .form-input {
|
|
740
|
-
width: 100%;
|
|
741
|
-
padding: 8px 12px;
|
|
742
|
-
font-family: var(--sans);
|
|
743
|
-
font-size: 13px;
|
|
744
|
-
color: var(--text);
|
|
745
|
-
background: var(--bg);
|
|
746
|
-
border: 1px solid var(--border);
|
|
747
|
-
border-radius: var(--radius);
|
|
748
|
-
outline: none;
|
|
749
|
-
}
|
|
750
|
-
.form-select:focus, .form-input:focus {
|
|
751
|
-
border-color: var(--accent);
|
|
752
|
-
}
|
|
753
|
-
.form-select option {
|
|
754
|
-
background: var(--bg);
|
|
755
|
-
color: var(--text);
|
|
756
|
-
}
|
|
757
|
-
.form-error {
|
|
758
|
-
font-size: 12px;
|
|
759
|
-
color: var(--red);
|
|
760
|
-
margin-top: 12px;
|
|
761
|
-
}
|
|
762
|
-
.form-warning {
|
|
763
|
-
font-size: 12px;
|
|
764
|
-
color: var(--yellow);
|
|
765
|
-
margin-top: 12px;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
/* Next-steps view */
|
|
769
|
-
.next-steps-success {
|
|
770
|
-
display: flex;
|
|
771
|
-
align-items: center;
|
|
772
|
-
gap: 8px;
|
|
773
|
-
font-size: 13px;
|
|
774
|
-
color: var(--green);
|
|
775
|
-
margin-bottom: 16px;
|
|
776
|
-
}
|
|
777
|
-
.next-steps-label {
|
|
778
|
-
font-size: 12px;
|
|
779
|
-
color: var(--text-dim);
|
|
780
|
-
margin-bottom: 8px;
|
|
781
|
-
}
|
|
782
|
-
.cmd-item {
|
|
783
|
-
display: flex;
|
|
784
|
-
align-items: center;
|
|
785
|
-
gap: 8px;
|
|
786
|
-
margin-bottom: 8px;
|
|
787
|
-
background: var(--bg);
|
|
788
|
-
border: 1px solid var(--border);
|
|
789
|
-
border-radius: var(--radius);
|
|
790
|
-
padding: 8px 12px;
|
|
791
|
-
font-family: var(--mono);
|
|
792
|
-
font-size: 12px;
|
|
793
|
-
color: var(--text);
|
|
794
|
-
}
|
|
795
|
-
.cmd-text {
|
|
796
|
-
flex: 1;
|
|
797
|
-
overflow-x: auto;
|
|
798
|
-
white-space: nowrap;
|
|
799
|
-
}
|
|
800
|
-
.cmd-copy {
|
|
801
|
-
background: none;
|
|
802
|
-
border: 1px solid var(--border);
|
|
803
|
-
border-radius: 4px;
|
|
804
|
-
color: var(--text-dim);
|
|
805
|
-
font-family: var(--sans);
|
|
806
|
-
font-size: 11px;
|
|
807
|
-
padding: 2px 8px;
|
|
808
|
-
cursor: pointer;
|
|
809
|
-
flex-shrink: 0;
|
|
810
|
-
transition: background 0.1s, color 0.1s;
|
|
811
|
-
}
|
|
812
|
-
.cmd-copy:hover { background: var(--bg-hover); color: var(--text); }
|
|
813
|
-
|
|
814
|
-
.modal-footer {
|
|
815
|
-
padding: 12px 20px;
|
|
816
|
-
border-top: 1px solid var(--border);
|
|
817
|
-
display: flex;
|
|
818
|
-
justify-content: flex-end;
|
|
819
|
-
gap: 8px;
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
/* Notification list in interactive panel */
|
|
823
|
-
.panel-interactive.has-notifs .panel-body {
|
|
824
|
-
display: block;
|
|
825
|
-
padding: 8px;
|
|
826
|
-
max-height: 240px;
|
|
827
|
-
overflow-y: auto;
|
|
828
|
-
color: var(--text);
|
|
829
|
-
}
|
|
830
|
-
.notif-card {
|
|
831
|
-
display: flex;
|
|
832
|
-
align-items: flex-start;
|
|
833
|
-
gap: 8px;
|
|
834
|
-
padding: 8px 10px;
|
|
835
|
-
background: var(--bg);
|
|
836
|
-
border: 1px solid var(--border);
|
|
837
|
-
border-radius: var(--radius);
|
|
838
|
-
margin-bottom: 6px;
|
|
839
|
-
font-size: 12px;
|
|
840
|
-
}
|
|
841
|
-
.notif-card:last-child { margin-bottom: 0; }
|
|
842
|
-
.notif-time {
|
|
843
|
-
color: var(--text-dim);
|
|
844
|
-
font-family: var(--mono);
|
|
845
|
-
font-size: 11px;
|
|
846
|
-
flex-shrink: 0;
|
|
847
|
-
white-space: nowrap;
|
|
848
|
-
}
|
|
849
|
-
.notif-msg {
|
|
850
|
-
flex: 1;
|
|
851
|
-
color: var(--text);
|
|
852
|
-
word-break: break-word;
|
|
853
|
-
}
|
|
854
|
-
.notif-dismiss {
|
|
855
|
-
background: none;
|
|
856
|
-
border: none;
|
|
857
|
-
color: var(--text-muted);
|
|
858
|
-
font-size: 16px;
|
|
859
|
-
cursor: pointer;
|
|
860
|
-
padding: 0 2px;
|
|
861
|
-
line-height: 1;
|
|
862
|
-
flex-shrink: 0;
|
|
863
|
-
}
|
|
864
|
-
.notif-dismiss:hover { color: var(--red); }
|
|
865
|
-
|
|
866
|
-
/* Sidebar notification badge */
|
|
867
|
-
.notif-badge {
|
|
868
|
-
font-size: 10px;
|
|
869
|
-
font-family: var(--mono);
|
|
870
|
-
padding: 1px 6px;
|
|
871
|
-
border-radius: 10px;
|
|
872
|
-
margin-left: 6px;
|
|
873
|
-
background: var(--accent);
|
|
874
|
-
color: #000;
|
|
875
|
-
font-weight: 600;
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
/* App-level tabs (Loops / Archives) */
|
|
879
|
-
.app-tabs {
|
|
880
|
-
display: flex;
|
|
881
|
-
gap: 0;
|
|
882
|
-
border-bottom: 1px solid var(--border);
|
|
883
|
-
margin-bottom: 24px;
|
|
884
|
-
}
|
|
885
|
-
.app-tab {
|
|
886
|
-
padding: 10px 20px;
|
|
887
|
-
font-family: var(--sans);
|
|
888
|
-
font-size: 13px;
|
|
889
|
-
font-weight: 500;
|
|
890
|
-
color: var(--text-dim);
|
|
891
|
-
background: none;
|
|
892
|
-
border: none;
|
|
893
|
-
border-bottom: 2px solid transparent;
|
|
894
|
-
cursor: pointer;
|
|
895
|
-
transition: color 0.1s, border-color 0.1s;
|
|
896
|
-
}
|
|
897
|
-
.app-tab:hover { color: var(--text); }
|
|
898
|
-
.app-tab.active {
|
|
899
|
-
color: var(--text);
|
|
900
|
-
border-bottom-color: var(--accent);
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
/* Archive timeline */
|
|
904
|
-
.archive-timeline {
|
|
905
|
-
display: flex;
|
|
906
|
-
flex-direction: column;
|
|
907
|
-
gap: 12px;
|
|
908
|
-
}
|
|
909
|
-
.archive-empty {
|
|
910
|
-
text-align: center;
|
|
911
|
-
padding: 48px 24px;
|
|
912
|
-
color: var(--text-muted);
|
|
913
|
-
font-size: 14px;
|
|
914
|
-
}
|
|
915
|
-
.archive-empty-icon {
|
|
916
|
-
font-size: 32px;
|
|
917
|
-
margin-bottom: 12px;
|
|
918
|
-
opacity: 0.4;
|
|
919
|
-
}
|
|
920
|
-
.archive-card {
|
|
921
|
-
background: var(--bg-surface);
|
|
922
|
-
border: 1px solid var(--border);
|
|
923
|
-
border-radius: var(--radius);
|
|
924
|
-
overflow: hidden;
|
|
925
|
-
transition: border-color 0.15s;
|
|
926
|
-
}
|
|
927
|
-
.archive-card:hover { border-color: var(--text-dim); }
|
|
928
|
-
.archive-card.expanded { border-color: var(--accent); }
|
|
929
|
-
.archive-card-header {
|
|
930
|
-
display: flex;
|
|
931
|
-
align-items: center;
|
|
932
|
-
justify-content: space-between;
|
|
933
|
-
padding: 14px 16px;
|
|
934
|
-
cursor: pointer;
|
|
935
|
-
transition: background 0.1s;
|
|
936
|
-
}
|
|
937
|
-
.archive-card-header:hover { background: var(--bg-hover); }
|
|
938
|
-
.archive-card-date {
|
|
939
|
-
font-family: var(--mono);
|
|
940
|
-
font-size: 13px;
|
|
941
|
-
font-weight: 600;
|
|
942
|
-
color: var(--text);
|
|
943
|
-
}
|
|
944
|
-
.archive-card-stats {
|
|
945
|
-
display: flex;
|
|
946
|
-
gap: 12px;
|
|
947
|
-
font-size: 12px;
|
|
948
|
-
color: var(--text-dim);
|
|
949
|
-
}
|
|
950
|
-
.archive-card-stat {
|
|
951
|
-
display: flex;
|
|
952
|
-
align-items: center;
|
|
953
|
-
gap: 4px;
|
|
954
|
-
}
|
|
955
|
-
.archive-card-stat .stat-val {
|
|
956
|
-
font-family: var(--mono);
|
|
957
|
-
color: var(--text);
|
|
958
|
-
}
|
|
959
|
-
.archive-card-chevron {
|
|
960
|
-
font-size: 12px;
|
|
961
|
-
color: var(--text-muted);
|
|
962
|
-
transition: transform 0.2s;
|
|
963
|
-
margin-left: 12px;
|
|
964
|
-
}
|
|
965
|
-
.archive-card.expanded .archive-card-chevron {
|
|
966
|
-
transform: rotate(90deg);
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
/* Archive file browser */
|
|
970
|
-
.archive-files {
|
|
971
|
-
border-top: 1px solid var(--border);
|
|
972
|
-
max-height: 400px;
|
|
973
|
-
overflow-y: auto;
|
|
974
|
-
}
|
|
975
|
-
.archive-file-item {
|
|
976
|
-
display: flex;
|
|
977
|
-
align-items: center;
|
|
978
|
-
gap: 8px;
|
|
979
|
-
padding: 8px 16px;
|
|
980
|
-
font-family: var(--mono);
|
|
981
|
-
font-size: 12px;
|
|
982
|
-
color: var(--text-dim);
|
|
983
|
-
cursor: pointer;
|
|
984
|
-
transition: background 0.1s;
|
|
985
|
-
border-bottom: 1px solid var(--border);
|
|
986
|
-
}
|
|
987
|
-
.archive-file-item:last-child { border-bottom: none; }
|
|
988
|
-
.archive-file-item:hover { background: var(--bg-hover); color: var(--text); }
|
|
989
|
-
.archive-file-item.active { background: var(--bg-active); color: var(--accent); }
|
|
990
|
-
.archive-file-icon { opacity: 0.5; font-size: 11px; }
|
|
991
|
-
|
|
992
|
-
/* Archive file viewer (inline) */
|
|
993
|
-
.archive-file-viewer {
|
|
994
|
-
border-top: 1px solid var(--border);
|
|
995
|
-
display: flex;
|
|
996
|
-
flex-direction: column;
|
|
997
|
-
max-height: 500px;
|
|
998
|
-
}
|
|
999
|
-
.archive-file-viewer-header {
|
|
1000
|
-
display: flex;
|
|
1001
|
-
align-items: center;
|
|
1002
|
-
justify-content: space-between;
|
|
1003
|
-
padding: 8px 16px;
|
|
1004
|
-
background: var(--bg-active);
|
|
1005
|
-
font-family: var(--mono);
|
|
1006
|
-
font-size: 12px;
|
|
1007
|
-
color: var(--text-dim);
|
|
1008
|
-
flex-shrink: 0;
|
|
1009
|
-
}
|
|
1010
|
-
.archive-file-viewer-close {
|
|
1011
|
-
background: none;
|
|
1012
|
-
border: none;
|
|
1013
|
-
color: var(--text-muted);
|
|
1014
|
-
font-size: 16px;
|
|
1015
|
-
cursor: pointer;
|
|
1016
|
-
padding: 0 4px;
|
|
1017
|
-
line-height: 1;
|
|
1018
|
-
}
|
|
1019
|
-
.archive-file-viewer-close:hover { color: var(--text); }
|
|
1020
|
-
.archive-file-content {
|
|
1021
|
-
flex: 1;
|
|
1022
|
-
overflow: auto;
|
|
1023
|
-
padding: 16px;
|
|
1024
|
-
font-family: var(--mono);
|
|
1025
|
-
font-size: 13px;
|
|
1026
|
-
line-height: 1.6;
|
|
1027
|
-
white-space: pre-wrap;
|
|
1028
|
-
word-wrap: break-word;
|
|
1029
|
-
background: var(--bg);
|
|
1030
|
-
color: var(--text);
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
/* Templates page */
|
|
1034
|
-
.templates-header {
|
|
1035
|
-
display: flex;
|
|
1036
|
-
align-items: center;
|
|
1037
|
-
justify-content: space-between;
|
|
1038
|
-
margin-bottom: 24px;
|
|
1039
|
-
}
|
|
1040
|
-
.templates-header h2 { font-size: 20px; font-weight: 600; }
|
|
1041
|
-
.template-grid {
|
|
1042
|
-
display: grid;
|
|
1043
|
-
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
1044
|
-
gap: 12px;
|
|
1045
|
-
}
|
|
1046
|
-
.template-card {
|
|
1047
|
-
background: var(--bg-surface);
|
|
1048
|
-
border: 1px solid var(--border);
|
|
1049
|
-
border-radius: var(--radius);
|
|
1050
|
-
padding: 16px;
|
|
1051
|
-
transition: border-color 0.15s;
|
|
1052
|
-
}
|
|
1053
|
-
.template-card:hover { border-color: var(--text-dim); }
|
|
1054
|
-
.template-card-header {
|
|
1055
|
-
display: flex;
|
|
1056
|
-
align-items: center;
|
|
1057
|
-
justify-content: space-between;
|
|
1058
|
-
margin-bottom: 8px;
|
|
1059
|
-
}
|
|
1060
|
-
.template-card-name { font-size: 14px; font-weight: 600; }
|
|
1061
|
-
.template-card-type {
|
|
1062
|
-
font-family: var(--mono);
|
|
1063
|
-
font-size: 10px;
|
|
1064
|
-
padding: 2px 8px;
|
|
1065
|
-
border-radius: 10px;
|
|
1066
|
-
}
|
|
1067
|
-
.template-card-type.built-in { background: rgba(88,166,255,0.1); color: var(--blue); }
|
|
1068
|
-
.template-card-type.custom { background: rgba(63,185,80,0.1); color: var(--green); }
|
|
1069
|
-
.template-card-desc { font-size: 12px; color: var(--text-dim); margin-bottom: 8px; }
|
|
1070
|
-
.template-card-meta {
|
|
1071
|
-
display: flex;
|
|
1072
|
-
align-items: center;
|
|
1073
|
-
justify-content: space-between;
|
|
1074
|
-
font-size: 11px;
|
|
1075
|
-
color: var(--text-muted);
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
/* Template builder */
|
|
1079
|
-
.template-builder { max-width: 800px; }
|
|
1080
|
-
.builder-section { margin-bottom: 24px; }
|
|
1081
|
-
.builder-section-title {
|
|
1082
|
-
font-size: 13px;
|
|
1083
|
-
font-weight: 600;
|
|
1084
|
-
margin-bottom: 12px;
|
|
1085
|
-
color: var(--text);
|
|
1086
|
-
}
|
|
1087
|
-
.loop-cards { display: flex; flex-direction: column; gap: 12px; }
|
|
1088
|
-
.loop-card {
|
|
1089
|
-
background: var(--bg-surface);
|
|
1090
|
-
border: 1px solid var(--border);
|
|
1091
|
-
border-radius: var(--radius);
|
|
1092
|
-
padding: 16px;
|
|
1093
|
-
}
|
|
1094
|
-
.loop-card-header {
|
|
1095
|
-
display: flex;
|
|
1096
|
-
align-items: center;
|
|
1097
|
-
justify-content: space-between;
|
|
1098
|
-
margin-bottom: 12px;
|
|
1099
|
-
}
|
|
1100
|
-
.loop-card-title { font-size: 13px; font-weight: 600; }
|
|
1101
|
-
.loop-card-remove {
|
|
1102
|
-
background: none;
|
|
1103
|
-
border: none;
|
|
1104
|
-
color: var(--text-muted);
|
|
1105
|
-
font-size: 16px;
|
|
1106
|
-
cursor: pointer;
|
|
1107
|
-
padding: 0 4px;
|
|
1108
|
-
line-height: 1;
|
|
1109
|
-
}
|
|
1110
|
-
.loop-card-remove:hover { color: var(--red); }
|
|
1111
|
-
.loop-card-grid {
|
|
1112
|
-
display: grid;
|
|
1113
|
-
grid-template-columns: 1fr 1fr;
|
|
1114
|
-
gap: 12px;
|
|
1115
|
-
}
|
|
1116
|
-
.loop-card-grid .form-group { margin-bottom: 0; }
|
|
1117
|
-
.loop-card-full { grid-column: 1 / -1; }
|
|
1118
|
-
.stage-tags {
|
|
1119
|
-
display: flex;
|
|
1120
|
-
flex-wrap: wrap;
|
|
1121
|
-
gap: 6px;
|
|
1122
|
-
padding: 6px 8px;
|
|
1123
|
-
background: var(--bg);
|
|
1124
|
-
border: 1px solid var(--border);
|
|
1125
|
-
border-radius: var(--radius);
|
|
1126
|
-
min-height: 36px;
|
|
1127
|
-
align-items: center;
|
|
1128
|
-
cursor: text;
|
|
1129
|
-
}
|
|
1130
|
-
.stage-tags:focus-within { border-color: var(--accent); }
|
|
1131
|
-
.stage-tag {
|
|
1132
|
-
display: flex;
|
|
1133
|
-
align-items: center;
|
|
1134
|
-
gap: 4px;
|
|
1135
|
-
font-family: var(--mono);
|
|
1136
|
-
font-size: 11px;
|
|
1137
|
-
padding: 2px 8px;
|
|
1138
|
-
background: rgba(88,166,255,0.1);
|
|
1139
|
-
color: var(--accent);
|
|
1140
|
-
border-radius: 4px;
|
|
1141
|
-
}
|
|
1142
|
-
.stage-tag-remove {
|
|
1143
|
-
background: none;
|
|
1144
|
-
border: none;
|
|
1145
|
-
color: var(--text-muted);
|
|
1146
|
-
font-size: 14px;
|
|
1147
|
-
cursor: pointer;
|
|
1148
|
-
padding: 0 2px;
|
|
1149
|
-
line-height: 1;
|
|
1150
|
-
}
|
|
1151
|
-
.stage-tag-remove:hover { color: var(--red); }
|
|
1152
|
-
.stage-tags input {
|
|
1153
|
-
border: none;
|
|
1154
|
-
background: none;
|
|
1155
|
-
color: var(--text);
|
|
1156
|
-
font-family: var(--sans);
|
|
1157
|
-
font-size: 13px;
|
|
1158
|
-
outline: none;
|
|
1159
|
-
min-width: 80px;
|
|
1160
|
-
flex: 1;
|
|
1161
|
-
}
|
|
1162
|
-
.multi-agent-fields {
|
|
1163
|
-
margin-top: 8px;
|
|
1164
|
-
padding: 12px;
|
|
1165
|
-
background: var(--bg);
|
|
1166
|
-
border: 1px solid var(--border);
|
|
1167
|
-
border-radius: var(--radius);
|
|
1168
|
-
}
|
|
1169
|
-
.toggle-wrap { display: flex; align-items: center; gap: 8px; }
|
|
1170
|
-
.toggle-input { accent-color: var(--accent); }
|
|
1171
|
-
.toggle-label { font-size: 12px; color: var(--text-dim); }
|
|
1172
|
-
.yaml-preview-section { margin-top: 24px; }
|
|
1173
|
-
.yaml-preview {
|
|
1174
|
-
background: var(--bg);
|
|
1175
|
-
border: 1px solid var(--border);
|
|
1176
|
-
border-radius: var(--radius);
|
|
1177
|
-
padding: 16px;
|
|
1178
|
-
font-family: var(--mono);
|
|
1179
|
-
font-size: 12px;
|
|
1180
|
-
line-height: 1.6;
|
|
1181
|
-
color: var(--text);
|
|
1182
|
-
white-space: pre-wrap;
|
|
1183
|
-
word-wrap: break-word;
|
|
1184
|
-
max-height: 400px;
|
|
1185
|
-
overflow-y: auto;
|
|
1186
|
-
}
|
|
1187
|
-
.builder-actions { display: flex; gap: 8px; margin-top: 20px; }
|
|
1188
|
-
.optional-toggle {
|
|
1189
|
-
font-size: 12px;
|
|
1190
|
-
color: var(--accent);
|
|
1191
|
-
background: none;
|
|
1192
|
-
border: none;
|
|
1193
|
-
cursor: pointer;
|
|
1194
|
-
padding: 0;
|
|
1195
|
-
margin-top: 8px;
|
|
1196
|
-
}
|
|
1197
|
-
.optional-toggle:hover { text-decoration: underline; }
|
|
1198
|
-
.optional-fields {
|
|
1199
|
-
margin-top: 12px;
|
|
1200
|
-
padding-top: 12px;
|
|
1201
|
-
border-top: 1px solid var(--border);
|
|
1202
|
-
}
|
|
1203
|
-
</style>
|
|
7
|
+
<link rel="stylesheet" href="styles.css">
|
|
1204
8
|
</head>
|
|
1205
9
|
<body>
|
|
1206
10
|
|
|
@@ -1231,2018 +35,6 @@
|
|
|
1231
35
|
<span>Events: <span id="eventCount">0</span></span>
|
|
1232
36
|
</div>
|
|
1233
37
|
|
|
1234
|
-
<script>
|
|
1235
|
-
(function() {
|
|
1236
|
-
// State
|
|
1237
|
-
let apps = [];
|
|
1238
|
-
let selectedApp = null;
|
|
1239
|
-
let selectedLoop = null;
|
|
1240
|
-
let eventCounter = 0;
|
|
1241
|
-
let promptDirty = false;
|
|
1242
|
-
let promptOriginal = '';
|
|
1243
|
-
let ws = null;
|
|
1244
|
-
let reconnectDelay = 1000;
|
|
1245
|
-
let activeEditTab = 'prompt';
|
|
1246
|
-
let promptViewMode = 'read';
|
|
1247
|
-
let cachedPromptValue = null;
|
|
1248
|
-
let notificationsList = [];
|
|
1249
|
-
let notifPermissionRequested = false;
|
|
1250
|
-
let audioCtx = null;
|
|
1251
|
-
let audioCtxInitialized = false;
|
|
1252
|
-
let activeAppTab = 'loops';
|
|
1253
|
-
let archivesData = [];
|
|
1254
|
-
let expandedArchive = null;
|
|
1255
|
-
let archiveFilesCache = {};
|
|
1256
|
-
let viewingArchiveFile = null;
|
|
1257
|
-
let currentPage = 'app';
|
|
1258
|
-
let templatesList = [];
|
|
1259
|
-
let showTemplateBuilder = false;
|
|
1260
|
-
let templateBuilderState = null;
|
|
1261
|
-
|
|
1262
|
-
// DOM refs
|
|
1263
|
-
const $ = (sel) => document.querySelector(sel);
|
|
1264
|
-
const hostDisplay = $('#hostDisplay');
|
|
1265
|
-
const sidebarApps = $('#sidebarApps');
|
|
1266
|
-
const content = $('#content');
|
|
1267
|
-
const statusDot = $('#statusDot');
|
|
1268
|
-
const statusText = $('#statusText');
|
|
1269
|
-
const lastUpdate = $('#lastUpdate');
|
|
1270
|
-
const eventCountEl = $('#eventCount');
|
|
1271
|
-
|
|
1272
|
-
hostDisplay.textContent = location.host;
|
|
1273
|
-
|
|
1274
|
-
// Fetch project context for header display
|
|
1275
|
-
fetch('/api/context')
|
|
1276
|
-
.then(r => r.json())
|
|
1277
|
-
.then(ctx => {
|
|
1278
|
-
hostDisplay.textContent = ctx.projectName + ' :' + ctx.port;
|
|
1279
|
-
})
|
|
1280
|
-
.catch(() => { /* keep location.host as fallback */ });
|
|
1281
|
-
|
|
1282
|
-
// WebSocket
|
|
1283
|
-
function connectWs() {
|
|
1284
|
-
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
1285
|
-
ws = new WebSocket(`${proto}//${location.host}/ws`);
|
|
1286
|
-
|
|
1287
|
-
ws.onopen = () => {
|
|
1288
|
-
statusDot.className = 'status-dot connected';
|
|
1289
|
-
statusText.textContent = 'Connected';
|
|
1290
|
-
reconnectDelay = 1000;
|
|
1291
|
-
};
|
|
1292
|
-
|
|
1293
|
-
ws.onclose = () => {
|
|
1294
|
-
statusDot.className = 'status-dot disconnected';
|
|
1295
|
-
statusText.textContent = 'Disconnected';
|
|
1296
|
-
setTimeout(connectWs, reconnectDelay);
|
|
1297
|
-
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
|
|
1298
|
-
};
|
|
1299
|
-
|
|
1300
|
-
ws.onerror = () => {
|
|
1301
|
-
ws.close();
|
|
1302
|
-
};
|
|
1303
|
-
|
|
1304
|
-
ws.onmessage = (e) => {
|
|
1305
|
-
const event = JSON.parse(e.data);
|
|
1306
|
-
eventCounter++;
|
|
1307
|
-
eventCountEl.textContent = eventCounter;
|
|
1308
|
-
lastUpdate.textContent = new Date().toLocaleTimeString();
|
|
1309
|
-
handleWsEvent(event);
|
|
1310
|
-
};
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
function handleWsEvent(event) {
|
|
1314
|
-
if (event.type === 'status:full') {
|
|
1315
|
-
apps = event.apps;
|
|
1316
|
-
renderSidebar();
|
|
1317
|
-
if (selectedApp) {
|
|
1318
|
-
const updated = apps.find(a => a.appName === selectedApp.appName);
|
|
1319
|
-
if (updated) {
|
|
1320
|
-
selectedApp = updated;
|
|
1321
|
-
renderContent();
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
} else if (event.type === 'tracker:updated') {
|
|
1325
|
-
if (selectedApp && selectedApp.appName === event.app) {
|
|
1326
|
-
// Update the loop status in our local state
|
|
1327
|
-
const loopEntry = selectedApp.loops.find(l => l.key === event.loop);
|
|
1328
|
-
if (loopEntry) {
|
|
1329
|
-
loopEntry.status = event.status;
|
|
1330
|
-
}
|
|
1331
|
-
renderContent();
|
|
1332
|
-
// Refresh tracker viewer if this loop is selected
|
|
1333
|
-
if (selectedLoop === event.loop) {
|
|
1334
|
-
loadTracker(event.app, event.loop);
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
} else if (event.type === 'file:changed') {
|
|
1338
|
-
if (selectedApp && selectedApp.appName === event.app) {
|
|
1339
|
-
// Refresh status
|
|
1340
|
-
fetchAppStatus(event.app);
|
|
1341
|
-
}
|
|
1342
|
-
} else if (event.type === 'notification:attention') {
|
|
1343
|
-
const n = event.notification;
|
|
1344
|
-
notificationsList.unshift(n);
|
|
1345
|
-
renderSidebar();
|
|
1346
|
-
renderContent();
|
|
1347
|
-
maybeRequestNotifPermission();
|
|
1348
|
-
showBrowserNotification(n);
|
|
1349
|
-
playNotificationChime();
|
|
1350
|
-
} else if (event.type === 'notification:dismissed') {
|
|
1351
|
-
notificationsList = notificationsList.filter(n => n.id !== event.id);
|
|
1352
|
-
renderSidebar();
|
|
1353
|
-
renderContent();
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
// API helpers
|
|
1358
|
-
async function fetchJson(url) {
|
|
1359
|
-
const res = await fetch(url);
|
|
1360
|
-
return res.json();
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
async function fetchApps() {
|
|
1364
|
-
apps = await fetchJson('/api/apps');
|
|
1365
|
-
renderSidebar();
|
|
1366
|
-
if (apps.length > 0 && !selectedApp) {
|
|
1367
|
-
selectApp(apps[0]);
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
async function fetchAppStatus(appName) {
|
|
1372
|
-
const statuses = await fetchJson(`/api/apps/${encodeURIComponent(appName)}/status`);
|
|
1373
|
-
if (selectedApp && selectedApp.appName === appName) {
|
|
1374
|
-
statuses.forEach(s => {
|
|
1375
|
-
const loop = selectedApp.loops.find(l => l.key === s.key);
|
|
1376
|
-
if (loop) loop.status = s;
|
|
1377
|
-
});
|
|
1378
|
-
renderContent();
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
// Sidebar
|
|
1383
|
-
function renderSidebar() {
|
|
1384
|
-
let html = '';
|
|
1385
|
-
for (const app of apps) {
|
|
1386
|
-
const appActive = selectedApp && selectedApp.appName === app.appName;
|
|
1387
|
-
html += `<div class="sidebar-item app-item${appActive ? ' active' : ''}" data-app="${esc(app.appName)}">
|
|
1388
|
-
${esc(app.appName)}
|
|
1389
|
-
<span class="badge">${esc(app.appType)}</span>
|
|
1390
|
-
</div>`;
|
|
1391
|
-
if (app.loops) {
|
|
1392
|
-
for (const loop of app.loops) {
|
|
1393
|
-
const loopActive = appActive && selectedLoop === loop.key;
|
|
1394
|
-
const loopNotifCount = notificationsList.filter(n => n.app === app.appName && n.loop === loop.key).length;
|
|
1395
|
-
const badgeHtml = loopNotifCount > 0 ? ` <span class="notif-badge">${loopNotifCount}</span>` : '';
|
|
1396
|
-
html += `<div class="sidebar-item loop-item${loopActive ? ' active' : ''}" data-app="${esc(app.appName)}" data-loop="${esc(loop.key)}">
|
|
1397
|
-
${esc(loop.name)}${badgeHtml}
|
|
1398
|
-
</div>`;
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
sidebarApps.innerHTML = html;
|
|
1403
|
-
|
|
1404
|
-
// "+ New App" button
|
|
1405
|
-
const newAppBtn = document.createElement('button');
|
|
1406
|
-
newAppBtn.className = 'new-app-btn';
|
|
1407
|
-
newAppBtn.innerHTML = '+ New App';
|
|
1408
|
-
newAppBtn.addEventListener('click', openCreateAppModal);
|
|
1409
|
-
sidebarApps.appendChild(newAppBtn);
|
|
1410
|
-
|
|
1411
|
-
// Event delegation
|
|
1412
|
-
sidebarApps.querySelectorAll('.app-item').forEach(el => {
|
|
1413
|
-
el.addEventListener('click', () => {
|
|
1414
|
-
const app = apps.find(a => a.appName === el.dataset.app);
|
|
1415
|
-
if (app) selectApp(app);
|
|
1416
|
-
});
|
|
1417
|
-
});
|
|
1418
|
-
sidebarApps.querySelectorAll('.loop-item').forEach(el => {
|
|
1419
|
-
el.addEventListener('click', () => {
|
|
1420
|
-
const app = apps.find(a => a.appName === el.dataset.app);
|
|
1421
|
-
if (app) {
|
|
1422
|
-
selectApp(app);
|
|
1423
|
-
selectLoop(el.dataset.loop);
|
|
1424
|
-
}
|
|
1425
|
-
});
|
|
1426
|
-
});
|
|
1427
|
-
|
|
1428
|
-
// Update Templates nav active state
|
|
1429
|
-
const templatesNav = document.getElementById('templatesNav');
|
|
1430
|
-
if (templatesNav) {
|
|
1431
|
-
templatesNav.classList.toggle('active', currentPage === 'templates');
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
function selectApp(app) {
|
|
1436
|
-
currentPage = 'app';
|
|
1437
|
-
selectedApp = app;
|
|
1438
|
-
selectedLoop = app.loops.length > 0 ? app.loops[0].key : null;
|
|
1439
|
-
promptDirty = false;
|
|
1440
|
-
activeEditTab = 'prompt';
|
|
1441
|
-
cachedPromptValue = null;
|
|
1442
|
-
activeAppTab = 'loops';
|
|
1443
|
-
archivesData = [];
|
|
1444
|
-
expandedArchive = null;
|
|
1445
|
-
archiveFilesCache = {};
|
|
1446
|
-
viewingArchiveFile = null;
|
|
1447
|
-
document.title = app.appName + ' - RalphFlow Dashboard';
|
|
1448
|
-
renderSidebar();
|
|
1449
|
-
renderContent();
|
|
1450
|
-
fetchAppStatus(app.appName);
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
function selectLoop(loopKey) {
|
|
1454
|
-
selectedLoop = loopKey;
|
|
1455
|
-
promptDirty = false;
|
|
1456
|
-
activeEditTab = 'prompt';
|
|
1457
|
-
cachedPromptValue = null;
|
|
1458
|
-
renderSidebar();
|
|
1459
|
-
renderContent();
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
// Main content
|
|
1463
|
-
function renderContent() {
|
|
1464
|
-
if (currentPage === 'templates') {
|
|
1465
|
-
renderTemplatesPage();
|
|
1466
|
-
return;
|
|
1467
|
-
}
|
|
1468
|
-
if (!selectedApp) {
|
|
1469
|
-
content.innerHTML = '<div class="content-empty">Select an app to view details</div>';
|
|
1470
|
-
return;
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
const app = selectedApp;
|
|
1474
|
-
const currentLoop = app.loops.find(l => l.key === selectedLoop);
|
|
1475
|
-
|
|
1476
|
-
let html = '';
|
|
1477
|
-
|
|
1478
|
-
// App header
|
|
1479
|
-
html += `<div class="app-header">
|
|
1480
|
-
<div style="display:flex;align-items:center;gap:10px;justify-content:space-between;width:100%">
|
|
1481
|
-
<div style="display:flex;align-items:center;gap:10px">
|
|
1482
|
-
<h2>${esc(app.appName)}</h2>
|
|
1483
|
-
<span class="app-type-badge">${esc(app.appType)}</span>
|
|
1484
|
-
</div>
|
|
1485
|
-
<div style="display:flex;gap:6px">
|
|
1486
|
-
<button class="btn btn-muted" style="font-size:12px;padding:4px 10px" onclick="openArchiveAppModal('${esc(app.appName)}')">Archive</button>
|
|
1487
|
-
<button class="btn btn-danger" style="font-size:12px;padding:4px 10px" onclick="openDeleteAppModal('${esc(app.appName)}')">Delete</button>
|
|
1488
|
-
</div>
|
|
1489
|
-
</div>
|
|
1490
|
-
${app.description ? `<div class="app-desc">${esc(app.description)}</div>` : ''}
|
|
1491
|
-
</div>`;
|
|
1492
|
-
|
|
1493
|
-
// App-level tabs: Loops | Archives
|
|
1494
|
-
html += `<div class="app-tabs">
|
|
1495
|
-
<button class="app-tab${activeAppTab === 'loops' ? ' active' : ''}" data-app-tab="loops">Loops</button>
|
|
1496
|
-
<button class="app-tab${activeAppTab === 'archives' ? ' active' : ''}" data-app-tab="archives">Archives</button>
|
|
1497
|
-
</div>`;
|
|
1498
|
-
|
|
1499
|
-
if (activeAppTab === 'archives') {
|
|
1500
|
-
html += '<div id="archivesContainer">Loading archives...</div>';
|
|
1501
|
-
content.innerHTML = html;
|
|
1502
|
-
|
|
1503
|
-
// Bind app tab clicks
|
|
1504
|
-
content.querySelectorAll('.app-tab').forEach(tab => {
|
|
1505
|
-
tab.addEventListener('click', () => switchAppTab(tab.dataset.appTab));
|
|
1506
|
-
});
|
|
1507
|
-
|
|
1508
|
-
loadArchives(app.appName);
|
|
1509
|
-
return;
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
// --- Loops tab content ---
|
|
1513
|
-
|
|
1514
|
-
// Pipeline
|
|
1515
|
-
html += '<div class="section"><div class="section-title">Pipeline</div><div class="pipeline">';
|
|
1516
|
-
app.loops.forEach((loop, i) => {
|
|
1517
|
-
if (i > 0) html += '<div class="pipeline-connector"></div>';
|
|
1518
|
-
const statusClass = getLoopStatusClass(loop);
|
|
1519
|
-
const isSelected = loop.key === selectedLoop;
|
|
1520
|
-
html += `<div class="pipeline-node${isSelected ? ' selected' : ''}" data-loop="${esc(loop.key)}">
|
|
1521
|
-
<span class="node-name">${esc(loop.name)}</span>
|
|
1522
|
-
<span class="node-status ${statusClass}">${statusClass}</span>
|
|
1523
|
-
</div>`;
|
|
1524
|
-
});
|
|
1525
|
-
html += '</div></div>';
|
|
1526
|
-
|
|
1527
|
-
// Commands section
|
|
1528
|
-
html += '<div class="section"><div class="section-title">Commands</div><div class="commands-list">';
|
|
1529
|
-
app.loops.forEach(loop => {
|
|
1530
|
-
const alias = loop.key.replace(/-loop$/, '');
|
|
1531
|
-
let cmd = `npx ralphflow run ${alias} -f ${app.appName}`;
|
|
1532
|
-
if (loop.multiAgent) cmd += ' --multi-agent';
|
|
1533
|
-
if (loop.model) cmd += ` --model ${loop.model}`;
|
|
1534
|
-
html += `<div class="cmd-item">
|
|
1535
|
-
<span class="cmd-text">${esc(cmd)}</span>
|
|
1536
|
-
<button class="cmd-copy" data-cmd="${esc(cmd)}">Copy</button>
|
|
1537
|
-
</div>`;
|
|
1538
|
-
});
|
|
1539
|
-
const e2eCmd = `npx ralphflow e2e -f ${app.appName}`;
|
|
1540
|
-
html += `<div class="cmd-item">
|
|
1541
|
-
<span class="cmd-text">${esc(e2eCmd)}</span>
|
|
1542
|
-
<button class="cmd-copy" data-cmd="${esc(e2eCmd)}">Copy</button>
|
|
1543
|
-
</div>`;
|
|
1544
|
-
html += '</div></div>';
|
|
1545
|
-
|
|
1546
|
-
// Loop detail — two-column three-panel layout
|
|
1547
|
-
if (currentLoop) {
|
|
1548
|
-
const st = currentLoop.status || {};
|
|
1549
|
-
|
|
1550
|
-
html += '<div class="panel-grid">';
|
|
1551
|
-
|
|
1552
|
-
// Left column: Interactive + Progress
|
|
1553
|
-
html += '<div class="panel-col-left">';
|
|
1554
|
-
|
|
1555
|
-
// Interactive panel
|
|
1556
|
-
const loopNotifs = notificationsList.filter(n => n.app === app.appName && n.loop === currentLoop.key);
|
|
1557
|
-
const hasNotifs = loopNotifs.length > 0;
|
|
1558
|
-
html += `<div class="panel panel-interactive${hasNotifs ? ' has-notifs' : ''}">
|
|
1559
|
-
<div class="panel-header">Interactive${hasNotifs ? ' <span style="color:var(--accent)">(' + loopNotifs.length + ')</span>' : ''}</div>
|
|
1560
|
-
<div class="panel-body">`;
|
|
1561
|
-
if (hasNotifs) {
|
|
1562
|
-
for (const n of loopNotifs) {
|
|
1563
|
-
const time = new Date(n.timestamp).toLocaleTimeString();
|
|
1564
|
-
const msg = extractNotifMessage(n.payload);
|
|
1565
|
-
html += `<div class="notif-card" data-notif-id="${esc(n.id)}">
|
|
1566
|
-
<span class="notif-time">${esc(time)}</span>
|
|
1567
|
-
<span class="notif-msg">${esc(msg)}</span>
|
|
1568
|
-
<button class="notif-dismiss" data-dismiss-id="${esc(n.id)}">×</button>
|
|
1569
|
-
</div>`;
|
|
1570
|
-
}
|
|
1571
|
-
} else {
|
|
1572
|
-
html += `<span class="bell-icon">🔔</span><span>No notifications</span>`;
|
|
1573
|
-
}
|
|
1574
|
-
html += '</div></div>';
|
|
1575
|
-
|
|
1576
|
-
// Progress panel
|
|
1577
|
-
html += `<div class="panel panel-progress">
|
|
1578
|
-
<div class="panel-header">Progress</div>
|
|
1579
|
-
<div class="panel-body">
|
|
1580
|
-
<div class="loop-meta">
|
|
1581
|
-
<div class="meta-card"><div class="meta-label">Stage</div><div class="meta-value">${esc(st.stage || '—')}</div></div>
|
|
1582
|
-
<div class="meta-card"><div class="meta-label">Active</div><div class="meta-value">${esc(st.active || 'none')}</div></div>
|
|
1583
|
-
<div class="meta-card">
|
|
1584
|
-
<div class="meta-label">Progress</div>
|
|
1585
|
-
<div class="meta-value">${st.completed || 0}/${st.total || 0}</div>
|
|
1586
|
-
<div class="progress-bar"><div class="progress-fill" style="width:${st.total ? (st.completed / st.total * 100) : 0}%"></div></div>
|
|
1587
|
-
</div>
|
|
1588
|
-
<div class="meta-card"><div class="meta-label">Stages</div><div class="meta-value" style="font-size:11px">${(currentLoop.stages || []).join(' → ')}</div></div>
|
|
1589
|
-
</div>`;
|
|
1590
|
-
|
|
1591
|
-
// Agent table
|
|
1592
|
-
if (st.agents && st.agents.length > 0) {
|
|
1593
|
-
html += `<div style="margin-top:16px">
|
|
1594
|
-
<table class="agent-table">
|
|
1595
|
-
<thead><tr><th>Agent</th><th>Active Task</th><th>Stage</th><th>Heartbeat</th></tr></thead>
|
|
1596
|
-
<tbody>`;
|
|
1597
|
-
for (const ag of st.agents) {
|
|
1598
|
-
html += `<tr><td>${esc(ag.name)}</td><td>${esc(ag.activeTask)}</td><td>${esc(ag.stage)}</td><td>${esc(ag.lastHeartbeat)}</td></tr>`;
|
|
1599
|
-
}
|
|
1600
|
-
html += '</tbody></table></div>';
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
// Tracker viewer (inside Progress panel)
|
|
1604
|
-
html += `<div class="tracker-viewer" id="trackerViewer">Loading...</div>`;
|
|
1605
|
-
|
|
1606
|
-
html += '</div></div>'; // close .panel-body + .panel-progress
|
|
1607
|
-
html += '</div>'; // close .panel-col-left
|
|
1608
|
-
|
|
1609
|
-
// Right column: Edit panel with tabs
|
|
1610
|
-
html += `<div class="panel panel-edit">
|
|
1611
|
-
<div class="edit-tabs">
|
|
1612
|
-
<button class="edit-tab${activeEditTab === 'prompt' ? ' active' : ''}" data-tab="prompt">Prompt</button>
|
|
1613
|
-
<button class="edit-tab${activeEditTab === 'tracker' ? ' active' : ''}" data-tab="tracker">Tracker</button>
|
|
1614
|
-
<button class="edit-tab${activeEditTab === 'config' ? ' active' : ''}" data-tab="config">Config</button>
|
|
1615
|
-
<div class="model-selector-wrap">
|
|
1616
|
-
<label>Model</label>
|
|
1617
|
-
<select class="model-selector" id="modelSelector">
|
|
1618
|
-
<option value="">Default</option>
|
|
1619
|
-
<option value="claude-opus-4-6">claude-opus-4-6</option>
|
|
1620
|
-
<option value="claude-sonnet-4-6">claude-sonnet-4-6</option>
|
|
1621
|
-
<option value="claude-haiku-4-5-20251001">claude-haiku-4-5-20251001</option>
|
|
1622
|
-
</select>
|
|
1623
|
-
<span class="model-save-ok" id="modelSaveOk">Saved</span>
|
|
1624
|
-
</div>
|
|
1625
|
-
</div>
|
|
1626
|
-
<div class="panel-body" id="editTabContent"></div>
|
|
1627
|
-
</div>`;
|
|
1628
|
-
|
|
1629
|
-
html += '</div>'; // close .panel-grid
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
content.innerHTML = html;
|
|
1633
|
-
|
|
1634
|
-
// Bind app-level tab clicks
|
|
1635
|
-
content.querySelectorAll('.app-tab').forEach(tab => {
|
|
1636
|
-
tab.addEventListener('click', () => switchAppTab(tab.dataset.appTab));
|
|
1637
|
-
});
|
|
1638
|
-
|
|
1639
|
-
// Bind pipeline node clicks
|
|
1640
|
-
content.querySelectorAll('.pipeline-node').forEach(el => {
|
|
1641
|
-
el.addEventListener('click', () => selectLoop(el.dataset.loop));
|
|
1642
|
-
});
|
|
1643
|
-
|
|
1644
|
-
// Bind command copy buttons
|
|
1645
|
-
content.querySelectorAll('.commands-list .cmd-copy').forEach(btn => {
|
|
1646
|
-
btn.addEventListener('click', () => {
|
|
1647
|
-
const cmd = btn.dataset.cmd || '';
|
|
1648
|
-
navigator.clipboard.writeText(cmd).then(() => {
|
|
1649
|
-
const orig = btn.textContent;
|
|
1650
|
-
btn.textContent = 'Copied!';
|
|
1651
|
-
setTimeout(() => { btn.textContent = orig; }, 1500);
|
|
1652
|
-
});
|
|
1653
|
-
});
|
|
1654
|
-
});
|
|
1655
|
-
|
|
1656
|
-
// Bind notification dismiss buttons
|
|
1657
|
-
content.querySelectorAll('.notif-dismiss').forEach(btn => {
|
|
1658
|
-
btn.addEventListener('click', () => dismissNotification(btn.dataset.dismissId));
|
|
1659
|
-
});
|
|
1660
|
-
|
|
1661
|
-
// Bind edit tabs + load content
|
|
1662
|
-
if (currentLoop) {
|
|
1663
|
-
content.querySelectorAll('.edit-tab').forEach(tab => {
|
|
1664
|
-
tab.addEventListener('click', () => switchEditTab(tab.dataset.tab, app.appName, currentLoop.key));
|
|
1665
|
-
});
|
|
1666
|
-
renderEditTabContent(app.appName, currentLoop.key);
|
|
1667
|
-
loadTracker(app.appName, currentLoop.key);
|
|
1668
|
-
loadModelSelector(app.appName, currentLoop.key);
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
function bindPromptEditor(appName, loopKey) {
|
|
1673
|
-
const editor = $('#promptEditor');
|
|
1674
|
-
if (!editor) return;
|
|
1675
|
-
|
|
1676
|
-
editor.addEventListener('input', () => {
|
|
1677
|
-
promptDirty = editor.value !== promptOriginal;
|
|
1678
|
-
updateDirtyState();
|
|
1679
|
-
});
|
|
1680
|
-
|
|
1681
|
-
editor.addEventListener('keydown', (e) => {
|
|
1682
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
|
|
1683
|
-
e.preventDefault();
|
|
1684
|
-
savePrompt(appName, loopKey);
|
|
1685
|
-
}
|
|
1686
|
-
});
|
|
1687
|
-
|
|
1688
|
-
const saveBtn = $('#savePromptBtn');
|
|
1689
|
-
const resetBtn = $('#resetPromptBtn');
|
|
1690
|
-
if (saveBtn) saveBtn.addEventListener('click', () => savePrompt(appName, loopKey));
|
|
1691
|
-
if (resetBtn) resetBtn.addEventListener('click', () => {
|
|
1692
|
-
editor.value = promptOriginal;
|
|
1693
|
-
promptDirty = false;
|
|
1694
|
-
updateDirtyState();
|
|
1695
|
-
});
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
async function loadPrompt(appName, loopKey) {
|
|
1699
|
-
const editor = $('#promptEditor');
|
|
1700
|
-
if (!editor) return;
|
|
1701
|
-
|
|
1702
|
-
try {
|
|
1703
|
-
const data = await fetchJson(`/api/apps/${encodeURIComponent(appName)}/loops/${encodeURIComponent(loopKey)}/prompt`);
|
|
1704
|
-
editor.value = data.content || '';
|
|
1705
|
-
promptOriginal = editor.value;
|
|
1706
|
-
promptDirty = false;
|
|
1707
|
-
updateDirtyState();
|
|
1708
|
-
} catch {
|
|
1709
|
-
editor.value = '(Error loading prompt)';
|
|
1710
|
-
}
|
|
1711
|
-
bindPromptEditor(appName, loopKey);
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
async function loadPromptPreview(appName, loopKey) {
|
|
1715
|
-
const preview = $('#promptPreview');
|
|
1716
|
-
if (!preview) return;
|
|
1717
|
-
try {
|
|
1718
|
-
const data = await fetchJson(`/api/apps/${encodeURIComponent(appName)}/loops/${encodeURIComponent(loopKey)}/prompt`);
|
|
1719
|
-
promptOriginal = data.content || '';
|
|
1720
|
-
preview.innerHTML = renderMarkdown(promptOriginal);
|
|
1721
|
-
} catch {
|
|
1722
|
-
preview.innerHTML = '<p style="color:var(--text-dim)">(Error loading prompt)</p>';
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
async function savePrompt(appName, loopKey) {
|
|
1727
|
-
const editor = $('#promptEditor');
|
|
1728
|
-
if (!editor || !promptDirty) return;
|
|
1729
|
-
|
|
1730
|
-
try {
|
|
1731
|
-
await fetch(`/api/apps/${encodeURIComponent(appName)}/loops/${encodeURIComponent(loopKey)}/prompt`, {
|
|
1732
|
-
method: 'PUT',
|
|
1733
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1734
|
-
body: JSON.stringify({ content: editor.value }),
|
|
1735
|
-
});
|
|
1736
|
-
promptOriginal = editor.value;
|
|
1737
|
-
promptDirty = false;
|
|
1738
|
-
updateDirtyState();
|
|
1739
|
-
const saveOk = $('#saveOk');
|
|
1740
|
-
if (saveOk) {
|
|
1741
|
-
saveOk.style.display = 'inline';
|
|
1742
|
-
setTimeout(() => { saveOk.style.display = 'none'; }, 2000);
|
|
1743
|
-
}
|
|
1744
|
-
} catch {
|
|
1745
|
-
alert('Failed to save prompt');
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
function updateDirtyState() {
|
|
1750
|
-
const saveBtn = $('#savePromptBtn');
|
|
1751
|
-
const resetBtn = $('#resetPromptBtn');
|
|
1752
|
-
const indicator = $('#dirtyIndicator');
|
|
1753
|
-
if (saveBtn) saveBtn.disabled = !promptDirty;
|
|
1754
|
-
if (resetBtn) resetBtn.disabled = !promptDirty;
|
|
1755
|
-
if (indicator) indicator.style.display = promptDirty ? 'inline' : 'none';
|
|
1756
|
-
}
|
|
1757
|
-
|
|
1758
|
-
async function loadTracker(appName, loopKey) {
|
|
1759
|
-
const viewer = $('#trackerViewer');
|
|
1760
|
-
if (!viewer) return;
|
|
1761
|
-
|
|
1762
|
-
try {
|
|
1763
|
-
const data = await fetchJson(`/api/apps/${encodeURIComponent(appName)}/loops/${encodeURIComponent(loopKey)}/tracker`);
|
|
1764
|
-
viewer.innerHTML = renderMarkdown(data.content || '(empty)');
|
|
1765
|
-
} catch {
|
|
1766
|
-
viewer.innerHTML = '(No tracker file found)';
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
function switchEditTab(tab, appName, loopKey) {
|
|
1771
|
-
if (tab === activeEditTab) return;
|
|
1772
|
-
if (activeEditTab === 'prompt') {
|
|
1773
|
-
const editor = $('#promptEditor');
|
|
1774
|
-
if (editor) {
|
|
1775
|
-
cachedPromptValue = editor.value;
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
activeEditTab = tab;
|
|
1779
|
-
document.querySelectorAll('.edit-tab').forEach(t => {
|
|
1780
|
-
t.classList.toggle('active', t.dataset.tab === tab);
|
|
1781
|
-
});
|
|
1782
|
-
renderEditTabContent(appName, loopKey);
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
function renderMarkdown(md) {
|
|
1786
|
-
if (!md) return '<p style="color:var(--text-dim)">(empty)</p>';
|
|
1787
|
-
let html = md;
|
|
1788
|
-
// Escape HTML
|
|
1789
|
-
html = html.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1790
|
-
// Fenced code blocks
|
|
1791
|
-
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) =>
|
|
1792
|
-
'<pre><code>' + code.trimEnd() + '</code></pre>'
|
|
1793
|
-
);
|
|
1794
|
-
// Tables
|
|
1795
|
-
html = html.replace(/((?:^\|.+\|$\n?)+)/gm, (block) => {
|
|
1796
|
-
const rows = block.trim().split('\n').filter(r => r.trim());
|
|
1797
|
-
if (rows.length < 2) return block;
|
|
1798
|
-
const parseRow = r => r.split('|').slice(1, -1).map(c => c.trim());
|
|
1799
|
-
const headers = parseRow(rows[0]);
|
|
1800
|
-
// Skip separator row
|
|
1801
|
-
const isSep = rows[1] && /^\|[\s:|-]+\|$/.test(rows[1].trim());
|
|
1802
|
-
const dataRows = rows.slice(isSep ? 2 : 1);
|
|
1803
|
-
let t = '<table><thead><tr>' + headers.map(h => '<th>' + h + '</th>').join('') + '</tr></thead><tbody>';
|
|
1804
|
-
for (const row of dataRows) {
|
|
1805
|
-
const cells = parseRow(row);
|
|
1806
|
-
t += '<tr>' + cells.map(c => '<td>' + c + '</td>').join('') + '</tr>';
|
|
1807
|
-
}
|
|
1808
|
-
t += '</tbody></table>';
|
|
1809
|
-
return t;
|
|
1810
|
-
});
|
|
1811
|
-
// Blockquotes
|
|
1812
|
-
html = html.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>');
|
|
1813
|
-
// Horizontal rules
|
|
1814
|
-
html = html.replace(/^---+$/gm, '<hr>');
|
|
1815
|
-
// Headings
|
|
1816
|
-
html = html.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
|
|
1817
|
-
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
|
|
1818
|
-
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
|
|
1819
|
-
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
|
|
1820
|
-
// Task list items
|
|
1821
|
-
html = html.replace(/^- \[x\] (.+)$/gm, '<li class="task-done">☑ $1</li>');
|
|
1822
|
-
html = html.replace(/^- \[ \] (.+)$/gm, '<li>☐ $1</li>');
|
|
1823
|
-
// Unordered list items
|
|
1824
|
-
html = html.replace(/^[-*] (.+)$/gm, '<li>$1</li>');
|
|
1825
|
-
// Ordered list items
|
|
1826
|
-
html = html.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
|
|
1827
|
-
// Wrap consecutive <li> in <ul>
|
|
1828
|
-
html = html.replace(/((?:<li[^>]*>.*<\/li>\n?)+)/g, '<ul>$1</ul>');
|
|
1829
|
-
// Inline code (but not inside <pre>)
|
|
1830
|
-
html = html.replace(/`([^`\n]+)`/g, '<code>$1</code>');
|
|
1831
|
-
// Bold and italic
|
|
1832
|
-
html = html.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
|
|
1833
|
-
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
1834
|
-
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
1835
|
-
// Links
|
|
1836
|
-
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
|
1837
|
-
// Mustache/template vars — highlight them
|
|
1838
|
-
html = html.replace(/\{\{(\w+)\}\}/g, '<code style="color:var(--purple)">{{$1}}</code>');
|
|
1839
|
-
// Paragraphs: wrap remaining plain text lines
|
|
1840
|
-
html = html.replace(/^(?!<[a-z/])((?!$).+)$/gm, '<p>$1</p>');
|
|
1841
|
-
// Clean up double-wrapped
|
|
1842
|
-
html = html.replace(/<p><(h[1-4]|ul|ol|li|blockquote|pre|table|hr)/g, '<$1');
|
|
1843
|
-
html = html.replace(/<\/(h[1-4]|ul|ol|li|blockquote|pre|table)><\/p>/g, '</$1>');
|
|
1844
|
-
return html;
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
function renderEditTabContent(appName, loopKey) {
|
|
1848
|
-
const container = $('#editTabContent');
|
|
1849
|
-
if (!container) return;
|
|
1850
|
-
|
|
1851
|
-
if (activeEditTab === 'prompt') {
|
|
1852
|
-
const isRead = promptViewMode === 'read';
|
|
1853
|
-
const isEdit = promptViewMode === 'edit';
|
|
1854
|
-
container.innerHTML = `
|
|
1855
|
-
<div class="prompt-mode-toggle">
|
|
1856
|
-
<button class="prompt-mode-btn${isRead ? ' active' : ''}" data-mode="read">Read</button>
|
|
1857
|
-
<button class="prompt-mode-btn${isEdit ? ' active' : ''}" data-mode="edit">Edit</button>
|
|
1858
|
-
</div>
|
|
1859
|
-
${isEdit ? `<div class="editor-wrap">
|
|
1860
|
-
<textarea class="editor" id="promptEditor" placeholder="Loading..."></textarea>
|
|
1861
|
-
<div class="editor-actions">
|
|
1862
|
-
<button class="btn btn-primary" id="savePromptBtn" disabled>Save</button>
|
|
1863
|
-
<button class="btn" id="resetPromptBtn" disabled>Reset</button>
|
|
1864
|
-
<span class="dirty-indicator" id="dirtyIndicator" style="display:none">Unsaved changes</span>
|
|
1865
|
-
<span class="save-ok" id="saveOk" style="display:none">Saved</span>
|
|
1866
|
-
</div>
|
|
1867
|
-
</div>` : `<div class="prompt-preview" id="promptPreview">Loading...</div>`}`;
|
|
1868
|
-
// Bind toggle buttons
|
|
1869
|
-
container.querySelectorAll('.prompt-mode-btn').forEach(btn => {
|
|
1870
|
-
btn.addEventListener('click', () => {
|
|
1871
|
-
if (btn.dataset.mode === promptViewMode) return;
|
|
1872
|
-
// Cache editor value before switching away from edit
|
|
1873
|
-
if (promptViewMode === 'edit') {
|
|
1874
|
-
const editor = $('#promptEditor');
|
|
1875
|
-
if (editor) cachedPromptValue = editor.value;
|
|
1876
|
-
}
|
|
1877
|
-
promptViewMode = btn.dataset.mode;
|
|
1878
|
-
renderEditTabContent(appName, loopKey);
|
|
1879
|
-
});
|
|
1880
|
-
});
|
|
1881
|
-
if (isEdit) {
|
|
1882
|
-
if (cachedPromptValue !== null) {
|
|
1883
|
-
const editor = $('#promptEditor');
|
|
1884
|
-
if (editor) {
|
|
1885
|
-
editor.value = cachedPromptValue;
|
|
1886
|
-
updateDirtyState();
|
|
1887
|
-
bindPromptEditor(appName, loopKey);
|
|
1888
|
-
}
|
|
1889
|
-
cachedPromptValue = null;
|
|
1890
|
-
} else {
|
|
1891
|
-
loadPrompt(appName, loopKey);
|
|
1892
|
-
}
|
|
1893
|
-
} else {
|
|
1894
|
-
// Read mode — render markdown preview
|
|
1895
|
-
if (cachedPromptValue !== null || promptOriginal) {
|
|
1896
|
-
const content = cachedPromptValue !== null ? cachedPromptValue : promptOriginal;
|
|
1897
|
-
const preview = $('#promptPreview');
|
|
1898
|
-
if (preview) preview.innerHTML = renderMarkdown(content);
|
|
1899
|
-
} else {
|
|
1900
|
-
// Need to fetch
|
|
1901
|
-
loadPromptPreview(appName, loopKey);
|
|
1902
|
-
}
|
|
1903
|
-
}
|
|
1904
|
-
} else if (activeEditTab === 'tracker') {
|
|
1905
|
-
container.innerHTML = '<pre class="code-viewer" id="editTrackerViewer">Loading...</pre>';
|
|
1906
|
-
loadEditTracker(appName, loopKey);
|
|
1907
|
-
} else if (activeEditTab === 'config') {
|
|
1908
|
-
container.innerHTML = '<pre class="code-viewer" id="editConfigViewer">Loading...</pre>';
|
|
1909
|
-
loadEditConfig(appName);
|
|
1910
|
-
}
|
|
1911
|
-
}
|
|
1912
|
-
|
|
1913
|
-
async function loadEditTracker(appName, loopKey) {
|
|
1914
|
-
const viewer = $('#editTrackerViewer');
|
|
1915
|
-
if (!viewer) return;
|
|
1916
|
-
try {
|
|
1917
|
-
const data = await fetchJson(`/api/apps/${encodeURIComponent(appName)}/loops/${encodeURIComponent(loopKey)}/tracker`);
|
|
1918
|
-
viewer.textContent = data.content || '(empty)';
|
|
1919
|
-
} catch {
|
|
1920
|
-
viewer.textContent = '(No tracker file found)';
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
async function loadEditConfig(appName) {
|
|
1925
|
-
const viewer = $('#editConfigViewer');
|
|
1926
|
-
if (!viewer) return;
|
|
1927
|
-
try {
|
|
1928
|
-
const data = await fetchJson(`/api/apps/${encodeURIComponent(appName)}/config`);
|
|
1929
|
-
viewer.textContent = data._rawYaml || JSON.stringify(data, null, 2);
|
|
1930
|
-
} catch {
|
|
1931
|
-
viewer.textContent = '(Error loading config)';
|
|
1932
|
-
}
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
|
-
async function loadModelSelector(appName, loopKey) {
|
|
1936
|
-
const selector = $('#modelSelector');
|
|
1937
|
-
if (!selector) return;
|
|
1938
|
-
|
|
1939
|
-
try {
|
|
1940
|
-
const data = await fetchJson(`/api/apps/${encodeURIComponent(appName)}/config`);
|
|
1941
|
-
const loopConfig = data.loops && data.loops[loopKey];
|
|
1942
|
-
const currentModel = loopConfig && loopConfig.model ? loopConfig.model : '';
|
|
1943
|
-
selector.value = currentModel;
|
|
1944
|
-
} catch {
|
|
1945
|
-
// Leave at default
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
selector.addEventListener('change', () => changeModel(appName, loopKey, selector.value));
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
|
-
async function changeModel(appName, loopKey, model) {
|
|
1952
|
-
const saveOk = $('#modelSaveOk');
|
|
1953
|
-
try {
|
|
1954
|
-
await fetch(`/api/apps/${encodeURIComponent(appName)}/config/model`, {
|
|
1955
|
-
method: 'PUT',
|
|
1956
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1957
|
-
body: JSON.stringify({ loop: loopKey, model: model || null }),
|
|
1958
|
-
});
|
|
1959
|
-
if (saveOk) {
|
|
1960
|
-
saveOk.classList.add('visible');
|
|
1961
|
-
setTimeout(() => saveOk.classList.remove('visible'), 2000);
|
|
1962
|
-
}
|
|
1963
|
-
// Refresh config tab if it's currently visible
|
|
1964
|
-
if (activeEditTab === 'config') {
|
|
1965
|
-
loadEditConfig(appName);
|
|
1966
|
-
}
|
|
1967
|
-
} catch {
|
|
1968
|
-
alert('Failed to update model');
|
|
1969
|
-
}
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
// Minimal markdown renderer
|
|
1973
|
-
function renderMarkdown(md) {
|
|
1974
|
-
let html = '';
|
|
1975
|
-
const lines = md.split('\n');
|
|
1976
|
-
let inTable = false;
|
|
1977
|
-
let tableHtml = '';
|
|
1978
|
-
|
|
1979
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1980
|
-
const line = lines[i];
|
|
1981
|
-
|
|
1982
|
-
// Table detection
|
|
1983
|
-
if (line.match(/^\|.+\|$/)) {
|
|
1984
|
-
if (!inTable) {
|
|
1985
|
-
inTable = true;
|
|
1986
|
-
tableHtml = '<table>';
|
|
1987
|
-
// Header row
|
|
1988
|
-
const cells = line.split('|').filter(Boolean).map(c => c.trim());
|
|
1989
|
-
tableHtml += '<thead><tr>' + cells.map(c => `<th>${esc(c)}</th>`).join('') + '</tr></thead><tbody>';
|
|
1990
|
-
continue;
|
|
1991
|
-
}
|
|
1992
|
-
// Separator row
|
|
1993
|
-
if (line.match(/^\|[\s\-|]+\|$/)) continue;
|
|
1994
|
-
// Data row
|
|
1995
|
-
const cells = line.split('|').filter(Boolean).map(c => c.trim());
|
|
1996
|
-
tableHtml += '<tr>' + cells.map(c => `<td>${esc(c)}</td>`).join('') + '</tr>';
|
|
1997
|
-
continue;
|
|
1998
|
-
} else if (inTable) {
|
|
1999
|
-
inTable = false;
|
|
2000
|
-
tableHtml += '</tbody></table>';
|
|
2001
|
-
html += tableHtml;
|
|
2002
|
-
tableHtml = '';
|
|
2003
|
-
}
|
|
2004
|
-
|
|
2005
|
-
// Headers
|
|
2006
|
-
if (line.startsWith('### ')) { html += `<h3>${esc(line.slice(4))}</h3>`; continue; }
|
|
2007
|
-
if (line.startsWith('## ')) { html += `<h2>${esc(line.slice(3))}</h2>`; continue; }
|
|
2008
|
-
if (line.startsWith('# ')) { html += `<h1>${esc(line.slice(2))}</h1>`; continue; }
|
|
2009
|
-
|
|
2010
|
-
// Checkboxes
|
|
2011
|
-
if (line.match(/^- \[x\]/i)) {
|
|
2012
|
-
html += `<div class="cb-done">${esc(line)}</div>`;
|
|
2013
|
-
continue;
|
|
2014
|
-
}
|
|
2015
|
-
if (line.match(/^- \[ \]/)) {
|
|
2016
|
-
html += `<div class="cb-todo">${esc(line)}</div>`;
|
|
2017
|
-
continue;
|
|
2018
|
-
}
|
|
2019
|
-
|
|
2020
|
-
// Regular lines
|
|
2021
|
-
html += line.trim() === '' ? '<br>' : `<div>${esc(line)}</div>`;
|
|
2022
|
-
}
|
|
2023
|
-
|
|
2024
|
-
if (inTable) {
|
|
2025
|
-
tableHtml += '</tbody></table>';
|
|
2026
|
-
html += tableHtml;
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
return html;
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
function getLoopStatusClass(loop) {
|
|
2033
|
-
if (!loop.status) return 'pending';
|
|
2034
|
-
const st = loop.status;
|
|
2035
|
-
if (st.total > 0 && st.completed === st.total) return 'complete';
|
|
2036
|
-
// Running if: has active agents, or has partial progress, or stage indicates activity (not idle/—)
|
|
2037
|
-
if (st.agents && st.agents.length > 0) return 'running';
|
|
2038
|
-
if (st.total > 0 && st.completed > 0 && st.completed < st.total) return 'running';
|
|
2039
|
-
if (st.stage && st.stage !== '—' && st.stage !== 'idle') return 'running';
|
|
2040
|
-
return 'pending';
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
|
-
function esc(s) {
|
|
2044
|
-
if (s == null) return '';
|
|
2045
|
-
const d = document.createElement('div');
|
|
2046
|
-
d.textContent = String(s);
|
|
2047
|
-
return d.innerHTML;
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
// Notifications
|
|
2051
|
-
async function fetchNotifications() {
|
|
2052
|
-
try {
|
|
2053
|
-
const data = await fetchJson('/api/notifications');
|
|
2054
|
-
notificationsList = Array.isArray(data) ? data : [];
|
|
2055
|
-
renderSidebar();
|
|
2056
|
-
renderContent();
|
|
2057
|
-
} catch { /* ignore */ }
|
|
2058
|
-
}
|
|
2059
|
-
|
|
2060
|
-
async function dismissNotification(id) {
|
|
2061
|
-
try {
|
|
2062
|
-
await fetch(`/api/notification/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
2063
|
-
notificationsList = notificationsList.filter(n => n.id !== id);
|
|
2064
|
-
renderSidebar();
|
|
2065
|
-
renderContent();
|
|
2066
|
-
} catch { /* ignore */ }
|
|
2067
|
-
}
|
|
2068
|
-
|
|
2069
|
-
function extractNotifMessage(payload) {
|
|
2070
|
-
if (!payload) return 'Attention needed';
|
|
2071
|
-
if (typeof payload === 'string') return payload;
|
|
2072
|
-
if (payload.message) return payload.message;
|
|
2073
|
-
if (payload.type) return payload.type;
|
|
2074
|
-
if (payload.event) return payload.event;
|
|
2075
|
-
return 'Attention needed';
|
|
2076
|
-
}
|
|
2077
|
-
|
|
2078
|
-
function maybeRequestNotifPermission() {
|
|
2079
|
-
if (notifPermissionRequested) return;
|
|
2080
|
-
if (!('Notification' in window)) return;
|
|
2081
|
-
if (Notification.permission === 'default') {
|
|
2082
|
-
notifPermissionRequested = true;
|
|
2083
|
-
Notification.requestPermission();
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
|
|
2087
|
-
function showBrowserNotification(n) {
|
|
2088
|
-
if (!('Notification' in window)) return;
|
|
2089
|
-
if (Notification.permission !== 'granted') return;
|
|
2090
|
-
if (document.hasFocus()) return;
|
|
2091
|
-
const msg = extractNotifMessage(n.payload);
|
|
2092
|
-
new Notification('RalphFlow — ' + (n.loop || 'Notification'), { body: msg });
|
|
2093
|
-
}
|
|
2094
|
-
|
|
2095
|
-
// Audio notification chime (Web Audio API)
|
|
2096
|
-
function initAudioContext() {
|
|
2097
|
-
if (audioCtxInitialized) return;
|
|
2098
|
-
audioCtxInitialized = true;
|
|
2099
|
-
try {
|
|
2100
|
-
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
2101
|
-
} catch (e) {
|
|
2102
|
-
// Silent fail — audio is best-effort
|
|
2103
|
-
}
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
|
-
function playNotificationChime() {
|
|
2107
|
-
if (!audioCtx) return;
|
|
2108
|
-
try {
|
|
2109
|
-
const now = audioCtx.currentTime;
|
|
2110
|
-
// First tone: E5 (659 Hz), 120ms
|
|
2111
|
-
const osc1 = audioCtx.createOscillator();
|
|
2112
|
-
const gain1 = audioCtx.createGain();
|
|
2113
|
-
osc1.type = 'sine';
|
|
2114
|
-
osc1.frequency.value = 659;
|
|
2115
|
-
gain1.gain.setValueAtTime(0.15, now);
|
|
2116
|
-
gain1.gain.exponentialRampToValueAtTime(0.001, now + 0.12);
|
|
2117
|
-
osc1.connect(gain1);
|
|
2118
|
-
gain1.connect(audioCtx.destination);
|
|
2119
|
-
osc1.start(now);
|
|
2120
|
-
osc1.stop(now + 0.12);
|
|
2121
|
-
// Second tone: A5 (880 Hz), 150ms, starts 80ms after first
|
|
2122
|
-
const osc2 = audioCtx.createOscillator();
|
|
2123
|
-
const gain2 = audioCtx.createGain();
|
|
2124
|
-
osc2.type = 'sine';
|
|
2125
|
-
osc2.frequency.value = 880;
|
|
2126
|
-
gain2.gain.setValueAtTime(0, now + 0.08);
|
|
2127
|
-
gain2.gain.linearRampToValueAtTime(0.12, now + 0.1);
|
|
2128
|
-
gain2.gain.exponentialRampToValueAtTime(0.001, now + 0.25);
|
|
2129
|
-
osc2.connect(gain2);
|
|
2130
|
-
gain2.connect(audioCtx.destination);
|
|
2131
|
-
osc2.start(now + 0.08);
|
|
2132
|
-
osc2.stop(now + 0.25);
|
|
2133
|
-
} catch (e) {
|
|
2134
|
-
// Silent fail
|
|
2135
|
-
}
|
|
2136
|
-
}
|
|
2137
|
-
|
|
2138
|
-
// Initialize audio context on first user interaction (autoplay policy)
|
|
2139
|
-
function onFirstInteraction() {
|
|
2140
|
-
initAudioContext();
|
|
2141
|
-
document.removeEventListener('click', onFirstInteraction);
|
|
2142
|
-
document.removeEventListener('keydown', onFirstInteraction);
|
|
2143
|
-
}
|
|
2144
|
-
document.addEventListener('click', onFirstInteraction);
|
|
2145
|
-
document.addEventListener('keydown', onFirstInteraction);
|
|
2146
|
-
|
|
2147
|
-
// App-level tab switching (Loops / Archives)
|
|
2148
|
-
function switchAppTab(tab) {
|
|
2149
|
-
if (tab === activeAppTab) return;
|
|
2150
|
-
activeAppTab = tab;
|
|
2151
|
-
expandedArchive = null;
|
|
2152
|
-
archiveFilesCache = {};
|
|
2153
|
-
viewingArchiveFile = null;
|
|
2154
|
-
renderContent();
|
|
2155
|
-
}
|
|
2156
|
-
|
|
2157
|
-
// Archives view
|
|
2158
|
-
async function loadArchives(appName) {
|
|
2159
|
-
const container = document.getElementById('archivesContainer');
|
|
2160
|
-
if (!container) return;
|
|
2161
|
-
|
|
2162
|
-
try {
|
|
2163
|
-
archivesData = await fetchJson(`/api/apps/${encodeURIComponent(appName)}/archives`);
|
|
2164
|
-
renderArchivesView(container, appName);
|
|
2165
|
-
} catch {
|
|
2166
|
-
container.innerHTML = '<div class="archive-empty"><div>Error loading archives</div></div>';
|
|
2167
|
-
}
|
|
2168
|
-
}
|
|
2169
|
-
|
|
2170
|
-
function renderArchivesView(container, appName) {
|
|
2171
|
-
if (archivesData.length === 0) {
|
|
2172
|
-
container.innerHTML = `<div class="archive-empty">
|
|
2173
|
-
<div class="archive-empty-icon">🗃</div>
|
|
2174
|
-
<div>No archives yet</div>
|
|
2175
|
-
<div style="margin-top:8px;font-size:12px">Use the Archive button to snapshot current work</div>
|
|
2176
|
-
</div>`;
|
|
2177
|
-
return;
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
let html = '<div class="archive-timeline">';
|
|
2181
|
-
for (const archive of archivesData) {
|
|
2182
|
-
const isExpanded = expandedArchive === archive.timestamp;
|
|
2183
|
-
const dateStr = formatArchiveTimestamp(archive.timestamp);
|
|
2184
|
-
html += `<div class="archive-card${isExpanded ? ' expanded' : ''}" data-archive="${esc(archive.timestamp)}">
|
|
2185
|
-
<div class="archive-card-header" data-archive-toggle="${esc(archive.timestamp)}">
|
|
2186
|
-
<span class="archive-card-date">${esc(dateStr)}</span>
|
|
2187
|
-
<div class="archive-card-stats">
|
|
2188
|
-
<span class="archive-card-stat">Stories: <span class="stat-val">${archive.summary.storyCount}</span></span>
|
|
2189
|
-
<span class="archive-card-stat">Tasks: <span class="stat-val">${archive.summary.taskCount}</span></span>
|
|
2190
|
-
<span class="archive-card-stat">Files: <span class="stat-val">${archive.fileCount}</span></span>
|
|
2191
|
-
<span class="archive-card-chevron">▶</span>
|
|
2192
|
-
</div>
|
|
2193
|
-
</div>`;
|
|
2194
|
-
|
|
2195
|
-
if (isExpanded) {
|
|
2196
|
-
const files = archiveFilesCache[archive.timestamp];
|
|
2197
|
-
if (files) {
|
|
2198
|
-
html += '<div class="archive-files">';
|
|
2199
|
-
for (const file of files) {
|
|
2200
|
-
const isActive = viewingArchiveFile === file.path;
|
|
2201
|
-
html += `<div class="archive-file-item${isActive ? ' active' : ''}" data-archive-file="${esc(file.path)}" data-archive-ts="${esc(archive.timestamp)}">
|
|
2202
|
-
<span class="archive-file-icon">📄</span>
|
|
2203
|
-
<span>${esc(file.path)}</span>
|
|
2204
|
-
</div>`;
|
|
2205
|
-
}
|
|
2206
|
-
html += '</div>';
|
|
2207
|
-
|
|
2208
|
-
if (viewingArchiveFile) {
|
|
2209
|
-
html += `<div class="archive-file-viewer">
|
|
2210
|
-
<div class="archive-file-viewer-header">
|
|
2211
|
-
<span>${esc(viewingArchiveFile)}</span>
|
|
2212
|
-
<button class="archive-file-viewer-close" data-close-viewer="true">×</button>
|
|
2213
|
-
</div>
|
|
2214
|
-
<div class="archive-file-content" id="archiveFileContent">Loading...</div>
|
|
2215
|
-
</div>`;
|
|
2216
|
-
}
|
|
2217
|
-
} else {
|
|
2218
|
-
html += '<div class="archive-files" style="padding:16px;color:var(--text-dim);font-size:12px">Loading files...</div>';
|
|
2219
|
-
}
|
|
2220
|
-
}
|
|
2221
|
-
|
|
2222
|
-
html += '</div>';
|
|
2223
|
-
}
|
|
2224
|
-
html += '</div>';
|
|
2225
|
-
|
|
2226
|
-
container.innerHTML = html;
|
|
2227
|
-
|
|
2228
|
-
// Bind archive card toggle clicks
|
|
2229
|
-
container.querySelectorAll('.archive-card-header').forEach(header => {
|
|
2230
|
-
header.addEventListener('click', () => toggleArchiveCard(appName, header.dataset.archiveToggle));
|
|
2231
|
-
});
|
|
2232
|
-
|
|
2233
|
-
// Bind archive file clicks
|
|
2234
|
-
container.querySelectorAll('.archive-file-item').forEach(item => {
|
|
2235
|
-
item.addEventListener('click', () => {
|
|
2236
|
-
viewArchiveFile(appName, item.dataset.archiveTs, item.dataset.archiveFile);
|
|
2237
|
-
});
|
|
2238
|
-
});
|
|
2239
|
-
|
|
2240
|
-
// Bind file viewer close button
|
|
2241
|
-
const closeBtn = container.querySelector('[data-close-viewer]');
|
|
2242
|
-
if (closeBtn) {
|
|
2243
|
-
closeBtn.addEventListener('click', () => {
|
|
2244
|
-
viewingArchiveFile = null;
|
|
2245
|
-
renderArchivesView(container, appName);
|
|
2246
|
-
});
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
|
-
// Load file content if viewer is open
|
|
2250
|
-
if (viewingArchiveFile && expandedArchive) {
|
|
2251
|
-
loadArchiveFileContent(appName, expandedArchive, viewingArchiveFile);
|
|
2252
|
-
}
|
|
2253
|
-
}
|
|
2254
|
-
|
|
2255
|
-
async function toggleArchiveCard(appName, timestamp) {
|
|
2256
|
-
const container = document.getElementById('archivesContainer');
|
|
2257
|
-
if (!container) return;
|
|
2258
|
-
|
|
2259
|
-
if (expandedArchive === timestamp) {
|
|
2260
|
-
expandedArchive = null;
|
|
2261
|
-
viewingArchiveFile = null;
|
|
2262
|
-
renderArchivesView(container, appName);
|
|
2263
|
-
return;
|
|
2264
|
-
}
|
|
2265
|
-
|
|
2266
|
-
expandedArchive = timestamp;
|
|
2267
|
-
viewingArchiveFile = null;
|
|
2268
|
-
|
|
2269
|
-
// Load files if not cached
|
|
2270
|
-
if (!archiveFilesCache[timestamp]) {
|
|
2271
|
-
renderArchivesView(container, appName);
|
|
2272
|
-
try {
|
|
2273
|
-
const files = await fetchJson(`/api/apps/${encodeURIComponent(appName)}/archives/${encodeURIComponent(timestamp)}/files`);
|
|
2274
|
-
archiveFilesCache[timestamp] = files;
|
|
2275
|
-
} catch {
|
|
2276
|
-
archiveFilesCache[timestamp] = [];
|
|
2277
|
-
}
|
|
2278
|
-
}
|
|
2279
|
-
|
|
2280
|
-
renderArchivesView(container, appName);
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
async function viewArchiveFile(appName, timestamp, filePath) {
|
|
2284
|
-
const container = document.getElementById('archivesContainer');
|
|
2285
|
-
if (!container) return;
|
|
2286
|
-
|
|
2287
|
-
viewingArchiveFile = filePath;
|
|
2288
|
-
renderArchivesView(container, appName);
|
|
2289
|
-
}
|
|
2290
|
-
|
|
2291
|
-
async function loadArchiveFileContent(appName, timestamp, filePath) {
|
|
2292
|
-
const contentEl = document.getElementById('archiveFileContent');
|
|
2293
|
-
if (!contentEl) return;
|
|
2294
|
-
|
|
2295
|
-
try {
|
|
2296
|
-
const data = await fetchJson(`/api/apps/${encodeURIComponent(appName)}/archives/${encodeURIComponent(timestamp)}/files/${filePath}`);
|
|
2297
|
-
contentEl.textContent = data.content || '(empty file)';
|
|
2298
|
-
} catch {
|
|
2299
|
-
contentEl.textContent = '(Error loading file)';
|
|
2300
|
-
}
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
|
-
function formatArchiveTimestamp(ts) {
|
|
2304
|
-
// Format: 2026-03-14_15-30 → Mar 14, 2026 at 15:30
|
|
2305
|
-
const match = ts.match(/^(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})/);
|
|
2306
|
-
if (!match) return ts;
|
|
2307
|
-
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
2308
|
-
const [, year, month, day, hour, min] = match;
|
|
2309
|
-
return `${months[parseInt(month, 10) - 1]} ${parseInt(day, 10)}, ${year} at ${hour}:${min}`;
|
|
2310
|
-
}
|
|
2311
|
-
|
|
2312
|
-
// Delete App Modal — exposed globally for inline onclick handlers
|
|
2313
|
-
window.openDeleteAppModal = openDeleteAppModal;
|
|
2314
|
-
function openDeleteAppModal(appName) {
|
|
2315
|
-
const existing = document.querySelector('.modal-overlay');
|
|
2316
|
-
if (existing) existing.remove();
|
|
2317
|
-
|
|
2318
|
-
const overlay = document.createElement('div');
|
|
2319
|
-
overlay.className = 'modal-overlay';
|
|
2320
|
-
overlay.innerHTML = `
|
|
2321
|
-
<div class="modal">
|
|
2322
|
-
<div class="modal-header">
|
|
2323
|
-
<h3>Delete App</h3>
|
|
2324
|
-
<button class="modal-close" data-action="close">×</button>
|
|
2325
|
-
</div>
|
|
2326
|
-
<div class="modal-body" id="deleteModalBody">
|
|
2327
|
-
<p style="margin-bottom:12px">Are you sure you want to delete <strong>${esc(appName)}</strong>?</p>
|
|
2328
|
-
<p style="color:var(--red);font-size:13px">This will permanently remove the app directory and all associated data. This action cannot be undone.</p>
|
|
2329
|
-
<div id="deleteModalMessage"></div>
|
|
2330
|
-
</div>
|
|
2331
|
-
<div class="modal-footer">
|
|
2332
|
-
<button class="btn" data-action="close">Cancel</button>
|
|
2333
|
-
<button class="btn btn-danger" id="deleteModalBtn">Delete</button>
|
|
2334
|
-
</div>
|
|
2335
|
-
</div>
|
|
2336
|
-
`;
|
|
2337
|
-
|
|
2338
|
-
document.body.appendChild(overlay);
|
|
2339
|
-
|
|
2340
|
-
overlay.addEventListener('click', (e) => {
|
|
2341
|
-
if (e.target === overlay || e.target.dataset.action === 'close') {
|
|
2342
|
-
overlay.remove();
|
|
2343
|
-
document.removeEventListener('keydown', escHandler);
|
|
2344
|
-
}
|
|
2345
|
-
});
|
|
2346
|
-
|
|
2347
|
-
const escHandler = (e) => {
|
|
2348
|
-
if (e.key === 'Escape') {
|
|
2349
|
-
overlay.remove();
|
|
2350
|
-
document.removeEventListener('keydown', escHandler);
|
|
2351
|
-
}
|
|
2352
|
-
};
|
|
2353
|
-
document.addEventListener('keydown', escHandler);
|
|
2354
|
-
|
|
2355
|
-
overlay.querySelector('#deleteModalBtn').addEventListener('click', () => submitDeleteApp(overlay, appName));
|
|
2356
|
-
}
|
|
2357
|
-
|
|
2358
|
-
async function submitDeleteApp(overlay, appName) {
|
|
2359
|
-
const msgEl = overlay.querySelector('#deleteModalMessage');
|
|
2360
|
-
const deleteBtn = overlay.querySelector('#deleteModalBtn');
|
|
2361
|
-
|
|
2362
|
-
deleteBtn.disabled = true;
|
|
2363
|
-
deleteBtn.textContent = 'Deleting...';
|
|
2364
|
-
msgEl.innerHTML = '';
|
|
2365
|
-
|
|
2366
|
-
try {
|
|
2367
|
-
const res = await fetch('/api/apps/' + encodeURIComponent(appName), { method: 'DELETE' });
|
|
2368
|
-
const data = await res.json();
|
|
2369
|
-
|
|
2370
|
-
if (!res.ok) {
|
|
2371
|
-
msgEl.innerHTML = `<div class="form-error">${esc(data.error || 'Failed to delete app')}</div>`;
|
|
2372
|
-
deleteBtn.disabled = false;
|
|
2373
|
-
deleteBtn.textContent = 'Delete';
|
|
2374
|
-
return;
|
|
2375
|
-
}
|
|
2376
|
-
|
|
2377
|
-
overlay.remove();
|
|
2378
|
-
|
|
2379
|
-
// Clean up client state
|
|
2380
|
-
notificationsList = notificationsList.filter(n => n.app !== appName);
|
|
2381
|
-
if (selectedApp && selectedApp.appName === appName) {
|
|
2382
|
-
selectedApp = null;
|
|
2383
|
-
selectedLoop = null;
|
|
2384
|
-
document.title = 'RalphFlow Dashboard';
|
|
2385
|
-
}
|
|
2386
|
-
fetchApps();
|
|
2387
|
-
} catch (err) {
|
|
2388
|
-
msgEl.innerHTML = '<div class="form-error">Network error — could not reach server</div>';
|
|
2389
|
-
deleteBtn.disabled = false;
|
|
2390
|
-
deleteBtn.textContent = 'Delete';
|
|
2391
|
-
}
|
|
2392
|
-
}
|
|
2393
|
-
|
|
2394
|
-
// Archive App Modal — exposed globally for inline onclick handlers
|
|
2395
|
-
window.openArchiveAppModal = openArchiveAppModal;
|
|
2396
|
-
function openArchiveAppModal(appName) {
|
|
2397
|
-
const existing = document.querySelector('.modal-overlay');
|
|
2398
|
-
if (existing) existing.remove();
|
|
2399
|
-
|
|
2400
|
-
const overlay = document.createElement('div');
|
|
2401
|
-
overlay.className = 'modal-overlay';
|
|
2402
|
-
overlay.innerHTML = `
|
|
2403
|
-
<div class="modal">
|
|
2404
|
-
<div class="modal-header">
|
|
2405
|
-
<h3>Archive App</h3>
|
|
2406
|
-
<button class="modal-close" data-action="close">×</button>
|
|
2407
|
-
</div>
|
|
2408
|
-
<div class="modal-body" id="archiveModalBody">
|
|
2409
|
-
<p style="margin-bottom:12px">Archive <strong>${esc(appName)}</strong>?</p>
|
|
2410
|
-
<p style="color:var(--text-dim);font-size:13px;margin-bottom:8px">This will snapshot all current work and reset to a clean slate:</p>
|
|
2411
|
-
<ul style="color:var(--text-dim);font-size:13px;margin-left:18px;margin-bottom:12px;line-height:1.6">
|
|
2412
|
-
<li>Stories, tasks, and trackers saved to <code style="font-family:var(--mono);font-size:12px;color:var(--text)">.archives/</code></li>
|
|
2413
|
-
<li>Tracker and data files reset to template defaults</li>
|
|
2414
|
-
<li>Prompts and config preserved</li>
|
|
2415
|
-
</ul>
|
|
2416
|
-
<div id="archiveModalMessage"></div>
|
|
2417
|
-
</div>
|
|
2418
|
-
<div class="modal-footer" id="archiveModalFooter">
|
|
2419
|
-
<button class="btn" data-action="close">Cancel</button>
|
|
2420
|
-
<button class="btn btn-primary" id="archiveModalBtn">Archive</button>
|
|
2421
|
-
</div>
|
|
2422
|
-
</div>
|
|
2423
|
-
`;
|
|
2424
|
-
|
|
2425
|
-
document.body.appendChild(overlay);
|
|
2426
|
-
|
|
2427
|
-
overlay.addEventListener('click', (e) => {
|
|
2428
|
-
if (e.target === overlay || e.target.dataset.action === 'close') {
|
|
2429
|
-
overlay.remove();
|
|
2430
|
-
document.removeEventListener('keydown', escHandler);
|
|
2431
|
-
}
|
|
2432
|
-
});
|
|
2433
|
-
|
|
2434
|
-
const escHandler = (e) => {
|
|
2435
|
-
if (e.key === 'Escape') {
|
|
2436
|
-
overlay.remove();
|
|
2437
|
-
document.removeEventListener('keydown', escHandler);
|
|
2438
|
-
}
|
|
2439
|
-
};
|
|
2440
|
-
document.addEventListener('keydown', escHandler);
|
|
2441
|
-
|
|
2442
|
-
overlay.querySelector('#archiveModalBtn').addEventListener('click', () => submitArchiveApp(overlay, appName));
|
|
2443
|
-
}
|
|
2444
|
-
|
|
2445
|
-
async function submitArchiveApp(overlay, appName) {
|
|
2446
|
-
const msgEl = overlay.querySelector('#archiveModalMessage');
|
|
2447
|
-
const archiveBtn = overlay.querySelector('#archiveModalBtn');
|
|
2448
|
-
|
|
2449
|
-
archiveBtn.disabled = true;
|
|
2450
|
-
archiveBtn.textContent = 'Archiving...';
|
|
2451
|
-
msgEl.innerHTML = '';
|
|
2452
|
-
|
|
2453
|
-
try {
|
|
2454
|
-
const res = await fetch('/api/apps/' + encodeURIComponent(appName) + '/archive', { method: 'POST' });
|
|
2455
|
-
const data = await res.json();
|
|
2456
|
-
|
|
2457
|
-
if (!res.ok) {
|
|
2458
|
-
msgEl.innerHTML = `<div class="form-error">${esc(data.error || 'Failed to archive app')}</div>`;
|
|
2459
|
-
archiveBtn.disabled = false;
|
|
2460
|
-
archiveBtn.textContent = 'Archive';
|
|
2461
|
-
return;
|
|
2462
|
-
}
|
|
2463
|
-
|
|
2464
|
-
// Show success state
|
|
2465
|
-
const body = overlay.querySelector('#archiveModalBody');
|
|
2466
|
-
const footer = overlay.querySelector('#archiveModalFooter');
|
|
2467
|
-
|
|
2468
|
-
body.innerHTML = `
|
|
2469
|
-
<p style="color:var(--green);margin-bottom:12px">Archived successfully.</p>
|
|
2470
|
-
<p style="font-size:13px;color:var(--text-dim)">Snapshot saved to <code style="font-family:var(--mono);font-size:12px;color:var(--text)">${esc(data.archivePath)}</code></p>
|
|
2471
|
-
<p style="font-size:13px;color:var(--text-dim);margin-top:8px">Timestamp: <strong style="color:var(--text)">${esc(data.timestamp)}</strong></p>
|
|
2472
|
-
`;
|
|
2473
|
-
footer.innerHTML = `<button class="btn btn-primary" data-action="close">Done</button>`;
|
|
2474
|
-
|
|
2475
|
-
// Clean up client state and refresh
|
|
2476
|
-
notificationsList = notificationsList.filter(n => n.app !== appName);
|
|
2477
|
-
fetchApps();
|
|
2478
|
-
} catch (err) {
|
|
2479
|
-
msgEl.innerHTML = '<div class="form-error">Network error — could not reach server</div>';
|
|
2480
|
-
archiveBtn.disabled = false;
|
|
2481
|
-
archiveBtn.textContent = 'Archive';
|
|
2482
|
-
}
|
|
2483
|
-
}
|
|
2484
|
-
|
|
2485
|
-
// Create App Modal
|
|
2486
|
-
async function openCreateAppModal() {
|
|
2487
|
-
// Remove any existing modal
|
|
2488
|
-
const existing = document.querySelector('.modal-overlay');
|
|
2489
|
-
if (existing) existing.remove();
|
|
2490
|
-
|
|
2491
|
-
// Fetch available templates (built-in + custom)
|
|
2492
|
-
let templates = [];
|
|
2493
|
-
try {
|
|
2494
|
-
templates = await fetchJson('/api/templates');
|
|
2495
|
-
} catch {
|
|
2496
|
-
templates = [
|
|
2497
|
-
{ name: 'code-implementation', type: 'built-in' },
|
|
2498
|
-
{ name: 'research', type: 'built-in' }
|
|
2499
|
-
];
|
|
2500
|
-
}
|
|
2501
|
-
|
|
2502
|
-
let optionsHtml = '';
|
|
2503
|
-
for (const tpl of templates) {
|
|
2504
|
-
optionsHtml += `<option value="${esc(tpl.name)}">${esc(tpl.name)}${tpl.type === 'custom' ? ' (custom)' : ''}</option>`;
|
|
2505
|
-
}
|
|
2506
|
-
|
|
2507
|
-
const overlay = document.createElement('div');
|
|
2508
|
-
overlay.className = 'modal-overlay';
|
|
2509
|
-
overlay.innerHTML = `
|
|
2510
|
-
<div class="modal">
|
|
2511
|
-
<div class="modal-header">
|
|
2512
|
-
<h3>Create New App</h3>
|
|
2513
|
-
<button class="modal-close" data-action="close">×</button>
|
|
2514
|
-
</div>
|
|
2515
|
-
<div class="modal-body" id="modalBody">
|
|
2516
|
-
<div class="form-group">
|
|
2517
|
-
<label class="form-label">Template</label>
|
|
2518
|
-
<select class="form-select" id="modalTemplate">
|
|
2519
|
-
${optionsHtml}
|
|
2520
|
-
</select>
|
|
2521
|
-
</div>
|
|
2522
|
-
<div class="form-group">
|
|
2523
|
-
<label class="form-label">App Name</label>
|
|
2524
|
-
<input class="form-input" id="modalName" type="text" placeholder="my-feature" autocomplete="off">
|
|
2525
|
-
</div>
|
|
2526
|
-
<div id="modalMessage"></div>
|
|
2527
|
-
</div>
|
|
2528
|
-
<div class="modal-footer" id="modalFooter">
|
|
2529
|
-
<button class="btn" data-action="close">Cancel</button>
|
|
2530
|
-
<button class="btn btn-primary" id="modalCreateBtn">Create</button>
|
|
2531
|
-
</div>
|
|
2532
|
-
</div>
|
|
2533
|
-
`;
|
|
2534
|
-
|
|
2535
|
-
document.body.appendChild(overlay);
|
|
2536
|
-
|
|
2537
|
-
// Close on overlay click or close buttons
|
|
2538
|
-
overlay.addEventListener('click', (e) => {
|
|
2539
|
-
if (e.target === overlay || e.target.dataset.action === 'close') {
|
|
2540
|
-
overlay.remove();
|
|
2541
|
-
}
|
|
2542
|
-
});
|
|
2543
|
-
|
|
2544
|
-
// Close on Escape
|
|
2545
|
-
const escHandler = (e) => {
|
|
2546
|
-
if (e.key === 'Escape') {
|
|
2547
|
-
overlay.remove();
|
|
2548
|
-
document.removeEventListener('keydown', escHandler);
|
|
2549
|
-
}
|
|
2550
|
-
};
|
|
2551
|
-
document.addEventListener('keydown', escHandler);
|
|
2552
|
-
|
|
2553
|
-
// Focus name input
|
|
2554
|
-
const nameInput = overlay.querySelector('#modalName');
|
|
2555
|
-
setTimeout(() => nameInput.focus(), 50);
|
|
2556
|
-
|
|
2557
|
-
// Submit on Enter in name input
|
|
2558
|
-
nameInput.addEventListener('keydown', (e) => {
|
|
2559
|
-
if (e.key === 'Enter') {
|
|
2560
|
-
e.preventDefault();
|
|
2561
|
-
submitCreateApp(overlay);
|
|
2562
|
-
}
|
|
2563
|
-
});
|
|
2564
|
-
|
|
2565
|
-
// Create button
|
|
2566
|
-
const createBtn = overlay.querySelector('#modalCreateBtn');
|
|
2567
|
-
createBtn.addEventListener('click', () => submitCreateApp(overlay));
|
|
2568
|
-
}
|
|
2569
|
-
|
|
2570
|
-
async function submitCreateApp(overlay) {
|
|
2571
|
-
const templateEl = overlay.querySelector('#modalTemplate');
|
|
2572
|
-
const nameEl = overlay.querySelector('#modalName');
|
|
2573
|
-
const msgEl = overlay.querySelector('#modalMessage');
|
|
2574
|
-
const createBtn = overlay.querySelector('#modalCreateBtn');
|
|
2575
|
-
|
|
2576
|
-
const template = templateEl.value;
|
|
2577
|
-
const name = nameEl.value.trim();
|
|
2578
|
-
|
|
2579
|
-
// Client-side validation
|
|
2580
|
-
if (!name) {
|
|
2581
|
-
msgEl.innerHTML = '<div class="form-error">Name is required</div>';
|
|
2582
|
-
nameEl.focus();
|
|
2583
|
-
return;
|
|
2584
|
-
}
|
|
2585
|
-
|
|
2586
|
-
// Disable button during request
|
|
2587
|
-
createBtn.disabled = true;
|
|
2588
|
-
createBtn.textContent = 'Creating...';
|
|
2589
|
-
msgEl.innerHTML = '';
|
|
2590
|
-
|
|
2591
|
-
try {
|
|
2592
|
-
const res = await fetch('/api/apps', {
|
|
2593
|
-
method: 'POST',
|
|
2594
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2595
|
-
body: JSON.stringify({ template, name }),
|
|
2596
|
-
});
|
|
2597
|
-
const data = await res.json();
|
|
2598
|
-
|
|
2599
|
-
if (!res.ok) {
|
|
2600
|
-
msgEl.innerHTML = `<div class="form-error">${esc(data.error || 'Failed to create app')}</div>`;
|
|
2601
|
-
createBtn.disabled = false;
|
|
2602
|
-
createBtn.textContent = 'Create';
|
|
2603
|
-
return;
|
|
2604
|
-
}
|
|
2605
|
-
|
|
2606
|
-
// Success — show next-steps view
|
|
2607
|
-
showNextSteps(overlay, data);
|
|
2608
|
-
} catch (err) {
|
|
2609
|
-
msgEl.innerHTML = '<div class="form-error">Network error — could not reach server</div>';
|
|
2610
|
-
createBtn.disabled = false;
|
|
2611
|
-
createBtn.textContent = 'Create';
|
|
2612
|
-
}
|
|
2613
|
-
}
|
|
2614
|
-
|
|
2615
|
-
function showNextSteps(overlay, data) {
|
|
2616
|
-
const body = overlay.querySelector('#modalBody');
|
|
2617
|
-
const footer = overlay.querySelector('#modalFooter');
|
|
2618
|
-
|
|
2619
|
-
let warningHtml = '';
|
|
2620
|
-
if (data.warning) {
|
|
2621
|
-
warningHtml = `<div class="form-warning">${esc(data.warning)}</div>`;
|
|
2622
|
-
}
|
|
2623
|
-
|
|
2624
|
-
let cmdsHtml = '';
|
|
2625
|
-
for (const cmd of data.commands) {
|
|
2626
|
-
cmdsHtml += `
|
|
2627
|
-
<div class="cmd-item">
|
|
2628
|
-
<span class="cmd-text">${esc(cmd)}</span>
|
|
2629
|
-
<button class="cmd-copy" data-cmd="${esc(cmd)}">Copy</button>
|
|
2630
|
-
</div>`;
|
|
2631
|
-
}
|
|
2632
|
-
|
|
2633
|
-
body.innerHTML = `
|
|
2634
|
-
<div class="next-steps-success">✓ Created ${esc(data.appName)}</div>
|
|
2635
|
-
${warningHtml}
|
|
2636
|
-
<div class="next-steps-label">Next steps — run one of these in your terminal:</div>
|
|
2637
|
-
${cmdsHtml}
|
|
2638
|
-
`;
|
|
2639
|
-
|
|
2640
|
-
footer.innerHTML = `<button class="btn btn-primary" data-action="close">Done</button>`;
|
|
2641
|
-
footer.querySelector('[data-action="close"]').addEventListener('click', () => overlay.remove());
|
|
2642
|
-
|
|
2643
|
-
// Copy-to-clipboard buttons
|
|
2644
|
-
body.querySelectorAll('.cmd-copy').forEach((btn) => {
|
|
2645
|
-
btn.addEventListener('click', () => {
|
|
2646
|
-
const cmd = btn.dataset.cmd || '';
|
|
2647
|
-
navigator.clipboard.writeText(cmd).then(() => {
|
|
2648
|
-
const orig = btn.textContent;
|
|
2649
|
-
btn.textContent = 'Copied!';
|
|
2650
|
-
setTimeout(() => { btn.textContent = orig; }, 1500);
|
|
2651
|
-
});
|
|
2652
|
-
});
|
|
2653
|
-
});
|
|
2654
|
-
}
|
|
2655
|
-
|
|
2656
|
-
// -----------------------------------------------------------------------
|
|
2657
|
-
// Templates page
|
|
2658
|
-
// -----------------------------------------------------------------------
|
|
2659
|
-
|
|
2660
|
-
async function fetchTemplates() {
|
|
2661
|
-
try {
|
|
2662
|
-
templatesList = await fetchJson('/api/templates');
|
|
2663
|
-
} catch {
|
|
2664
|
-
templatesList = [];
|
|
2665
|
-
}
|
|
2666
|
-
}
|
|
2667
|
-
|
|
2668
|
-
function createEmptyLoop() {
|
|
2669
|
-
return {
|
|
2670
|
-
name: '',
|
|
2671
|
-
stages: ['init'],
|
|
2672
|
-
completion: 'LOOP COMPLETE',
|
|
2673
|
-
model: 'claude-sonnet-4-6',
|
|
2674
|
-
multi_agent: false,
|
|
2675
|
-
max_agents: 3,
|
|
2676
|
-
strategy: 'parallel',
|
|
2677
|
-
agent_placeholder: '{{AGENT_NAME}}',
|
|
2678
|
-
data_files: [],
|
|
2679
|
-
entities: [],
|
|
2680
|
-
showOptional: false
|
|
2681
|
-
};
|
|
2682
|
-
}
|
|
2683
|
-
|
|
2684
|
-
function initTemplateBuilderState() {
|
|
2685
|
-
return { name: '', description: '', loops: [createEmptyLoop()] };
|
|
2686
|
-
}
|
|
2687
|
-
|
|
2688
|
-
async function renderTemplatesPage() {
|
|
2689
|
-
if (showTemplateBuilder) {
|
|
2690
|
-
renderTemplateBuilder();
|
|
2691
|
-
return;
|
|
2692
|
-
}
|
|
2693
|
-
|
|
2694
|
-
await fetchTemplates();
|
|
2695
|
-
|
|
2696
|
-
let html = '<div class="templates-header">';
|
|
2697
|
-
html += '<h2>Templates</h2>';
|
|
2698
|
-
html += '<button class="btn btn-primary" id="createTemplateBtn">Create Template</button>';
|
|
2699
|
-
html += '</div>';
|
|
2700
|
-
|
|
2701
|
-
if (templatesList.length === 0) {
|
|
2702
|
-
html += '<div class="content-empty">No templates found</div>';
|
|
2703
|
-
} else {
|
|
2704
|
-
html += '<div class="template-grid">';
|
|
2705
|
-
for (const tpl of templatesList) {
|
|
2706
|
-
html += `<div class="template-card">
|
|
2707
|
-
<div class="template-card-header">
|
|
2708
|
-
<span class="template-card-name">${esc(tpl.name)}</span>
|
|
2709
|
-
<span class="template-card-type ${tpl.type}">${esc(tpl.type)}</span>
|
|
2710
|
-
</div>
|
|
2711
|
-
${tpl.description ? `<div class="template-card-desc">${esc(tpl.description)}</div>` : ''}
|
|
2712
|
-
<div class="template-card-meta">
|
|
2713
|
-
<span>${tpl.loopCount} loop${tpl.loopCount !== 1 ? 's' : ''}</span>
|
|
2714
|
-
${tpl.type === 'custom' ? `<button class="btn btn-danger" style="font-size:11px;padding:2px 8px" data-delete-template="${esc(tpl.name)}">Delete</button>` : ''}
|
|
2715
|
-
</div>
|
|
2716
|
-
</div>`;
|
|
2717
|
-
}
|
|
2718
|
-
html += '</div>';
|
|
2719
|
-
}
|
|
2720
|
-
|
|
2721
|
-
content.innerHTML = html;
|
|
2722
|
-
|
|
2723
|
-
const createBtn = document.getElementById('createTemplateBtn');
|
|
2724
|
-
if (createBtn) {
|
|
2725
|
-
createBtn.addEventListener('click', () => {
|
|
2726
|
-
showTemplateBuilder = true;
|
|
2727
|
-
templateBuilderState = initTemplateBuilderState();
|
|
2728
|
-
renderTemplatesPage();
|
|
2729
|
-
});
|
|
2730
|
-
}
|
|
2731
|
-
|
|
2732
|
-
content.querySelectorAll('[data-delete-template]').forEach(btn => {
|
|
2733
|
-
btn.addEventListener('click', () => openDeleteTemplateModal(btn.dataset.deleteTemplate));
|
|
2734
|
-
});
|
|
2735
|
-
}
|
|
2736
|
-
|
|
2737
|
-
function renderTemplateBuilder() {
|
|
2738
|
-
const state = templateBuilderState;
|
|
2739
|
-
let html = '';
|
|
2740
|
-
|
|
2741
|
-
html += '<div class="templates-header">';
|
|
2742
|
-
html += '<div style="display:flex;align-items:center;gap:12px"><button class="btn btn-muted" id="builderBackBtn" style="padding:4px 10px">← Back</button><h2>Create Template</h2></div>';
|
|
2743
|
-
html += '</div>';
|
|
2744
|
-
|
|
2745
|
-
html += '<div class="template-builder">';
|
|
2746
|
-
|
|
2747
|
-
// Basic info
|
|
2748
|
-
html += '<div class="builder-section">';
|
|
2749
|
-
html += '<div class="builder-section-title">Basic Info</div>';
|
|
2750
|
-
html += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">';
|
|
2751
|
-
html += `<div class="form-group"><label class="form-label">Template Name</label>
|
|
2752
|
-
<input class="form-input" id="tplName" type="text" value="${esc(state.name)}" placeholder="my-pipeline" autocomplete="off"></div>`;
|
|
2753
|
-
html += `<div class="form-group"><label class="form-label">Description</label>
|
|
2754
|
-
<input class="form-input" id="tplDesc" type="text" value="${esc(state.description)}" placeholder="Pipeline description" autocomplete="off"></div>`;
|
|
2755
|
-
html += '</div></div>';
|
|
2756
|
-
|
|
2757
|
-
// Loops
|
|
2758
|
-
html += '<div class="builder-section">';
|
|
2759
|
-
html += '<div class="builder-section-title">Loops</div>';
|
|
2760
|
-
html += '<div class="loop-cards">';
|
|
2761
|
-
|
|
2762
|
-
state.loops.forEach((loop, i) => {
|
|
2763
|
-
html += `<div class="loop-card" data-loop-index="${i}">`;
|
|
2764
|
-
html += `<div class="loop-card-header">
|
|
2765
|
-
<span class="loop-card-title">Loop ${i + 1}</span>
|
|
2766
|
-
${state.loops.length > 1 ? `<button class="loop-card-remove" data-remove-loop="${i}">×</button>` : ''}
|
|
2767
|
-
</div>`;
|
|
2768
|
-
|
|
2769
|
-
html += '<div class="loop-card-grid">';
|
|
2770
|
-
|
|
2771
|
-
// Name
|
|
2772
|
-
html += `<div class="form-group"><label class="form-label">Name</label>
|
|
2773
|
-
<input class="form-input loop-input" data-loop-idx="${i}" data-field="name" type="text" value="${esc(loop.name)}" placeholder="Story" autocomplete="off"></div>`;
|
|
2774
|
-
|
|
2775
|
-
// Model
|
|
2776
|
-
html += `<div class="form-group"><label class="form-label">Model</label>
|
|
2777
|
-
<select class="form-select loop-input" data-loop-idx="${i}" data-field="model">
|
|
2778
|
-
<option value="claude-sonnet-4-6"${loop.model === 'claude-sonnet-4-6' ? ' selected' : ''}>claude-sonnet-4-6</option>
|
|
2779
|
-
<option value="claude-opus-4-6"${loop.model === 'claude-opus-4-6' ? ' selected' : ''}>claude-opus-4-6</option>
|
|
2780
|
-
<option value="claude-haiku-4-5-20251001"${loop.model === 'claude-haiku-4-5-20251001' ? ' selected' : ''}>claude-haiku-4-5-20251001</option>
|
|
2781
|
-
</select></div>`;
|
|
2782
|
-
|
|
2783
|
-
// Stages (tag input)
|
|
2784
|
-
html += `<div class="form-group loop-card-full"><label class="form-label">Stages</label>
|
|
2785
|
-
<div class="stage-tags" data-loop-idx="${i}">`;
|
|
2786
|
-
loop.stages.forEach((stage, si) => {
|
|
2787
|
-
html += `<span class="stage-tag">${esc(stage)}<button class="stage-tag-remove" data-loop-idx="${i}" data-stage-idx="${si}">×</button></span>`;
|
|
2788
|
-
});
|
|
2789
|
-
html += `<input type="text" placeholder="Type stage, press Enter" data-stage-input="${i}" autocomplete="off">`;
|
|
2790
|
-
html += '</div></div>';
|
|
2791
|
-
|
|
2792
|
-
// Completion
|
|
2793
|
-
html += `<div class="form-group loop-card-full"><label class="form-label">Completion String</label>
|
|
2794
|
-
<input class="form-input loop-input" data-loop-idx="${i}" data-field="completion" type="text" value="${esc(loop.completion)}" placeholder="LOOP COMPLETE" autocomplete="off"></div>`;
|
|
2795
|
-
|
|
2796
|
-
html += '</div>'; // close loop-card-grid
|
|
2797
|
-
|
|
2798
|
-
// Multi-agent toggle
|
|
2799
|
-
html += `<div style="margin-top:12px">
|
|
2800
|
-
<label class="toggle-wrap">
|
|
2801
|
-
<input type="checkbox" class="toggle-input" data-loop-idx="${i}" data-field="multi_agent" ${loop.multi_agent ? 'checked' : ''}>
|
|
2802
|
-
<span class="toggle-label">Multi-agent</span>
|
|
2803
|
-
</label>`;
|
|
2804
|
-
|
|
2805
|
-
if (loop.multi_agent) {
|
|
2806
|
-
html += `<div class="multi-agent-fields">
|
|
2807
|
-
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px">
|
|
2808
|
-
<div class="form-group" style="margin-bottom:0"><label class="form-label">Max Agents</label>
|
|
2809
|
-
<input class="form-input loop-input" data-loop-idx="${i}" data-field="max_agents" type="number" value="${loop.max_agents}" min="2" max="10"></div>
|
|
2810
|
-
<div class="form-group" style="margin-bottom:0"><label class="form-label">Strategy</label>
|
|
2811
|
-
<select class="form-select loop-input" data-loop-idx="${i}" data-field="strategy">
|
|
2812
|
-
<option value="parallel"${loop.strategy === 'parallel' ? ' selected' : ''}>parallel</option>
|
|
2813
|
-
<option value="sequential"${loop.strategy === 'sequential' ? ' selected' : ''}>sequential</option>
|
|
2814
|
-
</select></div>
|
|
2815
|
-
<div class="form-group" style="margin-bottom:0"><label class="form-label">Agent Placeholder</label>
|
|
2816
|
-
<input class="form-input loop-input" data-loop-idx="${i}" data-field="agent_placeholder" type="text" value="${esc(loop.agent_placeholder)}"></div>
|
|
2817
|
-
</div>
|
|
2818
|
-
</div>`;
|
|
2819
|
-
}
|
|
2820
|
-
html += '</div>';
|
|
2821
|
-
|
|
2822
|
-
// Optional fields toggle
|
|
2823
|
-
html += `<button class="optional-toggle" data-toggle-optional="${i}">${loop.showOptional ? 'Hide optional fields' : 'Show optional fields'}</button>`;
|
|
2824
|
-
|
|
2825
|
-
if (loop.showOptional) {
|
|
2826
|
-
html += '<div class="optional-fields">';
|
|
2827
|
-
html += '<div class="loop-card-grid">';
|
|
2828
|
-
html += `<div class="form-group"><label class="form-label">Data Files <span style="color:var(--text-muted)">(comma-separated)</span></label>
|
|
2829
|
-
<input class="form-input loop-input" data-loop-idx="${i}" data-field="data_files" type="text" value="${esc((loop.data_files || []).join(', '))}" placeholder="stories.md, tasks.md" autocomplete="off"></div>`;
|
|
2830
|
-
html += `<div class="form-group"><label class="form-label">Entities <span style="color:var(--text-muted)">(comma-separated)</span></label>
|
|
2831
|
-
<input class="form-input loop-input" data-loop-idx="${i}" data-field="entities" type="text" value="${esc((loop.entities || []).join(', '))}" placeholder="STORY, TASK" autocomplete="off"></div>`;
|
|
2832
|
-
html += '</div></div>';
|
|
2833
|
-
}
|
|
2834
|
-
|
|
2835
|
-
html += '</div>'; // close loop-card
|
|
2836
|
-
});
|
|
2837
|
-
|
|
2838
|
-
html += '</div>'; // close loop-cards
|
|
2839
|
-
html += `<button class="btn btn-muted" id="addLoopBtn" style="margin-top:12px;width:100%">+ Add Loop</button>`;
|
|
2840
|
-
html += '</div>'; // close builder-section
|
|
2841
|
-
|
|
2842
|
-
// YAML Preview
|
|
2843
|
-
html += '<div class="builder-section yaml-preview-section">';
|
|
2844
|
-
html += '<div class="builder-section-title">YAML Preview</div>';
|
|
2845
|
-
html += `<pre class="yaml-preview" id="yamlPreview">${esc(generateYamlPreview(state))}</pre>`;
|
|
2846
|
-
html += '</div>';
|
|
2847
|
-
|
|
2848
|
-
// Actions
|
|
2849
|
-
html += '<div class="builder-actions">';
|
|
2850
|
-
html += '<button class="btn" id="builderCancelBtn">Cancel</button>';
|
|
2851
|
-
html += '<button class="btn btn-primary" id="builderSaveBtn">Save Template</button>';
|
|
2852
|
-
html += '</div>';
|
|
2853
|
-
|
|
2854
|
-
html += '</div>'; // close template-builder
|
|
2855
|
-
|
|
2856
|
-
content.innerHTML = html;
|
|
2857
|
-
bindTemplateBuilderEvents();
|
|
2858
|
-
}
|
|
2859
|
-
|
|
2860
|
-
function bindTemplateBuilderEvents() {
|
|
2861
|
-
const backBtn = document.getElementById('builderBackBtn');
|
|
2862
|
-
if (backBtn) backBtn.addEventListener('click', () => {
|
|
2863
|
-
showTemplateBuilder = false;
|
|
2864
|
-
templateBuilderState = null;
|
|
2865
|
-
renderTemplatesPage();
|
|
2866
|
-
});
|
|
2867
|
-
|
|
2868
|
-
const cancelBtn = document.getElementById('builderCancelBtn');
|
|
2869
|
-
if (cancelBtn) cancelBtn.addEventListener('click', () => {
|
|
2870
|
-
showTemplateBuilder = false;
|
|
2871
|
-
templateBuilderState = null;
|
|
2872
|
-
renderTemplatesPage();
|
|
2873
|
-
});
|
|
2874
|
-
|
|
2875
|
-
const saveBtn = document.getElementById('builderSaveBtn');
|
|
2876
|
-
if (saveBtn) saveBtn.addEventListener('click', saveTemplate);
|
|
2877
|
-
|
|
2878
|
-
const addLoopBtn = document.getElementById('addLoopBtn');
|
|
2879
|
-
if (addLoopBtn) addLoopBtn.addEventListener('click', () => {
|
|
2880
|
-
captureBuilderInputs();
|
|
2881
|
-
templateBuilderState.loops.push(createEmptyLoop());
|
|
2882
|
-
renderTemplateBuilder();
|
|
2883
|
-
});
|
|
2884
|
-
|
|
2885
|
-
// Template name/desc inputs
|
|
2886
|
-
const tplName = document.getElementById('tplName');
|
|
2887
|
-
const tplDesc = document.getElementById('tplDesc');
|
|
2888
|
-
if (tplName) tplName.addEventListener('input', () => {
|
|
2889
|
-
templateBuilderState.name = tplName.value;
|
|
2890
|
-
updateYamlPreview();
|
|
2891
|
-
});
|
|
2892
|
-
if (tplDesc) tplDesc.addEventListener('input', () => {
|
|
2893
|
-
templateBuilderState.description = tplDesc.value;
|
|
2894
|
-
updateYamlPreview();
|
|
2895
|
-
});
|
|
2896
|
-
|
|
2897
|
-
// Loop text inputs
|
|
2898
|
-
content.querySelectorAll('.loop-input').forEach(input => {
|
|
2899
|
-
const idx = parseInt(input.dataset.loopIdx);
|
|
2900
|
-
const field = input.dataset.field;
|
|
2901
|
-
const evtType = input.tagName === 'SELECT' ? 'change' : 'input';
|
|
2902
|
-
|
|
2903
|
-
if (field === 'multi_agent') {
|
|
2904
|
-
input.addEventListener('change', () => {
|
|
2905
|
-
captureBuilderInputs();
|
|
2906
|
-
templateBuilderState.loops[idx].multi_agent = input.checked;
|
|
2907
|
-
renderTemplateBuilder();
|
|
2908
|
-
});
|
|
2909
|
-
return;
|
|
2910
|
-
}
|
|
2911
|
-
|
|
2912
|
-
input.addEventListener(evtType, () => {
|
|
2913
|
-
const loop = templateBuilderState.loops[idx];
|
|
2914
|
-
if (!loop) return;
|
|
2915
|
-
if (field === 'max_agents') {
|
|
2916
|
-
loop.max_agents = parseInt(input.value) || 3;
|
|
2917
|
-
} else if (field === 'data_files') {
|
|
2918
|
-
loop.data_files = input.value.split(',').map(s => s.trim()).filter(Boolean);
|
|
2919
|
-
} else if (field === 'entities') {
|
|
2920
|
-
loop.entities = input.value.split(',').map(s => s.trim()).filter(Boolean);
|
|
2921
|
-
} else {
|
|
2922
|
-
loop[field] = input.value;
|
|
2923
|
-
}
|
|
2924
|
-
updateYamlPreview();
|
|
2925
|
-
});
|
|
2926
|
-
});
|
|
2927
|
-
|
|
2928
|
-
// Stage tag inputs
|
|
2929
|
-
content.querySelectorAll('[data-stage-input]').forEach(input => {
|
|
2930
|
-
const idx = parseInt(input.dataset.stageInput);
|
|
2931
|
-
input.addEventListener('keydown', (e) => {
|
|
2932
|
-
if (e.key === 'Enter' || e.key === ',') {
|
|
2933
|
-
e.preventDefault();
|
|
2934
|
-
const value = input.value.trim().replace(/,/g, '');
|
|
2935
|
-
if (value && !templateBuilderState.loops[idx].stages.includes(value)) {
|
|
2936
|
-
captureBuilderInputs();
|
|
2937
|
-
templateBuilderState.loops[idx].stages.push(value);
|
|
2938
|
-
renderTemplateBuilder();
|
|
2939
|
-
setTimeout(() => {
|
|
2940
|
-
const newInput = content.querySelector(`[data-stage-input="${idx}"]`);
|
|
2941
|
-
if (newInput) newInput.focus();
|
|
2942
|
-
}, 0);
|
|
2943
|
-
}
|
|
2944
|
-
}
|
|
2945
|
-
});
|
|
2946
|
-
});
|
|
2947
|
-
|
|
2948
|
-
// Stage tag remove buttons
|
|
2949
|
-
content.querySelectorAll('.stage-tag-remove').forEach(btn => {
|
|
2950
|
-
btn.addEventListener('click', () => {
|
|
2951
|
-
captureBuilderInputs();
|
|
2952
|
-
const loopIdx = parseInt(btn.dataset.loopIdx);
|
|
2953
|
-
const stageIdx = parseInt(btn.dataset.stageIdx);
|
|
2954
|
-
templateBuilderState.loops[loopIdx].stages.splice(stageIdx, 1);
|
|
2955
|
-
renderTemplateBuilder();
|
|
2956
|
-
});
|
|
2957
|
-
});
|
|
2958
|
-
|
|
2959
|
-
// Remove loop buttons
|
|
2960
|
-
content.querySelectorAll('[data-remove-loop]').forEach(btn => {
|
|
2961
|
-
btn.addEventListener('click', () => {
|
|
2962
|
-
captureBuilderInputs();
|
|
2963
|
-
const idx = parseInt(btn.dataset.removeLoop);
|
|
2964
|
-
templateBuilderState.loops.splice(idx, 1);
|
|
2965
|
-
renderTemplateBuilder();
|
|
2966
|
-
});
|
|
2967
|
-
});
|
|
2968
|
-
|
|
2969
|
-
// Optional fields toggle
|
|
2970
|
-
content.querySelectorAll('[data-toggle-optional]').forEach(btn => {
|
|
2971
|
-
btn.addEventListener('click', () => {
|
|
2972
|
-
captureBuilderInputs();
|
|
2973
|
-
const idx = parseInt(btn.dataset.toggleOptional);
|
|
2974
|
-
templateBuilderState.loops[idx].showOptional = !templateBuilderState.loops[idx].showOptional;
|
|
2975
|
-
renderTemplateBuilder();
|
|
2976
|
-
});
|
|
2977
|
-
});
|
|
2978
|
-
}
|
|
2979
|
-
|
|
2980
|
-
// Capture current input values into state before re-render
|
|
2981
|
-
function captureBuilderInputs() {
|
|
2982
|
-
const state = templateBuilderState;
|
|
2983
|
-
if (!state) return;
|
|
2984
|
-
const tplName = document.getElementById('tplName');
|
|
2985
|
-
const tplDesc = document.getElementById('tplDesc');
|
|
2986
|
-
if (tplName) state.name = tplName.value;
|
|
2987
|
-
if (tplDesc) state.description = tplDesc.value;
|
|
2988
|
-
|
|
2989
|
-
content.querySelectorAll('.loop-input').forEach(input => {
|
|
2990
|
-
const idx = parseInt(input.dataset.loopIdx);
|
|
2991
|
-
const field = input.dataset.field;
|
|
2992
|
-
const loop = state.loops[idx];
|
|
2993
|
-
if (!loop || field === 'multi_agent') return;
|
|
2994
|
-
if (field === 'max_agents') {
|
|
2995
|
-
loop.max_agents = parseInt(input.value) || 3;
|
|
2996
|
-
} else if (field === 'data_files') {
|
|
2997
|
-
loop.data_files = input.value.split(',').map(s => s.trim()).filter(Boolean);
|
|
2998
|
-
} else if (field === 'entities') {
|
|
2999
|
-
loop.entities = input.value.split(',').map(s => s.trim()).filter(Boolean);
|
|
3000
|
-
} else {
|
|
3001
|
-
loop[field] = input.value;
|
|
3002
|
-
}
|
|
3003
|
-
});
|
|
3004
|
-
}
|
|
3005
|
-
|
|
3006
|
-
function updateYamlPreview() {
|
|
3007
|
-
const preview = document.getElementById('yamlPreview');
|
|
3008
|
-
if (preview && templateBuilderState) {
|
|
3009
|
-
preview.textContent = generateYamlPreview(templateBuilderState);
|
|
3010
|
-
}
|
|
3011
|
-
}
|
|
3012
|
-
|
|
3013
|
-
function generateYamlPreview(state) {
|
|
3014
|
-
let yaml = '';
|
|
3015
|
-
yaml += `name: ${state.name || 'my-template'}\n`;
|
|
3016
|
-
yaml += `description: "${state.description || ''}"\n`;
|
|
3017
|
-
yaml += `version: 1\n`;
|
|
3018
|
-
yaml += `dir: .ralph-flow\n`;
|
|
3019
|
-
yaml += `entities: {}\n`;
|
|
3020
|
-
yaml += `loops:\n`;
|
|
3021
|
-
|
|
3022
|
-
state.loops.forEach((loop, index) => {
|
|
3023
|
-
const baseName = (loop.name || `loop-${index + 1}`).toLowerCase().replace(/\s+/g, '-');
|
|
3024
|
-
const loopKey = baseName.endsWith('-loop') ? baseName : `${baseName}-loop`;
|
|
3025
|
-
const dirPrefix = String(index).padStart(2, '0');
|
|
3026
|
-
const loopDirName = `${dirPrefix}-${loopKey}`;
|
|
3027
|
-
|
|
3028
|
-
yaml += ` ${loopKey}:\n`;
|
|
3029
|
-
yaml += ` order: ${index}\n`;
|
|
3030
|
-
yaml += ` name: "${loop.name || `Loop ${index + 1}`}"\n`;
|
|
3031
|
-
yaml += ` prompt: ${loopDirName}/prompt.md\n`;
|
|
3032
|
-
yaml += ` tracker: ${loopDirName}/tracker.md\n`;
|
|
3033
|
-
yaml += ` stages: [${loop.stages.join(', ')}]\n`;
|
|
3034
|
-
yaml += ` completion: "${loop.completion || 'LOOP COMPLETE'}"\n`;
|
|
3035
|
-
|
|
3036
|
-
if (loop.multi_agent) {
|
|
3037
|
-
yaml += ` multi_agent:\n`;
|
|
3038
|
-
yaml += ` enabled: true\n`;
|
|
3039
|
-
yaml += ` max_agents: ${loop.max_agents || 3}\n`;
|
|
3040
|
-
yaml += ` strategy: ${loop.strategy || 'parallel'}\n`;
|
|
3041
|
-
yaml += ` agent_placeholder: "${loop.agent_placeholder || '{{AGENT_NAME}}'}"\n`;
|
|
3042
|
-
yaml += ` lock:\n`;
|
|
3043
|
-
yaml += ` file: ${loopDirName}/.tracker-lock\n`;
|
|
3044
|
-
yaml += ` type: echo\n`;
|
|
3045
|
-
yaml += ` stale_seconds: 60\n`;
|
|
3046
|
-
yaml += ` worktree:\n`;
|
|
3047
|
-
yaml += ` strategy: shared\n`;
|
|
3048
|
-
yaml += ` auto_merge: true\n`;
|
|
3049
|
-
} else {
|
|
3050
|
-
yaml += ` multi_agent: false\n`;
|
|
3051
|
-
}
|
|
3052
|
-
|
|
3053
|
-
yaml += ` model: ${loop.model || 'claude-sonnet-4-6'}\n`;
|
|
3054
|
-
yaml += ` cadence: 0\n`;
|
|
3055
|
-
|
|
3056
|
-
if (loop.data_files && loop.data_files.length > 0) {
|
|
3057
|
-
yaml += ` data_files:\n`;
|
|
3058
|
-
loop.data_files.forEach(f => { yaml += ` - ${loopDirName}/${f}\n`; });
|
|
3059
|
-
}
|
|
3060
|
-
if (loop.entities && loop.entities.length > 0) {
|
|
3061
|
-
yaml += ` entities: [${loop.entities.join(', ')}]\n`;
|
|
3062
|
-
}
|
|
3063
|
-
});
|
|
3064
|
-
|
|
3065
|
-
return yaml;
|
|
3066
|
-
}
|
|
3067
|
-
|
|
3068
|
-
async function saveTemplate() {
|
|
3069
|
-
captureBuilderInputs();
|
|
3070
|
-
const state = templateBuilderState;
|
|
3071
|
-
|
|
3072
|
-
if (!state.name || !state.name.trim()) {
|
|
3073
|
-
alert('Template name is required');
|
|
3074
|
-
return;
|
|
3075
|
-
}
|
|
3076
|
-
|
|
3077
|
-
for (let i = 0; i < state.loops.length; i++) {
|
|
3078
|
-
const loop = state.loops[i];
|
|
3079
|
-
if (!loop.name || !loop.name.trim()) {
|
|
3080
|
-
alert(`Loop ${i + 1}: name is required`);
|
|
3081
|
-
return;
|
|
3082
|
-
}
|
|
3083
|
-
if (loop.stages.length === 0) {
|
|
3084
|
-
alert(`Loop "${loop.name}": at least one stage is required`);
|
|
3085
|
-
return;
|
|
3086
|
-
}
|
|
3087
|
-
if (!loop.completion || !loop.completion.trim()) {
|
|
3088
|
-
alert(`Loop "${loop.name}": completion string is required`);
|
|
3089
|
-
return;
|
|
3090
|
-
}
|
|
3091
|
-
}
|
|
3092
|
-
|
|
3093
|
-
const definition = {
|
|
3094
|
-
name: state.name.trim(),
|
|
3095
|
-
description: state.description.trim(),
|
|
3096
|
-
loops: state.loops.map(loop => {
|
|
3097
|
-
const loopDef = {
|
|
3098
|
-
name: loop.name.trim(),
|
|
3099
|
-
stages: loop.stages,
|
|
3100
|
-
completion: loop.completion.trim(),
|
|
3101
|
-
model: loop.model || undefined,
|
|
3102
|
-
};
|
|
3103
|
-
if (loop.multi_agent) {
|
|
3104
|
-
loopDef.multi_agent = {
|
|
3105
|
-
enabled: true,
|
|
3106
|
-
max_agents: loop.max_agents || 3,
|
|
3107
|
-
strategy: loop.strategy || 'parallel',
|
|
3108
|
-
agent_placeholder: loop.agent_placeholder || '{{AGENT_NAME}}'
|
|
3109
|
-
};
|
|
3110
|
-
}
|
|
3111
|
-
if (loop.data_files && loop.data_files.length > 0) {
|
|
3112
|
-
loopDef.data_files = loop.data_files;
|
|
3113
|
-
}
|
|
3114
|
-
if (loop.entities && loop.entities.length > 0) {
|
|
3115
|
-
loopDef.entities = loop.entities;
|
|
3116
|
-
}
|
|
3117
|
-
return loopDef;
|
|
3118
|
-
})
|
|
3119
|
-
};
|
|
3120
|
-
|
|
3121
|
-
const saveBtn = document.getElementById('builderSaveBtn');
|
|
3122
|
-
if (saveBtn) {
|
|
3123
|
-
saveBtn.disabled = true;
|
|
3124
|
-
saveBtn.textContent = 'Saving...';
|
|
3125
|
-
}
|
|
3126
|
-
|
|
3127
|
-
try {
|
|
3128
|
-
const res = await fetch('/api/templates', {
|
|
3129
|
-
method: 'POST',
|
|
3130
|
-
headers: { 'Content-Type': 'application/json' },
|
|
3131
|
-
body: JSON.stringify(definition)
|
|
3132
|
-
});
|
|
3133
|
-
const data = await res.json();
|
|
3134
|
-
|
|
3135
|
-
if (!res.ok) {
|
|
3136
|
-
alert(data.error || 'Failed to save template');
|
|
3137
|
-
if (saveBtn) {
|
|
3138
|
-
saveBtn.disabled = false;
|
|
3139
|
-
saveBtn.textContent = 'Save Template';
|
|
3140
|
-
}
|
|
3141
|
-
return;
|
|
3142
|
-
}
|
|
3143
|
-
|
|
3144
|
-
showTemplateBuilder = false;
|
|
3145
|
-
templateBuilderState = null;
|
|
3146
|
-
templatesList = [];
|
|
3147
|
-
renderTemplatesPage();
|
|
3148
|
-
} catch {
|
|
3149
|
-
alert('Network error — could not reach server');
|
|
3150
|
-
if (saveBtn) {
|
|
3151
|
-
saveBtn.disabled = false;
|
|
3152
|
-
saveBtn.textContent = 'Save Template';
|
|
3153
|
-
}
|
|
3154
|
-
}
|
|
3155
|
-
}
|
|
3156
|
-
|
|
3157
|
-
function openDeleteTemplateModal(templateName) {
|
|
3158
|
-
const existing = document.querySelector('.modal-overlay');
|
|
3159
|
-
if (existing) existing.remove();
|
|
3160
|
-
|
|
3161
|
-
const overlay = document.createElement('div');
|
|
3162
|
-
overlay.className = 'modal-overlay';
|
|
3163
|
-
overlay.innerHTML = `
|
|
3164
|
-
<div class="modal">
|
|
3165
|
-
<div class="modal-header">
|
|
3166
|
-
<h3>Delete Template</h3>
|
|
3167
|
-
<button class="modal-close" data-action="close">×</button>
|
|
3168
|
-
</div>
|
|
3169
|
-
<div class="modal-body">
|
|
3170
|
-
<p style="margin-bottom:12px">Delete template <strong>${esc(templateName)}</strong>?</p>
|
|
3171
|
-
<p style="color:var(--red);font-size:13px">This will permanently remove the template. Apps already created from it are not affected.</p>
|
|
3172
|
-
<div id="deleteTemplateMessage"></div>
|
|
3173
|
-
</div>
|
|
3174
|
-
<div class="modal-footer">
|
|
3175
|
-
<button class="btn" data-action="close">Cancel</button>
|
|
3176
|
-
<button class="btn btn-danger" id="deleteTemplateBtn">Delete</button>
|
|
3177
|
-
</div>
|
|
3178
|
-
</div>
|
|
3179
|
-
`;
|
|
3180
|
-
|
|
3181
|
-
document.body.appendChild(overlay);
|
|
3182
|
-
|
|
3183
|
-
overlay.addEventListener('click', (e) => {
|
|
3184
|
-
if (e.target === overlay || e.target.dataset.action === 'close') {
|
|
3185
|
-
overlay.remove();
|
|
3186
|
-
document.removeEventListener('keydown', escHandler);
|
|
3187
|
-
}
|
|
3188
|
-
});
|
|
3189
|
-
|
|
3190
|
-
const escHandler = (e) => {
|
|
3191
|
-
if (e.key === 'Escape') {
|
|
3192
|
-
overlay.remove();
|
|
3193
|
-
document.removeEventListener('keydown', escHandler);
|
|
3194
|
-
}
|
|
3195
|
-
};
|
|
3196
|
-
document.addEventListener('keydown', escHandler);
|
|
3197
|
-
|
|
3198
|
-
overlay.querySelector('#deleteTemplateBtn').addEventListener('click', async () => {
|
|
3199
|
-
const btn = overlay.querySelector('#deleteTemplateBtn');
|
|
3200
|
-
const msgEl = overlay.querySelector('#deleteTemplateMessage');
|
|
3201
|
-
btn.disabled = true;
|
|
3202
|
-
btn.textContent = 'Deleting...';
|
|
3203
|
-
|
|
3204
|
-
try {
|
|
3205
|
-
const res = await fetch('/api/templates/' + encodeURIComponent(templateName), { method: 'DELETE' });
|
|
3206
|
-
const data = await res.json();
|
|
3207
|
-
|
|
3208
|
-
if (!res.ok) {
|
|
3209
|
-
msgEl.innerHTML = `<div class="form-error">${esc(data.error || 'Failed to delete')}</div>`;
|
|
3210
|
-
btn.disabled = false;
|
|
3211
|
-
btn.textContent = 'Delete';
|
|
3212
|
-
return;
|
|
3213
|
-
}
|
|
3214
|
-
|
|
3215
|
-
overlay.remove();
|
|
3216
|
-
templatesList = [];
|
|
3217
|
-
renderTemplatesPage();
|
|
3218
|
-
} catch {
|
|
3219
|
-
msgEl.innerHTML = '<div class="form-error">Network error</div>';
|
|
3220
|
-
btn.disabled = false;
|
|
3221
|
-
btn.textContent = 'Delete';
|
|
3222
|
-
}
|
|
3223
|
-
});
|
|
3224
|
-
}
|
|
3225
|
-
|
|
3226
|
-
// -----------------------------------------------------------------------
|
|
3227
|
-
// Init
|
|
3228
|
-
// -----------------------------------------------------------------------
|
|
3229
|
-
|
|
3230
|
-
// Templates nav click handler
|
|
3231
|
-
document.getElementById('templatesNav').addEventListener('click', () => {
|
|
3232
|
-
currentPage = 'templates';
|
|
3233
|
-
selectedApp = null;
|
|
3234
|
-
selectedLoop = null;
|
|
3235
|
-
showTemplateBuilder = false;
|
|
3236
|
-
templateBuilderState = null;
|
|
3237
|
-
document.title = 'Templates - RalphFlow Dashboard';
|
|
3238
|
-
renderSidebar();
|
|
3239
|
-
renderContent();
|
|
3240
|
-
});
|
|
3241
|
-
|
|
3242
|
-
fetchApps();
|
|
3243
|
-
fetchNotifications();
|
|
3244
|
-
connectWs();
|
|
3245
|
-
})();
|
|
3246
|
-
</script>
|
|
38
|
+
<script type="module" src="app.js"></script>
|
|
3247
39
|
</body>
|
|
3248
40
|
</html>
|