wop commited on
Commit
105015c
Β·
verified Β·
1 Parent(s): 5cb40cf

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +257 -128
templates/index.html CHANGED
@@ -7,25 +7,30 @@
7
  <title>{{ app_title }}</title>
8
  <style>
9
  :root {
10
- --bg: #0b0e14;
11
- --panel: #11151d;
12
- --panel2: #171c26;
13
- --border: rgba(255,255,255,.08);
14
- --border2: rgba(255,255,255,.13);
15
- --text: #ebeff7;
16
- --muted: #8b93a8;
17
- --accent: #6c83ff;
18
- --accent2: #a16eff;
19
  --good: #2dd4bf;
20
  --bad: #f87171;
21
  --warn: #fbbf24;
22
- --shadow: 0 20px 60px rgba(0,0,0,.35);
 
 
 
 
 
23
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
24
- --font: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
25
  }
26
  * { box-sizing: border-box; margin: 0; padding: 0; }
27
  html, body {
28
- width: 100%; height: 100%;
29
  background: radial-gradient(ellipse at top center, #161c2b 0%, var(--bg) 50%);
30
  color: var(--text);
31
  font-family: var(--font);
@@ -36,14 +41,16 @@
36
  content: "";
37
  position: fixed; inset: 0;
38
  background-image:
39
- linear-gradient(rgba(108,131,255,.03) 1px, transparent 1px),
40
- linear-gradient(90deg, rgba(108,131,255,.03) 1px, transparent 1px);
41
- background-size: 44px 44px;
42
- pointer-events: none; opacity: .3;
43
  }
44
  #app {
45
  position: relative; z-index: 1;
46
- height: 100%; display: flex; flex-direction: column;
 
 
47
  }
48
  /* ── Topbar ── */
49
  #topbar {
@@ -70,11 +77,11 @@
70
  border: 1px solid var(--border2);
71
  background: rgba(255,255,255,.03);
72
  color: var(--muted);
73
- border-radius: 10px;
74
  padding: 6px 12px;
75
  font: inherit; font-size: 12px;
76
  cursor: pointer;
77
- transition: all 180ms ease;
78
  display: flex; align-items: center; gap: 5px;
79
  }
80
  .top-btn:hover {
@@ -107,9 +114,11 @@
107
  }
108
  /* ── Chat area ── */
109
  #chat {
110
- flex: 1; overflow-y: auto;
111
  padding: 20px 14px 24px;
112
  scroll-behavior: smooth;
 
 
113
  }
114
  #chat::-webkit-scrollbar { width: 5px; }
115
  #chat::-webkit-scrollbar-track { background: transparent; }
@@ -120,7 +129,7 @@
120
  margin: 6vh auto 0; max-width: 480px; text-align: center;
121
  padding: 24px 20px;
122
  border: 1px solid var(--border);
123
- border-radius: 20px;
124
  background: rgba(255,255,255,.025);
125
  box-shadow: var(--shadow);
126
  animation: fadeUp 400ms ease both;
@@ -156,7 +165,7 @@
156
  .bubble {
157
  max-width: min(620px, calc(100vw - 100px));
158
  border: 1px solid var(--border);
159
- border-radius: 16px;
160
  padding: 10px 14px;
161
  line-height: 1.6; font-size: 14px;
162
  white-space: pre-wrap; word-break: break-word;
@@ -165,7 +174,7 @@
165
  .turn.user .bubble {
166
  background: linear-gradient(135deg, rgba(108,131,255,.15), rgba(161,110,255,.12));
167
  border-color: rgba(108,131,255,.2);
168
- border-radius: 16px 16px 4px 16px;
169
  }
170
  .turn-meta {
171
  margin-top: 3px;
@@ -187,7 +196,7 @@
187
  /* ── Best answer ── */
188
  .best-answer-bubble {
189
  border: 1px solid rgba(45,212,191,.15);
190
- border-radius: 4px 16px 16px 16px;
191
  padding: 10px 14px;
192
  background: rgba(45,212,191,.04);
193
  line-height: 1.6; font-size: 14px;
@@ -203,11 +212,11 @@
203
  border: 1px solid var(--border2);
204
  background: rgba(255,255,255,.02);
205
  color: var(--muted);
206
- border-radius: 8px;
207
  padding: 4px 9px;
208
  font: inherit; font-size: 11px;
209
  cursor: pointer;
210
- transition: all 160ms ease;
211
  display: inline-flex; align-items: center; gap: 3px;
212
  }
213
  .vote-btn:hover {
@@ -227,11 +236,11 @@
227
  border: 1px solid var(--border2);
228
  background: rgba(255,255,255,.02);
229
  color: var(--muted);
230
- border-radius: 8px;
231
  padding: 4px 9px;
232
  font: inherit; font-size: 11px;
233
  cursor: pointer;
234
- transition: all 160ms ease;
235
  }
236
  .action-btn:hover {
237
  border-color: rgba(108,131,255,.35);
@@ -242,11 +251,11 @@
242
  border: 1px solid rgba(45,212,191,.3);
243
  background: rgba(45,212,191,.08);
244
  color: var(--good);
245
- border-radius: 10px;
246
  padding: 8px 14px;
247
  font: inherit; font-size: 12px; font-weight: 600;
248
  cursor: pointer;
249
- transition: all 180ms ease;
250
  display: inline-flex; align-items: center; gap: 6px;
251
  margin-top: 8px;
252
  }
@@ -268,7 +277,7 @@
268
  min-height: 80px; max-height: 160px;
269
  resize: vertical;
270
  border: 1px solid var(--border2);
271
- border-radius: 12px;
272
  background: var(--panel);
273
  color: var(--text);
274
  font: inherit; font-size: 13px;
@@ -286,11 +295,11 @@
286
  border: 1px solid rgba(45,212,191,.4);
287
  background: rgba(45,212,191,.12);
288
  color: var(--good);
289
- border-radius: 8px;
290
  padding: 6px 14px;
291
  font: inherit; font-size: 12px; font-weight: 600;
292
  cursor: pointer;
293
- transition: all 160ms ease;
294
  }
295
  .write-submit:hover {
296
  background: rgba(45,212,191,.2);
@@ -301,7 +310,7 @@
301
  border: 1px solid var(--border);
302
  background: transparent;
303
  color: var(--muted);
304
- border-radius: 8px;
305
  padding: 6px 12px;
306
  font: inherit; font-size: 12px;
307
  cursor: pointer;
@@ -312,11 +321,11 @@
312
  border: 1px solid var(--border);
313
  background: rgba(255,255,255,.02);
314
  color: var(--muted);
315
- border-radius: 10px;
316
  padding: 6px 12px;
317
  font: inherit; font-size: 11px;
318
  cursor: pointer;
319
- transition: all 180ms ease;
320
  display: inline-flex; align-items: center; gap: 5px;
321
  }
322
  .other-answers-toggle:hover {
@@ -376,7 +385,7 @@
376
  .version-card {
377
  border: 1px solid var(--border);
378
  background: rgba(255,255,255,.02);
379
- border-radius: 10px;
380
  padding: 8px 10px; margin-top: 4px;
381
  animation: fadeUp 180ms ease both;
382
  }
@@ -417,11 +426,11 @@
417
  border: 1px solid rgba(108,131,255,.3);
418
  background: rgba(108,131,255,.1);
419
  color: var(--accent);
420
- border-radius: 8px;
421
  padding: 5px 12px;
422
  font: inherit; font-size: 11px;
423
  cursor: pointer;
424
- transition: all 160ms ease;
425
  }
426
  .propose-submit:hover {
427
  background: rgba(108,131,255,.18);
@@ -432,7 +441,7 @@
432
  border: 1px solid var(--border);
433
  background: transparent;
434
  color: var(--muted);
435
- border-radius: 8px;
436
  padding: 5px 10px;
437
  font: inherit; font-size: 11px;
438
  cursor: pointer;
@@ -447,7 +456,7 @@
447
  display: flex; gap: 4px; align-items: center;
448
  padding: 12px 16px;
449
  border: 1px solid var(--border);
450
- border-radius: 4px 16px 16px 16px;
451
  background: rgba(255,255,255,.03);
452
  }
453
  .typing-dots span {
@@ -504,6 +513,17 @@
504
  opacity: 1;
505
  transform: translateY(0);
506
  }
 
 
 
 
 
 
 
 
 
 
 
507
  .related-stack {
508
  margin-top: 14px;
509
  padding-top: 12px;
@@ -517,11 +537,11 @@
517
  border: 1px solid var(--border);
518
  background: rgba(255,255,255,.02);
519
  color: var(--muted);
520
- border-radius: 10px;
521
  padding: 6px 12px;
522
  font: inherit; font-size: 11px;
523
  cursor: pointer;
524
- transition: all 180ms ease;
525
  display: inline-flex; align-items: center; gap: 5px;
526
  }
527
  .related-toggle:hover {
@@ -561,16 +581,16 @@
561
  border-top: 1px solid var(--border);
562
  background: rgba(11,14,20,.85);
563
  backdrop-filter: blur(14px);
564
- padding: 10px 14px 14px;
565
  flex-shrink: 0;
566
  }
567
  .compose-inner {
568
  max-width: 760px; margin: 0 auto;
569
  border: 1px solid var(--border2);
570
- border-radius: 14px;
571
  padding: 8px 10px 6px;
572
  background: var(--panel);
573
- box-shadow: 0 8px 32px rgba(0,0,0,.25);
574
  transition: border-color 200ms ease;
575
  }
576
  .compose-inner:focus-within { border-color: rgba(108,131,255,.3); }
@@ -601,7 +621,7 @@
601
  color: white;
602
  background: linear-gradient(135deg, var(--accent), var(--accent2));
603
  box-shadow: 0 4px 14px rgba(108,131,255,.2);
604
- transition: transform 140ms ease, box-shadow 140ms ease;
605
  }
606
  .send-btn:hover {
607
  transform: translateY(-1px);
@@ -644,8 +664,9 @@
644
  border: 1px solid var(--border2);
645
  cursor: pointer;
646
  position: relative;
647
- transition: background 200ms ease;
648
  flex-shrink: 0;
 
649
  }
650
  .toggle.on {
651
  background: rgba(108,131,255,.35);
@@ -666,11 +687,11 @@
666
  left: 50%; bottom: 80px;
667
  transform: translateX(-50%) translateY(12px);
668
  opacity: 0; pointer-events: none;
669
- transition: all 200ms cubic-bezier(.4,0,.2,1);
670
  z-index: 50;
671
  background: rgba(17,21,29,.95);
672
  border: 1px solid var(--border2);
673
- border-radius: 10px;
674
  padding: 8px 14px;
675
  color: var(--text);
676
  font-family: var(--mono); font-size: 11px;
@@ -682,6 +703,46 @@
682
  #toast.good { border-color: rgba(45,212,191,.4); color: var(--good); }
683
  #toast.bad { border-color: rgba(248,113,113,.4); color: var(--bad); }
684
  .no-answer-bubble { border-style: dashed !important; color: var(--muted); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
685
  @media (max-width: 600px) {
686
  #topbar { padding: 0 10px; }
687
  .brand-sub { display: none; }
@@ -689,6 +750,7 @@
689
  .welcome { margin-top: 3vh; padding: 18px 14px; }
690
  .welcome h1 { font-size: 18px; }
691
  #settingsPanel { width: 100%; border-radius: 0 0 16px 16px; }
 
692
  }
693
  </style>
694
  </head>
@@ -707,7 +769,7 @@
707
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
708
  New chat
709
  </button>
710
- <button class="top-btn" id="settingsBtn">
711
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
712
  </button>
713
  </div>
@@ -717,38 +779,39 @@
717
  <span class="status-dot"></span>
718
  <span id="statusText">Thinking…</span>
719
  </div>
 
720
 
721
- <div id="settingsPanel">
722
- <div class="settings-title">Appearance</div>
723
- <div class="setting-row">
724
- <div><div class="setting-label">None</div><div class="setting-desc">Instant display</div></div>
725
- <div class="toggle" id="togNone" data-anim="none"></div>
726
  </div>
727
  <div class="setting-row">
728
- <div><div class="setting-label">AI typing</div><div class="setting-desc">Fast character stream</div></div>
729
- <div class="toggle" id="togAI" data-anim="ai"></div>
730
  </div>
731
  <div class="setting-row">
732
- <div><div class="setting-label">Human typing</div><div class="setting-desc">Irregular human speed</div></div>
733
- <div class="toggle" id="togHuman" data-anim="human"></div>
734
  </div>
735
  <div class="setting-row">
736
- <div><div class="setting-label">Diffusion</div><div class="setting-desc">Blur-to-clear reveal</div></div>
737
- <div class="toggle" id="togDiffusion" data-anim="diffusion"></div>
738
  </div>
739
  <div class="setting-row">
740
- <div><div class="setting-label">Diffusion v2</div><div class="setting-desc">Random text resolves in global steps</div></div>
741
- <div class="toggle" id="togDiffusionV2" data-anim="diffusion-v2"></div>
742
  </div>
743
  </div>
744
 
745
  <div id="chat">
746
  <div class="wrap">
747
  <div class="welcome" id="welcome">
748
- <h1>Ask anything. Get human made answers.</h1>
749
  <p>Type a question below. If a matching answer exists, it appears instantly. Otherwise, anyone can write the first answer.</p>
750
- <p>Do not share personal information</p>
751
- <p>All answers are from humans, if you didnt get an answer, come back tommorrow and ask again.</p>
752
  </div>
753
  <div id="transcript"></div>
754
  </div>
@@ -777,9 +840,14 @@
777
  currentQuestion: "",
778
  relatedAnswers: [],
779
  loading: false,
 
780
  animMode: localStorage.getItem("hi_anim") || "none",
781
  };
782
  const $ = id => document.getElementById(id);
 
 
 
 
783
  function getClientId() {
784
  let id = localStorage.getItem("hi_client_id");
785
  if (!id) {
@@ -813,11 +881,36 @@
813
  try { return new Date(iso).toLocaleString([], { month:"short", day:"numeric", hour:"2-digit", minute:"2-digit" }); }
814
  catch { return iso; }
815
  }
816
- function scrollBottom() {
817
  const c = $("chat");
818
- requestAnimationFrame(() => { c.scrollTop = c.scrollHeight; });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
819
  }
820
  function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
 
 
 
 
 
 
821
  function diffusionTokens(text) {
822
  return String(text || "").split(/(\s+)/).map((part, i) => {
823
  if (!part) return "";
@@ -897,41 +990,21 @@
897
  el.textContent = String(text || "");
898
  }
899
  async function animateText(el, text) {
 
900
  const mode = S.animMode;
901
- if (mode === "ai") {
902
- el.innerHTML = "";
903
- for (let i = 0; i < text.length; i++) {
904
- el.innerHTML = nl2br(text.slice(0, i + 1));
905
- scrollBottom();
906
- await sleep(12 + Math.random() * 8);
907
- }
908
- } else if (mode === "human") {
909
- el.innerHTML = "";
910
- for (let i = 0; i < text.length; i++) {
911
- el.innerHTML = nl2br(text.slice(0, i + 1));
912
- scrollBottom();
913
- const ch = text[i];
914
- let d = 25 + Math.random() * 55;
915
- if (ch === " ") d += Math.random() * 30;
916
- if (".!?".includes(ch)) d += 120 + Math.random() * 180;
917
- if (",;:".includes(ch)) d += 40 + Math.random() * 60;
918
- if (Math.random() < .03) d += 200 + Math.random() * 300;
919
- await sleep(d);
920
- }
921
- } else if (mode === "diffusion") {
922
- el.innerHTML = `<span class="diffusion-text">${diffusionTokens(text)}</span>`;
923
- scrollBottom();
924
- const tokens = Array.from(el.querySelectorAll(".diffusion-token"));
925
- for (let i = 0; i < tokens.length; i++) {
926
- tokens[i].classList.add("revealed");
927
- if (i % 5 === 0) scrollBottom();
928
- await sleep(26 + Math.random() * 30);
929
- }
930
- } else if (mode === "diffusion-v2") {
931
- await animateDiffusionV2(el, text);
932
- } else {
933
- el.innerHTML = nl2br(text);
934
  }
 
 
 
 
 
 
935
  }
936
  function showTyping() {
937
  removeTyping();
@@ -974,7 +1047,7 @@
974
  const others = (answer.versions||[]).filter(v => v.id !== act?.id);
975
  if (!others.length) return "";
976
  return `
977
- <button class="versions-toggle" data-toggle-versions="${answer.id}">
978
  <span class="arrow">β–Ά</span> ${others.length} version${others.length>1?"s":""}
979
  </button>
980
  <div class="versions-panel" id="vp-${answer.id}">
@@ -992,7 +1065,7 @@
992
  }
993
  function renderPropose(answerId) {
994
  return `
995
- <button class="action-btn" data-propose="${answerId}">Propose version</button>
996
  <div class="propose-panel" id="pp-${answerId}">
997
  <textarea class="propose-textarea" placeholder="Write a better version…" rows="3"></textarea>
998
  <div class="propose-actions">
@@ -1003,7 +1076,7 @@
1003
  }
1004
  function renderWriteAnswer() {
1005
  return `
1006
- <button class="write-answer-btn" id="writeAnswerBtn">
1007
  <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>
1008
  Write an answer
1009
  </button>
@@ -1038,7 +1111,7 @@
1038
  if (answers.length <= 1) return "";
1039
  const others = answers.slice(1);
1040
  return `
1041
- <button class="other-answers-toggle" id="otherAnswersToggle">
1042
  <span class="arrow">β–Ά</span> ${others.length} other answer${others.length>1?"s":""}
1043
  </button>
1044
  <div class="other-answers-panel" id="otherAnswersPanel">
@@ -1067,7 +1140,7 @@
1067
  return `
1068
  <div class="related-stack">
1069
  <div class="chip muted">from similar questions</div>
1070
- <button class="related-toggle" id="relatedToggle">
1071
  <span class="arrow">β–Ά</span> ${rel.length} related answer${rel.length > 1 ? "s" : ""}
1072
  </button>
1073
  <div class="related-panel" id="relatedPanel">
@@ -1091,15 +1164,17 @@
1091
  async function renderConversation(questionText, doAnimate) {
1092
  const tr = $("transcript");
1093
  const wl = $("welcome");
1094
- tr.innerHTML = "";
1095
  if (!S.conversation) {
1096
  wl.style.display = "block";
 
 
1097
  return;
1098
  }
1099
  wl.style.display = "none";
1100
  const q = questionText || S.conversation.question || "";
1101
  // Question bubble
1102
- tr.insertAdjacentHTML("beforeend", `
1103
  <div class="turn user">
1104
  <div>
1105
  <div class="bubble">${nl2br(q)}</div>
@@ -1113,7 +1188,7 @@
1113
  const answers = sortedAnswers(S.conversation);
1114
  if (!answers.length) {
1115
  // No answers yet β€” show placeholder + write button
1116
- tr.insertAdjacentHTML("beforeend", `
1117
  <div class="turn assistant">
1118
  <div class="avatar assistant">✦</div>
1119
  <div>
@@ -1126,7 +1201,7 @@
1126
  } else {
1127
  // Best answer + write more + other answers
1128
  const best = answers[0];
1129
- tr.insertAdjacentHTML("beforeend", `
1130
  <div class="turn assistant">
1131
  <div class="avatar assistant">✦</div>
1132
  <div style="min-width:0;flex:1;">
@@ -1136,13 +1211,16 @@
1136
  <div id="relatedMount"></div>
1137
  </div>
1138
  </div>`);
1139
- // Animate best answer text
 
 
 
1140
  const bestV = activeVersion(best);
1141
  if (bestV) {
1142
  const el = $("bestAnswerText");
1143
  if (doAnimate) {
1144
  await animateText(el, bestV.text || "");
1145
- } else {
1146
  el.innerHTML = nl2br(bestV.text || "");
1147
  }
1148
  }
@@ -1181,6 +1259,7 @@
1181
  if (!p) return;
1182
  const open = p.classList.toggle("open");
1183
  btn.querySelector(".arrow").style.transform = open ? "rotate(90deg)" : "";
 
1184
  };
1185
  });
1186
  // Other answers toggle
@@ -1190,6 +1269,7 @@
1190
  if (!p) return;
1191
  const open = p.classList.toggle("open");
1192
  oat.classList.toggle("open", open);
 
1193
  };
1194
  const rt = $("relatedToggle");
1195
  if (rt) rt.onclick = () => {
@@ -1197,18 +1277,26 @@
1197
  if (!p) return;
1198
  const open = p.classList.toggle("open");
1199
  rt.classList.toggle("open", open);
 
1200
  };
1201
  // Propose toggle
1202
  document.querySelectorAll("[data-propose]").forEach(btn => {
1203
  btn.onclick = () => {
1204
- const p = $("pp-" + btn.getAttribute("data-propose"));
1205
- if (p) p.classList.toggle("open");
 
 
 
 
1206
  };
1207
  });
1208
  document.querySelectorAll("[data-cancel-propose]").forEach(btn => {
1209
  btn.onclick = () => {
1210
- const p = $("pp-" + btn.getAttribute("data-cancel-propose"));
 
1211
  if (p) p.classList.remove("open");
 
 
1212
  };
1213
  });
1214
  // Submit proposal
@@ -1239,8 +1327,9 @@
1239
  if (wab) wab.onclick = () => {
1240
  const p = $("writePanel");
1241
  if (p) {
1242
- p.classList.toggle("open");
1243
- if (p.classList.contains("open")) {
 
1244
  const ta = $("writeTextarea");
1245
  if (ta) setTimeout(() => ta.focus(), 100);
1246
  }
@@ -1251,6 +1340,7 @@
1251
  if (wc) wc.onclick = () => {
1252
  const p = $("writePanel");
1253
  if (p) p.classList.remove("open");
 
1254
  };
1255
  // Submit answer
1256
  const ws = $("writeSubmit");
@@ -1282,12 +1372,22 @@
1282
  }
1283
  /* ── API ── */
1284
  async function callAPI(action, payload = {}) {
1285
- const resp = await fetch("/api", {
1286
- method: "POST",
1287
- headers: { "Content-Type": "application/json", "X-Client-Id": S.clientId },
1288
- body: JSON.stringify({ action, client_id: S.clientId, ...payload }),
1289
- });
1290
- return resp.json();
 
 
 
 
 
 
 
 
 
 
1291
  }
1292
  /* ── Ask (input bar is ONLY for asking) ── */
1293
  async function askQuestion(q) {
@@ -1339,24 +1439,40 @@
1339
  S.conversation = null;
1340
  S.currentQuestion = "";
1341
  S.relatedAnswers = [];
 
1342
  localStorage.removeItem("hi_last_cid");
1343
  $("transcript").innerHTML = "";
1344
  $("welcome").style.display = "block";
 
1345
  $("prompt").value = "";
1346
  $("prompt").focus();
1347
  }
1348
  /* ── Settings ── */
1349
  function initSettings() {
1350
  const panel = $("settingsPanel");
1351
- $("settingsBtn").onclick = () => panel.classList.toggle("open");
 
 
 
 
 
 
1352
  document.addEventListener("click", e => {
1353
- if (!panel.contains(e.target) && e.target !== $("settingsBtn") && !$("settingsBtn").contains(e.target))
1354
- panel.classList.remove("open");
 
 
 
 
 
1355
  });
1356
  const ids = ["togNone","togAI","togHuman","togDiffusion","togDiffusionV2"];
1357
  function sync() {
1358
  ids.forEach(id => {
1359
- $(id).classList.toggle("on", S.animMode === $(id).getAttribute("data-anim"));
 
 
 
1360
  });
1361
  }
1362
  ids.forEach(id => {
@@ -1366,11 +1482,24 @@
1366
  sync();
1367
  };
1368
  });
 
 
1369
  sync();
1370
  }
1371
  /* ── Init ── */
1372
  function init() {
 
 
 
 
 
 
 
1373
  S.clientId = getClientId();
 
 
 
 
1374
  $("sendBtn").onclick = submitPrompt;
1375
  $("newChatBtn").onclick = newChat;
1376
  $("prompt").addEventListener("input", e => autoGrow(e.target));
 
7
  <title>{{ app_title }}</title>
8
  <style>
9
  :root {
10
+ --bg: #08101a;
11
+ --panel: rgba(15, 22, 34, .94);
12
+ --panel2: rgba(20, 29, 44, .96);
13
+ --border: rgba(148, 163, 184, .14);
14
+ --border2: rgba(148, 163, 184, .2);
15
+ --text: #eef3f9;
16
+ --muted: #91a0b4;
17
+ --accent: #7ca6ff;
18
+ --accent2: #4fd1c5;
19
  --good: #2dd4bf;
20
  --bad: #f87171;
21
  --warn: #fbbf24;
22
+ --shadow: 0 18px 44px rgba(0,0,0,.34);
23
+ --shadow-soft: 0 8px 26px rgba(0,0,0,.2);
24
+ --radius-sm: 8px;
25
+ --radius-md: 12px;
26
+ --radius-lg: 16px;
27
+ --radius-xl: 22px;
28
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
29
+ --font: "Segoe UI Variable Text", "Segoe UI", Aptos, system-ui, -apple-system, sans-serif;
30
  }
31
  * { box-sizing: border-box; margin: 0; padding: 0; }
32
  html, body {
33
+ width: 100%; height: var(--app-height, 100%);
34
  background: radial-gradient(ellipse at top center, #161c2b 0%, var(--bg) 50%);
35
  color: var(--text);
36
  font-family: var(--font);
 
41
  content: "";
42
  position: fixed; inset: 0;
43
  background-image:
44
+ linear-gradient(rgba(124,166,255,.025) 1px, transparent 1px),
45
+ linear-gradient(90deg, rgba(124,166,255,.025) 1px, transparent 1px);
46
+ background-size: 56px 56px;
47
+ pointer-events: none; opacity: .16;
48
  }
49
  #app {
50
  position: relative; z-index: 1;
51
+ height: var(--app-height, 100%);
52
+ display: flex; flex-direction: column;
53
+ min-height: 0;
54
  }
55
  /* ── Topbar ── */
56
  #topbar {
 
77
  border: 1px solid var(--border2);
78
  background: rgba(255,255,255,.03);
79
  color: var(--muted);
80
+ border-radius: var(--radius-md);
81
  padding: 6px 12px;
82
  font: inherit; font-size: 12px;
83
  cursor: pointer;
84
+ transition: border-color 180ms ease, color 180ms ease, background 180ms ease, transform 180ms ease;
85
  display: flex; align-items: center; gap: 5px;
86
  }
87
  .top-btn:hover {
 
114
  }
115
  /* ── Chat area ── */
116
  #chat {
117
+ flex: 1; min-height: 0; overflow-y: auto;
118
  padding: 20px 14px 24px;
119
  scroll-behavior: smooth;
120
+ overscroll-behavior-y: contain;
121
+ -webkit-overflow-scrolling: touch;
122
  }
123
  #chat::-webkit-scrollbar { width: 5px; }
124
  #chat::-webkit-scrollbar-track { background: transparent; }
 
129
  margin: 6vh auto 0; max-width: 480px; text-align: center;
130
  padding: 24px 20px;
131
  border: 1px solid var(--border);
132
+ border-radius: var(--radius-xl);
133
  background: rgba(255,255,255,.025);
134
  box-shadow: var(--shadow);
135
  animation: fadeUp 400ms ease both;
 
165
  .bubble {
166
  max-width: min(620px, calc(100vw - 100px));
167
  border: 1px solid var(--border);
168
+ border-radius: var(--radius-lg);
169
  padding: 10px 14px;
170
  line-height: 1.6; font-size: 14px;
171
  white-space: pre-wrap; word-break: break-word;
 
174
  .turn.user .bubble {
175
  background: linear-gradient(135deg, rgba(108,131,255,.15), rgba(161,110,255,.12));
176
  border-color: rgba(108,131,255,.2);
177
+ border-radius: var(--radius-lg) var(--radius-lg) 4px var(--radius-lg);
178
  }
179
  .turn-meta {
180
  margin-top: 3px;
 
196
  /* ── Best answer ── */
197
  .best-answer-bubble {
198
  border: 1px solid rgba(45,212,191,.15);
199
+ border-radius: 4px var(--radius-lg) var(--radius-lg) var(--radius-lg);
200
  padding: 10px 14px;
201
  background: rgba(45,212,191,.04);
202
  line-height: 1.6; font-size: 14px;
 
212
  border: 1px solid var(--border2);
213
  background: rgba(255,255,255,.02);
214
  color: var(--muted);
215
+ border-radius: var(--radius-sm);
216
  padding: 4px 9px;
217
  font: inherit; font-size: 11px;
218
  cursor: pointer;
219
+ transition: border-color 160ms ease, color 160ms ease, background 160ms ease, transform 160ms ease;
220
  display: inline-flex; align-items: center; gap: 3px;
221
  }
222
  .vote-btn:hover {
 
236
  border: 1px solid var(--border2);
237
  background: rgba(255,255,255,.02);
238
  color: var(--muted);
239
+ border-radius: var(--radius-sm);
240
  padding: 4px 9px;
241
  font: inherit; font-size: 11px;
242
  cursor: pointer;
243
+ transition: border-color 160ms ease, color 160ms ease, background 160ms ease;
244
  }
245
  .action-btn:hover {
246
  border-color: rgba(108,131,255,.35);
 
251
  border: 1px solid rgba(45,212,191,.3);
252
  background: rgba(45,212,191,.08);
253
  color: var(--good);
254
+ border-radius: var(--radius-md);
255
  padding: 8px 14px;
256
  font: inherit; font-size: 12px; font-weight: 600;
257
  cursor: pointer;
258
+ transition: border-color 180ms ease, color 180ms ease, background 180ms ease, transform 180ms ease;
259
  display: inline-flex; align-items: center; gap: 6px;
260
  margin-top: 8px;
261
  }
 
277
  min-height: 80px; max-height: 160px;
278
  resize: vertical;
279
  border: 1px solid var(--border2);
280
+ border-radius: var(--radius-md);
281
  background: var(--panel);
282
  color: var(--text);
283
  font: inherit; font-size: 13px;
 
295
  border: 1px solid rgba(45,212,191,.4);
296
  background: rgba(45,212,191,.12);
297
  color: var(--good);
298
+ border-radius: var(--radius-sm);
299
  padding: 6px 14px;
300
  font: inherit; font-size: 12px; font-weight: 600;
301
  cursor: pointer;
302
+ transition: border-color 160ms ease, color 160ms ease, background 160ms ease;
303
  }
304
  .write-submit:hover {
305
  background: rgba(45,212,191,.2);
 
310
  border: 1px solid var(--border);
311
  background: transparent;
312
  color: var(--muted);
313
+ border-radius: var(--radius-sm);
314
  padding: 6px 12px;
315
  font: inherit; font-size: 12px;
316
  cursor: pointer;
 
321
  border: 1px solid var(--border);
322
  background: rgba(255,255,255,.02);
323
  color: var(--muted);
324
+ border-radius: var(--radius-md);
325
  padding: 6px 12px;
326
  font: inherit; font-size: 11px;
327
  cursor: pointer;
328
+ transition: border-color 180ms ease, color 180ms ease, background 180ms ease, transform 180ms ease;
329
  display: inline-flex; align-items: center; gap: 5px;
330
  }
331
  .other-answers-toggle:hover {
 
385
  .version-card {
386
  border: 1px solid var(--border);
387
  background: rgba(255,255,255,.02);
388
+ border-radius: var(--radius-md);
389
  padding: 8px 10px; margin-top: 4px;
390
  animation: fadeUp 180ms ease both;
391
  }
 
426
  border: 1px solid rgba(108,131,255,.3);
427
  background: rgba(108,131,255,.1);
428
  color: var(--accent);
429
+ border-radius: var(--radius-sm);
430
  padding: 5px 12px;
431
  font: inherit; font-size: 11px;
432
  cursor: pointer;
433
+ transition: border-color 160ms ease, color 160ms ease, background 160ms ease;
434
  }
435
  .propose-submit:hover {
436
  background: rgba(108,131,255,.18);
 
441
  border: 1px solid var(--border);
442
  background: transparent;
443
  color: var(--muted);
444
+ border-radius: var(--radius-sm);
445
  padding: 5px 10px;
446
  font: inherit; font-size: 11px;
447
  cursor: pointer;
 
456
  display: flex; gap: 4px; align-items: center;
457
  padding: 12px 16px;
458
  border: 1px solid var(--border);
459
+ border-radius: 4px var(--radius-lg) var(--radius-lg) var(--radius-lg);
460
  background: rgba(255,255,255,.03);
461
  }
462
  .typing-dots span {
 
513
  opacity: 1;
514
  transform: translateY(0);
515
  }
516
+ .answer-reveal {
517
+ opacity: 0;
518
+ transform: translateY(4px);
519
+ filter: blur(1px);
520
+ transition: opacity 180ms ease, transform 180ms ease, filter 180ms ease;
521
+ }
522
+ .answer-reveal.revealed {
523
+ opacity: 1;
524
+ transform: translateY(0);
525
+ filter: blur(0);
526
+ }
527
  .related-stack {
528
  margin-top: 14px;
529
  padding-top: 12px;
 
537
  border: 1px solid var(--border);
538
  background: rgba(255,255,255,.02);
539
  color: var(--muted);
540
+ border-radius: var(--radius-md);
541
  padding: 6px 12px;
542
  font: inherit; font-size: 11px;
543
  cursor: pointer;
544
+ transition: border-color 180ms ease, color 180ms ease, background 180ms ease, transform 180ms ease;
545
  display: inline-flex; align-items: center; gap: 5px;
546
  }
547
  .related-toggle:hover {
 
581
  border-top: 1px solid var(--border);
582
  background: rgba(11,14,20,.85);
583
  backdrop-filter: blur(14px);
584
+ padding: 10px 14px calc(14px + env(safe-area-inset-bottom));
585
  flex-shrink: 0;
586
  }
587
  .compose-inner {
588
  max-width: 760px; margin: 0 auto;
589
  border: 1px solid var(--border2);
590
+ border-radius: var(--radius-lg);
591
  padding: 8px 10px 6px;
592
  background: var(--panel);
593
+ box-shadow: var(--shadow-soft);
594
  transition: border-color 200ms ease;
595
  }
596
  .compose-inner:focus-within { border-color: rgba(108,131,255,.3); }
 
621
  color: white;
622
  background: linear-gradient(135deg, var(--accent), var(--accent2));
623
  box-shadow: 0 4px 14px rgba(108,131,255,.2);
624
+ transition: transform 140ms ease, box-shadow 140ms ease, filter 140ms ease;
625
  }
626
  .send-btn:hover {
627
  transform: translateY(-1px);
 
664
  border: 1px solid var(--border2);
665
  cursor: pointer;
666
  position: relative;
667
+ transition: background 200ms ease, border-color 200ms ease, box-shadow 200ms ease;
668
  flex-shrink: 0;
669
+ appearance: none;
670
  }
671
  .toggle.on {
672
  background: rgba(108,131,255,.35);
 
687
  left: 50%; bottom: 80px;
688
  transform: translateX(-50%) translateY(12px);
689
  opacity: 0; pointer-events: none;
690
+ transition: opacity 200ms cubic-bezier(.4,0,.2,1), transform 200ms cubic-bezier(.4,0,.2,1);
691
  z-index: 50;
692
  background: rgba(17,21,29,.95);
693
  border: 1px solid var(--border2);
694
+ border-radius: var(--radius-md);
695
  padding: 8px 14px;
696
  color: var(--text);
697
  font-family: var(--mono); font-size: 11px;
 
703
  #toast.good { border-color: rgba(45,212,191,.4); color: var(--good); }
704
  #toast.bad { border-color: rgba(248,113,113,.4); color: var(--bad); }
705
  .no-answer-bubble { border-style: dashed !important; color: var(--muted); }
706
+ #jumpLatest {
707
+ position: fixed;
708
+ left: 50%;
709
+ bottom: 132px;
710
+ transform: translateX(-50%) translateY(8px);
711
+ z-index: 55;
712
+ border: 1px solid rgba(124,166,255,.3);
713
+ background: rgba(14, 20, 31, .94);
714
+ color: var(--text);
715
+ border-radius: 999px;
716
+ padding: 8px 14px;
717
+ font: inherit;
718
+ font-size: 12px;
719
+ box-shadow: var(--shadow-soft);
720
+ backdrop-filter: blur(10px);
721
+ opacity: 0;
722
+ pointer-events: none;
723
+ transition: opacity 160ms ease, transform 160ms ease;
724
+ }
725
+ #jumpLatest.show {
726
+ opacity: 1;
727
+ pointer-events: auto;
728
+ transform: translateX(-50%) translateY(0);
729
+ }
730
+ #jumpLatest:focus-visible,
731
+ .top-btn:focus-visible,
732
+ .vote-btn:focus-visible,
733
+ .action-btn:focus-visible,
734
+ .write-answer-btn:focus-visible,
735
+ .other-answers-toggle:focus-visible,
736
+ .related-toggle:focus-visible,
737
+ .send-btn:focus-visible,
738
+ .write-submit:focus-visible,
739
+ .write-cancel:focus-visible,
740
+ .propose-submit:focus-visible,
741
+ .propose-cancel:focus-visible,
742
+ .toggle:focus-visible {
743
+ outline: 2px solid rgba(124,166,255,.65);
744
+ outline-offset: 2px;
745
+ }
746
  @media (max-width: 600px) {
747
  #topbar { padding: 0 10px; }
748
  .brand-sub { display: none; }
 
750
  .welcome { margin-top: 3vh; padding: 18px 14px; }
751
  .welcome h1 { font-size: 18px; }
752
  #settingsPanel { width: 100%; border-radius: 0 0 16px 16px; }
753
+ #jumpLatest { bottom: 120px; max-width: calc(100vw - 24px); white-space: nowrap; }
754
  }
755
  </style>
756
  </head>
 
769
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
770
  New chat
771
  </button>
772
+ <button class="top-btn" id="settingsBtn" aria-expanded="false" aria-controls="settingsPanel">
773
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
774
  </button>
775
  </div>
 
779
  <span class="status-dot"></span>
780
  <span id="statusText">Thinking…</span>
781
  </div>
782
+ <button id="jumpLatest" type="button" aria-label="Jump to latest content">New content below Β· Jump to latest</button>
783
 
784
+ <div id="settingsPanel">
785
+ <div class="settings-title">Appearance</div>
786
+ <div class="setting-row">
787
+ <div><div class="setting-label">Instant</div><div class="setting-desc">No response animation</div></div>
788
+ <button class="toggle" type="button" id="togNone" data-anim="none" aria-pressed="true"></button>
789
  </div>
790
  <div class="setting-row">
791
+ <div><div class="setting-label">Quick fade</div><div class="setting-desc">Light reveal on response</div></div>
792
+ <button class="toggle" type="button" id="togAI" data-anim="ai" aria-pressed="false"></button>
793
  </div>
794
  <div class="setting-row">
795
+ <div><div class="setting-label">Gentle fade</div><div class="setting-desc">A slightly slower reveal</div></div>
796
+ <button class="toggle" type="button" id="togHuman" data-anim="human" aria-pressed="false"></button>
797
  </div>
798
  <div class="setting-row">
799
+ <div><div class="setting-label">Soft reveal</div><div class="setting-desc">Low-motion blur-free animation</div></div>
800
+ <button class="toggle" type="button" id="togDiffusion" data-anim="diffusion" aria-pressed="false"></button>
801
  </div>
802
  <div class="setting-row">
803
+ <div><div class="setting-label">Slow reveal</div><div class="setting-desc">Longest subtle response transition</div></div>
804
+ <button class="toggle" type="button" id="togDiffusionV2" data-anim="diffusion-v2" aria-pressed="false"></button>
805
  </div>
806
  </div>
807
 
808
  <div id="chat">
809
  <div class="wrap">
810
  <div class="welcome" id="welcome">
811
+ <h1>Ask a question. Get answers from real people.</h1>
812
  <p>Type a question below. If a matching answer exists, it appears instantly. Otherwise, anyone can write the first answer.</p>
813
+ <p>Please do not share personal or sensitive information.</p>
814
+ <p>If no one answers yet, try again later. More people may see it over time.</p>
815
  </div>
816
  <div id="transcript"></div>
817
  </div>
 
840
  currentQuestion: "",
841
  relatedAnswers: [],
842
  loading: false,
843
+ atBottom: true,
844
  animMode: localStorage.getItem("hi_anim") || "none",
845
  };
846
  const $ = id => document.getElementById(id);
847
+ function updateAppHeight() {
848
+ const h = window.visualViewport?.height || window.innerHeight || document.documentElement.clientHeight;
849
+ document.documentElement.style.setProperty("--app-height", `${Math.round(h)}px`);
850
+ }
851
  function getClientId() {
852
  let id = localStorage.getItem("hi_client_id");
853
  if (!id) {
 
881
  try { return new Date(iso).toLocaleString([], { month:"short", day:"numeric", hour:"2-digit", minute:"2-digit" }); }
882
  catch { return iso; }
883
  }
884
+ function isNearBottom() {
885
  const c = $("chat");
886
+ return c.scrollHeight - c.scrollTop - c.clientHeight < 72;
887
+ }
888
+ function setJumpLatest(visible) {
889
+ const btn = $("jumpLatest");
890
+ if (!btn) return;
891
+ btn.classList.toggle("show", !!visible);
892
+ }
893
+ function scrollBottom(force = false) {
894
+ const c = $("chat");
895
+ if (!c) return;
896
+ const shouldScroll = force || S.atBottom || isNearBottom();
897
+ if (shouldScroll) {
898
+ requestAnimationFrame(() => {
899
+ c.scrollTop = c.scrollHeight;
900
+ setJumpLatest(false);
901
+ S.atBottom = true;
902
+ });
903
+ } else {
904
+ setJumpLatest(true);
905
+ }
906
  }
907
  function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
908
+ function appendHTML(target, html) {
909
+ if (!html) return;
910
+ const tpl = document.createElement("template");
911
+ tpl.innerHTML = html.trim();
912
+ target.appendChild(tpl.content);
913
+ }
914
  function diffusionTokens(text) {
915
  return String(text || "").split(/(\s+)/).map((part, i) => {
916
  if (!part) return "";
 
990
  el.textContent = String(text || "");
991
  }
992
  async function animateText(el, text) {
993
+ if (!el) return;
994
  const mode = S.animMode;
995
+ const timings = { none: 0, ai: 90, human: 140, diffusion: 110, "diffusion-v2": 160 };
996
+ el.classList.remove("answer-reveal", "revealed");
997
+ el.innerHTML = nl2br(text);
998
+ if (mode === "none") {
999
+ setJumpLatest(false);
1000
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1001
  }
1002
+ el.classList.add("answer-reveal");
1003
+ requestAnimationFrame(() => {
1004
+ el.classList.add("revealed");
1005
+ });
1006
+ scrollBottom();
1007
+ await sleep(timings[mode] || 120);
1008
  }
1009
  function showTyping() {
1010
  removeTyping();
 
1047
  const others = (answer.versions||[]).filter(v => v.id !== act?.id);
1048
  if (!others.length) return "";
1049
  return `
1050
+ <button class="versions-toggle" type="button" data-toggle-versions="${answer.id}" aria-controls="vp-${answer.id}" aria-expanded="false">
1051
  <span class="arrow">β–Ά</span> ${others.length} version${others.length>1?"s":""}
1052
  </button>
1053
  <div class="versions-panel" id="vp-${answer.id}">
 
1065
  }
1066
  function renderPropose(answerId) {
1067
  return `
1068
+ <button class="action-btn" type="button" data-propose="${answerId}" aria-controls="pp-${answerId}" aria-expanded="false">Propose version</button>
1069
  <div class="propose-panel" id="pp-${answerId}">
1070
  <textarea class="propose-textarea" placeholder="Write a better version…" rows="3"></textarea>
1071
  <div class="propose-actions">
 
1076
  }
1077
  function renderWriteAnswer() {
1078
  return `
1079
+ <button class="write-answer-btn" type="button" id="writeAnswerBtn" aria-controls="writePanel" aria-expanded="false">
1080
  <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>
1081
  Write an answer
1082
  </button>
 
1111
  if (answers.length <= 1) return "";
1112
  const others = answers.slice(1);
1113
  return `
1114
+ <button class="other-answers-toggle" type="button" id="otherAnswersToggle" aria-controls="otherAnswersPanel" aria-expanded="false">
1115
  <span class="arrow">β–Ά</span> ${others.length} other answer${others.length>1?"s":""}
1116
  </button>
1117
  <div class="other-answers-panel" id="otherAnswersPanel">
 
1140
  return `
1141
  <div class="related-stack">
1142
  <div class="chip muted">from similar questions</div>
1143
+ <button class="related-toggle" type="button" id="relatedToggle" aria-controls="relatedPanel" aria-expanded="false">
1144
  <span class="arrow">β–Ά</span> ${rel.length} related answer${rel.length > 1 ? "s" : ""}
1145
  </button>
1146
  <div class="related-panel" id="relatedPanel">
 
1164
  async function renderConversation(questionText, doAnimate) {
1165
  const tr = $("transcript");
1166
  const wl = $("welcome");
1167
+ const frag = document.createDocumentFragment();
1168
  if (!S.conversation) {
1169
  wl.style.display = "block";
1170
+ tr.replaceChildren();
1171
+ setJumpLatest(false);
1172
  return;
1173
  }
1174
  wl.style.display = "none";
1175
  const q = questionText || S.conversation.question || "";
1176
  // Question bubble
1177
+ appendHTML(frag, `
1178
  <div class="turn user">
1179
  <div>
1180
  <div class="bubble">${nl2br(q)}</div>
 
1188
  const answers = sortedAnswers(S.conversation);
1189
  if (!answers.length) {
1190
  // No answers yet β€” show placeholder + write button
1191
+ appendHTML(frag, `
1192
  <div class="turn assistant">
1193
  <div class="avatar assistant">✦</div>
1194
  <div>
 
1201
  } else {
1202
  // Best answer + write more + other answers
1203
  const best = answers[0];
1204
+ appendHTML(frag, `
1205
  <div class="turn assistant">
1206
  <div class="avatar assistant">✦</div>
1207
  <div style="min-width:0;flex:1;">
 
1211
  <div id="relatedMount"></div>
1212
  </div>
1213
  </div>`);
1214
+ }
1215
+ tr.replaceChildren(frag);
1216
+ if (answers.length) {
1217
+ const best = answers[0];
1218
  const bestV = activeVersion(best);
1219
  if (bestV) {
1220
  const el = $("bestAnswerText");
1221
  if (doAnimate) {
1222
  await animateText(el, bestV.text || "");
1223
+ } else if (el) {
1224
  el.innerHTML = nl2br(bestV.text || "");
1225
  }
1226
  }
 
1259
  if (!p) return;
1260
  const open = p.classList.toggle("open");
1261
  btn.querySelector(".arrow").style.transform = open ? "rotate(90deg)" : "";
1262
+ btn.setAttribute("aria-expanded", String(open));
1263
  };
1264
  });
1265
  // Other answers toggle
 
1269
  if (!p) return;
1270
  const open = p.classList.toggle("open");
1271
  oat.classList.toggle("open", open);
1272
+ oat.setAttribute("aria-expanded", String(open));
1273
  };
1274
  const rt = $("relatedToggle");
1275
  if (rt) rt.onclick = () => {
 
1277
  if (!p) return;
1278
  const open = p.classList.toggle("open");
1279
  rt.classList.toggle("open", open);
1280
+ rt.setAttribute("aria-expanded", String(open));
1281
  };
1282
  // Propose toggle
1283
  document.querySelectorAll("[data-propose]").forEach(btn => {
1284
  btn.onclick = () => {
1285
+ const id = btn.getAttribute("data-propose");
1286
+ const p = $("pp-" + id);
1287
+ if (p) {
1288
+ const open = p.classList.toggle("open");
1289
+ btn.setAttribute("aria-expanded", String(open));
1290
+ }
1291
  };
1292
  });
1293
  document.querySelectorAll("[data-cancel-propose]").forEach(btn => {
1294
  btn.onclick = () => {
1295
+ const id = btn.getAttribute("data-cancel-propose");
1296
+ const p = $("pp-" + id);
1297
  if (p) p.classList.remove("open");
1298
+ const trigger = Array.from(document.querySelectorAll("[data-propose]")).find(el => el.getAttribute("data-propose") === id);
1299
+ if (trigger) trigger.setAttribute("aria-expanded", "false");
1300
  };
1301
  });
1302
  // Submit proposal
 
1327
  if (wab) wab.onclick = () => {
1328
  const p = $("writePanel");
1329
  if (p) {
1330
+ const open = p.classList.toggle("open");
1331
+ wab.setAttribute("aria-expanded", String(open));
1332
+ if (open) {
1333
  const ta = $("writeTextarea");
1334
  if (ta) setTimeout(() => ta.focus(), 100);
1335
  }
 
1340
  if (wc) wc.onclick = () => {
1341
  const p = $("writePanel");
1342
  if (p) p.classList.remove("open");
1343
+ if (wab) wab.setAttribute("aria-expanded", "false");
1344
  };
1345
  // Submit answer
1346
  const ws = $("writeSubmit");
 
1372
  }
1373
  /* ── API ── */
1374
  async function callAPI(action, payload = {}) {
1375
+ try {
1376
+ const resp = await fetch("/api", {
1377
+ method: "POST",
1378
+ headers: { "Content-Type": "application/json", "X-Client-Id": S.clientId },
1379
+ body: JSON.stringify({ action, client_id: S.clientId, ...payload }),
1380
+ });
1381
+ const data = await resp.json().catch(() => null);
1382
+ if (!resp.ok) {
1383
+ return data && typeof data === "object"
1384
+ ? data
1385
+ : { ok: false, error: `Request failed (${resp.status})` };
1386
+ }
1387
+ return data || { ok: false, error: "Empty response from server" };
1388
+ } catch (err) {
1389
+ return { ok: false, error: err?.message || "Network error" };
1390
+ }
1391
  }
1392
  /* ── Ask (input bar is ONLY for asking) ── */
1393
  async function askQuestion(q) {
 
1439
  S.conversation = null;
1440
  S.currentQuestion = "";
1441
  S.relatedAnswers = [];
1442
+ S.atBottom = true;
1443
  localStorage.removeItem("hi_last_cid");
1444
  $("transcript").innerHTML = "";
1445
  $("welcome").style.display = "block";
1446
+ setJumpLatest(false);
1447
  $("prompt").value = "";
1448
  $("prompt").focus();
1449
  }
1450
  /* ── Settings ── */
1451
  function initSettings() {
1452
  const panel = $("settingsPanel");
1453
+ const btn = $("settingsBtn");
1454
+ function setOpen(open, focusBtn = false) {
1455
+ panel.classList.toggle("open", open);
1456
+ btn.setAttribute("aria-expanded", String(open));
1457
+ if (!open && focusBtn) btn.focus();
1458
+ }
1459
+ btn.onclick = () => setOpen(!panel.classList.contains("open"));
1460
  document.addEventListener("click", e => {
1461
+ if (!panel.contains(e.target) && e.target !== btn && !btn.contains(e.target))
1462
+ setOpen(false);
1463
+ });
1464
+ document.addEventListener("keydown", e => {
1465
+ if (e.key === "Escape" && panel.classList.contains("open")) {
1466
+ setOpen(false, true);
1467
+ }
1468
  });
1469
  const ids = ["togNone","togAI","togHuman","togDiffusion","togDiffusionV2"];
1470
  function sync() {
1471
  ids.forEach(id => {
1472
+ const el = $(id);
1473
+ const on = S.animMode === el.getAttribute("data-anim");
1474
+ el.classList.toggle("on", on);
1475
+ el.setAttribute("aria-pressed", String(on));
1476
  });
1477
  }
1478
  ids.forEach(id => {
 
1482
  sync();
1483
  };
1484
  });
1485
+ const jump = $("jumpLatest");
1486
+ if (jump) jump.onclick = () => scrollBottom(true);
1487
  sync();
1488
  }
1489
  /* ── Init ── */
1490
  function init() {
1491
+ updateAppHeight();
1492
+ window.addEventListener("resize", updateAppHeight);
1493
+ window.addEventListener("orientationchange", updateAppHeight);
1494
+ if (window.visualViewport) {
1495
+ window.visualViewport.addEventListener("resize", updateAppHeight);
1496
+ window.visualViewport.addEventListener("scroll", updateAppHeight);
1497
+ }
1498
  S.clientId = getClientId();
1499
+ $("chat").addEventListener("scroll", () => {
1500
+ S.atBottom = isNearBottom();
1501
+ if (S.atBottom) setJumpLatest(false);
1502
+ }, { passive: true });
1503
  $("sendBtn").onclick = submitPrompt;
1504
  $("newChatBtn").onclick = newChat;
1505
  $("prompt").addEventListener("input", e => autoGrow(e.target));