micode 0.8.4 → 0.8.5

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.
Files changed (63) hide show
  1. package/dist/index.js +21020 -0
  2. package/package.json +6 -5
  3. package/src/agents/artifact-searcher.ts +0 -1
  4. package/src/agents/bootstrapper.ts +164 -0
  5. package/src/agents/brainstormer.ts +140 -33
  6. package/src/agents/codebase-analyzer.ts +0 -1
  7. package/src/agents/codebase-locator.ts +0 -1
  8. package/src/agents/commander.ts +99 -10
  9. package/src/agents/executor.ts +18 -1
  10. package/src/agents/implementer.ts +83 -6
  11. package/src/agents/index.ts +29 -19
  12. package/src/agents/ledger-creator.ts +0 -1
  13. package/src/agents/octto.ts +132 -0
  14. package/src/agents/pattern-finder.ts +0 -1
  15. package/src/agents/planner.ts +139 -49
  16. package/src/agents/probe.ts +152 -0
  17. package/src/agents/project-initializer.ts +0 -1
  18. package/src/agents/reviewer.ts +75 -5
  19. package/src/config-loader.test.ts +226 -0
  20. package/src/config-loader.ts +132 -6
  21. package/src/hooks/artifact-auto-index.ts +2 -1
  22. package/src/hooks/auto-compact.ts +14 -21
  23. package/src/hooks/context-injector.ts +6 -13
  24. package/src/hooks/context-window-monitor.ts +8 -13
  25. package/src/hooks/ledger-loader.ts +4 -6
  26. package/src/hooks/token-aware-truncation.ts +11 -17
  27. package/src/index.ts +54 -22
  28. package/src/indexing/milestone-artifact-classifier.ts +26 -0
  29. package/src/indexing/milestone-artifact-ingest.ts +42 -0
  30. package/src/octto/constants.ts +20 -0
  31. package/src/octto/session/browser.ts +32 -0
  32. package/src/octto/session/index.ts +25 -0
  33. package/src/octto/session/server.ts +89 -0
  34. package/src/octto/session/sessions.ts +383 -0
  35. package/src/octto/session/types.ts +305 -0
  36. package/src/octto/session/utils.ts +25 -0
  37. package/src/octto/session/waiter.ts +139 -0
  38. package/src/octto/state/index.ts +5 -0
  39. package/src/octto/state/persistence.ts +65 -0
  40. package/src/octto/state/store.ts +161 -0
  41. package/src/octto/state/types.ts +51 -0
  42. package/src/octto/types.ts +376 -0
  43. package/src/octto/ui/bundle.ts +1650 -0
  44. package/src/octto/ui/index.ts +2 -0
  45. package/src/tools/artifact-index/index.ts +152 -3
  46. package/src/tools/artifact-index/schema.sql +21 -0
  47. package/src/tools/milestone-artifact-search.ts +48 -0
  48. package/src/tools/octto/brainstorm.ts +332 -0
  49. package/src/tools/octto/extractor.ts +95 -0
  50. package/src/tools/octto/factory.ts +89 -0
  51. package/src/tools/octto/formatters.ts +63 -0
  52. package/src/tools/octto/index.ts +27 -0
  53. package/src/tools/octto/processor.ts +165 -0
  54. package/src/tools/octto/questions.ts +508 -0
  55. package/src/tools/octto/responses.ts +135 -0
  56. package/src/tools/octto/session.ts +114 -0
  57. package/src/tools/octto/types.ts +21 -0
  58. package/src/tools/octto/utils.ts +4 -0
  59. package/src/tools/pty/manager.ts +13 -7
  60. package/src/tools/spawn-agent.ts +1 -3
  61. package/src/utils/config.ts +123 -0
  62. package/src/utils/errors.ts +57 -0
  63. package/src/utils/logger.ts +50 -0
@@ -0,0 +1,1650 @@
1
+ // src/octto/ui/bundle.ts
2
+
3
+ /**
4
+ * Returns the bundled HTML for the octto UI.
5
+ * Uses nof1 design system - IBM Plex Mono, terminal aesthetic.
6
+ */
7
+ export function getHtmlBundle(): string {
8
+ return `<!DOCTYPE html>
9
+ <html lang="en">
10
+ <head>
11
+ <meta charset="UTF-8">
12
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
13
+ <title>Octto</title>
14
+ <link rel="preconnect" href="https://fonts.googleapis.com">
15
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
16
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
17
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
18
+ <style>
19
+ :root {
20
+ --background: #ffffff;
21
+ --surface: #ffffff;
22
+ --surface-elevated: #f8f9fa;
23
+ --surface-hover: #f1f3f4;
24
+ --foreground: #000000;
25
+ --foreground-muted: #333333;
26
+ --foreground-subtle: #666666;
27
+ --border: #000000;
28
+ --border-subtle: #cccccc;
29
+ --accent-success: #00aa00;
30
+ --accent-error: #ff0000;
31
+ }
32
+
33
+ *, *:before, *:after {
34
+ box-sizing: border-box;
35
+ margin: 0;
36
+ padding: 0;
37
+ }
38
+
39
+ html, body {
40
+ height: 100%;
41
+ background: var(--background);
42
+ color: var(--foreground);
43
+ font-family: 'IBM Plex Mono', monospace;
44
+ font-size: 14px;
45
+ line-height: 1.5;
46
+ letter-spacing: -0.02em;
47
+ }
48
+
49
+ body {
50
+ position: relative;
51
+ }
52
+
53
+ body::before {
54
+ content: "";
55
+ position: fixed;
56
+ top: 0;
57
+ left: 0;
58
+ width: 100%;
59
+ height: 100%;
60
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.6' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='0.15'/%3E%3C/svg%3E");
61
+ background-size: 180px 180px;
62
+ pointer-events: none;
63
+ z-index: 1;
64
+ }
65
+
66
+ #root {
67
+ position: relative;
68
+ z-index: 2;
69
+ max-width: 640px;
70
+ margin: 0 auto;
71
+ padding: 2rem 1.5rem;
72
+ min-height: 100vh;
73
+ }
74
+
75
+ h1, h2, h3 {
76
+ font-weight: 700;
77
+ letter-spacing: 0.05em;
78
+ text-transform: uppercase;
79
+ }
80
+
81
+ .header {
82
+ text-align: center;
83
+ padding: 3rem 0;
84
+ }
85
+
86
+ .header h1 {
87
+ font-size: 1.5rem;
88
+ margin-bottom: 0.5rem;
89
+ }
90
+
91
+ .header p {
92
+ color: var(--foreground-subtle);
93
+ font-size: 0.875rem;
94
+ }
95
+
96
+ .spinner {
97
+ width: 24px;
98
+ height: 24px;
99
+ border: 2px solid var(--border-subtle);
100
+ border-top-color: var(--foreground);
101
+ border-radius: 50%;
102
+ animation: spin 1s linear infinite;
103
+ margin: 1.5rem auto 0;
104
+ }
105
+
106
+ @keyframes spin {
107
+ to { transform: rotate(360deg); }
108
+ }
109
+
110
+ .card {
111
+ background: var(--surface);
112
+ border: 1px solid var(--border);
113
+ padding: 1.5rem;
114
+ margin-bottom: 1rem;
115
+ }
116
+
117
+ .card-answered {
118
+ background: var(--surface-elevated);
119
+ border-color: var(--border-subtle);
120
+ opacity: 0.7;
121
+ padding: 1rem;
122
+ cursor: pointer;
123
+ transition: opacity 0.15s;
124
+ }
125
+
126
+ .card-answered:hover {
127
+ opacity: 0.85;
128
+ }
129
+
130
+ .card-answered.expanded {
131
+ opacity: 1;
132
+ cursor: default;
133
+ }
134
+
135
+ .card-answered .check {
136
+ color: var(--accent-success);
137
+ margin-right: 0.5rem;
138
+ }
139
+
140
+ .card-answered-header {
141
+ display: flex;
142
+ align-items: center;
143
+ cursor: pointer;
144
+ }
145
+
146
+ .card-answered-header .toggle {
147
+ margin-left: auto;
148
+ color: var(--foreground-subtle);
149
+ font-size: 0.75rem;
150
+ }
151
+
152
+ .card-answered-body {
153
+ margin-top: 1rem;
154
+ padding-top: 1rem;
155
+ border-top: 1px solid var(--border-subtle);
156
+ }
157
+
158
+ .readonly-answer {
159
+ background: var(--surface-hover);
160
+ padding: 0.75rem;
161
+ margin-top: 0.5rem;
162
+ font-size: 0.875rem;
163
+ }
164
+
165
+ .readonly-answer-label {
166
+ font-size: 0.6875rem;
167
+ text-transform: uppercase;
168
+ letter-spacing: 0.05em;
169
+ color: var(--foreground-subtle);
170
+ margin-bottom: 0.25rem;
171
+ }
172
+
173
+ .readonly-option {
174
+ padding: 0.5rem 0.75rem;
175
+ border: 1px solid var(--border-subtle);
176
+ margin-bottom: 0.25rem;
177
+ opacity: 0.6;
178
+ }
179
+
180
+ .readonly-option.selected {
181
+ opacity: 1;
182
+ border-color: var(--accent-success);
183
+ background: rgba(0, 170, 0, 0.05);
184
+ }
185
+
186
+ .readonly-option .check-mark {
187
+ color: var(--accent-success);
188
+ margin-right: 0.5rem;
189
+ }
190
+
191
+ .question-text {
192
+ font-size: 1rem;
193
+ font-weight: 600;
194
+ margin-bottom: 1.25rem;
195
+ line-height: 1.4;
196
+ }
197
+
198
+ .context {
199
+ color: var(--foreground-muted);
200
+ font-size: 0.875rem;
201
+ margin-bottom: 1rem;
202
+ padding-left: 1rem;
203
+ border-left: 2px solid var(--border-subtle);
204
+ }
205
+
206
+ .options {
207
+ display: flex;
208
+ flex-direction: column;
209
+ gap: 0.5rem;
210
+ }
211
+
212
+ .option {
213
+ display: flex;
214
+ align-items: flex-start;
215
+ gap: 0.75rem;
216
+ padding: 0.75rem;
217
+ border: 1px solid var(--border-subtle);
218
+ cursor: pointer;
219
+ transition: none;
220
+ }
221
+
222
+ .option:hover {
223
+ background: var(--surface-hover);
224
+ border-color: var(--border);
225
+ }
226
+
227
+ .option.recommended {
228
+ border-color: var(--border);
229
+ background: var(--surface-elevated);
230
+ }
231
+
232
+ .option input {
233
+ margin-top: 0.125rem;
234
+ accent-color: var(--foreground);
235
+ }
236
+
237
+ .option-content {
238
+ flex: 1;
239
+ }
240
+
241
+ .option-label {
242
+ font-weight: 500;
243
+ }
244
+
245
+ .option-desc {
246
+ font-size: 0.8125rem;
247
+ color: var(--foreground-subtle);
248
+ margin-top: 0.25rem;
249
+ }
250
+
251
+ .option-tag {
252
+ font-size: 0.6875rem;
253
+ text-transform: uppercase;
254
+ letter-spacing: 0.05em;
255
+ color: var(--foreground-muted);
256
+ margin-left: 0.5rem;
257
+ }
258
+
259
+ .btn {
260
+ display: inline-block;
261
+ background: var(--surface);
262
+ border: 1px solid var(--border);
263
+ color: var(--foreground);
264
+ font-family: 'IBM Plex Mono', monospace;
265
+ font-weight: 500;
266
+ font-size: 0.75rem;
267
+ text-transform: uppercase;
268
+ letter-spacing: 0.05em;
269
+ padding: 0.5rem 1rem;
270
+ cursor: pointer;
271
+ transition: none;
272
+ }
273
+
274
+ .btn:hover {
275
+ background: var(--surface-hover);
276
+ }
277
+
278
+ .btn:active {
279
+ background: var(--foreground);
280
+ color: var(--background);
281
+ }
282
+
283
+ .btn-primary {
284
+ background: var(--foreground);
285
+ color: var(--background);
286
+ }
287
+
288
+ .btn-primary:hover {
289
+ opacity: 0.9;
290
+ }
291
+
292
+ .btn-success {
293
+ border-color: var(--accent-success);
294
+ color: var(--accent-success);
295
+ }
296
+
297
+ .btn-success:hover {
298
+ background: var(--accent-success);
299
+ color: var(--background);
300
+ }
301
+
302
+ .btn-danger {
303
+ border-color: var(--accent-error);
304
+ color: var(--accent-error);
305
+ }
306
+
307
+ .btn-danger:hover {
308
+ background: var(--accent-error);
309
+ color: var(--background);
310
+ }
311
+
312
+ .btn-group {
313
+ display: flex;
314
+ gap: 0.5rem;
315
+ margin-top: 1.25rem;
316
+ }
317
+
318
+ .input, .textarea {
319
+ width: 100%;
320
+ padding: 0.75rem;
321
+ background: var(--surface);
322
+ border: 1px solid var(--border);
323
+ color: var(--foreground);
324
+ font-family: 'IBM Plex Mono', monospace;
325
+ font-size: 0.875rem;
326
+ }
327
+
328
+ .input:focus, .textarea:focus {
329
+ outline: none;
330
+ border-color: var(--foreground);
331
+ }
332
+
333
+ .textarea {
334
+ resize: vertical;
335
+ min-height: 100px;
336
+ }
337
+
338
+ .slider-container {
339
+ display: flex;
340
+ align-items: center;
341
+ gap: 1rem;
342
+ }
343
+
344
+ .slider-container input[type="range"] {
345
+ flex: 1;
346
+ height: 2px;
347
+ background: var(--border-subtle);
348
+ appearance: none;
349
+ -webkit-appearance: none;
350
+ }
351
+
352
+ .slider-container input[type="range"]::-webkit-slider-thumb {
353
+ appearance: none;
354
+ -webkit-appearance: none;
355
+ width: 16px;
356
+ height: 16px;
357
+ background: var(--foreground);
358
+ cursor: pointer;
359
+ }
360
+
361
+ .slider-value {
362
+ font-weight: 600;
363
+ min-width: 3rem;
364
+ text-align: center;
365
+ font-variant-numeric: tabular-nums;
366
+ }
367
+
368
+ .slider-labels {
369
+ color: var(--foreground-subtle);
370
+ font-size: 0.75rem;
371
+ }
372
+
373
+ .thumbs-container {
374
+ display: flex;
375
+ gap: 1rem;
376
+ }
377
+
378
+ .thumb-btn {
379
+ font-size: 2rem;
380
+ padding: 1rem 1.5rem;
381
+ border: 1px solid var(--border-subtle);
382
+ background: var(--surface);
383
+ cursor: pointer;
384
+ }
385
+
386
+ .thumb-btn:hover {
387
+ border-color: var(--border);
388
+ background: var(--surface-hover);
389
+ }
390
+
391
+ .queue-indicator {
392
+ text-align: center;
393
+ color: var(--foreground-subtle);
394
+ font-size: 0.75rem;
395
+ text-transform: uppercase;
396
+ letter-spacing: 0.05em;
397
+ margin-top: 1rem;
398
+ }
399
+
400
+ .branch-subtitle {
401
+ font-size: 0.75rem;
402
+ color: var(--foreground-subtle);
403
+ margin-top: 0.25rem;
404
+ margin-bottom: 0.75rem;
405
+ }
406
+
407
+ .thinking {
408
+ text-align: center;
409
+ padding: 2rem;
410
+ margin-top: 2rem;
411
+ margin-bottom: 2rem;
412
+ border: 1px dashed var(--border-subtle);
413
+ }
414
+
415
+ .thinking-text {
416
+ color: var(--foreground-subtle);
417
+ font-size: 0.75rem;
418
+ text-transform: uppercase;
419
+ letter-spacing: 0.05em;
420
+ margin-bottom: 1rem;
421
+ }
422
+
423
+ .thinking .spinner {
424
+ margin: 0 auto;
425
+ }
426
+
427
+ .review-content {
428
+ background: var(--surface-elevated);
429
+ border: 1px solid var(--border-subtle);
430
+ padding: 1rem;
431
+ margin-bottom: 1rem;
432
+ font-size: 0.875rem;
433
+ line-height: 1.6;
434
+ max-height: 400px;
435
+ overflow-y: auto;
436
+ }
437
+
438
+ .review-content h1, .review-content h2, .review-content h3,
439
+ .review-content h4, .review-content h5, .review-content h6 {
440
+ font-weight: 600;
441
+ margin: 1rem 0 0.5rem 0;
442
+ }
443
+
444
+ .review-content h1 { font-size: 1.25rem; }
445
+ .review-content h2 { font-size: 1.125rem; }
446
+ .review-content h3 { font-size: 1rem; }
447
+
448
+ .review-content p {
449
+ margin: 0.5rem 0;
450
+ }
451
+
452
+ .review-content ul, .review-content ol {
453
+ margin: 0.5rem 0;
454
+ padding-left: 1.5rem;
455
+ }
456
+
457
+ .review-content li {
458
+ margin: 0.25rem 0;
459
+ }
460
+
461
+ .review-content code {
462
+ background: var(--surface-hover);
463
+ padding: 0.125rem 0.25rem;
464
+ font-size: 0.8125rem;
465
+ }
466
+
467
+ .review-content pre {
468
+ background: var(--surface-hover);
469
+ padding: 0.75rem;
470
+ overflow-x: auto;
471
+ margin: 0.5rem 0;
472
+ }
473
+
474
+ .review-content pre code {
475
+ background: none;
476
+ padding: 0;
477
+ }
478
+
479
+ .review-content blockquote {
480
+ border-left: 2px solid var(--border);
481
+ padding-left: 1rem;
482
+ margin: 0.5rem 0;
483
+ color: var(--foreground-muted);
484
+ }
485
+
486
+ .feedback-input {
487
+ margin-top: 1rem;
488
+ }
489
+
490
+ .feedback-input label {
491
+ display: block;
492
+ font-size: 0.75rem;
493
+ text-transform: uppercase;
494
+ letter-spacing: 0.05em;
495
+ color: var(--foreground-subtle);
496
+ margin-bottom: 0.5rem;
497
+ }
498
+
499
+ .plan-section {
500
+ margin-bottom: 1.5rem;
501
+ }
502
+
503
+ .plan-section-title {
504
+ font-size: 1rem;
505
+ font-weight: 600;
506
+ margin-bottom: 0.5rem;
507
+ padding-bottom: 0.25rem;
508
+ border-bottom: 1px solid var(--border-subtle);
509
+ }
510
+
511
+ .session-ended {
512
+ text-align: center;
513
+ padding: 4rem 0;
514
+ }
515
+
516
+ .session-ended h1 {
517
+ margin-bottom: 0.5rem;
518
+ }
519
+
520
+ .session-ended p {
521
+ color: var(--foreground-subtle);
522
+ }
523
+
524
+ /* Show Options */
525
+ .options-with-pros {
526
+ display: flex;
527
+ flex-direction: column;
528
+ gap: 1rem;
529
+ }
530
+
531
+ .option-card {
532
+ border: 1px solid var(--border-subtle);
533
+ padding: 1rem;
534
+ }
535
+
536
+ .option-card.recommended {
537
+ border-color: var(--border);
538
+ background: var(--surface-elevated);
539
+ }
540
+
541
+ .option-header {
542
+ display: flex;
543
+ align-items: center;
544
+ gap: 0.5rem;
545
+ margin-bottom: 0.5rem;
546
+ }
547
+
548
+ .pros, .cons {
549
+ font-size: 0.8125rem;
550
+ margin-top: 0.5rem;
551
+ }
552
+
553
+ .pros { color: var(--accent-success); }
554
+ .cons { color: var(--accent-error); }
555
+
556
+ .pros ul, .cons ul {
557
+ margin: 0.25rem 0 0 1rem;
558
+ }
559
+
560
+ /* Show Diff */
561
+ .diff-filepath {
562
+ font-size: 0.75rem;
563
+ color: var(--foreground-subtle);
564
+ margin-bottom: 0.5rem;
565
+ font-family: monospace;
566
+ }
567
+
568
+ .diff-container {
569
+ display: grid;
570
+ grid-template-columns: 1fr 1fr;
571
+ gap: 0.5rem;
572
+ margin-bottom: 1rem;
573
+ }
574
+
575
+ .diff-side {
576
+ border: 1px solid var(--border-subtle);
577
+ overflow: auto;
578
+ max-height: 300px;
579
+ }
580
+
581
+ .diff-label {
582
+ font-size: 0.6875rem;
583
+ text-transform: uppercase;
584
+ padding: 0.25rem 0.5rem;
585
+ background: var(--surface-elevated);
586
+ border-bottom: 1px solid var(--border-subtle);
587
+ }
588
+
589
+ .diff-before .diff-label { color: var(--accent-error); }
590
+ .diff-after .diff-label { color: var(--accent-success); }
591
+
592
+ .diff-side pre {
593
+ margin: 0;
594
+ padding: 0.5rem;
595
+ font-size: 0.75rem;
596
+ white-space: pre-wrap;
597
+ }
598
+
599
+ /* Rank */
600
+ .rank-list {
601
+ display: flex;
602
+ flex-direction: column;
603
+ gap: 0.25rem;
604
+ }
605
+
606
+ .rank-item {
607
+ display: flex;
608
+ align-items: center;
609
+ gap: 0.75rem;
610
+ padding: 0.5rem 0.75rem;
611
+ border: 1px solid var(--border-subtle);
612
+ background: var(--surface);
613
+ cursor: grab;
614
+ }
615
+
616
+ .rank-item:active, .rank-item.dragging {
617
+ cursor: grabbing;
618
+ opacity: 0.5;
619
+ }
620
+
621
+ .rank-handle {
622
+ color: var(--foreground-subtle);
623
+ }
624
+
625
+ .rank-num {
626
+ font-weight: 600;
627
+ min-width: 1.5rem;
628
+ }
629
+
630
+ /* Rate */
631
+ .rate-list {
632
+ display: flex;
633
+ flex-direction: column;
634
+ gap: 0.75rem;
635
+ }
636
+
637
+ .rate-item {
638
+ display: flex;
639
+ justify-content: space-between;
640
+ align-items: center;
641
+ }
642
+
643
+ .rate-stars {
644
+ display: flex;
645
+ gap: 0.25rem;
646
+ }
647
+
648
+ .rate-star {
649
+ width: 2rem;
650
+ height: 2rem;
651
+ border: 1px solid var(--border-subtle);
652
+ background: var(--surface);
653
+ cursor: pointer;
654
+ font-size: 0.75rem;
655
+ }
656
+
657
+ .rate-star.selected {
658
+ background: var(--foreground);
659
+ color: var(--background);
660
+ border-color: var(--foreground);
661
+ }
662
+
663
+ /* Code Input */
664
+ .code-input {
665
+ font-family: 'IBM Plex Mono', monospace;
666
+ font-size: 0.8125rem;
667
+ }
668
+
669
+ .code-input-label {
670
+ font-size: 0.6875rem;
671
+ text-transform: uppercase;
672
+ color: var(--foreground-subtle);
673
+ margin-bottom: 0.25rem;
674
+ }
675
+
676
+ /* File Upload */
677
+ .file-upload {
678
+ margin-bottom: 1rem;
679
+ }
680
+
681
+ .file-upload input[type="file"] {
682
+ width: 100%;
683
+ padding: 0.5rem;
684
+ border: 1px dashed var(--border-subtle);
685
+ }
686
+
687
+ .image-preview {
688
+ display: flex;
689
+ flex-wrap: wrap;
690
+ margin-top: 0.5rem;
691
+ }
692
+
693
+ /* Emoji React */
694
+ .emoji-grid {
695
+ display: flex;
696
+ flex-wrap: wrap;
697
+ gap: 0.5rem;
698
+ }
699
+
700
+ .emoji-btn {
701
+ font-size: 2rem;
702
+ padding: 0.75rem;
703
+ border: 1px solid var(--border-subtle);
704
+ background: var(--surface);
705
+ cursor: pointer;
706
+ }
707
+
708
+ .emoji-btn:hover {
709
+ background: var(--surface-hover);
710
+ border-color: var(--border);
711
+ }
712
+
713
+ /* Keyboard focus styles */
714
+ .thumb-btn:focus,
715
+ .emoji-btn:focus,
716
+ .rate-star:focus,
717
+ .btn:focus {
718
+ outline: 2px solid var(--foreground);
719
+ outline-offset: 2px;
720
+ }
721
+ </style>
722
+ </head>
723
+ <body>
724
+ <div id="root">
725
+ <div class="header">
726
+ <h1>Octto</h1>
727
+ <p>Connecting to session...</p>
728
+ <div class="spinner"></div>
729
+ </div>
730
+ </div>
731
+
732
+ <script>
733
+ const wsUrl = 'ws://' + window.location.host + '/ws';
734
+ let ws = null;
735
+ let questions = [];
736
+ let expandedAnswers = new Set();
737
+
738
+ function connect() {
739
+ ws = new WebSocket(wsUrl);
740
+
741
+ ws.onopen = () => {
742
+ ws.send(JSON.stringify({ type: 'connected' }));
743
+ render();
744
+ };
745
+
746
+ ws.onmessage = (event) => {
747
+ const msg = JSON.parse(event.data);
748
+ if (msg.type === 'question') {
749
+ questions.push(msg);
750
+ render();
751
+ } else if (msg.type === 'cancel') {
752
+ questions = questions.filter(q => q.id !== msg.id);
753
+ render();
754
+ } else if (msg.type === 'end') {
755
+ document.getElementById('root').innerHTML =
756
+ '<div class="session-ended"><h1>Session Ended</h1><p>You can close this window.</p></div>';
757
+ }
758
+ };
759
+
760
+ ws.onclose = () => {
761
+ setTimeout(connect, 2000);
762
+ };
763
+ }
764
+
765
+ function render() {
766
+ const root = document.getElementById('root');
767
+
768
+ if (questions.length === 0) {
769
+ root.innerHTML = '<div class="header"><h1>Octto</h1><p>Waiting for questions...</p></div>';
770
+ return;
771
+ }
772
+
773
+ const pending = questions.filter(q => !q.answered);
774
+ const answered = questions.filter(q => q.answered);
775
+
776
+ let html = '';
777
+
778
+ // Show remaining count at top
779
+ if (pending.length > 1) {
780
+ html += '<div class="queue-indicator" style="margin-top: 0; margin-bottom: 1rem;">' + (pending.length - 1) + ' more question(s) remaining</div>';
781
+ }
782
+
783
+ // Show current question
784
+ if (pending.length > 0) {
785
+ const q = pending[0];
786
+ html += renderQuestion(q);
787
+ } else if (answered.length > 0) {
788
+ // All answered, waiting for more questions
789
+ html += '<div class="thinking">';
790
+ html += '<div class="thinking-text">Thinking...</div>';
791
+ html += '<div class="spinner"></div>';
792
+ html += '</div>';
793
+ }
794
+
795
+ // Show answered questions at bottom (collapsed or expanded)
796
+ for (const q of answered) {
797
+ const isExpanded = expandedAnswers.has(q.id);
798
+ // Extract branch name from context
799
+ let branchName = '';
800
+ const ctx = q.config.context || '';
801
+ const branchMatch = ctx.match(/^\\[([^\\]]+)\\]/);
802
+ if (branchMatch) branchName = branchMatch[1];
803
+
804
+ html += '<div class="card card-answered' + (isExpanded ? ' expanded' : '') + '" data-qid="' + q.id + '">';
805
+ html += '<div class="card-answered-header" onclick="toggleAnswered(\\'' + q.id + '\\')">';
806
+ html += '<span class="check">[OK]</span>';
807
+ html += '<div style="flex: 1;">';
808
+ html += '<span>' + escapeHtml(q.config.question) + '</span>';
809
+ if (branchName) html += '<div class="branch-subtitle" style="margin-bottom: 0; margin-top: 0.125rem;">' + escapeHtml(branchName) + '</div>';
810
+ html += '</div>';
811
+ html += '<span class="toggle">' + (isExpanded ? '\\u25B2 collapse' : '\\u25BC view') + '</span>';
812
+ html += '</div>';
813
+ if (isExpanded) {
814
+ html += '<div class="card-answered-body">';
815
+ html += renderAnsweredQuestion(q);
816
+ html += '</div>';
817
+ }
818
+ html += '</div>';
819
+ }
820
+
821
+ root.innerHTML = html;
822
+ attachListeners();
823
+ }
824
+
825
+ function renderQuestion(q) {
826
+ const config = q.config;
827
+ let html = '<div class="card">';
828
+
829
+ // Extract branch from context if present: "[Branch Scope] rest of context"
830
+ let branchName = '';
831
+ let remainingContext = config.context || '';
832
+ const branchMatch = remainingContext.match(/^\\[([^\\]]+)\\]\\s*/);
833
+ if (branchMatch) {
834
+ branchName = branchMatch[1];
835
+ remainingContext = remainingContext.substring(branchMatch[0].length);
836
+ }
837
+
838
+ html += '<div class="question-text">' + escapeHtml(config.question) + '</div>';
839
+ if (branchName) {
840
+ html += '<div class="branch-subtitle">' + escapeHtml(branchName) + '</div>';
841
+ }
842
+ if (remainingContext) {
843
+ html += '<div class="context">' + escapeHtml(remainingContext) + '</div>';
844
+ }
845
+
846
+ switch (q.questionType) {
847
+ case 'pick_one':
848
+ html += renderPickOne(q);
849
+ break;
850
+ case 'pick_many':
851
+ html += renderPickMany(q);
852
+ break;
853
+ case 'confirm':
854
+ html += renderConfirm(q);
855
+ break;
856
+ case 'ask_text':
857
+ html += renderAskText(q);
858
+ break;
859
+ case 'thumbs':
860
+ html += renderThumbs(q);
861
+ break;
862
+ case 'slider':
863
+ html += renderSlider(q);
864
+ break;
865
+ case 'review_section':
866
+ html += renderReviewSection(q);
867
+ break;
868
+ case 'show_plan':
869
+ html += renderShowPlan(q);
870
+ break;
871
+ case 'show_options':
872
+ html += renderShowOptions(q);
873
+ break;
874
+ case 'show_diff':
875
+ html += renderShowDiff(q);
876
+ break;
877
+ case 'rank':
878
+ html += renderRank(q);
879
+ break;
880
+ case 'rate':
881
+ html += renderRate(q);
882
+ break;
883
+ case 'ask_code':
884
+ html += renderAskCode(q);
885
+ break;
886
+ case 'ask_image':
887
+ html += renderAskImage(q);
888
+ break;
889
+ case 'ask_file':
890
+ html += renderAskFile(q);
891
+ break;
892
+ case 'emoji_react':
893
+ html += renderEmojiReact(q);
894
+ break;
895
+ default:
896
+ html += '<p>Question type "' + q.questionType + '" not yet implemented.</p>';
897
+ html += '<div class="btn-group"><button onclick="submitAnswer(\\'' + q.id + '\\', {})" class="btn">Skip</button></div>';
898
+ }
899
+
900
+ html += '</div>';
901
+ return html;
902
+ }
903
+
904
+ function renderPickOne(q) {
905
+ const options = q.config.options || [];
906
+ let html = '<div class="options">';
907
+ for (const opt of options) {
908
+ const isRecommended = q.config.recommended === opt.id;
909
+ html += '<label class="option' + (isRecommended ? ' recommended' : '') + '">';
910
+ html += '<input type="radio" name="pick_' + q.id + '" value="' + opt.id + '">';
911
+ html += '<div class="option-content">';
912
+ html += '<div class="option-label">' + escapeHtml(opt.label);
913
+ if (isRecommended) html += '<span class="option-tag">(recommended)</span>';
914
+ html += '</div>';
915
+ if (opt.description) html += '<div class="option-desc">' + escapeHtml(opt.description) + '</div>';
916
+ html += '</div></label>';
917
+ }
918
+ html += '</div>';
919
+ html += '<div class="btn-group"><button onclick="submitPickOne(\\'' + q.id + '\\')" class="btn btn-primary">Submit</button></div>';
920
+ return html;
921
+ }
922
+
923
+ function renderPickMany(q) {
924
+ const options = q.config.options || [];
925
+ let html = '<div class="options">';
926
+ for (const opt of options) {
927
+ html += '<label class="option">';
928
+ html += '<input type="checkbox" name="pick_' + q.id + '" value="' + opt.id + '">';
929
+ html += '<div class="option-content">';
930
+ html += '<div class="option-label">' + escapeHtml(opt.label) + '</div>';
931
+ if (opt.description) html += '<div class="option-desc">' + escapeHtml(opt.description) + '</div>';
932
+ html += '</div></label>';
933
+ }
934
+ html += '</div>';
935
+ html += '<div class="btn-group"><button onclick="submitPickMany(\\'' + q.id + '\\')" class="btn btn-primary">Submit</button></div>';
936
+ return html;
937
+ }
938
+
939
+ function renderConfirm(q) {
940
+ const yesLabel = q.config.yesLabel || 'Yes';
941
+ const noLabel = q.config.noLabel || 'No';
942
+ let html = '<div class="btn-group">';
943
+ html += '<button onclick="submitAnswer(\\'' + q.id + '\\', {choice: \\'yes\\'})" class="btn btn-success">' + escapeHtml(yesLabel) + '</button>';
944
+ html += '<button onclick="submitAnswer(\\'' + q.id + '\\', {choice: \\'no\\'})" class="btn btn-danger">' + escapeHtml(noLabel) + '</button>';
945
+ if (q.config.allowCancel) {
946
+ html += '<button onclick="submitAnswer(\\'' + q.id + '\\', {choice: \\'cancel\\'})" class="btn">Cancel</button>';
947
+ }
948
+ html += '</div>';
949
+ return html;
950
+ }
951
+
952
+ function renderAskText(q) {
953
+ const multiline = q.config.multiline;
954
+ let html = '';
955
+ if (multiline) {
956
+ html += '<textarea id="text_' + q.id + '" class="textarea" rows="4" placeholder="' + escapeHtml(q.config.placeholder || '') + '"></textarea>';
957
+ } else {
958
+ html += '<input type="text" id="text_' + q.id + '" class="input" placeholder="' + escapeHtml(q.config.placeholder || '') + '">';
959
+ }
960
+ html += '<div class="btn-group"><button onclick="submitText(\\'' + q.id + '\\')" class="btn btn-primary">Submit</button></div>';
961
+ return html;
962
+ }
963
+
964
+ function renderThumbs(q) {
965
+ let html = '<div class="thumbs-container">';
966
+ html += '<button onclick="submitAnswer(\\'' + q.id + '\\', {choice: \\'up\\'})" class="thumb-btn">\\uD83D\\uDC4D</button>';
967
+ html += '<button onclick="submitAnswer(\\'' + q.id + '\\', {choice: \\'down\\'})" class="thumb-btn">\\uD83D\\uDC4E</button>';
968
+ html += '</div>';
969
+ return html;
970
+ }
971
+
972
+ function renderSlider(q) {
973
+ const min = q.config.min;
974
+ const max = q.config.max;
975
+ const step = q.config.step || 1;
976
+ const defaultVal = q.config.defaultValue || Math.floor((min + max) / 2);
977
+ const labels = q.config.labels || {};
978
+ const minLabel = labels.min || String(min);
979
+ const maxLabel = labels.max || String(max);
980
+ let html = '<div class="slider-container">';
981
+ html += '<span class="slider-labels">' + escapeHtml(minLabel) + '</span>';
982
+ html += '<input type="range" id="slider_' + q.id + '" min="' + min + '" max="' + max + '" step="' + step + '" value="' + defaultVal + '">';
983
+ html += '<span class="slider-labels">' + escapeHtml(maxLabel) + '</span>';
984
+ html += '<span id="slider_val_' + q.id + '" class="slider-value">' + defaultVal + '</span>';
985
+ html += '</div>';
986
+ html += '<div class="btn-group"><button onclick="submitSlider(\\'' + q.id + '\\')" class="btn btn-primary">Submit</button></div>';
987
+ return html;
988
+ }
989
+
990
+
991
+ function renderReviewSection(q) {
992
+ let html = '';
993
+ // Render markdown content
994
+ const markdownHtml = typeof marked !== 'undefined' ? marked.parse(q.config.content || '') : escapeHtml(q.config.content || '');
995
+ html += '<div class="review-content">' + markdownHtml + '</div>';
996
+ html += '<div class="feedback-input">';
997
+ html += '<label for="feedback_' + q.id + '">Feedback (optional)</label>';
998
+ html += '<textarea id="feedback_' + q.id + '" class="textarea" rows="3" placeholder="Any suggestions or changes..."></textarea>';
999
+ html += '</div>';
1000
+ html += '<div class="btn-group">';
1001
+ html += '<button onclick="submitReview(\\'' + q.id + '\\', \\'approve\\')" class="btn btn-success">Approve</button>';
1002
+ html += '<button onclick="submitReview(\\'' + q.id + '\\', \\'revise\\')" class="btn btn-danger">Needs Revision</button>';
1003
+ html += '</div>';
1004
+ return html;
1005
+ }
1006
+
1007
+ function renderShowPlan(q) {
1008
+ let html = '';
1009
+
1010
+ // Render sections if provided
1011
+ if (q.config.sections && q.config.sections.length > 0) {
1012
+ for (const section of q.config.sections) {
1013
+ html += '<div class="plan-section">';
1014
+ html += '<h3 class="plan-section-title">' + escapeHtml(section.title) + '</h3>';
1015
+ const sectionHtml = typeof marked !== 'undefined' ? marked.parse(section.content || '') : escapeHtml(section.content || '');
1016
+ html += '<div class="review-content">' + sectionHtml + '</div>';
1017
+ html += '</div>';
1018
+ }
1019
+ } else if (q.config.markdown) {
1020
+ // Fallback to raw markdown
1021
+ const markdownHtml = typeof marked !== 'undefined' ? marked.parse(q.config.markdown) : escapeHtml(q.config.markdown);
1022
+ html += '<div class="review-content">' + markdownHtml + '</div>';
1023
+ }
1024
+
1025
+ html += '<div class="feedback-input">';
1026
+ html += '<label for="feedback_' + q.id + '">Feedback (optional)</label>';
1027
+ html += '<textarea id="feedback_' + q.id + '" class="textarea" rows="3" placeholder="Any suggestions or changes..."></textarea>';
1028
+ html += '</div>';
1029
+ html += '<div class="btn-group">';
1030
+ html += '<button onclick="submitReview(\\'' + q.id + '\\', \\'approve\\')" class="btn btn-success">Approve Plan</button>';
1031
+ html += '<button onclick="submitReview(\\'' + q.id + '\\', \\'revise\\')" class="btn btn-danger">Needs Changes</button>';
1032
+ html += '</div>';
1033
+ return html;
1034
+ }
1035
+
1036
+ function renderShowOptions(q) {
1037
+ const options = q.config.options || [];
1038
+ let html = '<div class="options-with-pros">';
1039
+ for (const opt of options) {
1040
+ const isRecommended = q.config.recommended === opt.id;
1041
+ html += '<div class="option-card' + (isRecommended ? ' recommended' : '') + '" data-id="' + opt.id + '">';
1042
+ html += '<div class="option-header">';
1043
+ html += '<input type="radio" name="opt_' + q.id + '" value="' + opt.id + '">';
1044
+ html += '<span class="option-label">' + escapeHtml(opt.label);
1045
+ if (isRecommended) html += ' <span class="option-tag">(recommended)</span>';
1046
+ html += '</span></div>';
1047
+ if (opt.description) html += '<div class="option-desc">' + escapeHtml(opt.description) + '</div>';
1048
+ if (opt.pros && opt.pros.length > 0) {
1049
+ html += '<div class="pros"><strong>Pros:</strong><ul>';
1050
+ for (const pro of opt.pros) html += '<li>' + escapeHtml(pro) + '</li>';
1051
+ html += '</ul></div>';
1052
+ }
1053
+ if (opt.cons && opt.cons.length > 0) {
1054
+ html += '<div class="cons"><strong>Cons:</strong><ul>';
1055
+ for (const con of opt.cons) html += '<li>' + escapeHtml(con) + '</li>';
1056
+ html += '</ul></div>';
1057
+ }
1058
+ html += '</div>';
1059
+ }
1060
+ html += '</div>';
1061
+ if (q.config.allowFeedback) {
1062
+ html += '<div class="feedback-input"><label>Feedback (optional)</label>';
1063
+ html += '<textarea id="feedback_' + q.id + '" class="textarea" rows="2"></textarea></div>';
1064
+ }
1065
+ html += '<div class="btn-group"><button onclick="submitShowOptions(\\'' + q.id + '\\')" class="btn btn-primary">Select</button></div>';
1066
+ return html;
1067
+ }
1068
+
1069
+ function renderShowDiff(q) {
1070
+ let html = '';
1071
+ if (q.config.filePath) {
1072
+ html += '<div class="diff-filepath">' + escapeHtml(q.config.filePath) + '</div>';
1073
+ }
1074
+ html += '<div class="diff-container">';
1075
+ html += '<div class="diff-side diff-before"><div class="diff-label">Before</div><pre><code>' + escapeHtml(q.config.before || '') + '</code></pre></div>';
1076
+ html += '<div class="diff-side diff-after"><div class="diff-label">After</div><pre><code>' + escapeHtml(q.config.after || '') + '</code></pre></div>';
1077
+ html += '</div>';
1078
+ html += '<div class="feedback-input"><label>Comments (optional)</label>';
1079
+ html += '<textarea id="feedback_' + q.id + '" class="textarea" rows="2"></textarea></div>';
1080
+ html += '<div class="btn-group">';
1081
+ html += '<button onclick="submitDiff(\\'' + q.id + '\\', \\'approve\\')" class="btn btn-success">Approve</button>';
1082
+ html += '<button onclick="submitDiff(\\'' + q.id + '\\', \\'reject\\')" class="btn btn-danger">Reject</button>';
1083
+ html += '<button onclick="submitDiff(\\'' + q.id + '\\', \\'edit\\')" class="btn">Edit</button>';
1084
+ html += '</div>';
1085
+ return html;
1086
+ }
1087
+
1088
+ function renderRank(q) {
1089
+ const options = q.config.options || [];
1090
+ let html = '<div class="rank-list" id="rank_' + q.id + '">';
1091
+ for (let i = 0; i < options.length; i++) {
1092
+ const opt = options[i];
1093
+ html += '<div class="rank-item" data-id="' + opt.id + '" draggable="true">';
1094
+ html += '<span class="rank-handle">\\u2630</span>';
1095
+ html += '<span class="rank-num">' + (i + 1) + '</span>';
1096
+ html += '<span class="rank-label">' + escapeHtml(opt.label) + '</span>';
1097
+ html += '</div>';
1098
+ }
1099
+ html += '</div>';
1100
+ html += '<div class="btn-group"><button onclick="submitRank(\\'' + q.id + '\\')" class="btn btn-primary">Submit Ranking</button></div>';
1101
+ return html;
1102
+ }
1103
+
1104
+ function renderRate(q) {
1105
+ const options = q.config.options || [];
1106
+ const min = q.config.min || 1;
1107
+ const max = q.config.max || 5;
1108
+ const labels = q.config.labels || {};
1109
+ let html = '<div class="rate-list">';
1110
+ for (const opt of options) {
1111
+ html += '<div class="rate-item">';
1112
+ html += '<div class="rate-label">' + escapeHtml(opt.label) + '</div>';
1113
+ html += '<div class="rate-stars" id="rate_' + q.id + '_' + opt.id + '">';
1114
+ for (let i = min; i <= max; i++) {
1115
+ html += '<button class="rate-star" data-value="' + i + '" onclick="setRating(\\'' + q.id + '\\', \\'' + opt.id + '\\', ' + i + ')">' + i + '</button>';
1116
+ }
1117
+
1118
+ html += '</div>';
1119
+
1120
+
1121
+ if (labels.min || labels.max) {
1122
+ html += '<div class="slider-labels">' + escapeHtml(labels.min || String(min)) + ' / ' + escapeHtml(labels.max || String(max)) + '</div>';
1123
+ }
1124
+ html += '</div>';
1125
+ }
1126
+ html += '</div>';
1127
+ html += '<div class="btn-group"><button onclick="submitRate(\\'' + q.id + '\\')" class="btn btn-primary">Submit Ratings</button></div>';
1128
+ return html;
1129
+ }
1130
+
1131
+ function renderAskCode(q) {
1132
+ let html = '';
1133
+ const lang = q.config.language || 'plaintext';
1134
+ html += '<div class="code-input-label">Language: ' + escapeHtml(lang) + '</div>';
1135
+ html += '<textarea id="code_' + q.id + '" class="textarea code-input" rows="10" placeholder="' + escapeHtml(q.config.placeholder || 'Enter code here...') + '"></textarea>';
1136
+ html += '<div class="btn-group"><button onclick="submitCode(\\'' + q.id + '\\')" class="btn btn-primary">Submit Code</button></div>';
1137
+ return html;
1138
+ }
1139
+
1140
+ function renderAskImage(q) {
1141
+ let html = '';
1142
+ const multiple = q.config.multiple ? 'multiple' : '';
1143
+ const accept = q.config.accept ? q.config.accept.join(',') : 'image/*';
1144
+ html += '<div class="file-upload">';
1145
+ html += '<input type="file" id="image_' + q.id + '" accept="' + accept + '" ' + multiple + ' onchange="previewImages(\\'' + q.id + '\\')">';
1146
+ html += '<div id="preview_' + q.id + '" class="image-preview"></div>';
1147
+ html += '</div>';
1148
+ html += '<div class="btn-group"><button onclick="submitImages(\\'' + q.id + '\\')" class="btn btn-primary">Upload</button></div>';
1149
+ return html;
1150
+ }
1151
+
1152
+
1153
+ function renderAskFile(q) {
1154
+ let html = '';
1155
+ const multiple = q.config.multiple ? 'multiple' : '';
1156
+ const accept = q.config.accept ? q.config.accept.join(',') : '';
1157
+ html += '<div class="file-upload">';
1158
+ html += '<input type="file" id="file_' + q.id + '" ' + (accept ? 'accept="' + accept + '"' : '') + ' ' + multiple + '>';
1159
+ html += '<div id="filelist_' + q.id + '" class="file-list"></div>';
1160
+ html += '</div>';
1161
+ html += '<div class="btn-group"><button onclick="submitFiles(\\'' + q.id + '\\')" class="btn btn-primary">Upload</button></div>';
1162
+ return html;
1163
+ }
1164
+
1165
+ function renderEmojiReact(q) {
1166
+ let html = '';
1167
+ const emojis = q.config.emojis || ['\\uD83D\\uDC4D', '\\uD83D\\uDC4E', '\\u2764\\uFE0F', '\\uD83C\\uDF89', '\\uD83D\\uDE15', '\\uD83D\\uDE80'];
1168
+ html += '<div class="emoji-grid">';
1169
+ for (const emoji of emojis) {
1170
+ html += '<button class="emoji-btn" onclick="submitAnswer(\\'' + q.id + '\\', {emoji: \\'' + emoji + '\\'})">' + emoji + '</button>';
1171
+ }
1172
+ html += '</div>';
1173
+ return html;
1174
+ }
1175
+
1176
+ function attachListeners() {
1177
+ document.querySelectorAll('input[type="range"]').forEach(slider => {
1178
+ const id = slider.id.replace('slider_', 'slider_val_');
1179
+ slider.oninput = () => {
1180
+ document.getElementById(id).textContent = slider.value;
1181
+ };
1182
+ });
1183
+ }
1184
+
1185
+ function submitAnswer(questionId, answer) {
1186
+ const q = questions.find(q => q.id === questionId);
1187
+ if (q) {
1188
+ q.answered = true;
1189
+ q.answer = answer; // Store answer for read-only view
1190
+ ws.send(JSON.stringify({ type: 'response', id: questionId, answer }));
1191
+ render();
1192
+ }
1193
+ }
1194
+
1195
+ function showError(questionId, message) {
1196
+ const existingError = document.getElementById('error_' + questionId);
1197
+ if (existingError) existingError.remove();
1198
+
1199
+ const card = document.querySelector('[data-qid="' + questionId + '"]') || document.querySelector('.card:not(.card-answered)');
1200
+ if (card) {
1201
+ const errorDiv = document.createElement('div');
1202
+ errorDiv.id = 'error_' + questionId;
1203
+ errorDiv.style.cssText = 'color: var(--accent-error); font-size: 0.875rem; margin-top: 0.5rem;';
1204
+ errorDiv.textContent = message;
1205
+ const btnGroup = card.querySelector('.btn-group');
1206
+ if (btnGroup) btnGroup.before(errorDiv);
1207
+ }
1208
+ }
1209
+
1210
+ function submitPickOne(questionId) {
1211
+ const selected = document.querySelector('input[name="pick_' + questionId + '"]:checked');
1212
+ if (!selected) {
1213
+ showError(questionId, 'Please select an option');
1214
+ return;
1215
+ }
1216
+ submitAnswer(questionId, { selected: selected.value });
1217
+ }
1218
+
1219
+ function submitPickMany(questionId) {
1220
+ const selected = Array.from(document.querySelectorAll('input[name="pick_' + questionId + '"]:checked')).map(el => el.value);
1221
+ submitAnswer(questionId, { selected });
1222
+ }
1223
+
1224
+ function submitText(questionId) {
1225
+ const input = document.getElementById('text_' + questionId);
1226
+ if (input) {
1227
+ submitAnswer(questionId, { text: input.value });
1228
+ }
1229
+ }
1230
+
1231
+ function submitSlider(questionId) {
1232
+ const slider = document.getElementById('slider_' + questionId);
1233
+ if (slider) {
1234
+ submitAnswer(questionId, { value: parseFloat(slider.value) });
1235
+ }
1236
+ }
1237
+
1238
+ function submitReview(questionId, decision) {
1239
+ const feedbackEl = document.getElementById('feedback_' + questionId);
1240
+ const feedback = feedbackEl ? feedbackEl.value : '';
1241
+ submitAnswer(questionId, { decision, feedback: feedback || undefined });
1242
+ }
1243
+
1244
+ function submitShowOptions(questionId) {
1245
+ const selected = document.querySelector('input[name="opt_' + questionId + '"]:checked');
1246
+ if (!selected) {
1247
+ showError(questionId, 'Please select an option');
1248
+ return;
1249
+ }
1250
+ const feedbackEl = document.getElementById('feedback_' + questionId);
1251
+ const feedback = feedbackEl ? feedbackEl.value : '';
1252
+ submitAnswer(questionId, { selected: selected.value, feedback: feedback || undefined });
1253
+ }
1254
+
1255
+ function submitDiff(questionId, decision) {
1256
+ const feedbackEl = document.getElementById('feedback_' + questionId);
1257
+ const feedback = feedbackEl ? feedbackEl.value : '';
1258
+ submitAnswer(questionId, { decision, feedback: feedback || undefined });
1259
+ }
1260
+
1261
+ function submitRank(questionId) {
1262
+ const container = document.getElementById('rank_' + questionId);
1263
+ const items = container.querySelectorAll('.rank-item');
1264
+ const ranking = Array.from(items).map((item, idx) => ({
1265
+ id: item.dataset.id,
1266
+ rank: idx + 1
1267
+ }));
1268
+ submitAnswer(questionId, { ranking });
1269
+ }
1270
+
1271
+ function submitRate(questionId) {
1272
+ const q = questions.find(q => q.id === questionId);
1273
+ if (!q) return;
1274
+ const ratings = {};
1275
+ for (const opt of (q.config.options || [])) {
1276
+ const container = document.getElementById('rate_' + questionId + '_' + opt.id);
1277
+ const selected = container.querySelector('.rate-star.selected');
1278
+ if (selected) {
1279
+ ratings[opt.id] = parseInt(selected.dataset.value);
1280
+ }
1281
+ }
1282
+ submitAnswer(questionId, { ratings });
1283
+ }
1284
+
1285
+ function setRating(questionId, optId, value) {
1286
+ const container = document.getElementById('rate_' + questionId + '_' + optId);
1287
+ container.querySelectorAll('.rate-star').forEach(btn => {
1288
+ btn.classList.toggle('selected', parseInt(btn.dataset.value) <= value);
1289
+ });
1290
+ }
1291
+
1292
+ function submitCode(questionId) {
1293
+ const textarea = document.getElementById('code_' + questionId);
1294
+ if (textarea) {
1295
+ submitAnswer(questionId, { code: textarea.value });
1296
+ }
1297
+ }
1298
+
1299
+ function submitImages(questionId) {
1300
+ const input = document.getElementById('image_' + questionId);
1301
+ if (input && input.files.length > 0) {
1302
+ // Convert to base64 for transport
1303
+ const promises = Array.from(input.files).map(file => {
1304
+ return new Promise((resolve) => {
1305
+ const reader = new FileReader();
1306
+ reader.onload = () => resolve({ name: file.name, type: file.type, data: reader.result });
1307
+ reader.readAsDataURL(file);
1308
+ });
1309
+ });
1310
+ Promise.all(promises).then(images => {
1311
+ submitAnswer(questionId, { images });
1312
+ });
1313
+ }
1314
+ }
1315
+
1316
+ function isAllowedFileType(file, allowed) {
1317
+ if (!allowed || allowed.length === 0) return true;
1318
+ const fileType = file.type || '';
1319
+ const fileName = file.name || '';
1320
+ return allowed.some(entry => {
1321
+ if (!entry) return false;
1322
+ if (entry.endsWith('/*')) {
1323
+ const prefix = entry.slice(0, -1);
1324
+ return fileType.startsWith(prefix);
1325
+ }
1326
+ if (entry.startsWith('.')) {
1327
+ return fileName.toLowerCase().endsWith(entry.toLowerCase());
1328
+ }
1329
+ return fileType === entry || fileName.toLowerCase().endsWith(entry.toLowerCase());
1330
+ });
1331
+ }
1332
+
1333
+ function previewImages(questionId) {
1334
+ const input = document.getElementById('image_' + questionId);
1335
+ const preview = document.getElementById('preview_' + questionId);
1336
+ preview.innerHTML = '';
1337
+ const q = questions.find(q => q.id === questionId);
1338
+ const allowed = q && q.config.accept ? q.config.accept : null;
1339
+ for (const file of input.files) {
1340
+ if (allowed && allowed.length > 0 && !isAllowedFileType(file, allowed)) {
1341
+ const warning = document.createElement('div');
1342
+ warning.textContent = 'Warning: ' + file.name + ' does not match allowed types.';
1343
+ warning.style.cssText = 'color: var(--accent-error); font-size: 0.75rem; margin: 0.25rem 0;';
1344
+ preview.appendChild(warning);
1345
+ }
1346
+ const img = document.createElement('img');
1347
+ img.src = URL.createObjectURL(file);
1348
+ img.style.maxWidth = '100px';
1349
+ img.style.maxHeight = '100px';
1350
+ img.style.margin = '4px';
1351
+ preview.appendChild(img);
1352
+ }
1353
+ }
1354
+
1355
+ function submitFiles(questionId) {
1356
+ const input = document.getElementById('file_' + questionId);
1357
+ if (input && input.files.length > 0) {
1358
+ const promises = Array.from(input.files).map(file => {
1359
+ return new Promise((resolve) => {
1360
+ const reader = new FileReader();
1361
+ reader.onload = () => resolve({ name: file.name, type: file.type, size: file.size, data: reader.result });
1362
+ reader.readAsDataURL(file);
1363
+ });
1364
+ });
1365
+ Promise.all(promises).then(files => {
1366
+ submitAnswer(questionId, { files });
1367
+ });
1368
+ }
1369
+ }
1370
+
1371
+ // Drag and drop for ranking
1372
+ document.addEventListener('dragstart', (e) => {
1373
+ if (e.target.classList.contains('rank-item')) {
1374
+ e.dataTransfer.setData('text/plain', e.target.dataset.id);
1375
+ e.target.classList.add('dragging');
1376
+ }
1377
+ });
1378
+ document.addEventListener('dragend', (e) => {
1379
+ if (e.target.classList.contains('rank-item')) {
1380
+ e.target.classList.remove('dragging');
1381
+ }
1382
+ });
1383
+ document.addEventListener('dragover', (e) => {
1384
+ e.preventDefault();
1385
+ const dragging = document.querySelector('.rank-item.dragging');
1386
+ const rankList = e.target.closest('.rank-list');
1387
+ if (dragging && rankList) {
1388
+ const siblings = [...rankList.querySelectorAll('.rank-item:not(.dragging)')];
1389
+ const nextSibling = siblings.find(sibling => {
1390
+ const rect = sibling.getBoundingClientRect();
1391
+ return e.clientY < rect.top + rect.height / 2;
1392
+ });
1393
+ rankList.insertBefore(dragging, nextSibling);
1394
+ // Update numbers
1395
+ rankList.querySelectorAll('.rank-item').forEach((item, idx) => {
1396
+ item.querySelector('.rank-num').textContent = idx + 1;
1397
+ });
1398
+ }
1399
+ });
1400
+
1401
+ function toggleAnswered(questionId) {
1402
+ if (expandedAnswers.has(questionId)) {
1403
+ expandedAnswers.delete(questionId);
1404
+ } else {
1405
+ expandedAnswers.add(questionId);
1406
+ }
1407
+ render();
1408
+ }
1409
+
1410
+ function renderAnsweredQuestion(q) {
1411
+ const config = q.config;
1412
+ const answer = q.answer || {};
1413
+ let html = '';
1414
+
1415
+ switch (q.questionType) {
1416
+ case 'pick_one':
1417
+ html += renderAnsweredPickOne(q, answer);
1418
+ break;
1419
+ case 'pick_many':
1420
+ html += renderAnsweredPickMany(q, answer);
1421
+ break;
1422
+ case 'confirm':
1423
+ html += renderAnsweredConfirm(q, answer);
1424
+ break;
1425
+ case 'ask_text':
1426
+ html += renderAnsweredText(q, answer);
1427
+ break;
1428
+ case 'thumbs':
1429
+ html += renderAnsweredThumbs(q, answer);
1430
+ break;
1431
+ case 'slider':
1432
+ html += renderAnsweredSlider(q, answer);
1433
+ break;
1434
+ case 'review_section':
1435
+ case 'show_plan':
1436
+ html += renderAnsweredReview(q, answer);
1437
+ break;
1438
+ case 'show_options':
1439
+ html += renderAnsweredShowOptions(q, answer);
1440
+ break;
1441
+ case 'show_diff':
1442
+ html += renderAnsweredDiff(q, answer);
1443
+ break;
1444
+ case 'rank':
1445
+ html += renderAnsweredRank(q, answer);
1446
+ break;
1447
+ case 'rate':
1448
+ html += renderAnsweredRate(q, answer);
1449
+ break;
1450
+ case 'ask_code':
1451
+ html += renderAnsweredCode(q, answer);
1452
+ break;
1453
+ case 'ask_image':
1454
+ case 'ask_file':
1455
+ html += renderAnsweredFile(q, answer);
1456
+ break;
1457
+ case 'emoji_react':
1458
+ html += renderAnsweredEmoji(q, answer);
1459
+ break;
1460
+ default:
1461
+ html += '<div class="readonly-answer"><pre>' + escapeHtml(JSON.stringify(answer, null, 2)) + '</pre></div>';
1462
+ }
1463
+
1464
+ return html;
1465
+ }
1466
+
1467
+ function renderAnsweredPickOne(q, answer) {
1468
+ const options = q.config.options || [];
1469
+ let html = '<div class="options">';
1470
+ for (const opt of options) {
1471
+ const isSelected = answer.selected === opt.id;
1472
+ html += '<div class="readonly-option' + (isSelected ? ' selected' : '') + '">';
1473
+ if (isSelected) html += '<span class="check-mark">\\u2713</span>';
1474
+ html += '<span>' + escapeHtml(opt.label) + '</span>';
1475
+ html += '</div>';
1476
+ }
1477
+ html += '</div>';
1478
+ return html;
1479
+ }
1480
+
1481
+ function renderAnsweredPickMany(q, answer) {
1482
+ const options = q.config.options || [];
1483
+ const selected = answer.selected || [];
1484
+ let html = '<div class="options">';
1485
+ for (const opt of options) {
1486
+ const isSelected = selected.includes(opt.id);
1487
+ html += '<div class="readonly-option' + (isSelected ? ' selected' : '') + '">';
1488
+ if (isSelected) html += '<span class="check-mark">\\u2713</span>';
1489
+ html += '<span>' + escapeHtml(opt.label) + '</span>';
1490
+ html += '</div>';
1491
+ }
1492
+ html += '</div>';
1493
+ return html;
1494
+ }
1495
+
1496
+ function renderAnsweredConfirm(q, answer) {
1497
+ const choice = answer.choice;
1498
+ const labels = { yes: q.config.yesLabel || 'Yes', no: q.config.noLabel || 'No', cancel: 'Cancel' };
1499
+ let html = '<div class="readonly-answer">';
1500
+ html += '<div class="readonly-answer-label">Answer</div>';
1501
+ html += '<strong>' + escapeHtml(labels[choice] || choice) + '</strong>';
1502
+ html += '</div>';
1503
+ return html;
1504
+ }
1505
+
1506
+ function renderAnsweredText(q, answer) {
1507
+ let html = '<div class="readonly-answer">';
1508
+ html += '<div class="readonly-answer-label">Response</div>';
1509
+ html += '<div>' + escapeHtml(answer.text || '') + '</div>';
1510
+ html += '</div>';
1511
+ return html;
1512
+ }
1513
+
1514
+ function renderAnsweredThumbs(q, answer) {
1515
+ const emoji = answer.choice === 'up' ? '\\uD83D\\uDC4D' : '\\uD83D\\uDC4E';
1516
+ let html = '<div class="readonly-answer">';
1517
+ html += '<span style="font-size: 2rem;">' + emoji + '</span>';
1518
+ html += '</div>';
1519
+ return html;
1520
+ }
1521
+
1522
+ function renderAnsweredSlider(q, answer) {
1523
+ const labels = q.config.labels || {};
1524
+ const minLabel = labels.min || String(q.config.min);
1525
+ const maxLabel = labels.max || String(q.config.max);
1526
+ let html = '<div class="readonly-answer">';
1527
+ html += '<div class="readonly-answer-label">Value</div>';
1528
+ html += '<strong style="font-size: 1.25rem;">' + answer.value + '</strong>';
1529
+ html += ' <span style="color: var(--foreground-subtle);">(range: ' + escapeHtml(minLabel) + ' - ' + escapeHtml(maxLabel) + ')</span>';
1530
+ html += '</div>';
1531
+ return html;
1532
+ }
1533
+
1534
+ function renderAnsweredReview(q, answer) {
1535
+ let html = '<div class="readonly-answer">';
1536
+ html += '<div class="readonly-answer-label">Decision</div>';
1537
+ html += '<strong>' + (answer.decision === 'approve' ? '\\u2713 Approved' : '\\u2717 Needs Revision') + '</strong>';
1538
+ if (answer.feedback) {
1539
+ html += '<div style="margin-top: 0.5rem;"><em>Feedback:</em> ' + escapeHtml(answer.feedback) + '</div>';
1540
+ }
1541
+ html += '</div>';
1542
+ return html;
1543
+ }
1544
+
1545
+ function renderAnsweredShowOptions(q, answer) {
1546
+ const options = q.config.options || [];
1547
+ let html = '<div class="options">';
1548
+ for (const opt of options) {
1549
+ const isSelected = answer.selected === opt.id;
1550
+ html += '<div class="readonly-option' + (isSelected ? ' selected' : '') + '">';
1551
+ if (isSelected) html += '<span class="check-mark">\\u2713</span>';
1552
+ html += '<span>' + escapeHtml(opt.label) + '</span>';
1553
+ html += '</div>';
1554
+ }
1555
+ html += '</div>';
1556
+ if (answer.feedback) {
1557
+ html += '<div class="readonly-answer"><div class="readonly-answer-label">Feedback</div>' + escapeHtml(answer.feedback) + '</div>';
1558
+ }
1559
+ return html;
1560
+ }
1561
+
1562
+ function renderAnsweredDiff(q, answer) {
1563
+ let html = '<div class="readonly-answer">';
1564
+ html += '<div class="readonly-answer-label">Decision</div>';
1565
+ const decisions = { approve: '\\u2713 Approved', reject: '\\u2717 Rejected', edit: '\\u270E Edit Requested' };
1566
+ html += '<strong>' + (decisions[answer.decision] || answer.decision) + '</strong>';
1567
+ if (answer.feedback) {
1568
+ html += '<div style="margin-top: 0.5rem;"><em>Comments:</em> ' + escapeHtml(answer.feedback) + '</div>';
1569
+ }
1570
+ html += '</div>';
1571
+ return html;
1572
+ }
1573
+
1574
+ function renderAnsweredRank(q, answer) {
1575
+ const ranking = answer.ranking || [];
1576
+ let html = '<div class="readonly-answer-label">Final Ranking</div>';
1577
+ html += '<div class="options">';
1578
+ for (const item of ranking) {
1579
+ const opt = (q.config.options || []).find(o => o.id === item.id);
1580
+ html += '<div class="readonly-option selected">';
1581
+ html += '<strong>' + item.rank + '.</strong> ' + escapeHtml(opt ? opt.label : item.id);
1582
+ html += '</div>';
1583
+ }
1584
+ html += '</div>';
1585
+ return html;
1586
+ }
1587
+
1588
+ function renderAnsweredRate(q, answer) {
1589
+ const ratings = answer.ratings || {};
1590
+ const labels = q.config.labels || {};
1591
+ const minLabel = labels.min || String(q.config.min || 1);
1592
+ const maxLabel = labels.max || String(q.config.max || 5);
1593
+ let html = '<div class="readonly-answer-label">Ratings</div>';
1594
+ html += '<div class="options">';
1595
+ for (const opt of (q.config.options || [])) {
1596
+ const rating = ratings[opt.id];
1597
+ html += '<div class="readonly-option' + (rating ? ' selected' : '') + '">';
1598
+ html += '<span>' + escapeHtml(opt.label) + '</span>';
1599
+ html += ' <strong style="margin-left: auto;">' + (rating || '-') + '</strong>';
1600
+ html += '</div>';
1601
+ }
1602
+ html += '</div>';
1603
+ if (labels.min || labels.max) {
1604
+ html += '<div class="readonly-answer" style="margin-top: 0.5rem;">';
1605
+ html += '<div class="readonly-answer-label">Scale</div>';
1606
+ html += '<div>' + escapeHtml(minLabel) + ' → ' + escapeHtml(maxLabel) + '</div>';
1607
+ html += '</div>';
1608
+ }
1609
+ return html;
1610
+ }
1611
+
1612
+ function renderAnsweredCode(q, answer) {
1613
+ let html = '<div class="readonly-answer">';
1614
+ html += '<div class="readonly-answer-label">Code (' + escapeHtml(q.config.language || 'plaintext') + ')</div>';
1615
+ html += '<pre style="margin: 0; white-space: pre-wrap;"><code>' + escapeHtml(answer.code || '') + '</code></pre>';
1616
+ html += '</div>';
1617
+ return html;
1618
+ }
1619
+
1620
+ function renderAnsweredFile(q, answer) {
1621
+ const files = answer.images || answer.files || [];
1622
+ let html = '<div class="readonly-answer">';
1623
+ html += '<div class="readonly-answer-label">Uploaded ' + files.length + ' file(s)</div>';
1624
+ html += '<ul style="margin: 0.5rem 0 0 1rem;">';
1625
+ for (const f of files) {
1626
+ html += '<li>' + escapeHtml(f.name) + '</li>';
1627
+ }
1628
+ html += '</ul>';
1629
+ html += '</div>';
1630
+ return html;
1631
+ }
1632
+
1633
+ function renderAnsweredEmoji(q, answer) {
1634
+ let html = '<div class="readonly-answer">';
1635
+ html += '<span style="font-size: 2rem;">' + (answer.emoji || '') + '</span>';
1636
+ html += '</div>';
1637
+ return html;
1638
+ }
1639
+
1640
+ function escapeHtml(text) {
1641
+ const div = document.createElement('div');
1642
+ div.textContent = text;
1643
+ return div.innerHTML;
1644
+ }
1645
+
1646
+ connect();
1647
+ </script>
1648
+ </body>
1649
+ </html>`;
1650
+ }