wop commited on
Commit
7a1ea71
·
verified ·
1 Parent(s): edeb4db

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +317 -336
templates/index.html CHANGED
@@ -19,11 +19,8 @@
19
  --bad: #f87171;
20
  --warn: #fbbf24;
21
  --shadow: 0 20px 60px rgba(0,0,0,.35);
22
- --r: 18px;
23
- --r2: 12px;
24
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
25
  --font: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
26
- --anim-speed: 1;
27
  }
28
  * { box-sizing: border-box; margin: 0; padding: 0; }
29
  html, body {
@@ -41,27 +38,23 @@
41
  linear-gradient(rgba(108,131,255,.03) 1px, transparent 1px),
42
  linear-gradient(90deg, rgba(108,131,255,.03) 1px, transparent 1px);
43
  background-size: 44px 44px;
44
- pointer-events: none;
45
- opacity: .3;
46
  }
47
  #app {
48
  position: relative; z-index: 1;
49
  height: 100%; display: flex; flex-direction: column;
50
  }
51
 
52
- /* ── Topbar ── */
53
  #topbar {
54
- height: 56px;
55
- padding: 0 16px;
56
  display: flex; align-items: center; justify-content: space-between;
57
  border-bottom: 1px solid var(--border);
58
  backdrop-filter: blur(14px);
59
  background: rgba(11,14,20,.78);
60
  flex-shrink: 0;
61
  }
62
- .brand {
63
- display: flex; align-items: center; gap: 10px; min-width: 0;
64
- }
65
  .logo {
66
  width: 30px; height: 30px; border-radius: 10px;
67
  display: grid; place-items: center; font-size: 15px;
@@ -69,13 +62,8 @@
69
  box-shadow: 0 6px 18px rgba(108,131,255,.25);
70
  flex: 0 0 auto;
71
  }
72
- .brand-title {
73
- font-weight: 700; letter-spacing: -.03em; font-size: 15px;
74
- }
75
- .brand-sub {
76
- color: var(--muted); font-size: 11px; font-family: var(--mono);
77
- margin-left: 2px;
78
- }
79
  .top-actions { display: flex; align-items: center; gap: 6px; }
80
  .top-btn {
81
  border: 1px solid var(--border2);
@@ -95,7 +83,7 @@
95
  }
96
  .top-btn svg { width: 14px; height: 14px; }
97
 
98
- /* ── Status bar ── */
99
  #statusbar {
100
  height: 0; overflow: hidden;
101
  transition: height 220ms ease, opacity 220ms ease;
@@ -104,13 +92,9 @@
104
  background: rgba(11,14,20,.6);
105
  display: flex; align-items: center; justify-content: center;
106
  font-size: 12px; font-family: var(--mono);
107
- color: var(--muted);
108
- flex-shrink: 0;
109
- }
110
- #statusbar.visible {
111
- height: 32px; opacity: 1;
112
- border-bottom-color: var(--border);
113
  }
 
114
  #statusbar .status-dot {
115
  width: 6px; height: 6px; border-radius: 50%;
116
  margin-right: 8px; display: inline-block;
@@ -122,7 +106,7 @@
122
  50% { opacity: 1; transform: scale(1.1); }
123
  }
124
 
125
- /* ── Chat area ── */
126
  #chat {
127
  flex: 1; overflow-y: auto;
128
  padding: 20px 14px 24px;
@@ -133,7 +117,7 @@
133
  #chat::-webkit-scrollbar-thumb { background: rgba(255,255,255,.1); border-radius: 9px; }
134
  .wrap { max-width: 760px; margin: 0 auto; }
135
 
136
- /* ── Welcome ── */
137
  .welcome {
138
  margin: 6vh auto 0; max-width: 480px; text-align: center;
139
  padding: 24px 20px;
@@ -143,23 +127,18 @@
143
  box-shadow: var(--shadow);
144
  animation: fadeUp 400ms ease both;
145
  }
146
- .welcome h1 {
147
- font-size: 22px; font-weight: 700; letter-spacing: -.03em;
148
- line-height: 1.3;
149
- }
150
- .welcome p {
151
- color: var(--muted); line-height: 1.6; margin-top: 8px; font-size: 13px;
152
- }
153
  @keyframes fadeUp {
154
  from { opacity: 0; transform: translateY(12px); }
155
  to { opacity: 1; transform: translateY(0); }
156
  }
157
 
158
- /* ── Turns ── */
159
  .turn {
160
  display: flex; gap: 10px; margin-bottom: 6px;
161
  align-items: flex-start;
162
- animation: fadeUp calc(280ms * var(--anim-speed)) ease both;
163
  }
164
  .turn.user { justify-content: flex-end; }
165
  .avatar {
@@ -185,40 +164,31 @@
185
  line-height: 1.6; font-size: 14px;
186
  white-space: pre-wrap; word-break: break-word;
187
  background: rgba(255,255,255,.03);
188
- transition: border-color 200ms ease;
189
  }
190
  .turn.user .bubble {
191
  background: linear-gradient(135deg, rgba(108,131,255,.15), rgba(161,110,255,.12));
192
  border-color: rgba(108,131,255,.2);
193
  border-radius: 16px 16px 4px 16px;
194
  }
195
- .turn.assistant .bubble {
196
- border-radius: 4px 16px 16px 16px;
197
- }
198
  .turn-meta {
199
  margin-top: 3px;
200
  font-size: 10px; color: var(--muted);
201
  font-family: var(--mono);
202
- display: flex; gap: 6px; align-items: center;
203
- flex-wrap: wrap;
204
  }
205
  .chip {
206
  border: 1px solid var(--border);
207
  border-radius: 999px;
208
  padding: 2px 7px;
209
  font-size: 9px;
210
- text-transform: uppercase;
211
- letter-spacing: .05em;
212
  }
213
  .chip.good { color: var(--good); border-color: rgba(45,212,191,.25); }
214
  .chip.muted { color: var(--muted); }
215
  .chip.warn { color: var(--warn); border-color: rgba(251,191,36,.25); }
216
  .chip.matched { color: var(--accent); border-color: rgba(108,131,255,.25); }
217
 
218
- /* ── Best answer bubble ── */
219
- .best-answer-wrap {
220
- margin-top: 2px;
221
- }
222
  .best-answer-bubble {
223
  border: 1px solid rgba(45,212,191,.15);
224
  border-radius: 4px 16px 16px 16px;
@@ -232,10 +202,8 @@
232
  display: flex; gap: 6px; align-items: center; flex-wrap: wrap;
233
  }
234
 
235
- /* ── Vote row ── */
236
- .vote-row {
237
- display: flex; gap: 4px; align-items: center; margin-top: 6px;
238
- }
239
  .vote-btn {
240
  border: 1px solid var(--border2);
241
  background: rgba(255,255,255,.02);
@@ -249,8 +217,7 @@
249
  }
250
  .vote-btn:hover {
251
  border-color: rgba(108,131,255,.35);
252
- color: var(--text);
253
- background: rgba(108,131,255,.07);
254
  }
255
  .vote-btn.voted-up {
256
  border-color: rgba(45,212,191,.5); color: var(--good);
@@ -261,6 +228,7 @@
261
  background: rgba(248,113,113,.08);
262
  }
263
  .vote-btn:active { transform: scale(.94); }
 
264
  .action-btn {
265
  border: 1px solid var(--border2);
266
  background: rgba(255,255,255,.02);
@@ -276,9 +244,80 @@
276
  color: var(--text);
277
  }
278
 
279
- /* ── Expandable other answers ── */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  .other-answers-toggle {
281
- margin-top: 6px;
282
  border: 1px solid var(--border);
283
  background: rgba(255,255,255,.02);
284
  color: var(--muted);
@@ -288,29 +327,22 @@
288
  cursor: pointer;
289
  transition: all 180ms ease;
290
  display: inline-flex; align-items: center; gap: 5px;
291
- width: auto;
292
  }
293
  .other-answers-toggle:hover {
294
- border-color: rgba(108,131,255,.3);
295
- color: var(--text);
296
  }
297
  .other-answers-toggle .arrow {
298
  display: inline-block;
299
  transition: transform 200ms ease;
300
  font-size: 10px;
301
  }
302
- .other-answers-toggle.open .arrow {
303
- transform: rotate(90deg);
304
- }
305
  .other-answers-panel {
306
  max-height: 0; overflow: hidden;
307
  transition: max-height 300ms ease, opacity 200ms ease;
308
- opacity: 0;
309
- margin-top: 4px;
310
- }
311
- .other-answers-panel.open {
312
- max-height: 2000px; opacity: 1;
313
  }
 
314
  .other-answer-card {
315
  border: 1px solid var(--border);
316
  border-radius: 12px;
@@ -320,8 +352,7 @@
320
  animation: fadeUp 200ms ease both;
321
  }
322
  .other-answer-head {
323
- display: flex; gap: 6px; flex-wrap: wrap;
324
- align-items: center;
325
  color: var(--muted); font-family: var(--mono); font-size: 10px;
326
  margin-bottom: 6px;
327
  }
@@ -330,7 +361,7 @@
330
  white-space: pre-wrap; word-break: break-word;
331
  }
332
 
333
- /* ── Versions inside other answers ── */
334
  .versions-toggle {
335
  margin-top: 4px;
336
  color: var(--muted); font-size: 10px;
@@ -345,18 +376,14 @@
345
  transition: max-height 280ms ease, opacity 180ms ease;
346
  opacity: 0;
347
  border-left: 2px solid var(--border2);
348
- padding-left: 10px;
349
- margin-top: 4px;
350
- }
351
- .versions-panel.open {
352
- max-height: 1500px; opacity: 1;
353
  }
 
354
  .version-card {
355
  border: 1px solid var(--border);
356
  background: rgba(255,255,255,.02);
357
  border-radius: 10px;
358
- padding: 8px 10px;
359
- margin-top: 4px;
360
  animation: fadeUp 180ms ease both;
361
  }
362
  .version-head {
@@ -369,16 +396,13 @@
369
  white-space: pre-wrap; word-break: break-word;
370
  }
371
 
372
- /* ── Propose version ── */
373
  .propose-panel {
374
  max-height: 0; overflow: hidden;
375
  transition: max-height 280ms ease, opacity 200ms ease;
376
- opacity: 0;
377
- margin-top: 6px;
378
- }
379
- .propose-panel.open {
380
- max-height: 400px; opacity: 1;
381
  }
 
382
  .propose-textarea {
383
  width: 100%;
384
  min-height: 60px; max-height: 140px;
@@ -393,13 +417,9 @@
393
  outline: none;
394
  transition: border-color 200ms ease;
395
  }
396
- .propose-textarea:focus {
397
- border-color: rgba(108,131,255,.4);
398
- }
399
  .propose-textarea::placeholder { color: #5a6178; }
400
- .propose-actions {
401
- display: flex; gap: 6px; margin-top: 6px;
402
- }
403
  .propose-submit {
404
  border: 1px solid rgba(108,131,255,.3);
405
  background: rgba(108,131,255,.1);
@@ -414,9 +434,7 @@
414
  background: rgba(108,131,255,.18);
415
  border-color: rgba(108,131,255,.5);
416
  }
417
- .propose-submit:disabled {
418
- opacity: .5; cursor: not-allowed;
419
- }
420
  .propose-cancel {
421
  border: 1px solid var(--border);
422
  background: transparent;
@@ -427,7 +445,7 @@
427
  cursor: pointer;
428
  }
429
 
430
- /* ── Typing indicator ── */
431
  .typing-indicator {
432
  display: flex; gap: 10px; margin-bottom: 6px;
433
  align-items: flex-start;
@@ -441,8 +459,7 @@
441
  background: rgba(255,255,255,.03);
442
  }
443
  .typing-dots span {
444
- width: 6px; height: 6px;
445
- border-radius: 50%;
446
  background: var(--muted);
447
  animation: typingBounce 1.1s ease infinite;
448
  }
@@ -453,18 +470,14 @@
453
  30% { transform: translateY(-5px); opacity: 1; }
454
  }
455
 
456
- /* ── Diffusion effect ── */
457
  .diffusion-text {
458
- filter: blur(4px);
459
- opacity: .4;
460
  transition: filter 600ms ease, opacity 600ms ease;
461
  }
462
- .diffusion-text.revealed {
463
- filter: blur(0);
464
- opacity: 1;
465
- }
466
 
467
- /* ── Composer ── */
468
  .compose {
469
  border-top: 1px solid var(--border);
470
  background: rgba(11,14,20,.85);
@@ -473,8 +486,7 @@
473
  flex-shrink: 0;
474
  }
475
  .compose-inner {
476
- max-width: 760px;
477
- margin: 0 auto;
478
  border: 1px solid var(--border2);
479
  border-radius: 14px;
480
  padding: 8px 10px 6px;
@@ -482,9 +494,7 @@
482
  box-shadow: 0 8px 32px rgba(0,0,0,.25);
483
  transition: border-color 200ms ease;
484
  }
485
- .compose-inner:focus-within {
486
- border-color: rgba(108,131,255,.3);
487
- }
488
  #prompt {
489
  width: 100%;
490
  min-height: 40px; max-height: 180px;
@@ -503,13 +513,9 @@
503
  border-top: 1px solid var(--border);
504
  padding-top: 6px;
505
  }
506
- .hint {
507
- color: var(--muted); font-size: 10px;
508
- font-family: var(--mono);
509
- }
510
  .send-btn {
511
- border: none;
512
- border-radius: 10px;
513
  padding: 7px 16px;
514
  cursor: pointer;
515
  font: inherit; font-size: 13px; font-weight: 600;
@@ -523,12 +529,9 @@
523
  box-shadow: 0 6px 20px rgba(108,131,255,.3);
524
  }
525
  .send-btn:active { transform: scale(.96); }
526
- .send-btn:disabled {
527
- opacity: .4; cursor: not-allowed;
528
- transform: none; box-shadow: none;
529
- }
530
 
531
- /* ── Settings panel ── */
532
  #settingsPanel {
533
  position: fixed;
534
  top: 56px; right: 0;
@@ -542,15 +545,11 @@
542
  transition: transform 250ms cubic-bezier(.4,0,.2,1);
543
  padding: 14px 16px;
544
  }
545
- #settingsPanel.open {
546
- transform: translateX(0);
547
- }
548
  .settings-title {
549
  font-size: 12px; font-weight: 700;
550
- text-transform: uppercase;
551
- letter-spacing: .06em;
552
- color: var(--muted);
553
- margin-bottom: 12px;
554
  }
555
  .setting-row {
556
  display: flex; align-items: center; justify-content: space-between;
@@ -558,12 +557,8 @@
558
  border-bottom: 1px solid var(--border);
559
  }
560
  .setting-row:last-child { border-bottom: none; }
561
- .setting-label {
562
- font-size: 12px; color: var(--text);
563
- }
564
- .setting-desc {
565
- font-size: 10px; color: var(--muted); margin-top: 1px;
566
- }
567
  .toggle {
568
  width: 36px; height: 20px;
569
  border-radius: 999px;
@@ -580,18 +575,15 @@
580
  }
581
  .toggle::after {
582
  content: "";
583
- position: absolute;
584
- top: 2px; left: 2px;
585
  width: 14px; height: 14px;
586
  border-radius: 50%;
587
  background: var(--text);
588
  transition: transform 200ms ease;
589
  }
590
- .toggle.on::after {
591
- transform: translateX(16px);
592
- }
593
 
594
- /* ── Toast ── */
595
  #toast {
596
  position: fixed;
597
  left: 50%; bottom: 80px;
@@ -609,19 +601,12 @@
609
  white-space: nowrap;
610
  backdrop-filter: blur(10px);
611
  }
612
- #toast.show {
613
- opacity: 1; transform: translateX(-50%) translateY(0);
614
- }
615
  #toast.good { border-color: rgba(45,212,191,.4); color: var(--good); }
616
  #toast.bad { border-color: rgba(248,113,113,.4); color: var(--bad); }
617
 
618
- /* ─── No-answer placeholder ─── */
619
- .no-answer-bubble {
620
- border-style: dashed !important;
621
- color: var(--muted);
622
- }
623
 
624
- /* ─── Responsive ─── */
625
  @media (max-width: 600px) {
626
  #topbar { padding: 0 10px; }
627
  .brand-sub { display: none; }
@@ -661,31 +646,19 @@
661
  <div id="settingsPanel">
662
  <div class="settings-title">Appearance</div>
663
  <div class="setting-row">
664
- <div>
665
- <div class="setting-label">None</div>
666
- <div class="setting-desc">Instant display</div>
667
- </div>
668
  <div class="toggle" id="togNone" data-anim="none"></div>
669
  </div>
670
  <div class="setting-row">
671
- <div>
672
- <div class="setting-label">AI typing</div>
673
- <div class="setting-desc">Typed letter by letter</div>
674
- </div>
675
  <div class="toggle" id="togAI" data-anim="ai"></div>
676
  </div>
677
  <div class="setting-row">
678
- <div>
679
- <div class="setting-label">Human typing</div>
680
- <div class="setting-desc">Irregular human speed</div>
681
- </div>
682
  <div class="toggle" id="togHuman" data-anim="human"></div>
683
  </div>
684
  <div class="setting-row">
685
- <div>
686
- <div class="setting-label">Diffusion</div>
687
- <div class="setting-desc">Blur-to-clear reveal</div>
688
- </div>
689
  <div class="toggle" id="togDiffusion" data-anim="diffusion"></div>
690
  </div>
691
  </div>
@@ -694,7 +667,7 @@
694
  <div class="wrap">
695
  <div class="welcome" id="welcome">
696
  <h1>Ask anything. Get human answers.</h1>
697
- <p>Questions are matched to existing conversations. If no answer exists yet, the community writes one.</p>
698
  </div>
699
  <div id="transcript"></div>
700
  </div>
@@ -704,7 +677,7 @@
704
  <div class="compose-inner">
705
  <textarea id="prompt" rows="1" placeholder="Ask a question…"></textarea>
706
  <div class="compose-row">
707
- <div class="hint" id="hint">Enter to send · Shift+Enter newline</div>
708
  <button class="send-btn" id="sendBtn">Ask</button>
709
  </div>
710
  </div>
@@ -713,13 +686,10 @@
713
 
714
  <div id="toast"></div>
715
 
716
- <script>
717
- window.__HI_INIT__ = {{ init_json | safe }};
718
- </script>
719
 
720
  <script>
721
  (() => {
722
- /* ── State ── */
723
  const S = {
724
  clientId: null,
725
  conversation: null,
@@ -729,7 +699,6 @@
729
  };
730
  const $ = id => document.getElementById(id);
731
 
732
- /* ── Client ID ── */
733
  function getClientId() {
734
  let id = localStorage.getItem("hi_client_id");
735
  if (!id) {
@@ -740,7 +709,6 @@
740
  return id;
741
  }
742
 
743
- /* ── Toast ── */
744
  function toast(msg, kind = "") {
745
  const t = $("toast");
746
  t.textContent = msg;
@@ -750,7 +718,6 @@
750
  t._t = setTimeout(() => { t.className = ""; }, 2000);
751
  }
752
 
753
- /* ── Status bar ── */
754
  function showStatus(text) {
755
  $("statusText").textContent = text;
756
  $("statusbar").classList.add("visible");
@@ -759,23 +726,21 @@
759
  $("statusbar").classList.remove("visible");
760
  }
761
 
762
- /* ── Helpers ── */
763
  function esc(s) {
764
  return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
765
  }
766
  function nl2br(s) { return esc(s).replace(/\n/g, "<br>"); }
767
  function fmtTime(iso) {
768
  if (!iso) return "";
769
- try {
770
- return new Date(iso).toLocaleString([], { month:"short", day:"numeric", hour:"2-digit", minute:"2-digit" });
771
- } catch { return iso; }
772
  }
773
  function scrollBottom() {
774
  const c = $("chat");
775
  requestAnimationFrame(() => { c.scrollTop = c.scrollHeight; });
776
  }
 
777
 
778
- /* ── Appearance animation ── */
779
  async function animateText(el, text) {
780
  const mode = S.animMode;
781
  if (mode === "ai") {
@@ -807,61 +772,36 @@
807
  el.innerHTML = nl2br(text);
808
  }
809
  }
810
- function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
811
 
812
- /* ── Typing indicator ── */
813
  function showTyping() {
814
  removeTyping();
815
- const h = `<div class="typing-indicator" id="typingInd">
816
- <div class="avatar assistant">✦</div>
817
- <div class="typing-dots"><span></span><span></span><span></span></div>
818
- </div>`;
819
- $("transcript").insertAdjacentHTML("beforeend", h);
820
  scrollBottom();
821
  }
822
- function removeTyping() {
823
- const el = $("typingInd");
824
- if (el) el.remove();
825
- }
826
 
827
- /* ── Active version logic ── */
828
  function activeVersion(answer) {
829
  const v = answer?.versions || [];
830
  if (!v.length) return null;
831
  let f = v.find(x => x.id === answer.active_version);
832
  if (f) return f;
833
  return [...v].sort((a, b) => {
834
- const d = (Number(b.votes||0)) - (Number(a.votes||0));
835
- if (d !== 0) return d;
836
- return String(b.created_at||"").localeCompare(String(a.created_at||""));
837
  })[0];
838
  }
839
- function answerScore(a) {
840
- const v = activeVersion(a);
841
- return v ? Number(v.votes||0) : 0;
842
- }
843
  function sortedAnswers(conv) {
844
- return [...(conv?.answers||[])].sort((a,b) => {
845
  const d = answerScore(b) - answerScore(a);
846
- if (d !== 0) return d;
847
- return String(b.created_at||"").localeCompare(String(a.created_at||""));
848
  });
849
  }
850
 
851
- /* ── Composer label ── */
852
- function setComposer() {
853
- const p = $("prompt"), b = $("sendBtn"), h = $("hint");
854
- if (!S.conversation) {
855
- p.placeholder = "Ask a question…";
856
- b.textContent = "Ask";
857
- h.textContent = "Enter to send · Shift+Enter newline";
858
- } else {
859
- p.placeholder = "Write an answer (optional)…";
860
- b.textContent = "Answer";
861
- h.textContent = "Enter to answer · Shift+Enter newline";
862
- }
863
- }
864
-
865
  /* ── Render helpers ── */
866
  function renderVoteRow(answerId, ver) {
867
  const vu = ver.votes_by_client && ver.votes_by_client[S.clientId] === 1;
@@ -873,8 +813,8 @@
873
  }
874
 
875
  function renderVersions(answer) {
876
- const active = activeVersion(answer);
877
- const others = (answer.versions||[]).filter(v => v.id !== active?.id);
878
  if (!others.length) return "";
879
  return `
880
  <button class="versions-toggle" data-toggle-versions="${answer.id}">
@@ -884,16 +824,13 @@
884
  ${others.map(v => `
885
  <div class="version-card">
886
  <div class="version-head">
887
- <span>${esc(v.author||"Anonymous")}</span>
888
- <span>·</span>
889
- <span>${fmtTime(v.created_at)}</span>
890
- <span>·</span>
891
  <span>votes: ${Number(v.votes||0)}</span>
892
  </div>
893
  <div class="version-text">${nl2br(v.text||"")}</div>
894
  ${renderVoteRow(answer.id, v)}
895
- </div>
896
- `).join("")}
897
  </div>`;
898
  }
899
 
@@ -909,6 +846,41 @@
909
  </div>`;
910
  }
911
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
912
  function renderOtherAnswers(answers) {
913
  if (answers.length <= 1) return "";
914
  const others = answers.slice(1);
@@ -917,19 +889,19 @@
917
  <span class="arrow">▶</span> ${others.length} other answer${others.length>1?"s":""}
918
  </button>
919
  <div class="other-answers-panel" id="otherAnswersPanel">
920
- ${others.map((a, idx) => {
921
  const v = activeVersion(a);
922
  if (!v) return "";
923
  return `
924
  <div class="other-answer-card">
925
  <div class="other-answer-head">
926
- <span>${esc(v.author||"Anonymous")}</span>
927
- <span>·</span>
928
  <span>${fmtTime(v.created_at)}</span>
929
  </div>
930
  <div class="other-answer-text">${nl2br(v.text||"")}</div>
931
  ${renderVoteRow(a.id, v)}
932
- <div style="margin-top:4px;display:flex;gap:4px;flex-wrap:wrap;">
933
  ${renderVersions(a)}
934
  ${renderPropose(a.id)}
935
  </div>
@@ -946,12 +918,13 @@
946
 
947
  if (!S.conversation) {
948
  wl.style.display = "block";
949
- setComposer();
950
  return;
951
  }
952
  wl.style.display = "none";
953
 
954
  const q = questionText || S.conversation.question || "";
 
 
955
  tr.insertAdjacentHTML("beforeend", `
956
  <div class="turn user">
957
  <div>
@@ -962,61 +935,53 @@
962
  </div>
963
  </div>
964
  <div class="avatar user">U</div>
965
- </div>
966
- `);
967
 
968
  const answers = sortedAnswers(S.conversation);
969
 
970
  if (!answers.length) {
 
971
  tr.insertAdjacentHTML("beforeend", `
972
  <div class="turn assistant">
973
  <div class="avatar assistant">✦</div>
974
  <div>
975
- <div class="bubble no-answer-bubble">No answer yet. You can write one below.</div>
976
  <div class="turn-meta"><span class="chip warn">awaiting answer</span></div>
 
977
  </div>
978
- </div>
979
- `);
980
  } else {
 
981
  const best = answers[0];
982
- const bv = activeVersion(best);
983
- if (bv) {
984
- tr.insertAdjacentHTML("beforeend", `
985
- <div class="turn assistant">
986
- <div class="avatar assistant">✦</div>
987
- <div class="best-answer-wrap">
988
- <div class="best-answer-bubble" id="bestAnswerText"></div>
989
- <div class="best-answer-meta turn-meta">
990
- <span class="chip good">best answer</span>
991
- <span>${esc(bv.author||"Anonymous")}</span>
992
- <span>·</span>
993
- <span>${fmtTime(bv.created_at)}</span>
994
- </div>
995
- ${renderVoteRow(best.id, bv)}
996
- <div style="display:flex;gap:4px;flex-wrap:wrap;margin-top:4px;">
997
- ${renderVersions(best)}
998
- ${renderPropose(best.id)}
999
- </div>
1000
- ${renderOtherAnswers(answers)}
1001
- </div>
1002
  </div>
1003
- `);
1004
 
 
 
 
 
1005
  if (doAnimate) {
1006
- await animateText($("bestAnswerText"), bv.text || "");
1007
  } else {
1008
- $("bestAnswerText").innerHTML = nl2br(bv.text || "");
1009
  }
1010
  }
1011
  }
1012
 
1013
- setComposer();
1014
  bindHandlers();
1015
  scrollBottom();
1016
  }
1017
 
1018
- /* ── Bind dynamic handlers ── */
1019
  function bindHandlers() {
 
1020
  document.querySelectorAll("[data-vote]").forEach(btn => {
1021
  btn.onclick = async () => {
1022
  if (!S.conversation) return;
@@ -1029,78 +994,115 @@
1029
  btn.style.transform = "";
1030
  if (res.ok) {
1031
  S.conversation = res.conversation;
1032
- localStorage.setItem("hi_last_cid", S.conversation.id);
1033
- renderConversation(S.currentQuestion, false);
1034
- } else {
1035
- toast(res.error || "Error", "bad");
1036
- }
1037
  };
1038
  });
1039
 
 
1040
  document.querySelectorAll("[data-toggle-versions]").forEach(btn => {
1041
  btn.onclick = () => {
1042
  const id = btn.getAttribute("data-toggle-versions");
1043
- const panel = $("vp-" + id);
1044
- if (!panel) return;
1045
- const open = panel.classList.toggle("open");
1046
  btn.querySelector(".arrow").style.transform = open ? "rotate(90deg)" : "";
1047
  };
1048
  });
1049
 
1050
- const oaToggle = $("otherAnswersToggle");
1051
- if (oaToggle) {
1052
- oaToggle.onclick = () => {
1053
- const panel = $("otherAnswersPanel");
1054
- if (!panel) return;
1055
- const open = panel.classList.toggle("open");
1056
- oaToggle.classList.toggle("open", open);
1057
- };
1058
- }
1059
 
 
1060
  document.querySelectorAll("[data-propose]").forEach(btn => {
1061
  btn.onclick = () => {
1062
- const id = btn.getAttribute("data-propose");
1063
- const panel = $("pp-" + id);
1064
- if (!panel) return;
1065
- panel.classList.toggle("open");
1066
  };
1067
  });
1068
-
1069
  document.querySelectorAll("[data-cancel-propose]").forEach(btn => {
1070
  btn.onclick = () => {
1071
- const id = btn.getAttribute("data-cancel-propose");
1072
- const panel = $("pp-" + id);
1073
- if (panel) panel.classList.remove("open");
1074
  };
1075
  });
1076
 
 
1077
  document.querySelectorAll("[data-submit-proposal]").forEach(btn => {
1078
  btn.onclick = async () => {
1079
  const aid = btn.getAttribute("data-submit-proposal");
1080
- const panel = $("pp-" + aid);
1081
- const ta = panel ? panel.querySelector("textarea") : null;
1082
  const text = ta ? ta.value.trim() : "";
1083
- if (!text) { toast("Empty proposal", "bad"); return; }
1084
  if (!S.conversation) return;
1085
  btn.disabled = true;
1086
  const orig = btn.textContent;
1087
  btn.textContent = "Saving…";
1088
  const res = await callAPI("propose", {
1089
- conversation_id: S.conversation.id,
1090
- answer_id: aid, text,
1091
  });
1092
- btn.disabled = false;
1093
- btn.textContent = orig;
1094
  if (res.ok) {
1095
  S.conversation = res.conversation;
1096
- localStorage.setItem("hi_last_cid", S.conversation.id);
1097
- renderConversation(S.currentQuestion, false);
1098
- toast("Version proposed", "good");
1099
- } else {
1100
- toast(res.error || "Error", "bad");
1101
- }
1102
  };
1103
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1104
  }
1105
 
1106
  /* ── API ── */
@@ -1113,46 +1115,34 @@
1113
  return resp.json();
1114
  }
1115
 
1116
- /* ── Ask ── */
1117
  async function askQuestion(q) {
1118
  showStatus("Searching for answers…");
1119
  showTyping();
 
 
1120
  const res = await callAPI("ask", { question: q });
1121
  removeTyping();
1122
  hideStatus();
 
 
1123
  if (!res.ok) { toast(res.error||"Error","bad"); return; }
1124
  S.conversation = res.conversation;
1125
  S.currentQuestion = q;
1126
- localStorage.setItem("hi_last_cid", S.conversation.id);
1127
- if (res.matched) toast("Matched existing answer","good");
1128
- else toast("New question created","good");
1129
  await renderConversation(q, true);
1130
  }
1131
 
1132
- /* ── Answer ── */
1133
- async function postAnswer(text) {
1134
- if (!S.conversation) return;
1135
- showStatus("Saving answer…");
1136
- const res = await callAPI("answer", { conversation_id: S.conversation.id, text });
1137
- hideStatus();
1138
- if (!res.ok) { toast(res.error||"Error","bad"); return; }
1139
- S.conversation = res.conversation;
1140
- localStorage.setItem("hi_last_cid", S.conversation.id);
1141
- renderConversation(S.currentQuestion, false);
1142
- toast("Answer saved","good");
1143
- }
1144
-
1145
- /* ── Submit ── */
1146
  async function submitPrompt() {
1147
  const p = $("prompt");
1148
  const text = p.value.trim();
1149
- if (!text) return;
1150
  p.value = "";
1151
  autoGrow(p);
1152
- $("sendBtn").disabled = true;
1153
- if (!S.conversation) await askQuestion(text);
1154
- else await postAnswer(text);
1155
- $("sendBtn").disabled = false;
1156
  }
1157
 
1158
  function autoGrow(el) {
@@ -1160,7 +1150,6 @@
1160
  el.style.height = Math.min(el.scrollHeight, 180) + "px";
1161
  }
1162
 
1163
- /* ── Load saved ── */
1164
  async function loadSaved() {
1165
  const id = localStorage.getItem("hi_last_cid");
1166
  if (!id) return;
@@ -1174,7 +1163,6 @@
1174
  }
1175
  }
1176
 
1177
- /* ── New chat ── */
1178
  function newChat() {
1179
  S.conversation = null;
1180
  S.currentQuestion = "";
@@ -1182,38 +1170,31 @@
1182
  $("transcript").innerHTML = "";
1183
  $("welcome").style.display = "block";
1184
  $("prompt").value = "";
1185
- setComposer();
1186
  $("prompt").focus();
1187
  }
1188
 
1189
  /* ── Settings ── */
1190
  function initSettings() {
1191
  const panel = $("settingsPanel");
1192
- $("settingsBtn").onclick = () => {
1193
- panel.classList.toggle("open");
1194
- };
1195
  document.addEventListener("click", e => {
1196
- if (!panel.contains(e.target) && e.target !== $("settingsBtn") && !$("settingsBtn").contains(e.target)) {
1197
  panel.classList.remove("open");
1198
- }
1199
  });
1200
-
1201
- const toggles = ["togNone","togAI","togHuman","togDiffusion"];
1202
- function updateToggles() {
1203
- toggles.forEach(id => {
1204
- const el = $(id);
1205
- const m = el.getAttribute("data-anim");
1206
- el.classList.toggle("on", S.animMode === m);
1207
  });
1208
  }
1209
- toggles.forEach(id => {
1210
  $(id).onclick = () => {
1211
  S.animMode = $(id).getAttribute("data-anim");
1212
  localStorage.setItem("hi_anim", S.animMode);
1213
- updateToggles();
1214
  };
1215
  });
1216
- updateToggles();
1217
  }
1218
 
1219
  /* ── Init ── */
@@ -1229,7 +1210,7 @@
1229
  const d = window.__HI_INIT__ || {};
1230
  if (d.client_id) S.clientId = d.client_id;
1231
  loadSaved().then(() => {
1232
- if (!S.conversation) { setComposer(); $("prompt").focus(); }
1233
  });
1234
  }
1235
  init();
 
19
  --bad: #f87171;
20
  --warn: #fbbf24;
21
  --shadow: 0 20px 60px rgba(0,0,0,.35);
 
 
22
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
23
  --font: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
 
24
  }
25
  * { box-sizing: border-box; margin: 0; padding: 0; }
26
  html, body {
 
38
  linear-gradient(rgba(108,131,255,.03) 1px, transparent 1px),
39
  linear-gradient(90deg, rgba(108,131,255,.03) 1px, transparent 1px);
40
  background-size: 44px 44px;
41
+ pointer-events: none; opacity: .3;
 
42
  }
43
  #app {
44
  position: relative; z-index: 1;
45
  height: 100%; display: flex; flex-direction: column;
46
  }
47
 
48
+ /* ── Topbar ── */
49
  #topbar {
50
+ height: 56px; padding: 0 16px;
 
51
  display: flex; align-items: center; justify-content: space-between;
52
  border-bottom: 1px solid var(--border);
53
  backdrop-filter: blur(14px);
54
  background: rgba(11,14,20,.78);
55
  flex-shrink: 0;
56
  }
57
+ .brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
 
 
58
  .logo {
59
  width: 30px; height: 30px; border-radius: 10px;
60
  display: grid; place-items: center; font-size: 15px;
 
62
  box-shadow: 0 6px 18px rgba(108,131,255,.25);
63
  flex: 0 0 auto;
64
  }
65
+ .brand-title { font-weight: 700; letter-spacing: -.03em; font-size: 15px; }
66
+ .brand-sub { color: var(--muted); font-size: 11px; font-family: var(--mono); margin-left: 2px; }
 
 
 
 
 
67
  .top-actions { display: flex; align-items: center; gap: 6px; }
68
  .top-btn {
69
  border: 1px solid var(--border2);
 
83
  }
84
  .top-btn svg { width: 14px; height: 14px; }
85
 
86
+ /* ── Status bar ── */
87
  #statusbar {
88
  height: 0; overflow: hidden;
89
  transition: height 220ms ease, opacity 220ms ease;
 
92
  background: rgba(11,14,20,.6);
93
  display: flex; align-items: center; justify-content: center;
94
  font-size: 12px; font-family: var(--mono);
95
+ color: var(--muted); flex-shrink: 0;
 
 
 
 
 
96
  }
97
+ #statusbar.visible { height: 32px; opacity: 1; border-bottom-color: var(--border); }
98
  #statusbar .status-dot {
99
  width: 6px; height: 6px; border-radius: 50%;
100
  margin-right: 8px; display: inline-block;
 
106
  50% { opacity: 1; transform: scale(1.1); }
107
  }
108
 
109
+ /* ── Chat area ── */
110
  #chat {
111
  flex: 1; overflow-y: auto;
112
  padding: 20px 14px 24px;
 
117
  #chat::-webkit-scrollbar-thumb { background: rgba(255,255,255,.1); border-radius: 9px; }
118
  .wrap { max-width: 760px; margin: 0 auto; }
119
 
120
+ /* ── Welcome ── */
121
  .welcome {
122
  margin: 6vh auto 0; max-width: 480px; text-align: center;
123
  padding: 24px 20px;
 
127
  box-shadow: var(--shadow);
128
  animation: fadeUp 400ms ease both;
129
  }
130
+ .welcome h1 { font-size: 22px; font-weight: 700; letter-spacing: -.03em; line-height: 1.3; }
131
+ .welcome p { color: var(--muted); line-height: 1.6; margin-top: 8px; font-size: 13px; }
 
 
 
 
 
132
  @keyframes fadeUp {
133
  from { opacity: 0; transform: translateY(12px); }
134
  to { opacity: 1; transform: translateY(0); }
135
  }
136
 
137
+ /* ── Turns ── */
138
  .turn {
139
  display: flex; gap: 10px; margin-bottom: 6px;
140
  align-items: flex-start;
141
+ animation: fadeUp 280ms ease both;
142
  }
143
  .turn.user { justify-content: flex-end; }
144
  .avatar {
 
164
  line-height: 1.6; font-size: 14px;
165
  white-space: pre-wrap; word-break: break-word;
166
  background: rgba(255,255,255,.03);
 
167
  }
168
  .turn.user .bubble {
169
  background: linear-gradient(135deg, rgba(108,131,255,.15), rgba(161,110,255,.12));
170
  border-color: rgba(108,131,255,.2);
171
  border-radius: 16px 16px 4px 16px;
172
  }
 
 
 
173
  .turn-meta {
174
  margin-top: 3px;
175
  font-size: 10px; color: var(--muted);
176
  font-family: var(--mono);
177
+ display: flex; gap: 6px; align-items: center; flex-wrap: wrap;
 
178
  }
179
  .chip {
180
  border: 1px solid var(--border);
181
  border-radius: 999px;
182
  padding: 2px 7px;
183
  font-size: 9px;
184
+ text-transform: uppercase; letter-spacing: .05em;
 
185
  }
186
  .chip.good { color: var(--good); border-color: rgba(45,212,191,.25); }
187
  .chip.muted { color: var(--muted); }
188
  .chip.warn { color: var(--warn); border-color: rgba(251,191,36,.25); }
189
  .chip.matched { color: var(--accent); border-color: rgba(108,131,255,.25); }
190
 
191
+ /* ── Best answer ── */
 
 
 
192
  .best-answer-bubble {
193
  border: 1px solid rgba(45,212,191,.15);
194
  border-radius: 4px 16px 16px 16px;
 
202
  display: flex; gap: 6px; align-items: center; flex-wrap: wrap;
203
  }
204
 
205
+ /* ── Vote ── */
206
+ .vote-row { display: flex; gap: 4px; align-items: center; margin-top: 6px; }
 
 
207
  .vote-btn {
208
  border: 1px solid var(--border2);
209
  background: rgba(255,255,255,.02);
 
217
  }
218
  .vote-btn:hover {
219
  border-color: rgba(108,131,255,.35);
220
+ color: var(--text); background: rgba(108,131,255,.07);
 
221
  }
222
  .vote-btn.voted-up {
223
  border-color: rgba(45,212,191,.5); color: var(--good);
 
228
  background: rgba(248,113,113,.08);
229
  }
230
  .vote-btn:active { transform: scale(.94); }
231
+
232
  .action-btn {
233
  border: 1px solid var(--border2);
234
  background: rgba(255,255,255,.02);
 
244
  color: var(--text);
245
  }
246
 
247
+ /* ── Write answer inline panel ── */
248
+ .write-answer-btn {
249
+ border: 1px solid rgba(45,212,191,.3);
250
+ background: rgba(45,212,191,.08);
251
+ color: var(--good);
252
+ border-radius: 10px;
253
+ padding: 8px 14px;
254
+ font: inherit; font-size: 12px; font-weight: 600;
255
+ cursor: pointer;
256
+ transition: all 180ms ease;
257
+ display: inline-flex; align-items: center; gap: 6px;
258
+ margin-top: 8px;
259
+ }
260
+ .write-answer-btn:hover {
261
+ background: rgba(45,212,191,.14);
262
+ border-color: rgba(45,212,191,.5);
263
+ }
264
+ .write-answer-btn svg { width: 14px; height: 14px; }
265
+
266
+ .write-panel {
267
+ max-height: 0; overflow: hidden;
268
+ transition: max-height 300ms ease, opacity 250ms ease, margin 200ms ease;
269
+ opacity: 0; margin-top: 0;
270
+ }
271
+ .write-panel.open {
272
+ max-height: 300px; opacity: 1; margin-top: 10px;
273
+ }
274
+ .write-textarea {
275
+ width: 100%;
276
+ min-height: 80px; max-height: 160px;
277
+ resize: vertical;
278
+ border: 1px solid var(--border2);
279
+ border-radius: 12px;
280
+ background: var(--panel);
281
+ color: var(--text);
282
+ font: inherit; font-size: 13px;
283
+ line-height: 1.55;
284
+ padding: 10px 12px;
285
+ outline: none;
286
+ transition: border-color 200ms ease;
287
+ }
288
+ .write-textarea:focus { border-color: rgba(45,212,191,.4); }
289
+ .write-textarea::placeholder { color: #5a6178; }
290
+ .write-actions {
291
+ display: flex; gap: 6px; margin-top: 6px;
292
+ }
293
+ .write-submit {
294
+ border: 1px solid rgba(45,212,191,.4);
295
+ background: rgba(45,212,191,.12);
296
+ color: var(--good);
297
+ border-radius: 8px;
298
+ padding: 6px 14px;
299
+ font: inherit; font-size: 12px; font-weight: 600;
300
+ cursor: pointer;
301
+ transition: all 160ms ease;
302
+ }
303
+ .write-submit:hover {
304
+ background: rgba(45,212,191,.2);
305
+ border-color: rgba(45,212,191,.6);
306
+ }
307
+ .write-submit:disabled { opacity: .4; cursor: not-allowed; }
308
+ .write-cancel {
309
+ border: 1px solid var(--border);
310
+ background: transparent;
311
+ color: var(--muted);
312
+ border-radius: 8px;
313
+ padding: 6px 12px;
314
+ font: inherit; font-size: 12px;
315
+ cursor: pointer;
316
+ }
317
+
318
+ /* ── Other answers expandable ── */
319
  .other-answers-toggle {
320
+ margin-top: 8px;
321
  border: 1px solid var(--border);
322
  background: rgba(255,255,255,.02);
323
  color: var(--muted);
 
327
  cursor: pointer;
328
  transition: all 180ms ease;
329
  display: inline-flex; align-items: center; gap: 5px;
 
330
  }
331
  .other-answers-toggle:hover {
332
+ border-color: rgba(108,131,255,.3); color: var(--text);
 
333
  }
334
  .other-answers-toggle .arrow {
335
  display: inline-block;
336
  transition: transform 200ms ease;
337
  font-size: 10px;
338
  }
339
+ .other-answers-toggle.open .arrow { transform: rotate(90deg); }
 
 
340
  .other-answers-panel {
341
  max-height: 0; overflow: hidden;
342
  transition: max-height 300ms ease, opacity 200ms ease;
343
+ opacity: 0; margin-top: 4px;
 
 
 
 
344
  }
345
+ .other-answers-panel.open { max-height: 3000px; opacity: 1; }
346
  .other-answer-card {
347
  border: 1px solid var(--border);
348
  border-radius: 12px;
 
352
  animation: fadeUp 200ms ease both;
353
  }
354
  .other-answer-head {
355
+ display: flex; gap: 6px; flex-wrap: wrap; align-items: center;
 
356
  color: var(--muted); font-family: var(--mono); font-size: 10px;
357
  margin-bottom: 6px;
358
  }
 
361
  white-space: pre-wrap; word-break: break-word;
362
  }
363
 
364
+ /* ── Versions ── */
365
  .versions-toggle {
366
  margin-top: 4px;
367
  color: var(--muted); font-size: 10px;
 
376
  transition: max-height 280ms ease, opacity 180ms ease;
377
  opacity: 0;
378
  border-left: 2px solid var(--border2);
379
+ padding-left: 10px; margin-top: 4px;
 
 
 
 
380
  }
381
+ .versions-panel.open { max-height: 1500px; opacity: 1; }
382
  .version-card {
383
  border: 1px solid var(--border);
384
  background: rgba(255,255,255,.02);
385
  border-radius: 10px;
386
+ padding: 8px 10px; margin-top: 4px;
 
387
  animation: fadeUp 180ms ease both;
388
  }
389
  .version-head {
 
396
  white-space: pre-wrap; word-break: break-word;
397
  }
398
 
399
+ /* ── Propose version ── */
400
  .propose-panel {
401
  max-height: 0; overflow: hidden;
402
  transition: max-height 280ms ease, opacity 200ms ease;
403
+ opacity: 0; margin-top: 6px;
 
 
 
 
404
  }
405
+ .propose-panel.open { max-height: 400px; opacity: 1; }
406
  .propose-textarea {
407
  width: 100%;
408
  min-height: 60px; max-height: 140px;
 
417
  outline: none;
418
  transition: border-color 200ms ease;
419
  }
420
+ .propose-textarea:focus { border-color: rgba(108,131,255,.4); }
 
 
421
  .propose-textarea::placeholder { color: #5a6178; }
422
+ .propose-actions { display: flex; gap: 6px; margin-top: 6px; }
 
 
423
  .propose-submit {
424
  border: 1px solid rgba(108,131,255,.3);
425
  background: rgba(108,131,255,.1);
 
434
  background: rgba(108,131,255,.18);
435
  border-color: rgba(108,131,255,.5);
436
  }
437
+ .propose-submit:disabled { opacity: .5; cursor: not-allowed; }
 
 
438
  .propose-cancel {
439
  border: 1px solid var(--border);
440
  background: transparent;
 
445
  cursor: pointer;
446
  }
447
 
448
+ /* ── Typing indicator ── */
449
  .typing-indicator {
450
  display: flex; gap: 10px; margin-bottom: 6px;
451
  align-items: flex-start;
 
459
  background: rgba(255,255,255,.03);
460
  }
461
  .typing-dots span {
462
+ width: 6px; height: 6px; border-radius: 50%;
 
463
  background: var(--muted);
464
  animation: typingBounce 1.1s ease infinite;
465
  }
 
470
  30% { transform: translateY(-5px); opacity: 1; }
471
  }
472
 
473
+ /* ── Diffusion ── */
474
  .diffusion-text {
475
+ filter: blur(4px); opacity: .4;
 
476
  transition: filter 600ms ease, opacity 600ms ease;
477
  }
478
+ .diffusion-text.revealed { filter: blur(0); opacity: 1; }
 
 
 
479
 
480
+ /* ── Composer (questions only) ── */
481
  .compose {
482
  border-top: 1px solid var(--border);
483
  background: rgba(11,14,20,.85);
 
486
  flex-shrink: 0;
487
  }
488
  .compose-inner {
489
+ max-width: 760px; margin: 0 auto;
 
490
  border: 1px solid var(--border2);
491
  border-radius: 14px;
492
  padding: 8px 10px 6px;
 
494
  box-shadow: 0 8px 32px rgba(0,0,0,.25);
495
  transition: border-color 200ms ease;
496
  }
497
+ .compose-inner:focus-within { border-color: rgba(108,131,255,.3); }
 
 
498
  #prompt {
499
  width: 100%;
500
  min-height: 40px; max-height: 180px;
 
513
  border-top: 1px solid var(--border);
514
  padding-top: 6px;
515
  }
516
+ .hint { color: var(--muted); font-size: 10px; font-family: var(--mono); }
 
 
 
517
  .send-btn {
518
+ border: none; border-radius: 10px;
 
519
  padding: 7px 16px;
520
  cursor: pointer;
521
  font: inherit; font-size: 13px; font-weight: 600;
 
529
  box-shadow: 0 6px 20px rgba(108,131,255,.3);
530
  }
531
  .send-btn:active { transform: scale(.96); }
532
+ .send-btn:disabled { opacity: .4; cursor: not-allowed; transform: none; box-shadow: none; }
 
 
 
533
 
534
+ /* ── Settings panel ── */
535
  #settingsPanel {
536
  position: fixed;
537
  top: 56px; right: 0;
 
545
  transition: transform 250ms cubic-bezier(.4,0,.2,1);
546
  padding: 14px 16px;
547
  }
548
+ #settingsPanel.open { transform: translateX(0); }
 
 
549
  .settings-title {
550
  font-size: 12px; font-weight: 700;
551
+ text-transform: uppercase; letter-spacing: .06em;
552
+ color: var(--muted); margin-bottom: 12px;
 
 
553
  }
554
  .setting-row {
555
  display: flex; align-items: center; justify-content: space-between;
 
557
  border-bottom: 1px solid var(--border);
558
  }
559
  .setting-row:last-child { border-bottom: none; }
560
+ .setting-label { font-size: 12px; color: var(--text); }
561
+ .setting-desc { font-size: 10px; color: var(--muted); margin-top: 1px; }
 
 
 
 
562
  .toggle {
563
  width: 36px; height: 20px;
564
  border-radius: 999px;
 
575
  }
576
  .toggle::after {
577
  content: "";
578
+ position: absolute; top: 2px; left: 2px;
 
579
  width: 14px; height: 14px;
580
  border-radius: 50%;
581
  background: var(--text);
582
  transition: transform 200ms ease;
583
  }
584
+ .toggle.on::after { transform: translateX(16px); }
 
 
585
 
586
+ /* ── Toast ── */
587
  #toast {
588
  position: fixed;
589
  left: 50%; bottom: 80px;
 
601
  white-space: nowrap;
602
  backdrop-filter: blur(10px);
603
  }
604
+ #toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
 
 
605
  #toast.good { border-color: rgba(45,212,191,.4); color: var(--good); }
606
  #toast.bad { border-color: rgba(248,113,113,.4); color: var(--bad); }
607
 
608
+ .no-answer-bubble { border-style: dashed !important; color: var(--muted); }
 
 
 
 
609
 
 
610
  @media (max-width: 600px) {
611
  #topbar { padding: 0 10px; }
612
  .brand-sub { display: none; }
 
646
  <div id="settingsPanel">
647
  <div class="settings-title">Appearance</div>
648
  <div class="setting-row">
649
+ <div><div class="setting-label">None</div><div class="setting-desc">Instant display</div></div>
 
 
 
650
  <div class="toggle" id="togNone" data-anim="none"></div>
651
  </div>
652
  <div class="setting-row">
653
+ <div><div class="setting-label">AI typing</div><div class="setting-desc">Fast character stream</div></div>
 
 
 
654
  <div class="toggle" id="togAI" data-anim="ai"></div>
655
  </div>
656
  <div class="setting-row">
657
+ <div><div class="setting-label">Human typing</div><div class="setting-desc">Irregular human speed</div></div>
 
 
 
658
  <div class="toggle" id="togHuman" data-anim="human"></div>
659
  </div>
660
  <div class="setting-row">
661
+ <div><div class="setting-label">Diffusion</div><div class="setting-desc">Blur-to-clear reveal</div></div>
 
 
 
662
  <div class="toggle" id="togDiffusion" data-anim="diffusion"></div>
663
  </div>
664
  </div>
 
667
  <div class="wrap">
668
  <div class="welcome" id="welcome">
669
  <h1>Ask anything. Get human answers.</h1>
670
+ <p>Type a question below. If a matching answer exists, it appears instantly. Otherwise, anyone can write the first answer.</p>
671
  </div>
672
  <div id="transcript"></div>
673
  </div>
 
677
  <div class="compose-inner">
678
  <textarea id="prompt" rows="1" placeholder="Ask a question…"></textarea>
679
  <div class="compose-row">
680
+ <div class="hint" id="hint">Enter to ask · Shift+Enter newline</div>
681
  <button class="send-btn" id="sendBtn">Ask</button>
682
  </div>
683
  </div>
 
686
 
687
  <div id="toast"></div>
688
 
689
+ <script>window.__HI_INIT__ = {{ init_json | safe }};</script>
 
 
690
 
691
  <script>
692
  (() => {
 
693
  const S = {
694
  clientId: null,
695
  conversation: null,
 
699
  };
700
  const $ = id => document.getElementById(id);
701
 
 
702
  function getClientId() {
703
  let id = localStorage.getItem("hi_client_id");
704
  if (!id) {
 
709
  return id;
710
  }
711
 
 
712
  function toast(msg, kind = "") {
713
  const t = $("toast");
714
  t.textContent = msg;
 
718
  t._t = setTimeout(() => { t.className = ""; }, 2000);
719
  }
720
 
 
721
  function showStatus(text) {
722
  $("statusText").textContent = text;
723
  $("statusbar").classList.add("visible");
 
726
  $("statusbar").classList.remove("visible");
727
  }
728
 
 
729
  function esc(s) {
730
  return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
731
  }
732
  function nl2br(s) { return esc(s).replace(/\n/g, "<br>"); }
733
  function fmtTime(iso) {
734
  if (!iso) return "";
735
+ try { return new Date(iso).toLocaleString([], { month:"short", day:"numeric", hour:"2-digit", minute:"2-digit" }); }
736
+ catch { return iso; }
 
737
  }
738
  function scrollBottom() {
739
  const c = $("chat");
740
  requestAnimationFrame(() => { c.scrollTop = c.scrollHeight; });
741
  }
742
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
743
 
 
744
  async function animateText(el, text) {
745
  const mode = S.animMode;
746
  if (mode === "ai") {
 
772
  el.innerHTML = nl2br(text);
773
  }
774
  }
 
775
 
 
776
  function showTyping() {
777
  removeTyping();
778
+ $("transcript").insertAdjacentHTML("beforeend", `
779
+ <div class="typing-indicator" id="typingInd">
780
+ <div class="avatar assistant"></div>
781
+ <div class="typing-dots"><span></span><span></span><span></span></div>
782
+ </div>`);
783
  scrollBottom();
784
  }
785
+ function removeTyping() { const el = $("typingInd"); if (el) el.remove(); }
 
 
 
786
 
 
787
  function activeVersion(answer) {
788
  const v = answer?.versions || [];
789
  if (!v.length) return null;
790
  let f = v.find(x => x.id === answer.active_version);
791
  if (f) return f;
792
  return [...v].sort((a, b) => {
793
+ const d = Number(b.votes||0) - Number(a.votes||0);
794
+ return d !== 0 ? d : String(b.created_at||"").localeCompare(String(a.created_at||""));
 
795
  })[0];
796
  }
797
+ function answerScore(a) { const v = activeVersion(a); return v ? Number(v.votes||0) : 0; }
 
 
 
798
  function sortedAnswers(conv) {
799
+ return [...(conv?.answers||[])].sort((a, b) => {
800
  const d = answerScore(b) - answerScore(a);
801
+ return d !== 0 ? d : String(b.created_at||"").localeCompare(String(a.created_at||""));
 
802
  });
803
  }
804
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
805
  /* ── Render helpers ── */
806
  function renderVoteRow(answerId, ver) {
807
  const vu = ver.votes_by_client && ver.votes_by_client[S.clientId] === 1;
 
813
  }
814
 
815
  function renderVersions(answer) {
816
+ const act = activeVersion(answer);
817
+ const others = (answer.versions||[]).filter(v => v.id !== act?.id);
818
  if (!others.length) return "";
819
  return `
820
  <button class="versions-toggle" data-toggle-versions="${answer.id}">
 
824
  ${others.map(v => `
825
  <div class="version-card">
826
  <div class="version-head">
827
+ <span>${esc(v.author||"Anonymous")}</span><span>·</span>
828
+ <span>${fmtTime(v.created_at)}</span><span>·</span>
 
 
829
  <span>votes: ${Number(v.votes||0)}</span>
830
  </div>
831
  <div class="version-text">${nl2br(v.text||"")}</div>
832
  ${renderVoteRow(answer.id, v)}
833
+ </div>`).join("")}
 
834
  </div>`;
835
  }
836
 
 
846
  </div>`;
847
  }
848
 
849
+ function renderWriteAnswer() {
850
+ return `
851
+ <button class="write-answer-btn" id="writeAnswerBtn">
852
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
853
+ Write an answer
854
+ </button>
855
+ <div class="write-panel" id="writePanel">
856
+ <textarea class="write-textarea" id="writeTextarea" placeholder="Write your answer here…" rows="4"></textarea>
857
+ <div class="write-actions">
858
+ <button class="write-submit" id="writeSubmit">Submit answer</button>
859
+ <button class="write-cancel" id="writeCancel">Cancel</button>
860
+ </div>
861
+ </div>`;
862
+ }
863
+
864
+ function renderAnswerBlock(answer, idx, isBest) {
865
+ const v = activeVersion(answer);
866
+ if (!v) return "";
867
+ const label = isBest ? `<span class="chip good">best answer</span>` : `<span class="chip muted">answer ${idx + 1}</span>`;
868
+ const bubbleId = isBest ? 'id="bestAnswerText"' : '';
869
+ const bubbleClass = isBest ? "best-answer-bubble" : "bubble";
870
+ return `
871
+ <div ${bubbleId} class="${bubbleClass}">${isBest ? "" : nl2br(v.text||"")}</div>
872
+ <div class="turn-meta" style="margin-top:4px;">
873
+ ${label}
874
+ <span>${esc(v.author||"Anonymous")}</span><span>·</span>
875
+ <span>${fmtTime(v.created_at)}</span>
876
+ </div>
877
+ ${renderVoteRow(answer.id, v)}
878
+ <div style="display:flex;gap:4px;flex-wrap:wrap;margin-top:4px;">
879
+ ${renderVersions(answer)}
880
+ ${renderPropose(answer.id)}
881
+ </div>`;
882
+ }
883
+
884
  function renderOtherAnswers(answers) {
885
  if (answers.length <= 1) return "";
886
  const others = answers.slice(1);
 
889
  <span class="arrow">▶</span> ${others.length} other answer${others.length>1?"s":""}
890
  </button>
891
  <div class="other-answers-panel" id="otherAnswersPanel">
892
+ ${others.map((a, i) => {
893
  const v = activeVersion(a);
894
  if (!v) return "";
895
  return `
896
  <div class="other-answer-card">
897
  <div class="other-answer-head">
898
+ <span class="chip muted">answer ${i + 2}</span>
899
+ <span>${esc(v.author||"Anonymous")}</span><span>·</span>
900
  <span>${fmtTime(v.created_at)}</span>
901
  </div>
902
  <div class="other-answer-text">${nl2br(v.text||"")}</div>
903
  ${renderVoteRow(a.id, v)}
904
+ <div style="display:flex;gap:4px;flex-wrap:wrap;margin-top:4px;">
905
  ${renderVersions(a)}
906
  ${renderPropose(a.id)}
907
  </div>
 
918
 
919
  if (!S.conversation) {
920
  wl.style.display = "block";
 
921
  return;
922
  }
923
  wl.style.display = "none";
924
 
925
  const q = questionText || S.conversation.question || "";
926
+
927
+ // Question bubble
928
  tr.insertAdjacentHTML("beforeend", `
929
  <div class="turn user">
930
  <div>
 
935
  </div>
936
  </div>
937
  <div class="avatar user">U</div>
938
+ </div>`);
 
939
 
940
  const answers = sortedAnswers(S.conversation);
941
 
942
  if (!answers.length) {
943
+ // No answers yet — show placeholder + write button
944
  tr.insertAdjacentHTML("beforeend", `
945
  <div class="turn assistant">
946
  <div class="avatar assistant">✦</div>
947
  <div>
948
+ <div class="bubble no-answer-bubble">No answer yet. Be the first to write one.</div>
949
  <div class="turn-meta"><span class="chip warn">awaiting answer</span></div>
950
+ ${renderWriteAnswer()}
951
  </div>
952
+ </div>`);
 
953
  } else {
954
+ // Best answer + write more + other answers
955
  const best = answers[0];
956
+ tr.insertAdjacentHTML("beforeend", `
957
+ <div class="turn assistant">
958
+ <div class="avatar assistant">✦</div>
959
+ <div style="min-width:0;flex:1;">
960
+ ${renderAnswerBlock(best, 0, true)}
961
+ ${renderWriteAnswer()}
962
+ ${renderOtherAnswers(answers)}
 
 
 
 
 
 
 
 
 
 
 
 
 
963
  </div>
964
+ </div>`);
965
 
966
+ // Animate best answer text
967
+ const bestV = activeVersion(best);
968
+ if (bestV) {
969
+ const el = $("bestAnswerText");
970
  if (doAnimate) {
971
+ await animateText(el, bestV.text || "");
972
  } else {
973
+ el.innerHTML = nl2br(bestV.text || "");
974
  }
975
  }
976
  }
977
 
 
978
  bindHandlers();
979
  scrollBottom();
980
  }
981
 
982
+ /* ── Bind handlers ── */
983
  function bindHandlers() {
984
+ // Votes
985
  document.querySelectorAll("[data-vote]").forEach(btn => {
986
  btn.onclick = async () => {
987
  if (!S.conversation) return;
 
994
  btn.style.transform = "";
995
  if (res.ok) {
996
  S.conversation = res.conversation;
997
+ save(); renderConversation(S.currentQuestion, false);
998
+ } else toast(res.error||"Error","bad");
 
 
 
999
  };
1000
  });
1001
 
1002
+ // Toggle versions
1003
  document.querySelectorAll("[data-toggle-versions]").forEach(btn => {
1004
  btn.onclick = () => {
1005
  const id = btn.getAttribute("data-toggle-versions");
1006
+ const p = $("vp-" + id);
1007
+ if (!p) return;
1008
+ const open = p.classList.toggle("open");
1009
  btn.querySelector(".arrow").style.transform = open ? "rotate(90deg)" : "";
1010
  };
1011
  });
1012
 
1013
+ // Other answers toggle
1014
+ const oat = $("otherAnswersToggle");
1015
+ if (oat) oat.onclick = () => {
1016
+ const p = $("otherAnswersPanel");
1017
+ if (!p) return;
1018
+ const open = p.classList.toggle("open");
1019
+ oat.classList.toggle("open", open);
1020
+ };
 
1021
 
1022
+ // Propose toggle
1023
  document.querySelectorAll("[data-propose]").forEach(btn => {
1024
  btn.onclick = () => {
1025
+ const p = $("pp-" + btn.getAttribute("data-propose"));
1026
+ if (p) p.classList.toggle("open");
 
 
1027
  };
1028
  });
 
1029
  document.querySelectorAll("[data-cancel-propose]").forEach(btn => {
1030
  btn.onclick = () => {
1031
+ const p = $("pp-" + btn.getAttribute("data-cancel-propose"));
1032
+ if (p) p.classList.remove("open");
 
1033
  };
1034
  });
1035
 
1036
+ // Submit proposal
1037
  document.querySelectorAll("[data-submit-proposal]").forEach(btn => {
1038
  btn.onclick = async () => {
1039
  const aid = btn.getAttribute("data-submit-proposal");
1040
+ const box = $("pp-" + aid);
1041
+ const ta = box ? box.querySelector("textarea") : null;
1042
  const text = ta ? ta.value.trim() : "";
1043
+ if (!text) { toast("Empty proposal","bad"); return; }
1044
  if (!S.conversation) return;
1045
  btn.disabled = true;
1046
  const orig = btn.textContent;
1047
  btn.textContent = "Saving…";
1048
  const res = await callAPI("propose", {
1049
+ conversation_id: S.conversation.id, answer_id: aid, text,
 
1050
  });
1051
+ btn.disabled = false; btn.textContent = orig;
 
1052
  if (res.ok) {
1053
  S.conversation = res.conversation;
1054
+ save(); renderConversation(S.currentQuestion, false);
1055
+ toast("Version proposed","good");
1056
+ } else toast(res.error||"Error","bad");
 
 
 
1057
  };
1058
  });
1059
+
1060
+ // Write answer toggle
1061
+ const wab = $("writeAnswerBtn");
1062
+ if (wab) wab.onclick = () => {
1063
+ const p = $("writePanel");
1064
+ if (p) {
1065
+ p.classList.toggle("open");
1066
+ if (p.classList.contains("open")) {
1067
+ const ta = $("writeTextarea");
1068
+ if (ta) setTimeout(() => ta.focus(), 100);
1069
+ }
1070
+ }
1071
+ };
1072
+
1073
+ // Cancel write
1074
+ const wc = $("writeCancel");
1075
+ if (wc) wc.onclick = () => {
1076
+ const p = $("writePanel");
1077
+ if (p) p.classList.remove("open");
1078
+ };
1079
+
1080
+ // Submit answer
1081
+ const ws = $("writeSubmit");
1082
+ if (ws) ws.onclick = async () => {
1083
+ const ta = $("writeTextarea");
1084
+ const text = ta ? ta.value.trim() : "";
1085
+ if (!text) { toast("Empty answer","bad"); return; }
1086
+ if (!S.conversation) return;
1087
+ ws.disabled = true;
1088
+ const orig = ws.textContent;
1089
+ ws.textContent = "Saving…";
1090
+ showStatus("Saving answer…");
1091
+ const res = await callAPI("answer", {
1092
+ conversation_id: S.conversation.id, text,
1093
+ });
1094
+ hideStatus();
1095
+ ws.disabled = false; ws.textContent = orig;
1096
+ if (res.ok) {
1097
+ S.conversation = res.conversation;
1098
+ save(); renderConversation(S.currentQuestion, false);
1099
+ toast("Answer saved","good");
1100
+ } else toast(res.error||"Error","bad");
1101
+ };
1102
+ }
1103
+
1104
+ function save() {
1105
+ if (S.conversation) localStorage.setItem("hi_last_cid", S.conversation.id);
1106
  }
1107
 
1108
  /* ── API ── */
 
1115
  return resp.json();
1116
  }
1117
 
1118
+ /* ── Ask (input bar is ONLY for asking) ── */
1119
  async function askQuestion(q) {
1120
  showStatus("Searching for answers…");
1121
  showTyping();
1122
+ S.loading = true;
1123
+ $("sendBtn").disabled = true;
1124
  const res = await callAPI("ask", { question: q });
1125
  removeTyping();
1126
  hideStatus();
1127
+ S.loading = false;
1128
+ $("sendBtn").disabled = false;
1129
  if (!res.ok) { toast(res.error||"Error","bad"); return; }
1130
  S.conversation = res.conversation;
1131
  S.currentQuestion = q;
1132
+ save();
1133
+ toast(res.matched ? "Existing answer found" : "New question created", "good");
 
1134
  await renderConversation(q, true);
1135
  }
1136
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1137
  async function submitPrompt() {
1138
  const p = $("prompt");
1139
  const text = p.value.trim();
1140
+ if (!text || S.loading) return;
1141
  p.value = "";
1142
  autoGrow(p);
1143
+ // Always ask a new question — even if a conversation is loaded,
1144
+ // because the input bar is ONLY for questions
1145
+ await askQuestion(text);
 
1146
  }
1147
 
1148
  function autoGrow(el) {
 
1150
  el.style.height = Math.min(el.scrollHeight, 180) + "px";
1151
  }
1152
 
 
1153
  async function loadSaved() {
1154
  const id = localStorage.getItem("hi_last_cid");
1155
  if (!id) return;
 
1163
  }
1164
  }
1165
 
 
1166
  function newChat() {
1167
  S.conversation = null;
1168
  S.currentQuestion = "";
 
1170
  $("transcript").innerHTML = "";
1171
  $("welcome").style.display = "block";
1172
  $("prompt").value = "";
 
1173
  $("prompt").focus();
1174
  }
1175
 
1176
  /* ── Settings ── */
1177
  function initSettings() {
1178
  const panel = $("settingsPanel");
1179
+ $("settingsBtn").onclick = () => panel.classList.toggle("open");
 
 
1180
  document.addEventListener("click", e => {
1181
+ if (!panel.contains(e.target) && e.target !== $("settingsBtn") && !$("settingsBtn").contains(e.target))
1182
  panel.classList.remove("open");
 
1183
  });
1184
+ const ids = ["togNone","togAI","togHuman","togDiffusion"];
1185
+ function sync() {
1186
+ ids.forEach(id => {
1187
+ $(id).classList.toggle("on", S.animMode === $(id).getAttribute("data-anim"));
 
 
 
1188
  });
1189
  }
1190
+ ids.forEach(id => {
1191
  $(id).onclick = () => {
1192
  S.animMode = $(id).getAttribute("data-anim");
1193
  localStorage.setItem("hi_anim", S.animMode);
1194
+ sync();
1195
  };
1196
  });
1197
+ sync();
1198
  }
1199
 
1200
  /* ── Init ── */
 
1210
  const d = window.__HI_INIT__ || {};
1211
  if (d.client_id) S.clientId = d.client_id;
1212
  loadSaved().then(() => {
1213
+ $("prompt").focus();
1214
  });
1215
  }
1216
  init();