clay-server 2.27.0-beta.11 → 2.27.0-beta.13

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.
@@ -0,0 +1,865 @@
1
+ // app-loop-ui.js - Ralph Loop UI, wizard, crafting/approval bars, preview modal
2
+ // Extracted from app.js (PR-31)
3
+
4
+ import { refreshIcons, iconHtml } from './icons.js';
5
+ import { escapeHtml } from './utils.js';
6
+
7
+ var _ctx = null;
8
+
9
+ // --- Module-owned state ---
10
+ var loopActive = false;
11
+ var loopAvailable = false;
12
+ var loopIteration = 0;
13
+ var loopMaxIterations = 0;
14
+ var loopBannerName = null;
15
+ var ralphPhase = "idle"; // idle | wizard | crafting | approval | executing | done
16
+ var ralphCraftingSessionId = null;
17
+ var ralphCraftingSource = null; // "ralph" or null (task)
18
+ var wizardStep = 1;
19
+ var wizardSource = "ralph"; // "ralph" or "task"
20
+ var wizardData = { name: "", task: "", maxIterations: 3, cron: null };
21
+ var ralphFilesReady = { promptReady: false, judgeReady: false, bothReady: false };
22
+ var ralphPreviewContent = { prompt: "", judge: "" };
23
+ var wizardMode = "draft"; // "draft" or "own"
24
+
25
+ // --- Getters / Setters ---
26
+ export function getLoopActive() { return loopActive; }
27
+ export function setLoopActive(v) { loopActive = v; }
28
+
29
+ export function getLoopAvailable() { return loopAvailable; }
30
+ export function setLoopAvailable(v) { loopAvailable = v; }
31
+
32
+ export function getLoopIteration() { return loopIteration; }
33
+ export function setLoopIteration(v) { loopIteration = v; }
34
+
35
+ export function getLoopMaxIterations() { return loopMaxIterations; }
36
+ export function setLoopMaxIterations(v) { loopMaxIterations = v; }
37
+
38
+ export function getLoopBannerName() { return loopBannerName; }
39
+ export function setLoopBannerName(v) { loopBannerName = v; }
40
+
41
+ export function getRalphPhase() { return ralphPhase; }
42
+ export function setRalphPhase(v) { ralphPhase = v; }
43
+
44
+ export function getRalphCraftingSessionId() { return ralphCraftingSessionId; }
45
+ export function setRalphCraftingSessionId(v) { ralphCraftingSessionId = v; }
46
+
47
+ export function getRalphCraftingSource() { return ralphCraftingSource; }
48
+ export function setRalphCraftingSource(v) { ralphCraftingSource = v; }
49
+
50
+ export function getRalphFilesReady() { return ralphFilesReady; }
51
+ export function setRalphFilesReady(v) { ralphFilesReady = v; }
52
+
53
+ export function getRalphPreviewContent() { return ralphPreviewContent; }
54
+ export function setRalphPreviewContent(v) { ralphPreviewContent = v; }
55
+
56
+ export function getWizardData() { return wizardData; }
57
+ export function setWizardData(v) { wizardData = v; }
58
+
59
+ // --- DOM refs for repeat picker (captured in init) ---
60
+ var repeatSelect = null;
61
+ var repeatTimeRow = null;
62
+ var repeatCustom = null;
63
+ var repeatUnitSelect = null;
64
+ var repeatDowRow = null;
65
+ var cronPreview = null;
66
+
67
+ // ========================================================
68
+ // Init
69
+ // ========================================================
70
+ export function initLoopUi(ctx) {
71
+ _ctx = ctx;
72
+
73
+ // Repeat picker DOM refs
74
+ repeatSelect = document.getElementById("ralph-repeat");
75
+ repeatTimeRow = document.getElementById("ralph-time-row");
76
+ repeatCustom = document.getElementById("ralph-custom-repeat");
77
+ repeatUnitSelect = document.getElementById("ralph-repeat-unit");
78
+ repeatDowRow = document.getElementById("ralph-custom-dow-row");
79
+ cronPreview = document.getElementById("ralph-cron-preview");
80
+
81
+ // --- Wizard button listeners ---
82
+ var wizardCloseBtn = document.getElementById("ralph-wizard-close");
83
+ var wizardBackdrop = document.querySelector(".ralph-wizard-backdrop");
84
+ var wizardBackBtn = document.getElementById("ralph-wizard-back");
85
+ var wizardSkipBtn = document.getElementById("ralph-wizard-skip");
86
+ var wizardNextBtn = document.getElementById("ralph-wizard-next");
87
+
88
+ if (wizardCloseBtn) wizardCloseBtn.addEventListener("click", closeRalphWizard);
89
+ if (wizardBackdrop) wizardBackdrop.addEventListener("click", closeRalphWizard);
90
+ if (wizardBackBtn) wizardBackBtn.addEventListener("click", wizardBack);
91
+ if (wizardSkipBtn) wizardSkipBtn.addEventListener("click", wizardSkip);
92
+ if (wizardNextBtn) wizardNextBtn.addEventListener("click", wizardNext);
93
+
94
+ // --- Mode tab switching ---
95
+ var modeTabs = document.querySelectorAll(".ralph-mode-tab");
96
+ for (var mt = 0; mt < modeTabs.length; mt++) {
97
+ modeTabs[mt].addEventListener("click", function () {
98
+ wizardMode = this.getAttribute("data-mode");
99
+ updateWizardModeTabs();
100
+ });
101
+ }
102
+
103
+ // --- Repeat picker handlers ---
104
+ if (repeatSelect) {
105
+ repeatSelect.addEventListener("change", updateRepeatUI);
106
+ }
107
+ if (repeatUnitSelect) {
108
+ repeatUnitSelect.addEventListener("change", function () {
109
+ if (repeatDowRow) repeatDowRow.style.display = this.value === "week" ? "" : "none";
110
+ updateRepeatUI();
111
+ });
112
+ }
113
+
114
+ var timeInput = document.getElementById("ralph-time");
115
+ if (timeInput) timeInput.addEventListener("change", updateRepeatUI);
116
+
117
+ // DOW buttons in custom repeat
118
+ var customDowBtns = document.querySelectorAll("#ralph-custom-repeat .sched-dow-btn");
119
+ for (var di = 0; di < customDowBtns.length; di++) {
120
+ customDowBtns[di].addEventListener("click", function () {
121
+ this.classList.toggle("active");
122
+ updateRepeatUI();
123
+ });
124
+ }
125
+
126
+ // --- Preview modal listeners ---
127
+ var previewBackdrop = document.querySelector("#ralph-preview-modal .confirm-backdrop");
128
+ if (previewBackdrop) previewBackdrop.addEventListener("click", closeRalphPreviewModal);
129
+
130
+ // Run now button in preview modal
131
+ var previewRunBtn = document.getElementById("ralph-preview-run");
132
+ if (previewRunBtn) {
133
+ previewRunBtn.addEventListener("click", function (e) {
134
+ e.stopPropagation();
135
+ closeRalphPreviewModal();
136
+ // Trigger the same flow as the sticky start button
137
+ var stickyStart = document.querySelector(".ralph-sticky-start");
138
+ if (stickyStart) {
139
+ stickyStart.click();
140
+ }
141
+ });
142
+ }
143
+
144
+ // Delete/cancel button in preview modal
145
+ var previewDeleteBtn = document.getElementById("ralph-preview-delete");
146
+ if (previewDeleteBtn) {
147
+ previewDeleteBtn.addEventListener("click", function (e) {
148
+ e.stopPropagation();
149
+ closeRalphPreviewModal();
150
+ // Trigger the same flow as the sticky dismiss button
151
+ var stickyDismiss = document.querySelector(".ralph-sticky-dismiss");
152
+ if (stickyDismiss) {
153
+ stickyDismiss.click();
154
+ }
155
+ });
156
+ }
157
+
158
+ var previewTabs = document.querySelectorAll(".ralph-tab");
159
+ for (var ti = 0; ti < previewTabs.length; ti++) {
160
+ previewTabs[ti].addEventListener("click", function() {
161
+ showRalphPreviewTab(this.getAttribute("data-tab"));
162
+ });
163
+ }
164
+ }
165
+
166
+ // ========================================================
167
+ // Loop UI (exported)
168
+ // ========================================================
169
+
170
+ export function updateLoopInputVisibility(loop) {
171
+ var inputArea = document.getElementById("input-area");
172
+ if (!inputArea) return;
173
+ if (loop && loop.active && loop.role !== "crafting") {
174
+ inputArea.style.display = "none";
175
+ } else {
176
+ inputArea.style.display = "";
177
+ }
178
+ }
179
+
180
+ export function updateLoopButton() {
181
+ var section = document.getElementById("ralph-loop-section");
182
+ if (!section) return;
183
+
184
+ var busy = loopActive || ralphPhase === "executing";
185
+ var phase = busy ? "executing" : ralphPhase;
186
+
187
+ var statusHtml = "";
188
+ var statusClass = "";
189
+ var clickAction = "wizard"; // default
190
+
191
+ if (phase === "crafting") {
192
+ statusHtml = '<span class="ralph-section-status crafting">' + iconHtml("loader", "icon-spin") + ' Crafting\u2026</span>';
193
+ clickAction = "none";
194
+ } else if (phase === "approval") {
195
+ statusHtml = '<span class="ralph-section-status ready">Ready</span>';
196
+ statusClass = "ralph-section-ready";
197
+ clickAction = "none";
198
+ } else if (phase === "executing") {
199
+ var iterText = loopIteration > 0 ? "Running \u00b7 iteration " + loopIteration + "/" + loopMaxIterations : "Starting\u2026";
200
+ statusHtml = '<span class="ralph-section-status running">' + iconHtml("loader", "icon-spin") + ' ' + iterText + '</span>';
201
+ statusClass = "ralph-section-running";
202
+ clickAction = "popover";
203
+ } else if (phase === "done") {
204
+ statusHtml = '<span class="ralph-section-status done">\u2713 Done</span>';
205
+ statusHtml += '<a href="#" class="ralph-section-tasks-link">View in Scheduled Tasks</a>';
206
+ statusClass = "ralph-section-done";
207
+ clickAction = "wizard";
208
+ } else {
209
+ // idle
210
+ statusHtml = '<span class="ralph-section-hint">Start a new loop</span>';
211
+ }
212
+
213
+ section.className = "ralph-loop-section" + (statusClass ? " " + statusClass : "");
214
+ section.innerHTML =
215
+ '<div class="ralph-section-inner">' +
216
+ '<div class="ralph-section-header">' +
217
+ '<span class="ralph-section-icon">' + iconHtml("repeat") + '</span>' +
218
+ '<span class="ralph-section-label">Ralph Loop</span>' +
219
+ '<span class="loop-experimental"><i data-lucide="flask-conical"></i> experimental</span>' +
220
+ '</div>' +
221
+ '<div class="ralph-section-body">' + statusHtml + '</div>' +
222
+ '</div>';
223
+
224
+ refreshIcons();
225
+
226
+ // Click handler on header
227
+ var header = section.querySelector(".ralph-section-header");
228
+ if (header) {
229
+ header.style.cursor = clickAction === "none" ? "default" : "pointer";
230
+ header.addEventListener("click", function() {
231
+ if (clickAction === "popover") {
232
+ toggleLoopPopover();
233
+ } else if (clickAction === "wizard") {
234
+ openRalphWizard();
235
+ }
236
+ });
237
+ }
238
+
239
+ // "View in Scheduled Tasks" link
240
+ var tasksLink = section.querySelector(".ralph-section-tasks-link");
241
+ if (tasksLink) {
242
+ tasksLink.addEventListener("click", function(e) {
243
+ e.preventDefault();
244
+ e.stopPropagation();
245
+ _ctx.openSchedulerToTab("library");
246
+ });
247
+ }
248
+ }
249
+
250
+ export function showLoopBanner(show) {
251
+ var stickyEl = document.getElementById("ralph-sticky");
252
+ if (!stickyEl) { updateLoopButton(); return; }
253
+ if (!show) {
254
+ stickyEl.classList.add("hidden");
255
+ stickyEl.classList.remove("ralph-running");
256
+ stickyEl.innerHTML = "";
257
+ updateLoopButton();
258
+ return;
259
+ }
260
+
261
+ var ws = _ctx.getWs();
262
+ var bannerLabel = loopBannerName || "Loop";
263
+ stickyEl.innerHTML =
264
+ '<div class="ralph-sticky-inner">' +
265
+ '<div class="ralph-sticky-header">' +
266
+ '<span class="ralph-sticky-icon">' + iconHtml("repeat") + '</span>' +
267
+ '<span class="ralph-sticky-label">' + escapeHtml(bannerLabel) + '</span>' +
268
+ '<span class="ralph-sticky-status" id="loop-status">Starting\u2026</span>' +
269
+ '<button class="ralph-sticky-action ralph-sticky-stop" title="Stop loop">' + iconHtml("square") + '</button>' +
270
+ '</div>' +
271
+ '</div>';
272
+ stickyEl.classList.remove("hidden", "ralph-ready");
273
+ stickyEl.classList.add("ralph-running");
274
+ refreshIcons();
275
+
276
+ stickyEl.querySelector(".ralph-sticky-stop").addEventListener("click", function(e) {
277
+ e.stopPropagation();
278
+ var w = _ctx.getWs();
279
+ if (w && w.readyState === 1) {
280
+ w.send(JSON.stringify({ type: "loop_stop" }));
281
+ }
282
+ });
283
+ updateLoopButton();
284
+ }
285
+
286
+ export function updateLoopBanner(iteration, maxIterations, phase) {
287
+ var statusEl = document.getElementById("loop-status");
288
+ if (!statusEl) return;
289
+ var text;
290
+ if (phase === "stopping") {
291
+ text = "Stopping\u2026";
292
+ } else if (maxIterations <= 1) {
293
+ text = phase === "judging" ? "judging\u2026" : "running";
294
+ } else {
295
+ text = "#" + iteration + "/" + maxIterations;
296
+ if (phase === "judging") text += " judging\u2026";
297
+ else text += " running";
298
+ }
299
+ statusEl.textContent = text;
300
+ }
301
+
302
+ export function updateRalphBars() {
303
+ // Task source uses the scheduler panel, not the sticky bar
304
+ var isTaskSource = ralphCraftingSource !== "ralph";
305
+ var onCraftingSession = ralphCraftingSessionId && _ctx.activeSessionId === ralphCraftingSessionId;
306
+ // If approval phase but no craftingSessionId (recovered after server restart), show bar anyway
307
+ var recoveredApproval = ralphPhase === "approval" && !ralphCraftingSessionId;
308
+ if (!isTaskSource && ralphPhase === "crafting" && onCraftingSession) {
309
+ showRalphCraftingBar(true);
310
+ } else {
311
+ showRalphCraftingBar(false);
312
+ }
313
+ if (!isTaskSource && ralphPhase === "approval" && (onCraftingSession || recoveredApproval)) {
314
+ showRalphApprovalBar(true);
315
+ } else {
316
+ showRalphApprovalBar(false);
317
+ }
318
+ // Restore running loop banner on session switch
319
+ if (loopActive && ralphPhase === "executing") {
320
+ showLoopBanner(true);
321
+ if (loopIteration > 0) {
322
+ updateLoopBanner(loopIteration, loopMaxIterations, "running");
323
+ }
324
+ }
325
+
326
+ // Restore debate sticky on session switch
327
+ var debateStickyState = _ctx.debateStickyState;
328
+ if (debateStickyState && debateStickyState.phase) {
329
+ _ctx.showDebateSticky(debateStickyState.phase, debateStickyState.msg);
330
+ } else {
331
+ _ctx.showDebateSticky("hide", null);
332
+ }
333
+ }
334
+
335
+ // ========================================================
336
+ // Internal: toggleLoopPopover
337
+ // ========================================================
338
+
339
+ function toggleLoopPopover() {
340
+ var existing = document.getElementById("loop-status-modal");
341
+ if (existing) {
342
+ existing.remove();
343
+ return;
344
+ }
345
+
346
+ var ws = _ctx.getWs();
347
+ var taskPreview = wizardData.task || "\u2014";
348
+ if (taskPreview.length > 120) taskPreview = taskPreview.substring(0, 120) + "\u2026";
349
+ var statusText = "Iteration #" + loopIteration + " / " + loopMaxIterations;
350
+
351
+ var modal = document.createElement("div");
352
+ modal.id = "loop-status-modal";
353
+ modal.className = "loop-status-modal";
354
+ modal.innerHTML =
355
+ '<div class="loop-status-backdrop"></div>' +
356
+ '<div class="loop-status-dialog">' +
357
+ '<div class="loop-status-dialog-header">' +
358
+ '<span class="loop-status-dialog-icon">' + iconHtml("repeat") + '</span>' +
359
+ '<span class="loop-status-dialog-title">Ralph Loop</span>' +
360
+ '<button class="loop-status-dialog-close" title="Close">' + iconHtml("x") + '</button>' +
361
+ '</div>' +
362
+ '<div class="loop-status-dialog-body">' +
363
+ '<div class="loop-status-dialog-row">' +
364
+ '<span class="loop-status-dialog-label">Progress</span>' +
365
+ '<span class="loop-status-dialog-value">' + escapeHtml(statusText) + '</span>' +
366
+ '</div>' +
367
+ '<div class="loop-status-dialog-row">' +
368
+ '<span class="loop-status-dialog-label">Task</span>' +
369
+ '<span class="loop-status-dialog-value loop-status-dialog-task">' + escapeHtml(taskPreview) + '</span>' +
370
+ '</div>' +
371
+ '</div>' +
372
+ '<div class="loop-status-dialog-footer">' +
373
+ '<button class="loop-status-dialog-stop">' + iconHtml("square") + ' Stop loop</button>' +
374
+ '</div>' +
375
+ '</div>';
376
+
377
+ document.body.appendChild(modal);
378
+ refreshIcons();
379
+
380
+ function closeModal() { modal.remove(); }
381
+
382
+ modal.querySelector(".loop-status-backdrop").addEventListener("click", closeModal);
383
+ modal.querySelector(".loop-status-dialog-close").addEventListener("click", closeModal);
384
+
385
+ modal.querySelector(".loop-status-dialog-stop").addEventListener("click", function(e) {
386
+ e.stopPropagation();
387
+ closeModal();
388
+ _ctx.showConfirm("Stop the running " + (loopBannerName || "loop") + "?", function() {
389
+ var w = _ctx.getWs();
390
+ if (w && w.readyState === 1) {
391
+ w.send(JSON.stringify({ type: "loop_stop" }));
392
+ }
393
+ });
394
+ });
395
+ }
396
+
397
+ // ========================================================
398
+ // Internal: requireClayRalph
399
+ // ========================================================
400
+
401
+ function requireClayRalph(cb) {
402
+ _ctx.requireSkills({
403
+ title: "Skill Installation Required",
404
+ reason: "This feature requires the following skill to be installed.",
405
+ skills: [{ name: "clay-ralph", url: "https://github.com/chadbyte/clay-ralph", scope: "global" }]
406
+ }, cb);
407
+ }
408
+
409
+ // ========================================================
410
+ // Ralph Wizard (exported: openRalphWizard, closeRalphWizard)
411
+ // ========================================================
412
+
413
+ export function openRalphWizard(source) {
414
+ requireClayRalph(function () {
415
+ wizardSource = source || "ralph";
416
+ wizardData = { name: "", task: "", maxIterations: 3 };
417
+ var el = document.getElementById("ralph-wizard");
418
+ if (!el) return;
419
+
420
+ var taskEl = document.getElementById("ralph-task");
421
+ if (taskEl) taskEl.value = "";
422
+ var promptInput = document.getElementById("ralph-prompt-input");
423
+ if (promptInput) promptInput.value = "";
424
+ var judgeInput = document.getElementById("ralph-judge-input");
425
+ if (judgeInput) judgeInput.value = "";
426
+ var iterEl = document.getElementById("ralph-max-iterations");
427
+ if (iterEl) iterEl.value = "25";
428
+
429
+ // Update text based on source
430
+ var isTask = wizardSource === "task";
431
+ var headerSpan = el.querySelector(".ralph-wizard-header > span");
432
+ if (headerSpan) headerSpan.textContent = isTask ? "New Task" : "New Ralph Loop";
433
+
434
+ var step2heading = el.querySelector('.ralph-step[data-step="2"] h3');
435
+ if (step2heading) step2heading.textContent = isTask ? "Describe your task" : "What do you want to build?";
436
+
437
+ var draftHint = el.querySelector('.ralph-mode-panel[data-mode="draft"] .ralph-hint');
438
+ if (draftHint) draftHint.textContent = isTask
439
+ ? "Describe what you want done. Clay will craft a precise prompt and you can review it before scheduling."
440
+ : "Write a rough idea, Clay will refine it into detailed instructions. You can review and edit everything before the loop starts.";
441
+
442
+ var ownHint = el.querySelector('.ralph-mode-panel[data-mode="own"] .ralph-hint');
443
+ if (ownHint) ownHint.textContent = isTask
444
+ ? "Paste the prompt to run. It will execute as-is when triggered."
445
+ : "Paste your PROMPT.md content. JUDGE.md is optional; if omitted, Clay will generate it for you.";
446
+
447
+ // Update task description placeholder
448
+ if (taskEl) taskEl.placeholder = isTask
449
+ ? "e.g. Check for dependency updates and create a summary"
450
+ : "e.g. Add dark mode toggle to the settings page";
451
+
452
+ wizardMode = "draft";
453
+ updateWizardModeTabs();
454
+
455
+ if (wizardSource === "task") {
456
+ // Tasks skip step 1 (Ralph intro), go directly to step 2
457
+ wizardStep = 2;
458
+ } else {
459
+ wizardStep = 1;
460
+ }
461
+ el.classList.remove("hidden");
462
+ var statusEl = document.getElementById("ralph-install-status");
463
+ if (statusEl) { statusEl.classList.add("hidden"); statusEl.innerHTML = ""; }
464
+ updateWizardStep();
465
+ });
466
+ }
467
+
468
+ export function closeRalphWizard() {
469
+ var el = document.getElementById("ralph-wizard");
470
+ if (el) el.classList.add("hidden");
471
+ }
472
+
473
+ // --- Internal wizard helpers ---
474
+
475
+ function updateWizardModeTabs() {
476
+ var tabs = document.querySelectorAll(".ralph-mode-tab");
477
+ var panels = document.querySelectorAll(".ralph-mode-panel");
478
+ for (var i = 0; i < tabs.length; i++) {
479
+ if (tabs[i].getAttribute("data-mode") === wizardMode) {
480
+ tabs[i].classList.add("active");
481
+ } else {
482
+ tabs[i].classList.remove("active");
483
+ }
484
+ }
485
+ for (var j = 0; j < panels.length; j++) {
486
+ if (panels[j].getAttribute("data-mode") === wizardMode) {
487
+ panels[j].classList.add("active");
488
+ } else {
489
+ panels[j].classList.remove("active");
490
+ }
491
+ }
492
+ }
493
+
494
+ function updateWizardStep() {
495
+ var steps = document.querySelectorAll(".ralph-step");
496
+ for (var i = 0; i < steps.length; i++) {
497
+ var stepNum = parseInt(steps[i].getAttribute("data-step"), 10);
498
+ if (stepNum === wizardStep) {
499
+ steps[i].classList.add("active");
500
+ } else {
501
+ steps[i].classList.remove("active");
502
+ }
503
+ }
504
+ var dots = document.querySelectorAll(".ralph-dot");
505
+ for (var j = 0; j < dots.length; j++) {
506
+ var dotStep = parseInt(dots[j].getAttribute("data-step"), 10);
507
+ dots[j].classList.remove("active", "done");
508
+ if (dotStep === wizardStep) dots[j].classList.add("active");
509
+ else if (dotStep < wizardStep) dots[j].classList.add("done");
510
+ }
511
+
512
+ var backBtn = document.getElementById("ralph-wizard-back");
513
+ var skipBtn = document.getElementById("ralph-wizard-skip");
514
+ var nextBtn = document.getElementById("ralph-wizard-next");
515
+ if (backBtn) {
516
+ backBtn.style.visibility = (wizardStep === 1 && wizardSource !== "task") ? "hidden" : "visible";
517
+ backBtn.textContent = (wizardSource === "task" && wizardStep <= 2) ? "Cancel" : "Back";
518
+ }
519
+ if (skipBtn) skipBtn.style.display = "none";
520
+ if (nextBtn) nextBtn.textContent = wizardStep === 2 ? "Launch" : "Get Started";
521
+ }
522
+
523
+ function collectWizardData() {
524
+ var iterEl = document.getElementById("ralph-max-iterations");
525
+ wizardData.name = "";
526
+ wizardData.maxIterations = iterEl ? parseInt(iterEl.value, 10) || 3 : 3;
527
+ wizardData.cron = null;
528
+ wizardData.mode = wizardMode;
529
+
530
+ if (wizardMode === "draft") {
531
+ var taskEl = document.getElementById("ralph-task");
532
+ wizardData.task = taskEl ? taskEl.value.trim() : "";
533
+ wizardData.promptText = null;
534
+ wizardData.judgeText = null;
535
+ } else {
536
+ var promptInput = document.getElementById("ralph-prompt-input");
537
+ var judgeInput = document.getElementById("ralph-judge-input");
538
+ wizardData.task = "";
539
+ wizardData.promptText = promptInput ? promptInput.value.trim() : "";
540
+ wizardData.judgeText = judgeInput ? judgeInput.value.trim() : "";
541
+ }
542
+ }
543
+
544
+ function buildWizardCron() {
545
+ if (!repeatSelect) return null;
546
+ var preset = repeatSelect.value;
547
+ if (preset === "none") return null;
548
+
549
+ var timeEl = document.getElementById("ralph-time");
550
+ var timeVal = timeEl ? timeEl.value : "09:00";
551
+ var timeParts = timeVal.split(":");
552
+ var hour = parseInt(timeParts[0], 10) || 9;
553
+ var minute = parseInt(timeParts[1], 10) || 0;
554
+
555
+ if (preset === "daily") return minute + " " + hour + " * * *";
556
+ if (preset === "weekdays") return minute + " " + hour + " * * 1-5";
557
+ if (preset === "weekly") return minute + " " + hour + " * * " + new Date().getDay();
558
+ if (preset === "monthly") return minute + " " + hour + " " + new Date().getDate() + " * *";
559
+
560
+ if (preset === "custom") {
561
+ var unitEl = document.getElementById("ralph-repeat-unit");
562
+ var unit = unitEl ? unitEl.value : "day";
563
+ if (unit === "day") return minute + " " + hour + " * * *";
564
+ if (unit === "month") return minute + " " + hour + " " + new Date().getDate() + " * *";
565
+ // week: collect selected days
566
+ var dowBtns = document.querySelectorAll("#ralph-custom-repeat .sched-dow-btn.active");
567
+ var days = [];
568
+ for (var i = 0; i < dowBtns.length; i++) {
569
+ days.push(dowBtns[i].dataset.dow);
570
+ }
571
+ if (days.length === 0) days.push(String(new Date().getDay()));
572
+ return minute + " " + hour + " * * " + days.join(",");
573
+ }
574
+ return null;
575
+ }
576
+
577
+ function cronToHumanText(cron) {
578
+ if (!cron) return "";
579
+ var parts = cron.trim().split(/\s+/);
580
+ if (parts.length !== 5) return cron;
581
+ var m = parts[0], h = parts[1], dom = parts[2], dow = parts[4];
582
+ var pad = function(n) { return (parseInt(n,10) < 10 ? "0" : "") + parseInt(n,10); };
583
+ var t = pad(h) + ":" + pad(m);
584
+ var dayNames = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
585
+ if (dow === "*" && dom === "*") return "Every day at " + t;
586
+ if (dow === "1-5" && dom === "*") return "Weekdays at " + t;
587
+ if (dom !== "*" && dow === "*") return "Monthly on day " + dom + " at " + t;
588
+ if (dow !== "*" && dom === "*") {
589
+ var ds = dow.split(",").map(function(d) { return dayNames[parseInt(d,10)] || d; });
590
+ return "Every " + ds.join(", ") + " at " + t;
591
+ }
592
+ return cron;
593
+ }
594
+
595
+ function wizardNext() {
596
+ collectWizardData();
597
+
598
+ if (wizardStep === 1) {
599
+ wizardStep++;
600
+ updateWizardStep();
601
+ return;
602
+ }
603
+
604
+ if (wizardStep === 2) {
605
+ if (wizardMode === "draft") {
606
+ var taskEl = document.getElementById("ralph-task");
607
+ if (!wizardData.task) {
608
+ if (taskEl) { taskEl.focus(); taskEl.style.borderColor = "#e74c3c"; setTimeout(function() { taskEl.style.borderColor = ""; }, 2000); }
609
+ return;
610
+ }
611
+ } else {
612
+ var promptInput = document.getElementById("ralph-prompt-input");
613
+ if (!wizardData.promptText) {
614
+ if (promptInput) { promptInput.focus(); promptInput.style.borderColor = "#e74c3c"; setTimeout(function() { promptInput.style.borderColor = ""; }, 2000); }
615
+ return;
616
+ }
617
+ }
618
+ wizardSubmit();
619
+ return;
620
+ }
621
+ wizardStep++;
622
+ updateWizardStep();
623
+ }
624
+
625
+ function wizardBack() {
626
+ if (wizardSource === "task" && wizardStep <= 2) {
627
+ closeRalphWizard();
628
+ return;
629
+ }
630
+ if (wizardStep > 1) {
631
+ collectWizardData();
632
+ wizardStep--;
633
+ updateWizardStep();
634
+ }
635
+ }
636
+
637
+ function wizardSkip() {
638
+ if (wizardStep < 2) {
639
+ wizardStep++;
640
+ updateWizardStep();
641
+ }
642
+ }
643
+
644
+ function wizardSubmit() {
645
+ collectWizardData();
646
+ wizardData.source = wizardSource === "task" ? "task" : undefined;
647
+ closeRalphWizard();
648
+ var ws = _ctx.getWs();
649
+ if (ws && ws.readyState === 1) {
650
+ ws.send(JSON.stringify({ type: "ralph_wizard_complete", data: wizardData }));
651
+ }
652
+ }
653
+
654
+ function updateRepeatUI() {
655
+ if (!repeatSelect) return;
656
+ var val = repeatSelect.value;
657
+ var isScheduled = val !== "none";
658
+ if (repeatTimeRow) repeatTimeRow.style.display = isScheduled ? "" : "none";
659
+ if (repeatCustom) repeatCustom.style.display = val === "custom" ? "" : "none";
660
+ if (cronPreview) cronPreview.style.display = isScheduled ? "" : "none";
661
+ if (isScheduled) {
662
+ var cron = buildWizardCron();
663
+ var humanEl = document.getElementById("ralph-cron-human");
664
+ var cronEl = document.getElementById("ralph-cron-expr");
665
+ if (humanEl) humanEl.textContent = cronToHumanText(cron);
666
+ if (cronEl) cronEl.textContent = cron || "";
667
+ }
668
+ }
669
+
670
+ // ========================================================
671
+ // Crafting / Approval bars (exported)
672
+ // ========================================================
673
+
674
+ export function showRalphCraftingBar(show) {
675
+ var stickyEl = document.getElementById("ralph-sticky");
676
+ if (!stickyEl) return;
677
+ if (!show) {
678
+ stickyEl.classList.add("hidden");
679
+ stickyEl.innerHTML = "";
680
+ return;
681
+ }
682
+ stickyEl.innerHTML =
683
+ '<div class="ralph-sticky-inner">' +
684
+ '<div class="ralph-sticky-header">' +
685
+ '<span class="ralph-sticky-icon">' + iconHtml("repeat") + '</span>' +
686
+ '<span class="ralph-sticky-label">Ralph</span>' +
687
+ '<span class="ralph-sticky-status">' + iconHtml("loader", "icon-spin") + ' Preparing\u2026</span>' +
688
+ '<button class="ralph-sticky-cancel" title="Cancel">' + iconHtml("x") + '</button>' +
689
+ '</div>' +
690
+ '</div>';
691
+ stickyEl.classList.remove("hidden");
692
+ refreshIcons();
693
+
694
+ var cancelBtn = stickyEl.querySelector(".ralph-sticky-cancel");
695
+ if (cancelBtn) {
696
+ cancelBtn.addEventListener("click", function(e) {
697
+ e.stopPropagation();
698
+ var ws = _ctx.getWs();
699
+ if (ws && ws.readyState === 1) {
700
+ ws.send(JSON.stringify({ type: "ralph_cancel_crafting" }));
701
+ }
702
+ showRalphCraftingBar(false);
703
+ showRalphApprovalBar(false);
704
+ });
705
+ }
706
+ }
707
+
708
+ export function showRalphApprovalBar(show) {
709
+ var stickyEl = document.getElementById("ralph-sticky");
710
+ if (!stickyEl) return;
711
+ if (!show) {
712
+ // Only clear if we're in approval mode (don't clobber crafting)
713
+ if (ralphPhase !== "crafting") {
714
+ stickyEl.classList.add("hidden");
715
+ stickyEl.innerHTML = "";
716
+ }
717
+ return;
718
+ }
719
+
720
+ var basePath = _ctx.basePath;
721
+ stickyEl.innerHTML =
722
+ '<div class="ralph-sticky-inner">' +
723
+ '<div class="ralph-sticky-header" id="ralph-sticky-header">' +
724
+ '<span class="ralph-sticky-icon">' + iconHtml("repeat") + '</span>' +
725
+ '<span class="ralph-sticky-label">Ralph</span>' +
726
+ '<span class="ralph-sticky-status" id="ralph-sticky-status">Ready</span>' +
727
+ '<button class="ralph-sticky-action ralph-sticky-preview" title="Preview files">' + iconHtml("eye") + '</button>' +
728
+ '<button class="ralph-sticky-action ralph-sticky-start" title="' + (wizardData.cron ? 'Schedule' : 'Start loop') + '">' + iconHtml(wizardData.cron ? "calendar-clock" : "play") + '</button>' +
729
+ '<button class="ralph-sticky-action ralph-sticky-dismiss" title="Cancel and discard">' + iconHtml("x") + '</button>' +
730
+ '</div>' +
731
+ '</div>';
732
+ stickyEl.classList.remove("hidden");
733
+ refreshIcons();
734
+
735
+ stickyEl.querySelector(".ralph-sticky-preview").addEventListener("click", function(e) {
736
+ e.stopPropagation();
737
+ var ws = _ctx.getWs();
738
+ if (ws && ws.readyState === 1) {
739
+ ws.send(JSON.stringify({ type: "ralph_preview_files" }));
740
+ }
741
+ });
742
+
743
+ stickyEl.querySelector(".ralph-sticky-start").addEventListener("click", function(e) {
744
+ e.stopPropagation();
745
+ // Check for uncommitted changes before starting
746
+ fetch(basePath + "api/git-dirty")
747
+ .then(function (res) { return res.json(); })
748
+ .then(function (data) {
749
+ if (data.dirty) {
750
+ _ctx.showConfirm("You have uncommitted changes. Ralph Loop uses git diff to track progress \u2014 uncommitted files may cause unexpected results.\n\nStart anyway?", function () {
751
+ var ws = _ctx.getWs();
752
+ if (ws && ws.readyState === 1) {
753
+ ws.send(JSON.stringify({ type: "loop_start" }));
754
+ }
755
+ stickyEl.classList.add("hidden");
756
+ stickyEl.innerHTML = "";
757
+ });
758
+ } else {
759
+ var ws = _ctx.getWs();
760
+ if (ws && ws.readyState === 1) {
761
+ ws.send(JSON.stringify({ type: "loop_start" }));
762
+ }
763
+ stickyEl.classList.add("hidden");
764
+ stickyEl.innerHTML = "";
765
+ }
766
+ })
767
+ .catch(function () {
768
+ // If check fails, just start
769
+ var ws = _ctx.getWs();
770
+ if (ws && ws.readyState === 1) {
771
+ ws.send(JSON.stringify({ type: "loop_start" }));
772
+ }
773
+ stickyEl.classList.add("hidden");
774
+ stickyEl.innerHTML = "";
775
+ });
776
+ });
777
+
778
+ stickyEl.querySelector(".ralph-sticky-dismiss").addEventListener("click", function(e) {
779
+ e.stopPropagation();
780
+ _ctx.showConfirm("Discard this Ralph Loop setup?", function() {
781
+ var ws = _ctx.getWs();
782
+ if (ws && ws.readyState === 1) {
783
+ ws.send(JSON.stringify({ type: "ralph_wizard_cancel" }));
784
+ }
785
+ stickyEl.classList.add("hidden");
786
+ stickyEl.classList.remove("ralph-ready");
787
+ stickyEl.innerHTML = "";
788
+ });
789
+ });
790
+
791
+ updateRalphApprovalStatus();
792
+ }
793
+
794
+ export function updateRalphApprovalStatus() {
795
+ var stickyEl = document.getElementById("ralph-sticky");
796
+ var statusEl = document.getElementById("ralph-sticky-status");
797
+ var startBtn = document.querySelector(".ralph-sticky-start");
798
+ if (!statusEl) return;
799
+
800
+ if (ralphFilesReady.bothReady) {
801
+ statusEl.textContent = "Ready";
802
+ if (startBtn) startBtn.disabled = false;
803
+ if (stickyEl) stickyEl.classList.add("ralph-ready");
804
+ } else if (ralphFilesReady.promptReady || ralphFilesReady.judgeReady) {
805
+ statusEl.textContent = "Partial\u2026";
806
+ if (startBtn) startBtn.disabled = true;
807
+ if (stickyEl) stickyEl.classList.remove("ralph-ready");
808
+ } else {
809
+ statusEl.textContent = "Waiting\u2026";
810
+ if (startBtn) startBtn.disabled = true;
811
+ if (stickyEl) stickyEl.classList.remove("ralph-ready");
812
+ }
813
+ }
814
+
815
+ // ========================================================
816
+ // Preview modal (exported: openRalphPreviewModal)
817
+ // ========================================================
818
+
819
+ export function openRalphPreviewModal() {
820
+ var modal = document.getElementById("ralph-preview-modal");
821
+ if (!modal) return;
822
+ modal.classList.remove("hidden");
823
+
824
+ // Set name from wizard data
825
+ var nameEl = document.getElementById("ralph-preview-name");
826
+ if (nameEl) {
827
+ var name = (wizardData && wizardData.name) || "Ralph Loop";
828
+ nameEl.textContent = name;
829
+ }
830
+
831
+ // Update run button label based on cron
832
+ var runBtn = document.getElementById("ralph-preview-run");
833
+ if (runBtn) {
834
+ var hasCron = wizardData && wizardData.cron;
835
+ runBtn.innerHTML = iconHtml(hasCron ? "calendar-clock" : "play") + " " + (hasCron ? "Schedule" : "Run now");
836
+ runBtn.disabled = !(ralphFilesReady && ralphFilesReady.bothReady);
837
+ }
838
+
839
+ showRalphPreviewTab("prompt");
840
+ refreshIcons();
841
+ }
842
+
843
+ function closeRalphPreviewModal() {
844
+ var modal = document.getElementById("ralph-preview-modal");
845
+ if (modal) modal.classList.add("hidden");
846
+ }
847
+
848
+ function showRalphPreviewTab(tab) {
849
+ var tabs = document.querySelectorAll("#ralph-preview-modal .ralph-tab");
850
+ for (var i = 0; i < tabs.length; i++) {
851
+ if (tabs[i].getAttribute("data-tab") === tab) {
852
+ tabs[i].classList.add("active");
853
+ } else {
854
+ tabs[i].classList.remove("active");
855
+ }
856
+ }
857
+ var body = document.getElementById("ralph-preview-body");
858
+ if (!body) return;
859
+ var content = tab === "prompt" ? ralphPreviewContent.prompt : ralphPreviewContent.judge;
860
+ if (typeof marked !== "undefined" && marked.parse) {
861
+ body.innerHTML = '<div class="md-content">' + DOMPurify.sanitize(marked.parse(content)) + '</div>';
862
+ } else {
863
+ body.textContent = content;
864
+ }
865
+ }