wop commited on
Commit
545bc85
Β·
verified Β·
1 Parent(s): 20b22af

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +99 -64
templates/index.html CHANGED
@@ -45,7 +45,6 @@
45
  position: relative; z-index: 1;
46
  height: 100%; display: flex; flex-direction: column;
47
  }
48
-
49
  /* ── Topbar ── */
50
  #topbar {
51
  height: 56px; padding: 0 16px;
@@ -84,7 +83,6 @@
84
  background: rgba(108,131,255,.06);
85
  }
86
  .top-btn svg { width: 14px; height: 14px; }
87
-
88
  /* ── Status bar ── */
89
  #statusbar {
90
  height: 0; overflow: hidden;
@@ -107,7 +105,6 @@
107
  0%, 100% { opacity: .4; transform: scale(.85); }
108
  50% { opacity: 1; transform: scale(1.1); }
109
  }
110
-
111
  /* ── Chat area ── */
112
  #chat {
113
  flex: 1; overflow-y: auto;
@@ -118,7 +115,6 @@
118
  #chat::-webkit-scrollbar-track { background: transparent; }
119
  #chat::-webkit-scrollbar-thumb { background: rgba(255,255,255,.1); border-radius: 9px; }
120
  .wrap { max-width: 760px; margin: 0 auto; }
121
-
122
  /* ── Welcome ── */
123
  .welcome {
124
  margin: 6vh auto 0; max-width: 480px; text-align: center;
@@ -135,7 +131,6 @@
135
  from { opacity: 0; transform: translateY(12px); }
136
  to { opacity: 1; transform: translateY(0); }
137
  }
138
-
139
  /* ── Turns ── */
140
  .turn {
141
  display: flex; gap: 10px; margin-bottom: 6px;
@@ -189,7 +184,6 @@
189
  .chip.muted { color: var(--muted); }
190
  .chip.warn { color: var(--warn); border-color: rgba(251,191,36,.25); }
191
  .chip.matched { color: var(--accent); border-color: rgba(108,131,255,.25); }
192
-
193
  /* ── Best answer ── */
194
  .best-answer-bubble {
195
  border: 1px solid rgba(45,212,191,.15);
@@ -203,7 +197,6 @@
203
  margin-top: 4px;
204
  display: flex; gap: 6px; align-items: center; flex-wrap: wrap;
205
  }
206
-
207
  /* ── Vote ── */
208
  .vote-row { display: flex; gap: 4px; align-items: center; margin-top: 6px; }
209
  .vote-btn {
@@ -230,7 +223,6 @@
230
  background: rgba(248,113,113,.08);
231
  }
232
  .vote-btn:active { transform: scale(.94); }
233
-
234
  .action-btn {
235
  border: 1px solid var(--border2);
236
  background: rgba(255,255,255,.02);
@@ -245,7 +237,6 @@
245
  border-color: rgba(108,131,255,.35);
246
  color: var(--text);
247
  }
248
-
249
  /* ── Write answer inline panel ── */
250
  .write-answer-btn {
251
  border: 1px solid rgba(45,212,191,.3);
@@ -264,7 +255,6 @@
264
  border-color: rgba(45,212,191,.5);
265
  }
266
  .write-answer-btn svg { width: 14px; height: 14px; }
267
-
268
  .write-panel {
269
  max-height: 0; overflow: hidden;
270
  transition: max-height 300ms ease, opacity 250ms ease, margin 200ms ease;
@@ -316,7 +306,6 @@
316
  font: inherit; font-size: 12px;
317
  cursor: pointer;
318
  }
319
-
320
  /* ── Other answers expandable ── */
321
  .other-answers-toggle {
322
  margin-top: 8px;
@@ -366,7 +355,6 @@
366
  font-size: 13px; line-height: 1.6;
367
  white-space: pre-wrap; word-break: break-word;
368
  }
369
-
370
  /* ── Versions ── */
371
  .versions-toggle {
372
  margin-top: 4px;
@@ -401,7 +389,6 @@
401
  font-size: 12px; line-height: 1.55;
402
  white-space: pre-wrap; word-break: break-word;
403
  }
404
-
405
  /* ── Propose version ── */
406
  .propose-panel {
407
  max-height: 0; overflow: hidden;
@@ -450,7 +437,6 @@
450
  font: inherit; font-size: 11px;
451
  cursor: pointer;
452
  }
453
-
454
  /* ── Typing indicator ── */
455
  .typing-indicator {
456
  display: flex; gap: 10px; margin-bottom: 6px;
@@ -475,7 +461,6 @@
475
  0%, 60%, 100% { transform: translateY(0); opacity: .35; }
476
  30% { transform: translateY(-5px); opacity: 1; }
477
  }
478
-
479
  /* ── Diffusion ── */
480
  .diffusion-text {
481
  display: inline;
@@ -496,6 +481,29 @@
496
  opacity: 1;
497
  transform: translateY(0);
498
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  .related-stack {
500
  margin-top: 14px;
501
  padding-top: 12px;
@@ -548,7 +556,6 @@
548
  line-height: 1.5;
549
  margin-top: 6px;
550
  }
551
-
552
  /* ── Composer (questions only) ── */
553
  .compose {
554
  border-top: 1px solid var(--border);
@@ -602,7 +609,6 @@
602
  }
603
  .send-btn:active { transform: scale(.96); }
604
  .send-btn:disabled { opacity: .4; cursor: not-allowed; transform: none; box-shadow: none; }
605
-
606
  /* ── Settings panel ── */
607
  #settingsPanel {
608
  position: fixed;
@@ -654,7 +660,6 @@
654
  transition: transform 200ms ease;
655
  }
656
  .toggle.on::after { transform: translateX(16px); }
657
-
658
  /* ── Toast ── */
659
  #toast {
660
  position: fixed;
@@ -676,9 +681,7 @@
676
  #toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
677
  #toast.good { border-color: rgba(45,212,191,.4); color: var(--good); }
678
  #toast.bad { border-color: rgba(248,113,113,.4); color: var(--bad); }
679
-
680
  .no-answer-bubble { border-style: dashed !important; color: var(--muted); }
681
-
682
  @media (max-width: 600px) {
683
  #topbar { padding: 0 10px; }
684
  .brand-sub { display: none; }
@@ -733,6 +736,10 @@
733
  <div><div class="setting-label">Diffusion</div><div class="setting-desc">Blur-to-clear reveal</div></div>
734
  <div class="toggle" id="togDiffusion" data-anim="diffusion"></div>
735
  </div>
 
 
 
 
736
  </div>
737
 
738
  <div id="chat">
@@ -771,7 +778,6 @@
771
  animMode: localStorage.getItem("hi_anim") || "none",
772
  };
773
  const $ = id => document.getElementById(id);
774
-
775
  function getClientId() {
776
  let id = localStorage.getItem("hi_client_id");
777
  if (!id) {
@@ -781,7 +787,6 @@
781
  }
782
  return id;
783
  }
784
-
785
  function toast(msg, kind = "") {
786
  const t = $("toast");
787
  t.textContent = msg;
@@ -790,7 +795,6 @@
790
  clearTimeout(t._t);
791
  t._t = setTimeout(() => { t.className = ""; }, 2000);
792
  }
793
-
794
  function showStatus(text) {
795
  $("statusText").textContent = text;
796
  $("statusbar").classList.add("visible");
@@ -798,7 +802,6 @@
798
  function hideStatus() {
799
  $("statusbar").classList.remove("visible");
800
  }
801
-
802
  function esc(s) {
803
  return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
804
  }
@@ -823,7 +826,74 @@
823
  return `<span class="diffusion-token" style="--blur:${blur}px;--opacity:${opacity};--shift:${shift}px;">${esc(part)}</span>`;
824
  }).join("");
825
  }
826
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
827
  async function animateText(el, text) {
828
  const mode = S.animMode;
829
  if (mode === "ai") {
@@ -855,11 +925,12 @@
855
  if (i % 5 === 0) scrollBottom();
856
  await sleep(26 + Math.random() * 30);
857
  }
 
 
858
  } else {
859
  el.innerHTML = nl2br(text);
860
  }
861
  }
862
-
863
  function showTyping() {
864
  removeTyping();
865
  $("transcript").insertAdjacentHTML("beforeend", `
@@ -870,7 +941,6 @@
870
  scrollBottom();
871
  }
872
  function removeTyping() { const el = $("typingInd"); if (el) el.remove(); }
873
-
874
  function activeVersion(answer) {
875
  const v = answer?.versions || [];
876
  if (!v.length) return null;
@@ -888,7 +958,6 @@
888
  return d !== 0 ? d : String(b.created_at||"").localeCompare(String(a.created_at||""));
889
  });
890
  }
891
-
892
  /* ── Render helpers ── */
893
  function renderVoteRow(answerId, ver) {
894
  const vu = ver.votes_by_client && ver.votes_by_client[S.clientId] === 1;
@@ -898,7 +967,6 @@
898
  <button class="vote-btn ${vd?"voted-down":""}" data-vote="${answerId}|${ver.id}|-1">β–Ό</button>
899
  </div>`;
900
  }
901
-
902
  function renderVersions(answer) {
903
  const act = activeVersion(answer);
904
  const others = (answer.versions||[]).filter(v => v.id !== act?.id);
@@ -920,7 +988,6 @@
920
  </div>`).join("")}
921
  </div>`;
922
  }
923
-
924
  function renderPropose(answerId) {
925
  return `
926
  <button class="action-btn" data-propose="${answerId}">Propose version</button>
@@ -932,7 +999,6 @@
932
  </div>
933
  </div>`;
934
  }
935
-
936
  function renderWriteAnswer() {
937
  return `
938
  <button class="write-answer-btn" id="writeAnswerBtn">
@@ -947,7 +1013,6 @@
947
  </div>
948
  </div>`;
949
  }
950
-
951
  function renderAnswerBlock(answer, idx, isBest) {
952
  const v = activeVersion(answer);
953
  if (!v) return "";
@@ -967,7 +1032,6 @@
967
  ${renderPropose(answer.id)}
968
  </div>`;
969
  }
970
-
971
  function renderOtherAnswers(answers) {
972
  if (answers.length <= 1) return "";
973
  const others = answers.slice(1);
@@ -996,10 +1060,8 @@
996
  }).join("")}
997
  </div>`;
998
  }
999
-
1000
  function renderRelated(rel) {
1001
  if (!rel || !rel.length) return "";
1002
-
1003
  return `
1004
  <div class="related-stack">
1005
  <div class="chip muted">from similar questions</div>
@@ -1023,21 +1085,17 @@
1023
  </div>
1024
  `;
1025
  }
1026
-
1027
  /* ── Main render ── */
1028
  async function renderConversation(questionText, doAnimate) {
1029
  const tr = $("transcript");
1030
  const wl = $("welcome");
1031
  tr.innerHTML = "";
1032
-
1033
  if (!S.conversation) {
1034
  wl.style.display = "block";
1035
  return;
1036
  }
1037
  wl.style.display = "none";
1038
-
1039
  const q = questionText || S.conversation.question || "";
1040
-
1041
  // Question bubble
1042
  tr.insertAdjacentHTML("beforeend", `
1043
  <div class="turn user">
@@ -1050,9 +1108,7 @@
1050
  </div>
1051
  <div class="avatar user">U</div>
1052
  </div>`);
1053
-
1054
  const answers = sortedAnswers(S.conversation);
1055
-
1056
  if (!answers.length) {
1057
  // No answers yet β€” show placeholder + write button
1058
  tr.insertAdjacentHTML("beforeend", `
@@ -1078,7 +1134,6 @@
1078
  <div id="relatedMount"></div>
1079
  </div>
1080
  </div>`);
1081
-
1082
  // Animate best answer text
1083
  const bestV = activeVersion(best);
1084
  if (bestV) {
@@ -1090,16 +1145,13 @@
1090
  }
1091
  }
1092
  }
1093
-
1094
  const relatedMount = $("relatedMount");
1095
  if (relatedMount && S.relatedAnswers.length) {
1096
  relatedMount.innerHTML = renderRelated(S.relatedAnswers);
1097
  }
1098
-
1099
  bindHandlers();
1100
  scrollBottom();
1101
  }
1102
-
1103
  /* ── Bind handlers ── */
1104
  function bindHandlers() {
1105
  // Votes
@@ -1119,7 +1171,6 @@
1119
  } else toast(res.error||"Error","bad");
1120
  };
1121
  });
1122
-
1123
  // Toggle versions
1124
  document.querySelectorAll("[data-toggle-versions]").forEach(btn => {
1125
  btn.onclick = () => {
@@ -1130,7 +1181,6 @@
1130
  btn.querySelector(".arrow").style.transform = open ? "rotate(90deg)" : "";
1131
  };
1132
  });
1133
-
1134
  // Other answers toggle
1135
  const oat = $("otherAnswersToggle");
1136
  if (oat) oat.onclick = () => {
@@ -1139,7 +1189,6 @@
1139
  const open = p.classList.toggle("open");
1140
  oat.classList.toggle("open", open);
1141
  };
1142
-
1143
  const rt = $("relatedToggle");
1144
  if (rt) rt.onclick = () => {
1145
  const p = $("relatedPanel");
@@ -1147,7 +1196,6 @@
1147
  const open = p.classList.toggle("open");
1148
  rt.classList.toggle("open", open);
1149
  };
1150
-
1151
  // Propose toggle
1152
  document.querySelectorAll("[data-propose]").forEach(btn => {
1153
  btn.onclick = () => {
@@ -1161,7 +1209,6 @@
1161
  if (p) p.classList.remove("open");
1162
  };
1163
  });
1164
-
1165
  // Submit proposal
1166
  document.querySelectorAll("[data-submit-proposal]").forEach(btn => {
1167
  btn.onclick = async () => {
@@ -1185,7 +1232,6 @@
1185
  } else toast(res.error||"Error","bad");
1186
  };
1187
  });
1188
-
1189
  // Write answer toggle
1190
  const wab = $("writeAnswerBtn");
1191
  if (wab) wab.onclick = () => {
@@ -1198,14 +1244,12 @@
1198
  }
1199
  }
1200
  };
1201
-
1202
  // Cancel write
1203
  const wc = $("writeCancel");
1204
  if (wc) wc.onclick = () => {
1205
  const p = $("writePanel");
1206
  if (p) p.classList.remove("open");
1207
  };
1208
-
1209
  // Submit answer
1210
  const ws = $("writeSubmit");
1211
  if (ws) ws.onclick = async () => {
@@ -1231,11 +1275,9 @@
1231
  } else toast(res.error||"Error","bad");
1232
  };
1233
  }
1234
-
1235
  function save() {
1236
  if (S.conversation) localStorage.setItem("hi_last_cid", S.conversation.id);
1237
  }
1238
-
1239
  /* ── API ── */
1240
  async function callAPI(action, payload = {}) {
1241
  const resp = await fetch("/api", {
@@ -1245,7 +1287,6 @@
1245
  });
1246
  return resp.json();
1247
  }
1248
-
1249
  /* ── Ask (input bar is ONLY for asking) ── */
1250
  async function askQuestion(q) {
1251
  showStatus("Searching for answers…");
@@ -1265,7 +1306,6 @@
1265
  toast(res.matched ? "Existing answer found" : "New question created", "good");
1266
  await renderConversation(q, true);
1267
  }
1268
-
1269
  async function submitPrompt() {
1270
  const p = $("prompt");
1271
  const text = p.value.trim();
@@ -1276,12 +1316,10 @@
1276
  // because the input bar is ONLY for questions
1277
  await askQuestion(text);
1278
  }
1279
-
1280
  function autoGrow(el) {
1281
  el.style.height = "auto";
1282
  el.style.height = Math.min(el.scrollHeight, 180) + "px";
1283
  }
1284
-
1285
  async function loadSaved() {
1286
  const id = localStorage.getItem("hi_last_cid");
1287
  if (!id) return;
@@ -1295,7 +1333,6 @@
1295
  renderConversation(S.currentQuestion, false);
1296
  }
1297
  }
1298
-
1299
  function newChat() {
1300
  S.conversation = null;
1301
  S.currentQuestion = "";
@@ -1306,7 +1343,6 @@
1306
  $("prompt").value = "";
1307
  $("prompt").focus();
1308
  }
1309
-
1310
  /* ── Settings ── */
1311
  function initSettings() {
1312
  const panel = $("settingsPanel");
@@ -1315,7 +1351,7 @@
1315
  if (!panel.contains(e.target) && e.target !== $("settingsBtn") && !$("settingsBtn").contains(e.target))
1316
  panel.classList.remove("open");
1317
  });
1318
- const ids = ["togNone","togAI","togHuman","togDiffusion"];
1319
  function sync() {
1320
  ids.forEach(id => {
1321
  $(id).classList.toggle("on", S.animMode === $(id).getAttribute("data-anim"));
@@ -1330,7 +1366,6 @@
1330
  });
1331
  sync();
1332
  }
1333
-
1334
  /* ── Init ── */
1335
  function init() {
1336
  S.clientId = getClientId();
@@ -1351,4 +1386,4 @@
1351
  })();
1352
  </script>
1353
  </body>
1354
- </html>
 
45
  position: relative; z-index: 1;
46
  height: 100%; display: flex; flex-direction: column;
47
  }
 
48
  /* ── Topbar ── */
49
  #topbar {
50
  height: 56px; padding: 0 16px;
 
83
  background: rgba(108,131,255,.06);
84
  }
85
  .top-btn svg { width: 14px; height: 14px; }
 
86
  /* ── Status bar ── */
87
  #statusbar {
88
  height: 0; overflow: hidden;
 
105
  0%, 100% { opacity: .4; transform: scale(.85); }
106
  50% { opacity: 1; transform: scale(1.1); }
107
  }
 
108
  /* ── Chat area ── */
109
  #chat {
110
  flex: 1; overflow-y: auto;
 
115
  #chat::-webkit-scrollbar-track { background: transparent; }
116
  #chat::-webkit-scrollbar-thumb { background: rgba(255,255,255,.1); border-radius: 9px; }
117
  .wrap { max-width: 760px; margin: 0 auto; }
 
118
  /* ── Welcome ── */
119
  .welcome {
120
  margin: 6vh auto 0; max-width: 480px; text-align: center;
 
131
  from { opacity: 0; transform: translateY(12px); }
132
  to { opacity: 1; transform: translateY(0); }
133
  }
 
134
  /* ── Turns ── */
135
  .turn {
136
  display: flex; gap: 10px; margin-bottom: 6px;
 
184
  .chip.muted { color: var(--muted); }
185
  .chip.warn { color: var(--warn); border-color: rgba(251,191,36,.25); }
186
  .chip.matched { color: var(--accent); border-color: rgba(108,131,255,.25); }
 
187
  /* ── Best answer ── */
188
  .best-answer-bubble {
189
  border: 1px solid rgba(45,212,191,.15);
 
197
  margin-top: 4px;
198
  display: flex; gap: 6px; align-items: center; flex-wrap: wrap;
199
  }
 
200
  /* ── Vote ── */
201
  .vote-row { display: flex; gap: 4px; align-items: center; margin-top: 6px; }
202
  .vote-btn {
 
223
  background: rgba(248,113,113,.08);
224
  }
225
  .vote-btn:active { transform: scale(.94); }
 
226
  .action-btn {
227
  border: 1px solid var(--border2);
228
  background: rgba(255,255,255,.02);
 
237
  border-color: rgba(108,131,255,.35);
238
  color: var(--text);
239
  }
 
240
  /* ── Write answer inline panel ── */
241
  .write-answer-btn {
242
  border: 1px solid rgba(45,212,191,.3);
 
255
  border-color: rgba(45,212,191,.5);
256
  }
257
  .write-answer-btn svg { width: 14px; height: 14px; }
 
258
  .write-panel {
259
  max-height: 0; overflow: hidden;
260
  transition: max-height 300ms ease, opacity 250ms ease, margin 200ms ease;
 
306
  font: inherit; font-size: 12px;
307
  cursor: pointer;
308
  }
 
309
  /* ── Other answers expandable ── */
310
  .other-answers-toggle {
311
  margin-top: 8px;
 
355
  font-size: 13px; line-height: 1.6;
356
  white-space: pre-wrap; word-break: break-word;
357
  }
 
358
  /* ── Versions ── */
359
  .versions-toggle {
360
  margin-top: 4px;
 
389
  font-size: 12px; line-height: 1.55;
390
  white-space: pre-wrap; word-break: break-word;
391
  }
 
392
  /* ── Propose version ── */
393
  .propose-panel {
394
  max-height: 0; overflow: hidden;
 
437
  font: inherit; font-size: 11px;
438
  cursor: pointer;
439
  }
 
440
  /* ── Typing indicator ── */
441
  .typing-indicator {
442
  display: flex; gap: 10px; margin-bottom: 6px;
 
461
  0%, 60%, 100% { transform: translateY(0); opacity: .35; }
462
  30% { transform: translateY(-5px); opacity: 1; }
463
  }
 
464
  /* ── Diffusion ── */
465
  .diffusion-text {
466
  display: inline;
 
481
  opacity: 1;
482
  transform: translateY(0);
483
  }
484
+ .diffusion-v2-text {
485
+ display: inline;
486
+ }
487
+ .diffusion-v2-char {
488
+ display: inline-block;
489
+ min-width: .04em;
490
+ opacity: .82;
491
+ color: rgba(235,239,247,.92);
492
+ transition:
493
+ color 180ms ease,
494
+ opacity 180ms ease,
495
+ transform 180ms ease;
496
+ }
497
+ .diffusion-v2-char.pending {
498
+ color: rgba(139,147,168,.88);
499
+ opacity: .9;
500
+ transform: translateY(calc(var(--drift, 0) * 1px));
501
+ }
502
+ .diffusion-v2-char.resolved {
503
+ color: var(--text);
504
+ opacity: 1;
505
+ transform: translateY(0);
506
+ }
507
  .related-stack {
508
  margin-top: 14px;
509
  padding-top: 12px;
 
556
  line-height: 1.5;
557
  margin-top: 6px;
558
  }
 
559
  /* ── Composer (questions only) ── */
560
  .compose {
561
  border-top: 1px solid var(--border);
 
609
  }
610
  .send-btn:active { transform: scale(.96); }
611
  .send-btn:disabled { opacity: .4; cursor: not-allowed; transform: none; box-shadow: none; }
 
612
  /* ── Settings panel ── */
613
  #settingsPanel {
614
  position: fixed;
 
660
  transition: transform 200ms ease;
661
  }
662
  .toggle.on::after { transform: translateX(16px); }
 
663
  /* ── Toast ── */
664
  #toast {
665
  position: fixed;
 
681
  #toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
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; }
 
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">
 
778
  animMode: localStorage.getItem("hi_anim") || "none",
779
  };
780
  const $ = id => document.getElementById(id);
 
781
  function getClientId() {
782
  let id = localStorage.getItem("hi_client_id");
783
  if (!id) {
 
787
  }
788
  return id;
789
  }
 
790
  function toast(msg, kind = "") {
791
  const t = $("toast");
792
  t.textContent = msg;
 
795
  clearTimeout(t._t);
796
  t._t = setTimeout(() => { t.className = ""; }, 2000);
797
  }
 
798
  function showStatus(text) {
799
  $("statusText").textContent = text;
800
  $("statusbar").classList.add("visible");
 
802
  function hideStatus() {
803
  $("statusbar").classList.remove("visible");
804
  }
 
805
  function esc(s) {
806
  return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
807
  }
 
826
  return `<span class="diffusion-token" style="--blur:${blur}px;--opacity:${opacity};--shift:${shift}px;">${esc(part)}</span>`;
827
  }).join("");
828
  }
829
+ function escAttr(s) {
830
+ return String(s)
831
+ .replace(/&/g,"&amp;")
832
+ .replace(/"/g,"&quot;")
833
+ .replace(/</g,"&lt;")
834
+ .replace(/>/g,"&gt;");
835
+ }
836
+ function randomGlyphFor(ch) {
837
+ if (/\s/.test(ch)) return ch;
838
+ const pools = {
839
+ upper: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
840
+ lower: "abcdefghijklmnopqrstuvwxyz",
841
+ digit: "0123456789",
842
+ punct: ".,!?;:-_=+/*#%&$@<>[]{}()"
843
+ };
844
+ let pool = pools.punct;
845
+ if (/[A-Z]/.test(ch)) pool = pools.upper;
846
+ else if (/[a-z]/.test(ch)) pool = pools.lower;
847
+ else if (/[0-9]/.test(ch)) pool = pools.digit;
848
+ return pool[Math.floor(Math.random() * pool.length)] || ch;
849
+ }
850
+ function buildDiffusionV2(text) {
851
+ const chars = Array.from(String(text || ""));
852
+ const total = chars.filter(ch => !/\s/.test(ch)).length || 1;
853
+ let seen = 0;
854
+ const meta = chars.map((ch, i) => {
855
+ if (/\s/.test(ch)) return { ch, threshold: -1, drift: 0 };
856
+ const base = seen / total;
857
+ seen += 1;
858
+ const jitter = (Math.random() - 0.5) * 0.18;
859
+ const tailBoost = Math.pow(i / Math.max(chars.length - 1, 1), 1.35) * 0.18;
860
+ return {
861
+ ch,
862
+ threshold: Math.min(.985, Math.max(.04, base * 0.82 + tailBoost + jitter)),
863
+ drift: Math.round((Math.random() - 0.5) * 4)
864
+ };
865
+ });
866
+ const html = meta.map((item, i) => {
867
+ if (/\s/.test(item.ch)) return esc(item.ch);
868
+ const start = randomGlyphFor(item.ch);
869
+ return `<span class="diffusion-v2-char pending" data-i="${i}" data-final="${escAttr(item.ch)}" style="--drift:${item.drift};">${esc(start)}</span>`;
870
+ }).join("");
871
+ return { html, meta };
872
+ }
873
+ async function animateDiffusionV2(el, text) {
874
+ const { html, meta } = buildDiffusionV2(text);
875
+ el.innerHTML = `<span class="diffusion-v2-text">${html}</span>`;
876
+ scrollBottom();
877
+ const nodes = Array.from(el.querySelectorAll(".diffusion-v2-char"));
878
+ const steps = Math.max(10, Math.min(22, Math.ceil(String(text || "").length / 8) + 8));
879
+ for (let step = 0; step <= steps; step++) {
880
+ const progress = step / steps;
881
+ let charNodeIndex = 0;
882
+ for (let i = 0; i < meta.length; i++) {
883
+ const item = meta[i];
884
+ if (/\s/.test(item.ch)) continue;
885
+ const node = nodes[charNodeIndex++];
886
+ if (!node) continue;
887
+ const resolved = progress >= item.threshold;
888
+ node.classList.toggle("resolved", resolved);
889
+ node.classList.toggle("pending", !resolved);
890
+ node.textContent = resolved ? item.ch : randomGlyphFor(item.ch);
891
+ }
892
+ scrollBottom();
893
+ await sleep(42 + Math.random() * 26);
894
+ }
895
+ el.textContent = String(text || "");
896
+ }
897
  async function animateText(el, text) {
898
  const mode = S.animMode;
899
  if (mode === "ai") {
 
925
  if (i % 5 === 0) scrollBottom();
926
  await sleep(26 + Math.random() * 30);
927
  }
928
+ } else if (mode === "diffusion-v2") {
929
+ await animateDiffusionV2(el, text);
930
  } else {
931
  el.innerHTML = nl2br(text);
932
  }
933
  }
 
934
  function showTyping() {
935
  removeTyping();
936
  $("transcript").insertAdjacentHTML("beforeend", `
 
941
  scrollBottom();
942
  }
943
  function removeTyping() { const el = $("typingInd"); if (el) el.remove(); }
 
944
  function activeVersion(answer) {
945
  const v = answer?.versions || [];
946
  if (!v.length) return null;
 
958
  return d !== 0 ? d : String(b.created_at||"").localeCompare(String(a.created_at||""));
959
  });
960
  }
 
961
  /* ── Render helpers ── */
962
  function renderVoteRow(answerId, ver) {
963
  const vu = ver.votes_by_client && ver.votes_by_client[S.clientId] === 1;
 
967
  <button class="vote-btn ${vd?"voted-down":""}" data-vote="${answerId}|${ver.id}|-1">β–Ό</button>
968
  </div>`;
969
  }
 
970
  function renderVersions(answer) {
971
  const act = activeVersion(answer);
972
  const others = (answer.versions||[]).filter(v => v.id !== act?.id);
 
988
  </div>`).join("")}
989
  </div>`;
990
  }
 
991
  function renderPropose(answerId) {
992
  return `
993
  <button class="action-btn" data-propose="${answerId}">Propose version</button>
 
999
  </div>
1000
  </div>`;
1001
  }
 
1002
  function renderWriteAnswer() {
1003
  return `
1004
  <button class="write-answer-btn" id="writeAnswerBtn">
 
1013
  </div>
1014
  </div>`;
1015
  }
 
1016
  function renderAnswerBlock(answer, idx, isBest) {
1017
  const v = activeVersion(answer);
1018
  if (!v) return "";
 
1032
  ${renderPropose(answer.id)}
1033
  </div>`;
1034
  }
 
1035
  function renderOtherAnswers(answers) {
1036
  if (answers.length <= 1) return "";
1037
  const others = answers.slice(1);
 
1060
  }).join("")}
1061
  </div>`;
1062
  }
 
1063
  function renderRelated(rel) {
1064
  if (!rel || !rel.length) return "";
 
1065
  return `
1066
  <div class="related-stack">
1067
  <div class="chip muted">from similar questions</div>
 
1085
  </div>
1086
  `;
1087
  }
 
1088
  /* ── Main render ── */
1089
  async function renderConversation(questionText, doAnimate) {
1090
  const tr = $("transcript");
1091
  const wl = $("welcome");
1092
  tr.innerHTML = "";
 
1093
  if (!S.conversation) {
1094
  wl.style.display = "block";
1095
  return;
1096
  }
1097
  wl.style.display = "none";
 
1098
  const q = questionText || S.conversation.question || "";
 
1099
  // Question bubble
1100
  tr.insertAdjacentHTML("beforeend", `
1101
  <div class="turn user">
 
1108
  </div>
1109
  <div class="avatar user">U</div>
1110
  </div>`);
 
1111
  const answers = sortedAnswers(S.conversation);
 
1112
  if (!answers.length) {
1113
  // No answers yet β€” show placeholder + write button
1114
  tr.insertAdjacentHTML("beforeend", `
 
1134
  <div id="relatedMount"></div>
1135
  </div>
1136
  </div>`);
 
1137
  // Animate best answer text
1138
  const bestV = activeVersion(best);
1139
  if (bestV) {
 
1145
  }
1146
  }
1147
  }
 
1148
  const relatedMount = $("relatedMount");
1149
  if (relatedMount && S.relatedAnswers.length) {
1150
  relatedMount.innerHTML = renderRelated(S.relatedAnswers);
1151
  }
 
1152
  bindHandlers();
1153
  scrollBottom();
1154
  }
 
1155
  /* ── Bind handlers ── */
1156
  function bindHandlers() {
1157
  // Votes
 
1171
  } else toast(res.error||"Error","bad");
1172
  };
1173
  });
 
1174
  // Toggle versions
1175
  document.querySelectorAll("[data-toggle-versions]").forEach(btn => {
1176
  btn.onclick = () => {
 
1181
  btn.querySelector(".arrow").style.transform = open ? "rotate(90deg)" : "";
1182
  };
1183
  });
 
1184
  // Other answers toggle
1185
  const oat = $("otherAnswersToggle");
1186
  if (oat) oat.onclick = () => {
 
1189
  const open = p.classList.toggle("open");
1190
  oat.classList.toggle("open", open);
1191
  };
 
1192
  const rt = $("relatedToggle");
1193
  if (rt) rt.onclick = () => {
1194
  const p = $("relatedPanel");
 
1196
  const open = p.classList.toggle("open");
1197
  rt.classList.toggle("open", open);
1198
  };
 
1199
  // Propose toggle
1200
  document.querySelectorAll("[data-propose]").forEach(btn => {
1201
  btn.onclick = () => {
 
1209
  if (p) p.classList.remove("open");
1210
  };
1211
  });
 
1212
  // Submit proposal
1213
  document.querySelectorAll("[data-submit-proposal]").forEach(btn => {
1214
  btn.onclick = async () => {
 
1232
  } else toast(res.error||"Error","bad");
1233
  };
1234
  });
 
1235
  // Write answer toggle
1236
  const wab = $("writeAnswerBtn");
1237
  if (wab) wab.onclick = () => {
 
1244
  }
1245
  }
1246
  };
 
1247
  // Cancel write
1248
  const wc = $("writeCancel");
1249
  if (wc) wc.onclick = () => {
1250
  const p = $("writePanel");
1251
  if (p) p.classList.remove("open");
1252
  };
 
1253
  // Submit answer
1254
  const ws = $("writeSubmit");
1255
  if (ws) ws.onclick = async () => {
 
1275
  } else toast(res.error||"Error","bad");
1276
  };
1277
  }
 
1278
  function save() {
1279
  if (S.conversation) localStorage.setItem("hi_last_cid", S.conversation.id);
1280
  }
 
1281
  /* ── API ── */
1282
  async function callAPI(action, payload = {}) {
1283
  const resp = await fetch("/api", {
 
1287
  });
1288
  return resp.json();
1289
  }
 
1290
  /* ── Ask (input bar is ONLY for asking) ── */
1291
  async function askQuestion(q) {
1292
  showStatus("Searching for answers…");
 
1306
  toast(res.matched ? "Existing answer found" : "New question created", "good");
1307
  await renderConversation(q, true);
1308
  }
 
1309
  async function submitPrompt() {
1310
  const p = $("prompt");
1311
  const text = p.value.trim();
 
1316
  // because the input bar is ONLY for questions
1317
  await askQuestion(text);
1318
  }
 
1319
  function autoGrow(el) {
1320
  el.style.height = "auto";
1321
  el.style.height = Math.min(el.scrollHeight, 180) + "px";
1322
  }
 
1323
  async function loadSaved() {
1324
  const id = localStorage.getItem("hi_last_cid");
1325
  if (!id) return;
 
1333
  renderConversation(S.currentQuestion, false);
1334
  }
1335
  }
 
1336
  function newChat() {
1337
  S.conversation = null;
1338
  S.currentQuestion = "";
 
1343
  $("prompt").value = "";
1344
  $("prompt").focus();
1345
  }
 
1346
  /* ── Settings ── */
1347
  function initSettings() {
1348
  const panel = $("settingsPanel");
 
1351
  if (!panel.contains(e.target) && e.target !== $("settingsBtn") && !$("settingsBtn").contains(e.target))
1352
  panel.classList.remove("open");
1353
  });
1354
+ const ids = ["togNone","togAI","togHuman","togDiffusion","togDiffusionV2"];
1355
  function sync() {
1356
  ids.forEach(id => {
1357
  $(id).classList.toggle("on", S.animMode === $(id).getAttribute("data-anim"));
 
1366
  });
1367
  sync();
1368
  }
 
1369
  /* ── Init ── */
1370
  function init() {
1371
  S.clientId = getClientId();
 
1386
  })();
1387
  </script>
1388
  </body>
1389
+ </html>