ask-user-question-plus 1.0.0

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,1184 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>AskUserQuestionPlus - Claude Code TUI</title>
7
+ <style>
8
+ :root {
9
+ /* Dark Theme (Default) */
10
+ --bg-grad-start: #1a1a1a;
11
+ --bg-grad-end: #050505;
12
+ --surface-color: #161616;
13
+ --text-color: #e0e0e0;
14
+ --dim-color: #888888;
15
+ --purple-color: #bc80ff;
16
+ --green-color: #2fb07a;
17
+ --blue-color: #4da6ff;
18
+ --warn-color: #e2b714;
19
+ --selection-bg: #2a2a2a;
20
+ --tab-bg: #1a1a1a;
21
+ --tab-active-bg: #161616;
22
+ --border-color: #333;
23
+ --border-glow: rgba(188, 128, 255, 0.15);
24
+ --shadow-color: rgba(0, 0, 0, 0.5);
25
+ --font-family: "JetBrains Mono", "Menlo", "Monaco", "Courier New",
26
+ monospace;
27
+ --btn-disabled-bg: #333;
28
+ --btn-disabled-text: #666;
29
+ }
30
+
31
+ [data-theme="light"] {
32
+ --bg-grad-start: #e0e0e0;
33
+ --bg-grad-end: #ffffff;
34
+ --surface-color: #ffffff;
35
+ --text-color: #333333;
36
+ --dim-color: #666666;
37
+ --purple-color: #6f42c1;
38
+ --green-color: #22863a;
39
+ --blue-color: #0366d6;
40
+ --warn-color: #b08800;
41
+ --selection-bg: #f0f0f0;
42
+ --tab-bg: #e1e4e8;
43
+ --tab-active-bg: #ffffff;
44
+ --border-color: #d1d5da;
45
+ --border-glow: rgba(0, 0, 0, 0.05);
46
+ --shadow-color: rgba(0, 0, 0, 0.1);
47
+ --btn-disabled-bg: #e1e4e8;
48
+ --btn-disabled-text: #959da5;
49
+ }
50
+
51
+ * {
52
+ box-sizing: border-box;
53
+ outline: none;
54
+ }
55
+
56
+ body {
57
+ background: radial-gradient(
58
+ circle at center,
59
+ var(--bg-grad-start) 0%,
60
+ var(--bg-grad-end) 100%
61
+ );
62
+ color: var(--text-color);
63
+ font-family: var(--font-family);
64
+ margin: 0;
65
+ padding: 24px;
66
+ display: flex;
67
+ justify-content: center;
68
+ align-items: center;
69
+ line-height: 1.4;
70
+ font-size: 16px;
71
+ height: 100vh;
72
+ overflow: hidden;
73
+ transition: background 0.3s;
74
+ }
75
+
76
+ #app {
77
+ max-width: 1000px;
78
+ width: 100%;
79
+ display: flex;
80
+ flex-direction: column;
81
+ height: auto;
82
+ min-height: 80vh;
83
+ max-height: 80vh;
84
+ border: 1px solid var(--border-color);
85
+ padding: 40px;
86
+ border-radius: 12px;
87
+ background: var(--surface-color);
88
+ box-shadow: 0 20px 50px var(--shadow-color), 0 0 20px var(--border-glow);
89
+ position: relative;
90
+ transition: background 0.3s, border-color 0.3s, box-shadow 0.3s;
91
+ }
92
+
93
+ #app::before {
94
+ content: "";
95
+ position: absolute;
96
+ top: 0;
97
+ left: 0;
98
+ right: 0;
99
+ height: 2px;
100
+ background: linear-gradient(
101
+ 90deg,
102
+ transparent,
103
+ var(--purple-color),
104
+ transparent
105
+ );
106
+ border-radius: 12px 12px 0 0;
107
+ opacity: 0.5;
108
+ }
109
+
110
+ /* Header & Icons */
111
+ .header {
112
+ color: var(--dim-color);
113
+ font-size: 12px;
114
+ margin-bottom: 20px;
115
+ border-bottom: 1px solid var(--border-color);
116
+ padding-bottom: 8px;
117
+ display: flex;
118
+ justify-content: space-between;
119
+ align-items: center;
120
+ }
121
+
122
+ .header-actions {
123
+ display: flex;
124
+ align-items: center;
125
+ }
126
+
127
+ .icon-btn {
128
+ cursor: pointer;
129
+ opacity: 0.6;
130
+ transition: opacity 0.2s;
131
+ display: flex;
132
+ align-items: center;
133
+ color: var(--text-color);
134
+ text-decoration: none;
135
+ background: none;
136
+ border: none;
137
+ padding: 5px;
138
+ }
139
+
140
+ .icon-btn:hover {
141
+ opacity: 1;
142
+ }
143
+
144
+ /* Star Shine Animation */
145
+ .icon-btn:hover .star-icon {
146
+ animation: shine 1.5s infinite ease-in-out;
147
+ fill: var(--warn-color) !important;
148
+ }
149
+
150
+ /* Light mode specific star adjustment */
151
+ [data-theme="light"] .icon-btn:hover .star-icon {
152
+ fill: #ffb700 !important; /* Bright Golden Yellow */
153
+ filter: drop-shadow(
154
+ 0 0 4px rgba(255, 183, 0, 0.6)
155
+ ); /* Add explicit glow */
156
+ animation: shine-light 1.5s infinite ease-in-out;
157
+ }
158
+
159
+ @keyframes shine {
160
+ 0% {
161
+ filter: drop-shadow(0 0 0px var(--warn-color));
162
+ transform: scale(1);
163
+ }
164
+ 50% {
165
+ filter: drop-shadow(0 0 5px var(--warn-color));
166
+ transform: scale(1.1);
167
+ }
168
+ 100% {
169
+ filter: drop-shadow(0 0 0px var(--warn-color));
170
+ transform: scale(1);
171
+ }
172
+ }
173
+
174
+ @keyframes shine-light {
175
+ 0% {
176
+ transform: scale(1);
177
+ }
178
+ 50% {
179
+ transform: scale(1.15);
180
+ }
181
+ 100% {
182
+ transform: scale(1);
183
+ }
184
+ }
185
+
186
+ .icon-btn svg {
187
+ width: 16px;
188
+ height: 16px;
189
+ fill: currentColor;
190
+ transition: fill 0.2s;
191
+ }
192
+
193
+ /* Tabs Navigation */
194
+ .tabs-container {
195
+ display: flex;
196
+ margin-bottom: 30px;
197
+ border-bottom: 1px solid var(--border-color);
198
+ flex-shrink: 0;
199
+ overflow-x: auto;
200
+ overflow-y: hidden;
201
+ scrollbar-width: none; /* Hide scrollbar Firefox */
202
+ -ms-overflow-style: none; /* Hide scrollbar IE/Edge */
203
+ }
204
+
205
+ .tabs-container::-webkit-scrollbar {
206
+ display: none; /* Hide scrollbar Chrome/Safari */
207
+ }
208
+
209
+ .tab-item {
210
+ padding: 8px 16px;
211
+ margin-right: 4px;
212
+ background: var(--tab-bg);
213
+ color: var(--dim-color);
214
+ cursor: pointer;
215
+ border-top-left-radius: 4px;
216
+ border-top-right-radius: 4px;
217
+ font-size: 13px;
218
+ font-weight: bold;
219
+ transition: background 0.2s, color 0.2s;
220
+ white-space: nowrap;
221
+ }
222
+
223
+ .tab-item.active {
224
+ background: var(--surface-color);
225
+ color: var(--text-color);
226
+ border-bottom: 2px solid var(--purple-color);
227
+ margin-bottom: -1px;
228
+ border-top: 1px solid transparent;
229
+ }
230
+
231
+ .tab-number {
232
+ margin-right: 6px;
233
+ opacity: 0.7;
234
+ }
235
+
236
+ /* Question Container */
237
+ #current-question-container {
238
+ flex: 1;
239
+ overflow-y: auto;
240
+ overflow-x: hidden;
241
+ position: relative;
242
+ padding-right: 10px;
243
+ }
244
+
245
+ /* Review Mode Styles */
246
+ .review-block {
247
+ margin-bottom: 15px;
248
+ border: 1px solid transparent;
249
+ border-bottom: 1px dashed var(--border-color);
250
+ padding: 15px;
251
+ border-radius: 6px;
252
+ transition: background 0.15s, border-color 0.15s;
253
+ }
254
+
255
+ .review-block:hover {
256
+ background: var(--selection-bg);
257
+ }
258
+
259
+ /* Keyboard navigation active state */
260
+ .review-block.active-review {
261
+ background: var(--selection-bg);
262
+ border-color: var(--purple-color);
263
+ border-bottom: 1px solid var(--purple-color);
264
+ }
265
+
266
+ .review-header {
267
+ color: var(--dim-color);
268
+ font-size: 12px;
269
+ text-transform: uppercase;
270
+ margin-bottom: 8px;
271
+ font-weight: bold;
272
+ }
273
+
274
+ .review-question {
275
+ font-size: 15px;
276
+ margin-bottom: 8px;
277
+ color: var(--text-color);
278
+ }
279
+
280
+ .review-answer {
281
+ color: var(--green-color);
282
+ font-family: var(--font-family);
283
+ padding-left: 15px;
284
+ border-left: 2px solid var(--green-color);
285
+ }
286
+
287
+ .review-answer-item {
288
+ margin-bottom: 4px;
289
+ }
290
+
291
+ /* Question Block */
292
+ .question-header {
293
+ display: flex;
294
+ align-items: flex-start;
295
+ margin-bottom: 20px;
296
+ }
297
+
298
+ .q-text {
299
+ font-weight: bold;
300
+ flex: 1;
301
+ font-size: 1.1em;
302
+ }
303
+
304
+ /* Options Area */
305
+ .options-list {
306
+ padding-left: 0; /* Aligned with question text */
307
+ }
308
+
309
+ .option-item {
310
+ display: flex;
311
+ align-items: flex-start;
312
+ padding: 12px 12px;
313
+ margin: 4px 0;
314
+ cursor: pointer;
315
+ border-radius: 4px;
316
+ transition: none;
317
+ border: 1px solid transparent;
318
+ }
319
+
320
+ .option-item.active {
321
+ background-color: var(--selection-bg);
322
+ border-color: var(--border-color);
323
+ }
324
+
325
+ .option-indicator {
326
+ margin-right: 12px;
327
+ font-weight: bold;
328
+ min-width: 20px;
329
+ }
330
+
331
+ .option-content {
332
+ flex: 1;
333
+ }
334
+
335
+ .option-label {
336
+ display: flex;
337
+ align-items: center;
338
+ }
339
+
340
+ .option-desc {
341
+ color: var(--dim-color);
342
+ font-size: 13px;
343
+ margin-top: 2px;
344
+ }
345
+
346
+ .recommended-tag {
347
+ color: var(--green-color);
348
+ font-size: 12px;
349
+ margin-left: 8px;
350
+ font-style: italic;
351
+ }
352
+
353
+ /* Multi-select indicators */
354
+ .checkbox-box {
355
+ color: var(--dim-color);
356
+ margin-right: 8px;
357
+ }
358
+ .checkbox-box.checked {
359
+ color: var(--green-color);
360
+ }
361
+
362
+ /* Other Input Area */
363
+ .other-input-wrapper {
364
+ margin-top: 12px;
365
+ display: none;
366
+ }
367
+
368
+ .other-input-wrapper.visible {
369
+ display: block;
370
+ }
371
+
372
+ .other-input {
373
+ background: transparent;
374
+ border: none;
375
+ border-bottom: 1px solid var(--dim-color);
376
+ color: var(--blue-color);
377
+ font-family: var(--font-family);
378
+ width: 100%;
379
+ padding: 4px 0;
380
+ font-size: 14px;
381
+ }
382
+
383
+ .other-input:focus {
384
+ border-bottom-color: var(--blue-color);
385
+ }
386
+
387
+ /* Footer / Navigation Hints */
388
+ .footer {
389
+ margin-top: 20px;
390
+ padding-top: 20px;
391
+ border-top: 1px solid var(--border-color);
392
+ flex-shrink: 0;
393
+ display: flex;
394
+ justify-content: space-between;
395
+ align-items: center;
396
+ }
397
+
398
+ .key-hints {
399
+ display: flex;
400
+ flex-wrap: wrap;
401
+ gap: 20px;
402
+ flex: 1;
403
+ }
404
+
405
+ .key-hint-item {
406
+ font-size: 12px;
407
+ color: var(--dim-color);
408
+ display: flex;
409
+ align-items: center;
410
+ }
411
+
412
+ .key-tag {
413
+ background: #333;
414
+ color: #eee;
415
+ padding: 2px 6px;
416
+ border-radius: 3px;
417
+ margin-right: 6px;
418
+ font-size: 11px;
419
+ font-family: inherit;
420
+ }
421
+
422
+ [data-theme="light"] .key-tag {
423
+ background: #e1e4e8;
424
+ color: #24292e;
425
+ }
426
+
427
+ .submit-btn {
428
+ background-color: var(--green-color);
429
+ color: white; /* Always white for readability on green */
430
+ border: none;
431
+ padding: 10px 24px;
432
+ font-family: var(--font-family);
433
+ font-weight: bold;
434
+ cursor: pointer;
435
+ border-radius: 4px;
436
+ display: flex;
437
+ align-items: center;
438
+ gap: 10px;
439
+ flex-shrink: 0;
440
+ transition: opacity 0.2s, background-color 0.2s;
441
+ }
442
+
443
+ [data-theme="light"] .submit-btn {
444
+ color: white;
445
+ }
446
+
447
+ .submit-btn:disabled {
448
+ background-color: var(--btn-disabled-bg);
449
+ color: var(--btn-disabled-text);
450
+ cursor: not-allowed;
451
+ opacity: 1; /* Reset opacity logic */
452
+ }
453
+
454
+ .submit-shortcut {
455
+ font-size: 11px;
456
+ opacity: 0.8;
457
+ background: rgba(0, 0, 0, 0.2);
458
+ padding: 2px 4px;
459
+ border-radius: 3px;
460
+ }
461
+
462
+ .submit-btn:hover:not(:disabled) {
463
+ opacity: 0.9;
464
+ }
465
+
466
+ /* Selection specific icons */
467
+ .radio-dot {
468
+ color: var(--dim-color);
469
+ margin-right: 8px;
470
+ }
471
+ .radio-dot.checked {
472
+ color: var(--green-color);
473
+ }
474
+
475
+ /* Error Message in Review */
476
+ .error-msg {
477
+ color: #ff4d4f; /* Red */
478
+ font-size: 13px;
479
+ margin-top: 5px;
480
+ display: flex;
481
+ align-items: center;
482
+ }
483
+
484
+ /* Custom Scrollbar for container */
485
+ #current-question-container::-webkit-scrollbar {
486
+ width: 8px;
487
+ }
488
+ #current-question-container::-webkit-scrollbar-track {
489
+ background: transparent;
490
+ }
491
+ #current-question-container::-webkit-scrollbar-thumb {
492
+ background: #444;
493
+ border-radius: 4px;
494
+ }
495
+ [data-theme="light"]
496
+ #current-question-container::-webkit-scrollbar-thumb {
497
+ background: #ccc;
498
+ }
499
+
500
+ /* Empty State */
501
+ #empty-state {
502
+ display: flex;
503
+ flex-direction: column;
504
+ align-items: center;
505
+ justify-content: center;
506
+ height: 100%;
507
+ color: var(--dim-color);
508
+ text-align: center;
509
+ }
510
+ .spinner {
511
+ width: 40px;
512
+ height: 40px;
513
+ border: 3px solid rgba(188, 128, 255, 0.1);
514
+ border-top-color: var(--purple-color);
515
+ border-radius: 50%;
516
+ animation: spin 1s infinite linear;
517
+ margin-bottom: 20px;
518
+ }
519
+ @keyframes spin {
520
+ to {
521
+ transform: rotate(360deg);
522
+ }
523
+ }
524
+ </style>
525
+ </head>
526
+ <body>
527
+ <div id="app">
528
+ <div class="header">
529
+ <div style="display: flex; flex-direction: column">
530
+ <span style="font-weight: bold">AskUserQuestionPlus</span>
531
+ <span id="session-info" style="font-size: 11px; opacity: 0.7"
532
+ >Waiting for connection...</span
533
+ >
534
+ </div>
535
+
536
+ <div class="header-actions">
537
+ <!-- Theme Toggle -->
538
+ <div class="icon-btn" onclick="toggleTheme()" title="Toggle Theme">
539
+ <svg id="theme-icon" viewBox="0 0 24 24">
540
+ <path
541
+ d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.93c.39-.39.39-1.02 0-1.41-.39-.39-1.02-.39-1.41 0l-1.42 1.42c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0l1.42-1.42zm13.48 14.14c.39-.39.39-1.02 0-1.41-.39-.39-1.02-.39-1.41 0l-1.42 1.42c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0l1.42-1.42zM19.97 5.93c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41l-1.42-1.42c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41l1.42 1.42zM4.93 19.97c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41l-1.42-1.42c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41l1.42 1.42z"
542
+ />
543
+ </svg>
544
+ </div>
545
+
546
+ <!-- GitHub Star -->
547
+ <a
548
+ href="https://github.com/changjunjie/ask-user-question-plus"
549
+ target="_blank"
550
+ class="icon-btn"
551
+ title="Star on GitHub"
552
+ >
553
+ <svg class="star-icon" viewBox="0 0 16 16">
554
+ <path
555
+ fill-rule="evenodd"
556
+ d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"
557
+ ></path>
558
+ </svg>
559
+ <span style="font-size: 12px; font-weight: bold; margin-left: 4px"
560
+ >Star</span
561
+ >
562
+ </a>
563
+ </div>
564
+ </div>
565
+
566
+ <div class="tabs-container" id="tabs-container"></div>
567
+
568
+ <div id="current-question-container">
569
+ <div id="empty-state">
570
+ <div class="spinner"></div>
571
+ <p>Waiting ...</p>
572
+ </div>
573
+ </div>
574
+
575
+ <div class="footer">
576
+ <div class="key-hints">
577
+ <div class="key-hint-item">
578
+ <span class="key-tag">↑↓</span> 选项/滚动
579
+ </div>
580
+ <div class="key-hint-item">
581
+ <span class="key-tag">← →/Tab</span> 切换问题
582
+ </div>
583
+ <div class="key-hint-item">
584
+ <span class="key-tag">Enter/Space</span>
585
+ <span id="space-hint-text"></span>
586
+ </div>
587
+ </div>
588
+ <button class="submit-btn" id="submit-all" disabled>
589
+ Go Submit
590
+ <span class="submit-shortcut" id="submit-shortcut-text">⌘+Enter</span>
591
+ </button>
592
+ </div>
593
+ </div>
594
+
595
+ <script>
596
+ // --- State ---
597
+ let QUESTIONS_DATA = [];
598
+ let answers = {};
599
+ let activeQuestionIndex = 0;
600
+ let activeOptionIndex = 0;
601
+ let activeReviewIndex = 0;
602
+ let shouldFocusOther = false;
603
+ let ws = null;
604
+ let sessionId =
605
+ new URLSearchParams(window.location.search).get("sessionId") ||
606
+ "default";
607
+ const isMac = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
608
+
609
+ // --- WebSocket Logic ---
610
+ function connectWS() {
611
+ // Robust WS URL construction
612
+ let wsHost = window.location.host;
613
+ // If running locally but port is inferred/different, force 3456 for WS as per backend config
614
+ if (
615
+ window.location.hostname === "localhost" ||
616
+ window.location.hostname === "127.0.0.1"
617
+ ) {
618
+ // Assuming backend is always 3456 based on src/index.ts
619
+ wsHost = `${window.location.hostname}:3456`;
620
+ }
621
+
622
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
623
+ const wsUrl = `${protocol}//${wsHost}/ws?sessionId=${sessionId}`;
624
+
625
+ console.error("Connecting to WS:", wsUrl);
626
+
627
+ ws = new WebSocket(wsUrl);
628
+
629
+ ws.onopen = () => {
630
+ console.error("WS Open");
631
+ document.getElementById(
632
+ "session-info"
633
+ ).textContent = `Session: ${sessionId} (Connected)`;
634
+ document.getElementById("session-info").style.color =
635
+ "var(--green-color)";
636
+ ws.send(JSON.stringify({ type: "GET_STATE" }));
637
+ };
638
+
639
+ ws.onerror = (err) => {
640
+ console.error("WS Error:", err);
641
+ document.getElementById(
642
+ "session-info"
643
+ ).textContent = `Connection Error. Check console.`;
644
+ document.getElementById("session-info").style.color = "#ff4d4f";
645
+ };
646
+
647
+ ws.onmessage = (event) => {
648
+ console.error("WS Message:", event.data);
649
+ try {
650
+ const message = JSON.parse(event.data);
651
+
652
+ if (
653
+ message.type === "SYNC_STATE" ||
654
+ message.type === "NEW_QUESTION"
655
+ ) {
656
+ const payload = message.payload;
657
+
658
+ const questions =
659
+ message.type === "SYNC_STATE" ? payload.activeData : payload;
660
+
661
+ if (questions && questions.length > 0) {
662
+ QUESTIONS_DATA = questions;
663
+ initAnswers();
664
+ activeQuestionIndex = 0;
665
+ activeOptionIndex = 0;
666
+ render();
667
+ } else {
668
+ QUESTIONS_DATA = [];
669
+ renderEmpty();
670
+ }
671
+ }
672
+ } catch (e) {
673
+ console.error("WS Message Error:", e);
674
+ }
675
+ };
676
+
677
+ ws.onclose = () => {
678
+ document.getElementById(
679
+ "session-info"
680
+ ).textContent = `Disconnected. Reconnecting...`;
681
+ setTimeout(connectWS, 2000);
682
+ };
683
+ }
684
+
685
+ function initAnswers() {
686
+ try {
687
+ answers = {};
688
+ QUESTIONS_DATA.forEach((q) => {
689
+ // No default selection
690
+ if (q.type === "single") {
691
+ answers[q.id] = { value: null, otherText: "" };
692
+ } else {
693
+ answers[q.id] = { values: [], otherText: "" };
694
+ }
695
+ });
696
+ } catch (e) {
697
+ console.error("Error init answers:", e);
698
+ }
699
+ }
700
+
701
+ // --- Render Functions ---
702
+ function renderEmpty() {
703
+ document.getElementById("tabs-container").innerHTML = "";
704
+ document.getElementById("current-question-container").innerHTML = `
705
+ <div id="empty-state">
706
+ <div class="spinner"></div>
707
+ <p>Waiting for Claude...</p>
708
+ </div>
709
+ `;
710
+ const submitBtn = document.getElementById("submit-all");
711
+ submitBtn.disabled = true;
712
+ submitBtn.textContent = "WAITING...";
713
+ }
714
+
715
+ function renderTabs() {
716
+ const tabsContainer = document.getElementById("tabs-container");
717
+ const tabsHtml = QUESTIONS_DATA.map(
718
+ (q, idx) => `
719
+ <div id="tab-${idx}" class="tab-item ${
720
+ idx === activeQuestionIndex ? "active" : ""
721
+ }" onclick="switchToQuestion(${idx})">
722
+ <span class="tab-number">${idx + 1}.</span>${q.header}
723
+ </div>
724
+ `
725
+ ).join("");
726
+
727
+ const isReviewActive = activeQuestionIndex === QUESTIONS_DATA.length;
728
+ const reviewTabHtml = `
729
+ <div id="tab-${QUESTIONS_DATA.length}" class="tab-item ${
730
+ isReviewActive ? "active" : ""
731
+ }" onclick="switchToQuestion(${QUESTIONS_DATA.length})">
732
+ Submit
733
+ </div>
734
+ `;
735
+
736
+ tabsContainer.innerHTML = tabsHtml + reviewTabHtml;
737
+
738
+ // Scroll active tab into view
739
+ const activeTab = document.getElementById(`tab-${activeQuestionIndex}`);
740
+ if (activeTab) {
741
+ activeTab.scrollIntoView({
742
+ behavior: "smooth",
743
+ block: "nearest",
744
+ inline: "center",
745
+ });
746
+ }
747
+ }
748
+
749
+ function renderQuestion() {
750
+ const container = document.getElementById("current-question-container");
751
+ if (activeQuestionIndex === QUESTIONS_DATA.length) {
752
+ renderReview();
753
+ return;
754
+ }
755
+
756
+ const q = QUESTIONS_DATA[activeQuestionIndex];
757
+ const currentAnswer = answers[q.id];
758
+
759
+ // Ensure an "Other" option exists for every question
760
+ const options = [...q.options];
761
+ if (!options.find((o) => o.isOther || o.value === "other")) {
762
+ options.push({
763
+ value: "other",
764
+ label: "Other",
765
+ description: "Custom input",
766
+ isOther: true,
767
+ });
768
+ }
769
+
770
+ const optionsHtml = options
771
+ .map((opt, oIdx) => {
772
+ const isActiveOption = oIdx === activeOptionIndex;
773
+ const isSelected =
774
+ q.type === "single"
775
+ ? currentAnswer.value === opt.value
776
+ : currentAnswer.values.includes(opt.value);
777
+
778
+ let indicator = "";
779
+ if (q.type === "single") {
780
+ indicator = isSelected
781
+ ? '<span class="radio-dot checked">●</span>'
782
+ : '<span class="radio-dot">○</span>';
783
+ } else {
784
+ indicator = isSelected
785
+ ? '<span class="checkbox-box checked">◉</span>'
786
+ : '<span class="checkbox-box">◯</span>';
787
+ }
788
+
789
+ const showOtherInput = opt.isOther && isSelected;
790
+
791
+ return `
792
+ <div id="opt-${oIdx}" class="option-item ${
793
+ isActiveOption ? "active" : ""
794
+ }"
795
+ onclick="handleOptionClick(${activeQuestionIndex}, ${oIdx})">
796
+ <div class="option-indicator">${indicator}</div>
797
+ <div class="option-content">
798
+ <div class="option-label">
799
+ ${opt.label}
800
+ ${
801
+ opt.recommended
802
+ ? '<span class="recommended-tag">(推荐)</span>'
803
+ : ""
804
+ }
805
+ </div>
806
+ ${
807
+ opt.description
808
+ ? `<div class="option-desc">${opt.description}</div>`
809
+ : ""
810
+ }
811
+
812
+ <div class="other-input-wrapper ${
813
+ showOtherInput ? "visible" : ""
814
+ }">
815
+ <input type="text"
816
+ class="other-input"
817
+ id="input-${q.id}-${oIdx}"
818
+ placeholder="Please specify..."
819
+ value="${currentAnswer.otherText}"
820
+ oninput="updateOtherText('${
821
+ q.id
822
+ }', this.value)"
823
+ onclick="event.stopPropagation()">
824
+ </div>
825
+ </div>
826
+ </div>
827
+ `;
828
+ })
829
+ .join("");
830
+
831
+ container.innerHTML = `
832
+ <div class="question-block">
833
+ <div class="question-header">
834
+ <span class="q-text">${q.text}${
835
+ q.type === "multiple"
836
+ ? ' <span style="color:var(--dim-color); font-size:0.8em">(可多选)</span>'
837
+ : ""
838
+ }</span>
839
+ </div>
840
+ <div class="options-list">
841
+ ${optionsHtml}
842
+ </div>
843
+ </div>
844
+ `;
845
+
846
+ // Auto-scroll to active option
847
+ const activeEl = document.getElementById(`opt-${activeOptionIndex}`);
848
+ if (activeEl) {
849
+ activeEl.scrollIntoView({ behavior: "smooth", block: "nearest" });
850
+ }
851
+
852
+ if (shouldFocusOther) {
853
+ const opt = options[activeOptionIndex];
854
+ if (opt.isOther) {
855
+ const inputEl = document.getElementById(
856
+ `input-${q.id}-${activeOptionIndex}`
857
+ );
858
+ if (inputEl) {
859
+ inputEl.focus();
860
+ const len = inputEl.value.length;
861
+ inputEl.setSelectionRange(len, len);
862
+ }
863
+ }
864
+ shouldFocusOther = false;
865
+ }
866
+ }
867
+
868
+ function renderReview() {
869
+ const container = document.getElementById("current-question-container");
870
+ const summaryHtml = QUESTIONS_DATA.map((q, idx) => {
871
+ const ans = answers[q.id];
872
+ let displayValue = "";
873
+ let error = null;
874
+ const isActive = idx === activeReviewIndex;
875
+
876
+ if (q.type === "single") {
877
+ if (!ans.value) {
878
+ error = "未选择答案";
879
+ } else {
880
+ const opt = [
881
+ ...q.options,
882
+ { value: "other", label: "Other", isOther: true },
883
+ ].find((o) => o.value === ans.value);
884
+ if (opt) {
885
+ displayValue = opt.label;
886
+ if (opt.isOther) {
887
+ if (!ans.otherText.trim()) {
888
+ error = "请填写自定义内容";
889
+ displayValue += ": [Empty]";
890
+ } else {
891
+ displayValue += `: ${ans.otherText}`;
892
+ }
893
+ }
894
+ }
895
+ }
896
+ } else {
897
+ if (!ans.values || ans.values.length === 0) {
898
+ error = "请至少选择一项";
899
+ } else {
900
+ displayValue = ans.values
901
+ .map((v) => {
902
+ const opt = [
903
+ ...q.options,
904
+ { value: "other", label: "Other", isOther: true },
905
+ ].find((o) => o.value === v);
906
+ if (!opt) return v;
907
+ let label = opt.label;
908
+ if (opt.isOther) {
909
+ if (!ans.otherText.trim()) {
910
+ error = "请填写自定义内容";
911
+ label += ": [Empty]";
912
+ } else {
913
+ label += `: ${ans.otherText}`;
914
+ }
915
+ }
916
+ return `<div class="review-answer-item">• ${label}</div>`;
917
+ })
918
+ .join("");
919
+ }
920
+ }
921
+
922
+ return `
923
+ <div id="review-item-${idx}" class="review-block ${
924
+ isActive ? "active-review" : ""
925
+ }"
926
+ onclick="switchToQuestion(${idx})"
927
+ style="cursor:pointer">
928
+ <div class="review-header">${q.header}</div>
929
+ <div class="review-question">${q.text}</div>
930
+ ${
931
+ error
932
+ ? `<div class="error-msg">⚠ ${error}</div>`
933
+ : `<div class="review-answer">${displayValue}</div>`
934
+ }
935
+ </div>
936
+ `;
937
+ }).join("");
938
+
939
+ container.innerHTML = `
940
+ <div class="question-block">
941
+ <div class="question-header">
942
+ <span class="q-text">请检查您的回答,确认无误后提交!</span>
943
+ </div>
944
+ ${summaryHtml}
945
+ </div>
946
+ `;
947
+
948
+ const activeBlock = document.getElementById(
949
+ `review-item-${activeReviewIndex}`
950
+ );
951
+ if (activeBlock) {
952
+ activeBlock.scrollIntoView({ behavior: "smooth", block: "nearest" });
953
+ }
954
+ }
955
+
956
+ function updateFooterButton() {
957
+ const submitBtn = document.getElementById("submit-all");
958
+ const spaceHint = document.getElementById("space-hint-text");
959
+ const isReview = activeQuestionIndex === QUESTIONS_DATA.length;
960
+ const isValid = validateAll();
961
+ const shortcutText = isMac ? "⌘+Enter" : "Ctrl+Enter";
962
+
963
+ if (isReview) {
964
+ submitBtn.innerHTML = `Submt Answers <span class="submit-shortcut" id="submit-shortcut-text">${shortcutText}</span>`;
965
+ submitBtn.disabled = !isValid;
966
+ if (spaceHint) spaceHint.textContent = "跳转";
967
+ } else {
968
+ submitBtn.innerHTML = `Go Submit <span class="submit-shortcut" id="submit-shortcut-text">${shortcutText}</span>`;
969
+ submitBtn.disabled = QUESTIONS_DATA.length === 0;
970
+ if (spaceHint) spaceHint.textContent = "选择";
971
+ }
972
+ }
973
+
974
+ function render() {
975
+ if (QUESTIONS_DATA.length === 0) {
976
+ renderEmpty();
977
+ return;
978
+ }
979
+ renderTabs();
980
+ renderQuestion();
981
+ updateFooterButton();
982
+ }
983
+
984
+ // --- Event Handlers ---
985
+ function toggleTheme() {
986
+ const body = document.body;
987
+ const isLight = body.getAttribute("data-theme") === "light";
988
+ if (isLight) {
989
+ body.removeAttribute("data-theme");
990
+ document.getElementById("theme-icon").innerHTML =
991
+ '<path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.93c.39-.39.39-1.02 0-1.41-.39-.39-1.02-.39-1.41 0l-1.42 1.42c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0l1.42-1.42zm13.48 14.14c.39-.39.39-1.02 0-1.41-.39-.39-1.02-.39-1.41 0l-1.42 1.42c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0l1.42-1.42zM19.97 5.93c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41l-1.42-1.42c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41l1.42 1.42zM4.93 19.97c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41l-1.42-1.42c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41l1.42 1.42z"/>';
992
+ } else {
993
+ body.setAttribute("data-theme", "light");
994
+ document.getElementById("theme-icon").innerHTML =
995
+ '<path d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9c0-.46-.04-.92-.1-1.36-.98 1.37-2.58 2.26-4.4 2.26-2.98 0-5.4-2.42-5.4-5.4 0-1.81.89-3.42 2.26-4.4-.44-.06-.9-.1-1.36-.1z"/>';
996
+ }
997
+ }
998
+
999
+ function switchToQuestion(idx) {
1000
+ activeQuestionIndex = idx;
1001
+ activeOptionIndex = 0;
1002
+ if (idx === QUESTIONS_DATA.length) {
1003
+ activeReviewIndex = 0;
1004
+ }
1005
+ render();
1006
+ }
1007
+
1008
+ function handleOptionClick(qIdx, oIdx) {
1009
+ activeQuestionIndex = qIdx;
1010
+ activeOptionIndex = oIdx;
1011
+ toggleSelection(qIdx, oIdx);
1012
+ }
1013
+
1014
+ function toggleSelection(qIdx, oIdx) {
1015
+ const q = QUESTIONS_DATA[qIdx];
1016
+ const ans = answers[q.id];
1017
+
1018
+ const options = [...q.options];
1019
+ if (!options.find((o) => o.isOther || o.value === "other")) {
1020
+ options.push({ value: "other", label: "Other", isOther: true });
1021
+ }
1022
+ const opt = options[oIdx];
1023
+
1024
+ if (q.type === "single") {
1025
+ ans.value = opt.value;
1026
+ // Auto advance for single choice if not "Other"
1027
+ if (!opt.isOther) {
1028
+ setTimeout(() => {
1029
+ const totalTabs = QUESTIONS_DATA.length + 1;
1030
+ activeQuestionIndex = (activeQuestionIndex + 1) % totalTabs;
1031
+ switchToQuestion(activeQuestionIndex);
1032
+ }, 200);
1033
+ }
1034
+ } else {
1035
+ const idx = ans.values.indexOf(opt.value);
1036
+ if (idx > -1) {
1037
+ ans.values.splice(idx, 1);
1038
+ } else {
1039
+ ans.values.push(opt.value);
1040
+ }
1041
+ }
1042
+
1043
+ if (opt.isOther) {
1044
+ const isSelected =
1045
+ q.type === "single"
1046
+ ? ans.value === opt.value
1047
+ : ans.values.includes(opt.value);
1048
+
1049
+ if (isSelected) {
1050
+ shouldFocusOther = true;
1051
+ }
1052
+ }
1053
+
1054
+ render();
1055
+ }
1056
+
1057
+ function updateOtherText(qId, text) {
1058
+ answers[qId].otherText = text;
1059
+ updateFooterButton();
1060
+ }
1061
+
1062
+ function validateAll() {
1063
+ for (const q of QUESTIONS_DATA) {
1064
+ const ans = answers[q.id];
1065
+ if (q.type === "single") {
1066
+ if (!ans.value) return false;
1067
+ if (ans.value === "other" && !ans.otherText.trim()) return false;
1068
+ } else {
1069
+ if (!ans.values || ans.values.length === 0) return false;
1070
+ if (ans.values.includes("other") && !ans.otherText.trim())
1071
+ return false;
1072
+ }
1073
+ }
1074
+ return true;
1075
+ }
1076
+
1077
+ function handleSubmitAction() {
1078
+ const isReview = activeQuestionIndex === QUESTIONS_DATA.length;
1079
+ if (isReview) {
1080
+ if (!validateAll()) return;
1081
+
1082
+ ws.send(
1083
+ JSON.stringify({
1084
+ type: "SUBMIT_ANSWERS",
1085
+ payload: { answers },
1086
+ })
1087
+ );
1088
+
1089
+ const submitBtn = document.getElementById("submit-all");
1090
+ submitBtn.textContent = "SUBMITTING...";
1091
+ submitBtn.disabled = true;
1092
+
1093
+ setTimeout(() => {
1094
+ alert("提交成功!即将关闭...");
1095
+ submitBtn.textContent = "DONE";
1096
+ window.close();
1097
+ document.body.innerHTML =
1098
+ '<div style="color:var(--text-color); font-family:var(--font-family); display:flex; flex-direction:column; align-items:center; justify-content:center; height:100vh;"><h1>Answers Submitted</h1><p>You can now return to Claude Code CLI and close this tab.</p></div>';
1099
+ }, 500);
1100
+ } else {
1101
+ switchToQuestion(QUESTIONS_DATA.length);
1102
+ }
1103
+ }
1104
+
1105
+ document
1106
+ .getElementById("submit-all")
1107
+ .addEventListener("click", handleSubmitAction);
1108
+
1109
+ document.addEventListener("keydown", (e) => {
1110
+ if (QUESTIONS_DATA.length === 0) return;
1111
+
1112
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
1113
+ e.preventDefault();
1114
+ handleSubmitAction();
1115
+ return;
1116
+ }
1117
+ const activeElement = document.activeElement;
1118
+ const isInputFocused = activeElement.tagName === "INPUT";
1119
+ if (e.key === "Tab") {
1120
+ e.preventDefault();
1121
+ const totalTabs = QUESTIONS_DATA.length + 1;
1122
+ if (e.shiftKey)
1123
+ activeQuestionIndex =
1124
+ (activeQuestionIndex - 1 + totalTabs) % totalTabs;
1125
+ else activeQuestionIndex = (activeQuestionIndex + 1) % totalTabs;
1126
+ switchToQuestion(activeQuestionIndex);
1127
+ return;
1128
+ }
1129
+ if (
1130
+ (e.key === "ArrowRight" || e.key === "ArrowLeft") &&
1131
+ !isInputFocused
1132
+ ) {
1133
+ e.preventDefault();
1134
+ const totalTabs = QUESTIONS_DATA.length + 1;
1135
+ if (e.key === "ArrowRight")
1136
+ activeQuestionIndex = (activeQuestionIndex + 1) % totalTabs;
1137
+ else
1138
+ activeQuestionIndex =
1139
+ (activeQuestionIndex - 1 + totalTabs) % totalTabs;
1140
+ switchToQuestion(activeQuestionIndex);
1141
+ return;
1142
+ }
1143
+ if (activeQuestionIndex === QUESTIONS_DATA.length) {
1144
+ if (e.key === "ArrowDown") {
1145
+ e.preventDefault();
1146
+ activeReviewIndex = (activeReviewIndex + 1) % QUESTIONS_DATA.length;
1147
+ renderReview();
1148
+ } else if (e.key === "ArrowUp") {
1149
+ e.preventDefault();
1150
+ activeReviewIndex =
1151
+ (activeReviewIndex - 1 + QUESTIONS_DATA.length) %
1152
+ QUESTIONS_DATA.length;
1153
+ renderReview();
1154
+ } else if (e.key === " " || e.key === "Enter") {
1155
+ e.preventDefault();
1156
+ switchToQuestion(activeReviewIndex);
1157
+ }
1158
+ return;
1159
+ }
1160
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
1161
+ e.preventDefault();
1162
+ const q = QUESTIONS_DATA[activeQuestionIndex];
1163
+ const optCount =
1164
+ q.options.length +
1165
+ (q.options.find((o) => o.isOther || o.value === "other") ? 0 : 1);
1166
+ if (e.key === "ArrowDown")
1167
+ activeOptionIndex = (activeOptionIndex + 1) % optCount;
1168
+ else
1169
+ activeOptionIndex = (activeOptionIndex - 1 + optCount) % optCount;
1170
+ if (isInputFocused) activeElement.blur();
1171
+ render();
1172
+ return;
1173
+ }
1174
+ if ((e.key === " " || e.key === "Enter") && !isInputFocused) {
1175
+ e.preventDefault();
1176
+ toggleSelection(activeQuestionIndex, activeOptionIndex);
1177
+ return;
1178
+ }
1179
+ });
1180
+
1181
+ connectWS();
1182
+ </script>
1183
+ </body>
1184
+ </html>