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

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +884 -594
templates/index.html CHANGED
@@ -17,416 +17,618 @@
17
  --accent2: #a16eff;
18
  --good: #2dd4bf;
19
  --bad: #f87171;
 
20
  --shadow: 0 20px 60px rgba(0,0,0,.35);
21
  --r: 18px;
22
  --r2: 12px;
23
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
24
  --font: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
 
25
  }
26
-
27
- * { box-sizing: border-box; }
28
  html, body {
29
- width: 100%;
30
- height: 100%;
31
- margin: 0;
32
- background: radial-gradient(circle at top, #161c2b 0%, var(--bg) 46%);
33
  color: var(--text);
34
  font-family: var(--font);
35
  overflow: hidden;
 
36
  }
37
-
38
  body::before {
39
  content: "";
40
- position: fixed;
41
- inset: 0;
42
  background-image:
43
- linear-gradient(rgba(108,131,255,.04) 1px, transparent 1px),
44
- linear-gradient(90deg, rgba(108,131,255,.04) 1px, transparent 1px);
45
  background-size: 44px 44px;
46
  pointer-events: none;
47
- opacity: .35;
48
  }
49
-
50
  #app {
51
- position: relative;
52
- z-index: 1;
53
- height: 100%;
54
- display: flex;
55
- flex-direction: column;
56
  }
57
 
 
58
  #topbar {
59
- height: 72px;
60
- padding: 0 22px;
61
- display: flex;
62
- align-items: center;
63
- justify-content: space-between;
64
  border-bottom: 1px solid var(--border);
65
- backdrop-filter: blur(12px);
66
- background: rgba(11,14,20,.72);
 
67
  }
68
-
69
  .brand {
70
- display: flex;
71
- align-items: center;
72
- gap: 12px;
73
- min-width: 0;
74
  }
75
-
76
  .logo {
77
- width: 36px;
78
- height: 36px;
79
- border-radius: 12px;
80
- display: grid;
81
- place-items: center;
82
  background: linear-gradient(135deg, var(--accent), var(--accent2));
83
- box-shadow: 0 10px 24px rgba(108,131,255,.25);
84
  flex: 0 0 auto;
85
  }
86
-
87
  .brand-title {
88
- font-weight: 800;
89
- letter-spacing: -.03em;
90
- font-size: 18px;
91
  }
92
-
93
  .brand-sub {
94
- margin-top: 2px;
95
- color: var(--muted);
96
- font-size: 12px;
97
- font-family: var(--mono);
98
- }
99
-
100
- .top-actions {
101
- display: flex;
102
- align-items: center;
103
- gap: 10px;
104
  }
105
-
106
- .pill {
107
- border: 1px solid var(--border);
108
  background: rgba(255,255,255,.03);
109
- color: var(--text);
110
- border-radius: 999px;
111
- padding: 8px 12px;
112
- font-size: 12px;
113
- font-family: var(--mono);
114
- }
115
-
116
- .button {
117
- border: 1px solid transparent;
118
- border-radius: 12px;
119
- padding: 10px 14px;
120
  cursor: pointer;
121
- font: inherit;
122
- color: white;
123
- background: linear-gradient(135deg, var(--accent), var(--accent2));
124
- box-shadow: 0 10px 26px rgba(108,131,255,.2);
125
  }
126
-
127
- .button.secondary {
128
- background: rgba(255,255,255,.04);
129
- border-color: var(--border);
130
- box-shadow: none;
131
  color: var(--text);
 
132
  }
 
133
 
134
- #chat {
135
- flex: 1;
136
- overflow-y: auto;
137
- padding: 26px 18px 30px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  }
139
 
140
- .wrap {
141
- max-width: 980px;
142
- margin: 0 auto;
 
 
143
  }
 
 
 
 
144
 
 
145
  .welcome {
146
- margin: 8vh auto 0;
147
- max-width: 560px;
148
- text-align: center;
149
- padding: 28px 24px;
150
  border: 1px solid var(--border);
151
- border-radius: 24px;
152
- background: rgba(255,255,255,.03);
153
  box-shadow: var(--shadow);
 
154
  }
155
-
156
  .welcome h1 {
157
- margin: 0;
158
- font-size: 28px;
159
- letter-spacing: -.04em;
160
  }
161
-
162
  .welcome p {
163
- color: var(--muted);
164
- line-height: 1.7;
165
- margin: 12px 0 0;
 
 
166
  }
167
 
 
168
  .turn {
169
- display: flex;
170
- gap: 12px;
171
- margin: 0 0 18px;
172
  align-items: flex-start;
 
173
  }
174
-
175
- .turn.user {
176
- justify-content: flex-end;
177
- }
178
-
179
  .avatar {
180
- width: 34px;
181
- height: 34px;
182
- border-radius: 50%;
183
- display: grid;
184
- place-items: center;
185
- font-size: 14px;
186
- flex: 0 0 auto;
187
  }
188
-
189
  .avatar.user {
190
  background: linear-gradient(135deg, #1f2b63, #2d1d58);
191
- border: 1px solid rgba(108,131,255,.25);
192
  }
193
-
194
  .avatar.assistant {
195
  background: linear-gradient(135deg, #163d34, #183c54);
196
- border: 1px solid rgba(45,212,191,.25);
197
  }
198
-
199
  .bubble {
200
- max-width: min(760px, calc(100vw - 120px));
201
  border: 1px solid var(--border);
202
- border-radius: 18px;
203
- padding: 14px 16px;
204
- line-height: 1.65;
205
- white-space: pre-wrap;
206
- word-break: break-word;
207
  background: rgba(255,255,255,.03);
 
208
  }
209
-
210
  .turn.user .bubble {
211
- background: linear-gradient(135deg, rgba(108,131,255,.18), rgba(161,110,255,.16));
212
- border-color: rgba(108,131,255,.24);
213
- border-radius: 18px 18px 6px 18px;
214
- box-shadow: 0 10px 28px rgba(108,131,255,.1);
215
  }
216
-
217
  .turn.assistant .bubble {
218
- background: rgba(255,255,255,.035);
219
- border-radius: 6px 18px 18px 18px;
220
  }
221
-
222
- .meta {
223
- margin-top: 6px;
224
- font-size: 11px;
225
- color: var(--muted);
226
  font-family: var(--mono);
227
- display: flex;
228
  flex-wrap: wrap;
229
- gap: 8px;
230
- align-items: center;
231
  }
232
-
233
  .chip {
234
  border: 1px solid var(--border);
235
  border-radius: 999px;
236
- padding: 3px 8px;
237
- font-size: 10px;
238
  text-transform: uppercase;
239
- letter-spacing: .06em;
240
  }
241
-
242
- .chip.good { color: var(--good); }
243
  .chip.muted { color: var(--muted); }
244
- .chip.bad { color: var(--bad); }
 
245
 
246
- .answer-card {
247
- margin-top: 10px;
248
- padding: 14px;
249
- border: 1px solid var(--border);
250
- border-radius: 18px;
251
- background: rgba(255,255,255,.03);
252
  }
253
-
254
- .answer-top {
255
- display: flex;
256
- gap: 12px;
257
- align-items: flex-start;
 
 
258
  }
259
-
260
- .answer-main {
261
- flex: 1;
262
- min-width: 0;
263
  }
264
 
265
- .answer-head {
266
- display: flex;
267
- gap: 8px;
268
- flex-wrap: wrap;
269
- align-items: center;
270
- margin-bottom: 10px;
 
271
  color: var(--muted);
272
- font-family: var(--mono);
273
- font-size: 11px;
 
 
 
 
274
  }
275
-
276
- .answer-text {
277
- white-space: pre-wrap;
278
- word-break: break-word;
279
- line-height: 1.7;
280
- font-size: 14px;
281
  }
282
-
283
- .actions {
284
- display: flex;
285
- gap: 8px;
286
- flex-wrap: wrap;
287
- margin-top: 12px;
288
  }
289
-
290
- .mini {
 
 
 
 
291
  border: 1px solid var(--border2);
292
  background: rgba(255,255,255,.02);
293
- color: var(--text);
294
- border-radius: 999px;
295
- padding: 7px 10px;
296
- font: inherit;
297
- font-size: 12px;
298
  cursor: pointer;
 
299
  }
300
-
301
- .mini:hover {
302
- border-color: rgba(108,131,255,.4);
303
- background: rgba(108,131,255,.08);
304
  }
305
 
306
- .mini.voted-up {
307
- border-color: rgba(45,212,191,.6);
308
- color: var(--good);
 
 
 
 
 
 
 
 
 
 
309
  }
310
-
311
- .mini.voted-down {
312
- border-color: rgba(248,113,113,.6);
313
- color: var(--bad);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  }
315
 
316
- .versions {
317
- margin-top: 12px;
 
 
 
 
 
 
 
 
 
 
 
 
318
  border-left: 2px solid var(--border2);
319
- padding-left: 12px;
320
- display: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  }
322
 
323
- .versions.open {
324
- display: block;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  }
326
 
327
- .version {
328
- margin-top: 10px;
 
 
 
 
 
 
 
329
  border: 1px solid var(--border);
330
- background: rgba(255,255,255,.025);
331
- border-radius: 14px;
332
- padding: 12px;
 
 
 
 
 
 
 
 
 
 
 
333
  }
334
 
335
- .version .meta {
336
- margin-top: 0;
337
- margin-bottom: 8px;
 
 
 
 
 
 
338
  }
339
 
 
340
  .compose {
341
  border-top: 1px solid var(--border);
342
- background: rgba(11,14,20,.84);
343
  backdrop-filter: blur(14px);
344
- padding: 16px 18px 18px;
 
345
  }
346
-
347
  .compose-inner {
348
- max-width: 980px;
349
  margin: 0 auto;
350
  border: 1px solid var(--border2);
351
- border-radius: 18px;
352
- padding: 12px 12px 10px;
353
  background: var(--panel);
354
- box-shadow: var(--shadow);
 
 
 
 
355
  }
356
-
357
  #prompt {
358
  width: 100%;
359
- min-height: 56px;
360
- max-height: 240px;
361
- resize: vertical;
362
- border: none;
363
- outline: none;
364
  background: transparent;
365
  color: var(--text);
366
- font: inherit;
367
- line-height: 1.65;
368
- padding: 2px 2px 8px;
369
- white-space: pre-wrap;
370
  }
371
-
372
- #prompt::placeholder {
373
- color: #6f768a;
374
- }
375
-
376
  .compose-row {
377
- display: flex;
378
- align-items: center;
379
- justify-content: space-between;
380
- gap: 12px;
381
  border-top: 1px solid var(--border);
382
- padding-top: 10px;
383
  }
384
-
385
  .hint {
386
- color: var(--muted);
387
- font-size: 11px;
388
  font-family: var(--mono);
389
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
 
391
- .send {
392
- min-width: 102px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  }
394
 
 
395
  #toast {
396
  position: fixed;
397
- left: 50%;
398
- bottom: 96px;
399
- transform: translateX(-50%) translateY(16px);
400
- opacity: 0;
401
- pointer-events: none;
402
- transition: 220ms ease;
403
  z-index: 50;
404
- background: rgba(17,21,29,.96);
405
  border: 1px solid var(--border2);
406
- border-radius: 999px;
407
- padding: 10px 16px;
408
  color: var(--text);
409
- font-family: var(--mono);
410
- font-size: 12px;
411
  box-shadow: var(--shadow);
412
  white-space: nowrap;
 
413
  }
414
-
415
  #toast.show {
416
- opacity: 1;
417
- transform: translateX(-50%) translateY(0);
418
  }
 
 
419
 
420
- #toast.good { border-color: rgba(45,212,191,.45); color: var(--good); }
421
- #toast.bad { border-color: rgba(248,113,113,.45); color: var(--bad); }
 
 
 
422
 
423
- @media (max-width: 720px) {
424
- #topbar { padding: 0 14px; }
 
425
  .brand-sub { display: none; }
426
- .pill { display: none; }
427
- .bubble { max-width: calc(100vw - 96px); }
428
- .turn { gap: 8px; }
429
- .welcome { margin-top: 4vh; }
430
  }
431
  </style>
432
  </head>
@@ -437,36 +639,73 @@
437
  <div class="logo">🧠</div>
438
  <div>
439
  <div class="brand-title">Human Intelligence</div>
440
- <div class="brand-sub">anonymous · answer-engine chat</div>
441
  </div>
442
  </div>
443
  <div class="top-actions">
444
- <div class="pill">Anonymous mode</div>
445
- <button class="button secondary" id="newQuestionBtn">New question</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
  </div>
447
  </div>
448
 
449
  <div id="chat">
450
  <div class="wrap">
451
  <div class="welcome" id="welcome">
452
- <h1>Ask a question. Answers appear right here.</h1>
453
- <p>
454
- The app searches existing conversation objects behind the scenes,
455
- but the view stays in one continuous chat. If an answer exists,
456
- it shows up here. If not, you can write the first one.
457
- </p>
458
  </div>
459
-
460
  <div id="transcript"></div>
461
  </div>
462
  </div>
463
 
464
  <div class="compose">
465
  <div class="compose-inner">
466
- <textarea id="prompt" placeholder="Ask a question..."></textarea>
467
  <div class="compose-row">
468
- <div class="hint" id="hint">Press Enter to send · Shift+Enter for newline</div>
469
- <button class="button send" id="sendBtn">Ask</button>
470
  </div>
471
  </div>
472
  </div>
@@ -480,235 +719,244 @@
480
 
481
  <script>
482
  (() => {
 
483
  const S = {
484
  clientId: null,
485
  conversation: null,
486
  currentQuestion: "",
487
  loading: false,
 
488
  };
 
489
 
490
- const $ = (id) => document.getElementById(id);
491
-
492
  function getClientId() {
493
  let id = localStorage.getItem("hi_client_id");
494
  if (!id) {
495
- id = (crypto.randomUUID ? crypto.randomUUID() : String(Date.now() + Math.random()))
496
- .replace(/-/g, "")
497
- .slice(0, 16);
498
  localStorage.setItem("hi_client_id", id);
499
  }
500
  return id;
501
  }
502
 
 
503
  function toast(msg, kind = "") {
504
  const t = $("toast");
505
  t.textContent = msg;
506
- t.className = "";
507
- t.classList.add("show");
508
  if (kind) t.classList.add(kind);
509
- clearTimeout(t._timer);
510
- t._timer = setTimeout(() => {
511
- t.className = "";
512
- }, 2200);
513
  }
514
 
515
- function escapeHtml(s) {
516
- return String(s)
517
- .replace(/&/g, "&amp;")
518
- .replace(/</g, "&lt;")
519
- .replace(/>/g, "&gt;")
520
- .replace(/"/g, "&quot;")
521
- .replace(/\n/g, "<br>");
522
  }
523
 
 
 
 
 
 
524
  function fmtTime(iso) {
525
  if (!iso) return "";
526
  try {
527
- return new Date(iso).toLocaleString([], {
528
- month: "short",
529
- day: "numeric",
530
- hour: "2-digit",
531
- minute: "2-digit",
532
- });
533
- } catch {
534
- return iso;
535
- }
536
  }
537
 
538
- function scrollToBottom() {
539
- const chat = $("chat");
540
- chat.scrollTop = chat.scrollHeight;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
541
  }
542
 
 
543
  function activeVersion(answer) {
544
- const versions = answer?.versions || [];
545
- if (!versions.length) return null;
546
- const id = answer.active_version;
547
- let found = versions.find(v => v.id === id);
548
- if (found) return found;
549
- return [...versions].sort((a, b) => {
550
- const av = Number(a.votes || 0);
551
- const bv = Number(b.votes || 0);
552
- if (bv !== av) return bv - av;
553
- return String(b.created_at || "").localeCompare(String(a.created_at || ""));
554
  })[0];
555
  }
556
-
557
- function answerScore(answer) {
558
- const v = activeVersion(answer);
559
- if (!v) return 0;
560
- return Number(v.votes || 0);
561
  }
562
-
563
- function sortedAnswers(conversation) {
564
- return [...(conversation?.answers || [])].sort((a, b) => {
565
- const sa = answerScore(a);
566
- const sb = answerScore(b);
567
- if (sb !== sa) return sb - sa;
568
- return String(b.created_at || "").localeCompare(String(a.created_at || ""));
569
  });
570
  }
571
 
572
- function setComposerLabel() {
573
- const hasConv = !!S.conversation;
574
- const answers = S.conversation?.answers || [];
575
- const prompt = $("prompt");
576
- const sendBtn = $("sendBtn");
577
-
578
- if (!hasConv) {
579
- prompt.placeholder = "Ask a question...";
580
- sendBtn.textContent = "Ask";
581
- $("hint").textContent = "Press Enter to ask · Shift+Enter for newline";
582
- return;
583
- }
584
-
585
- if (!answers.length) {
586
- prompt.placeholder = "Write the first answer...";
587
- sendBtn.textContent = "Answer";
588
- $("hint").textContent = "Press Enter to answer · Shift+Enter for newline";
589
- return;
590
  }
 
591
 
592
- prompt.placeholder = "Add another answer...";
593
- sendBtn.textContent = "Answer";
594
- $("hint").textContent = "Press Enter to answer · Shift+Enter for newline";
 
 
 
 
 
595
  }
596
 
597
- function renderEmptyAnswerPlaceholder(assistantText) {
 
 
 
598
  return `
599
- <div class="turn assistant">
600
- <div class="avatar assistant"></div>
601
- <div>
602
- <div class="bubble" style="border-style:dashed;">
603
- ${escapeHtml(assistantText || "No answer yet. You can write one.")}
604
- </div>
605
- <div class="meta">
606
- <span class="chip muted">open answer turn</span>
607
- <span>Write the first answer below.</span>
 
 
 
 
 
 
608
  </div>
609
- </div>
610
- </div>
611
- `;
612
  }
613
 
614
- function renderAnswerCard(answer, idx) {
615
- const active = activeVersion(answer);
616
- if (!active) return "";
617
-
618
- const others = (answer.versions || []).filter(v => v.id !== active.id);
 
 
 
 
 
 
619
 
620
- const versionsPanel = others.length ? `
621
- <button class="mini" type="button" data-toggle-versions="${answer.id}">
622
- Show versions (${others.length})
 
 
 
623
  </button>
624
- <div class="versions" id="versions-${answer.id}">
625
- ${others.map(v => {
626
- const up = Number(v.votes || 0);
627
- const votedUp = v.votes_by_client && v.votes_by_client[S.clientId] === 1;
628
- const votedDown = v.votes_by_client && v.votes_by_client[S.clientId] === -1;
629
  return `
630
- <div class="version">
631
- <div class="meta">
632
- <span>version ${escapeHtml(v.id.slice(0, 6))}</span>
633
- <span>·</span>
634
- <span>${escapeHtml(v.author || "Anonymous")}</span>
635
  <span>·</span>
636
  <span>${fmtTime(v.created_at)}</span>
637
- <span>·</span>
638
- <span>votes: ${up}</span>
639
- ${v.id === active.id ? `<span class="chip good">active</span>` : ""}
640
  </div>
641
- <div class="answer-text">${escapeHtml(v.text || "")}</div>
642
- <div class="actions">
643
- <button class="mini ${votedUp ? "voted-up" : ""}" type="button"
644
- data-vote="${answer.id}|${v.id}|1">▲ Upvote</button>
645
- <button class="mini ${votedDown ? "voted-down" : ""}" type="button"
646
- data-vote="${answer.id}|${v.id}|-1">▼ Downvote</button>
647
  </div>
648
- </div>
649
- `;
650
  }).join("")}
651
- </div>
652
- ` : "";
653
-
654
- const activeVotedUp = active.votes_by_client && active.votes_by_client[S.clientId] === 1;
655
- const activeVotedDown = active.votes_by_client && active.votes_by_client[S.clientId] === -1;
656
-
657
- return `
658
- <div class="answer-card" data-answer-id="${answer.id}">
659
- <div class="answer-top">
660
- <div class="avatar assistant">🧑</div>
661
- <div class="answer-main">
662
- <div class="answer-head">
663
- <span>answer ${idx + 1}</span>
664
- <span>·</span>
665
- <span>${escapeHtml(active.author || "Anonymous")}</span>
666
- <span>·</span>
667
- <span>${fmtTime(active.created_at)}</span>
668
- <span class="chip good">active</span>
669
- </div>
670
- <div class="bubble">
671
- <div class="answer-text">${escapeHtml(active.text || "")}</div>
672
- </div>
673
- <div class="actions">
674
- <button class="mini ${activeVotedUp ? "voted-up" : ""}" type="button"
675
- data-vote="${answer.id}|${active.id}|1">▲ ${Number(active.votes || 0)}</button>
676
- <button class="mini ${activeVotedDown ? "voted-down" : ""}" type="button"
677
- data-vote="${answer.id}|${active.id}|-1">▼</button>
678
- <button class="mini" type="button" data-propose="${answer.id}">Propose version</button>
679
- </div>
680
- ${versionsPanel}
681
- <div class="versions" id="propose-${answer.id}" style="display:none; margin-top:12px;">
682
- <textarea class="mini-proposal" rows="4" placeholder="Propose a better version..."></textarea>
683
- <div class="actions">
684
- <button class="mini" type="button" data-submit-proposal="${answer.id}">Submit version</button>
685
- </div>
686
- </div>
687
- </div>
688
- </div>
689
- </div>
690
- `;
691
  }
692
 
693
- function renderConversation(questionText) {
694
- const transcript = $("transcript");
695
- const welcome = $("welcome");
696
- transcript.innerHTML = "";
 
697
 
698
  if (!S.conversation) {
699
- welcome.style.display = "block";
700
- setComposerLabel();
701
  return;
702
  }
703
-
704
- welcome.style.display = "none";
705
 
706
  const q = questionText || S.conversation.question || "";
707
- transcript.insertAdjacentHTML("beforeend", `
708
  <div class="turn user">
709
  <div>
710
- <div class="bubble">${escapeHtml(q)}</div>
711
- <div class="meta">
712
  <span class="chip muted">question</span>
713
  <span>${fmtTime(S.conversation.created_at)}</span>
714
  </div>
@@ -718,94 +966,136 @@
718
  `);
719
 
720
  const answers = sortedAnswers(S.conversation);
 
721
  if (!answers.length) {
722
- transcript.insertAdjacentHTML("beforeend", renderEmptyAnswerPlaceholder(
723
- S.conversation.assistant_text || "No answer yet. You can write one."
724
- ));
 
 
 
 
 
 
725
  } else {
726
- answers.forEach((answer, idx) => {
727
- transcript.insertAdjacentHTML("beforeend", renderAnswerCard(answer, idx));
728
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
729
  }
730
 
731
- setComposerLabel();
732
- bindDynamicHandlers();
733
- scrollToBottom();
734
  }
735
 
736
- function bindDynamicHandlers() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
737
  document.querySelectorAll("[data-toggle-versions]").forEach(btn => {
738
  btn.onclick = () => {
739
  const id = btn.getAttribute("data-toggle-versions");
740
- const panel = document.getElementById(`versions-${id}`);
741
  if (!panel) return;
742
- panel.classList.toggle("open");
743
- btn.textContent = panel.classList.contains("open")
744
- ? "Hide versions"
745
- : `Show versions (${(S.conversation?.answers || []).find(a => a.id === id)?.versions?.length - 1 || 0})`;
746
  };
747
  });
748
 
 
 
 
 
 
 
 
 
 
 
749
  document.querySelectorAll("[data-propose]").forEach(btn => {
750
  btn.onclick = () => {
751
  const id = btn.getAttribute("data-propose");
752
- const box = document.getElementById(`propose-${id}`);
753
- if (!box) return;
754
- box.style.display = box.style.display === "none" ? "block" : "none";
755
  };
756
  });
757
 
758
- document.querySelectorAll("[data-submit-proposal]").forEach(btn => {
759
- btn.onclick = async () => {
760
- const answerId = btn.getAttribute("data-submit-proposal");
761
- const box = document.getElementById(`propose-${answerId}`);
762
- const textarea = box ? box.querySelector("textarea") : null;
763
- const text = textarea ? textarea.value.trim() : "";
764
- if (!text) {
765
- toast("Empty proposal", "bad");
766
- return;
767
- }
768
- if (!S.conversation) return;
769
-
770
- btn.disabled = true;
771
- const old = btn.textContent;
772
- btn.textContent = "Submitting...";
773
-
774
- const res = await callBackend("propose", {
775
- conversation_id: S.conversation.id,
776
- answer_id: answerId,
777
- text,
778
- });
779
-
780
- btn.disabled = false;
781
- btn.textContent = old;
782
-
783
- if (res.ok) {
784
- S.conversation = res.conversation;
785
- localStorage.setItem("hi_last_conversation_id", S.conversation.id);
786
- renderConversation(S.currentQuestion);
787
- toast("Version saved", "good");
788
- } else {
789
- toast(res.error || "Error", "bad");
790
- }
791
  };
792
  });
793
 
794
- document.querySelectorAll("[data-vote]").forEach(btn => {
795
  btn.onclick = async () => {
 
 
 
 
 
796
  if (!S.conversation) return;
797
- const [answerId, versionId, delta] = btn.getAttribute("data-vote").split("|");
798
- const res = await callBackend("vote", {
 
 
799
  conversation_id: S.conversation.id,
800
- answer_id: answerId,
801
- version_id: versionId,
802
- delta: Number(delta),
803
  });
804
-
 
805
  if (res.ok) {
806
  S.conversation = res.conversation;
807
- localStorage.setItem("hi_last_conversation_id", S.conversation.id);
808
- renderConversation(S.currentQuestion);
 
809
  } else {
810
  toast(res.error || "Error", "bad");
811
  }
@@ -813,135 +1103,135 @@
813
  });
814
  }
815
 
816
- async function callBackend(action, payload = {}) {
817
- const body = {
818
- action,
819
- client_id: S.clientId,
820
- ...payload,
821
- };
822
-
823
  const resp = await fetch("/api", {
824
  method: "POST",
825
- headers: {
826
- "Content-Type": "application/json",
827
- "X-Client-Id": S.clientId,
828
- },
829
- body: JSON.stringify(body),
830
  });
831
-
832
- return await resp.json();
833
  }
834
 
835
- async function askQuestion(question) {
836
- const res = await callBackend("ask", { question });
837
- if (!res.ok) {
838
- toast(res.error || "Error", "bad");
839
- return;
840
- }
841
-
 
842
  S.conversation = res.conversation;
843
- S.currentQuestion = question;
844
- localStorage.setItem("hi_last_conversation_id", S.conversation.id);
845
-
846
- if (res.matched) {
847
- toast("Existing answer found", "good");
848
- } else {
849
- toast("New conversation created", "good");
850
- }
851
-
852
- renderConversation(question);
853
  }
854
 
 
855
  async function postAnswer(text) {
856
  if (!S.conversation) return;
857
-
858
- const res = await callBackend("answer", {
859
- conversation_id: S.conversation.id,
860
- text,
861
- });
862
-
863
- if (!res.ok) {
864
- toast(res.error || "Error", "bad");
865
- return;
866
- }
867
-
868
  S.conversation = res.conversation;
869
- localStorage.setItem("hi_last_conversation_id", S.conversation.id);
870
- renderConversation(S.currentQuestion);
871
- toast("Answer saved", "good");
872
  }
873
 
 
874
  async function submitPrompt() {
875
- const input = $("prompt");
876
- const text = input.value.trim();
877
  if (!text) return;
878
-
879
- input.value = "";
880
- autoGrow(input);
881
-
882
- if (!S.conversation) {
883
- await askQuestion(text);
884
- return;
885
- }
886
-
887
- await postAnswer(text);
888
  }
889
 
890
- function autoGrow(node) {
891
- node.style.height = "auto";
892
- node.style.height = Math.min(node.scrollHeight, 240) + "px";
893
  }
894
 
895
- async function loadSavedConversation() {
896
- const savedId = localStorage.getItem("hi_last_conversation_id");
897
- if (!savedId) return;
898
-
899
- const res = await callBackend("get_conversation", { conversation_id: savedId });
 
 
900
  if (res.ok && res.conversation) {
901
  S.conversation = res.conversation;
902
  S.currentQuestion = res.conversation.question || "";
903
- renderConversation(S.currentQuestion);
904
  }
905
  }
906
 
907
- function newQuestion() {
 
908
  S.conversation = null;
909
  S.currentQuestion = "";
910
- localStorage.removeItem("hi_last_conversation_id");
911
  $("transcript").innerHTML = "";
912
  $("welcome").style.display = "block";
913
  $("prompt").value = "";
914
- setComposerLabel();
915
  $("prompt").focus();
916
  }
917
 
918
- function init() {
919
- S.clientId = getClientId();
920
-
921
- $("sendBtn").addEventListener("click", submitPrompt);
922
- $("newQuestionBtn").addEventListener("click", newQuestion);
923
-
924
- $("prompt").addEventListener("input", (e) => autoGrow(e.target));
925
- $("prompt").addEventListener("keydown", (e) => {
926
- if (e.key === "Enter" && !e.shiftKey) {
927
- e.preventDefault();
928
- submitPrompt();
929
  }
930
  });
931
 
932
- const initData = window.__HI_INIT__ || {};
933
- if (initData.client_id) {
934
- S.clientId = initData.client_id;
 
 
 
 
935
  }
936
-
937
- loadSavedConversation().then(() => {
938
- if (!S.conversation) {
939
- setComposerLabel();
940
- $("prompt").focus();
941
- }
942
  });
 
943
  }
944
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
945
  init();
946
  })();
947
  </script>
 
17
  --accent2: #a16eff;
18
  --good: #2dd4bf;
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 {
30
+ width: 100%; height: 100%;
31
+ background: radial-gradient(ellipse at top center, #161c2b 0%, var(--bg) 50%);
 
 
32
  color: var(--text);
33
  font-family: var(--font);
34
  overflow: hidden;
35
+ -webkit-font-smoothing: antialiased;
36
  }
 
37
  body::before {
38
  content: "";
39
+ position: fixed; inset: 0;
 
40
  background-image:
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;
 
 
 
68
  background: linear-gradient(135deg, var(--accent), var(--accent2));
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);
82
  background: rgba(255,255,255,.03);
83
+ color: var(--muted);
84
+ border-radius: 10px;
85
+ padding: 6px 12px;
86
+ font: inherit; font-size: 12px;
 
 
 
 
 
 
 
87
  cursor: pointer;
88
+ transition: all 180ms ease;
89
+ display: flex; align-items: center; gap: 5px;
 
 
90
  }
91
+ .top-btn:hover {
92
+ border-color: rgba(108,131,255,.35);
 
 
 
93
  color: var(--text);
94
+ background: rgba(108,131,255,.06);
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;
102
+ opacity: 0;
103
+ border-bottom: 1px solid transparent;
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;
117
+ background: var(--accent);
118
+ animation: pulse-dot 1.2s ease infinite;
119
+ }
120
+ @keyframes pulse-dot {
121
+ 0%, 100% { opacity: .4; transform: scale(.85); }
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;
129
+ scroll-behavior: smooth;
130
  }
131
+ #chat::-webkit-scrollbar { width: 5px; }
132
+ #chat::-webkit-scrollbar-track { background: transparent; }
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;
 
 
140
  border: 1px solid var(--border);
141
+ border-radius: 20px;
142
+ background: rgba(255,255,255,.025);
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 {
166
+ width: 28px; height: 28px; border-radius: 50%;
167
+ display: grid; place-items: center;
168
+ font-size: 12px; flex: 0 0 auto;
169
+ transition: transform 200ms ease;
 
 
 
170
  }
171
+ .avatar:hover { transform: scale(1.1); }
172
  .avatar.user {
173
  background: linear-gradient(135deg, #1f2b63, #2d1d58);
174
+ border: 1px solid rgba(108,131,255,.2);
175
  }
 
176
  .avatar.assistant {
177
  background: linear-gradient(135deg, #163d34, #183c54);
178
+ border: 1px solid rgba(45,212,191,.2);
179
  }
 
180
  .bubble {
181
+ max-width: min(620px, calc(100vw - 100px));
182
  border: 1px solid var(--border);
183
+ border-radius: 16px;
184
+ padding: 10px 14px;
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;
225
+ padding: 10px 14px;
226
+ background: rgba(45,212,191,.04);
227
+ line-height: 1.6; font-size: 14px;
228
+ white-space: pre-wrap; word-break: break-word;
229
  }
230
+ .best-answer-meta {
231
+ margin-top: 4px;
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);
242
  color: var(--muted);
243
+ border-radius: 8px;
244
+ padding: 4px 9px;
245
+ font: inherit; font-size: 11px;
246
+ cursor: pointer;
247
+ transition: all 160ms ease;
248
+ display: inline-flex; align-items: center; gap: 3px;
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);
257
+ background: rgba(45,212,191,.08);
 
 
 
258
  }
259
+ .vote-btn.voted-down {
260
+ border-color: rgba(248,113,113,.5); color: var(--bad);
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);
267
+ color: var(--muted);
268
+ border-radius: 8px;
269
+ padding: 4px 9px;
270
+ font: inherit; font-size: 11px;
 
271
  cursor: pointer;
272
+ transition: all 160ms ease;
273
  }
274
+ .action-btn:hover {
275
+ border-color: rgba(108,131,255,.35);
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);
285
+ border-radius: 10px;
286
+ padding: 6px 12px;
287
+ font: inherit; font-size: 11px;
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;
317
+ padding: 10px 12px;
318
+ margin-top: 6px;
319
+ background: rgba(255,255,255,.02);
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
+ }
328
+ .other-answer-text {
329
+ font-size: 13px; line-height: 1.6;
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;
337
+ cursor: pointer; font-family: var(--mono);
338
+ display: inline-flex; align-items: center; gap: 4px;
339
+ border: none; background: none; padding: 0;
340
+ transition: color 150ms ease;
341
+ }
342
+ .versions-toggle:hover { color: var(--text); }
343
+ .versions-panel {
344
+ max-height: 0; overflow: hidden;
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 {
363
+ font-size: 10px; color: var(--muted); font-family: var(--mono);
364
+ display: flex; gap: 5px; flex-wrap: wrap; align-items: center;
365
+ margin-bottom: 4px;
366
+ }
367
+ .version-text {
368
+ font-size: 12px; line-height: 1.55;
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;
385
+ resize: vertical;
386
+ border: 1px solid var(--border2);
387
+ border-radius: 10px;
388
+ background: var(--panel);
389
+ color: var(--text);
390
+ font: inherit; font-size: 13px;
391
+ line-height: 1.55;
392
+ padding: 8px 10px;
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);
406
+ color: var(--accent);
407
+ border-radius: 8px;
408
+ padding: 5px 12px;
409
+ font: inherit; font-size: 11px;
410
+ cursor: pointer;
411
+ transition: all 160ms ease;
412
+ }
413
+ .propose-submit:hover {
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;
423
+ color: var(--muted);
424
+ border-radius: 8px;
425
+ padding: 5px 10px;
426
+ font: inherit; font-size: 11px;
427
+ cursor: pointer;
428
  }
429
 
430
+ /* ─── Typing indicator ─── */
431
+ .typing-indicator {
432
+ display: flex; gap: 10px; margin-bottom: 6px;
433
+ align-items: flex-start;
434
+ animation: fadeUp 250ms ease both;
435
+ }
436
+ .typing-dots {
437
+ display: flex; gap: 4px; align-items: center;
438
+ padding: 12px 16px;
439
  border: 1px solid var(--border);
440
+ border-radius: 4px 16px 16px 16px;
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
+ }
449
+ .typing-dots span:nth-child(2) { animation-delay: .15s; }
450
+ .typing-dots span:nth-child(3) { animation-delay: .3s; }
451
+ @keyframes typingBounce {
452
+ 0%, 60%, 100% { transform: translateY(0); opacity: .35; }
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);
471
  backdrop-filter: blur(14px);
472
+ padding: 10px 14px 14px;
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;
481
  background: var(--panel);
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;
491
+ resize: none;
492
+ border: none; outline: none;
 
 
493
  background: transparent;
494
  color: var(--text);
495
+ font: inherit; font-size: 14px;
496
+ line-height: 1.55;
497
+ padding: 2px 2px 4px;
 
498
  }
499
+ #prompt::placeholder { color: #5a6178; }
 
 
 
 
500
  .compose-row {
501
+ display: flex; align-items: center; justify-content: space-between;
502
+ gap: 8px;
 
 
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;
516
+ color: white;
517
+ background: linear-gradient(135deg, var(--accent), var(--accent2));
518
+ box-shadow: 0 4px 14px rgba(108,131,255,.2);
519
+ transition: transform 140ms ease, box-shadow 140ms ease;
520
+ }
521
+ .send-btn:hover {
522
+ transform: translateY(-1px);
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;
535
+ width: 260px;
536
+ background: var(--panel);
537
+ border: 1px solid var(--border2);
538
+ border-radius: 0 0 0 16px;
539
+ box-shadow: var(--shadow);
540
+ z-index: 100;
541
+ transform: translateX(100%);
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;
557
+ padding: 6px 0;
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;
570
+ background: rgba(255,255,255,.1);
571
+ border: 1px solid var(--border2);
572
+ cursor: pointer;
573
+ position: relative;
574
+ transition: background 200ms ease;
575
+ flex-shrink: 0;
576
+ }
577
+ .toggle.on {
578
+ background: rgba(108,131,255,.35);
579
+ border-color: rgba(108,131,255,.5);
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;
598
+ transform: translateX(-50%) translateY(12px);
599
+ opacity: 0; pointer-events: none;
600
+ transition: all 200ms cubic-bezier(.4,0,.2,1);
 
 
601
  z-index: 50;
602
+ background: rgba(17,21,29,.95);
603
  border: 1px solid var(--border2);
604
+ border-radius: 10px;
605
+ padding: 8px 14px;
606
  color: var(--text);
607
+ font-family: var(--mono); font-size: 11px;
 
608
  box-shadow: var(--shadow);
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; }
628
+ .bubble { max-width: calc(100vw - 80px); }
629
+ .welcome { margin-top: 3vh; padding: 18px 14px; }
630
+ .welcome h1 { font-size: 18px; }
631
+ #settingsPanel { width: 100%; border-radius: 0 0 16px 16px; }
632
  }
633
  </style>
634
  </head>
 
639
  <div class="logo">🧠</div>
640
  <div>
641
  <div class="brand-title">Human Intelligence</div>
642
+ <div class="brand-sub">community answers</div>
643
  </div>
644
  </div>
645
  <div class="top-actions">
646
+ <button class="top-btn" id="newChatBtn">
647
+ <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>
648
+ New chat
649
+ </button>
650
+ <button class="top-btn" id="settingsBtn">
651
+ <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>
652
+ </button>
653
+ </div>
654
+ </div>
655
+
656
+ <div id="statusbar">
657
+ <span class="status-dot"></span>
658
+ <span id="statusText">Thinking…</span>
659
+ </div>
660
+
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>
692
 
693
  <div id="chat">
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>
701
  </div>
702
 
703
  <div class="compose">
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>
711
  </div>
 
719
 
720
  <script>
721
  (() => {
722
+ /* ── State ── */
723
  const S = {
724
  clientId: null,
725
  conversation: null,
726
  currentQuestion: "",
727
  loading: false,
728
+ animMode: localStorage.getItem("hi_anim") || "none",
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) {
736
+ id = (crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(36) + Math.random().toString(36).slice(2))
737
+ .replace(/-/g, "").slice(0, 16);
 
738
  localStorage.setItem("hi_client_id", id);
739
  }
740
  return id;
741
  }
742
 
743
+ /* ── Toast ── */
744
  function toast(msg, kind = "") {
745
  const t = $("toast");
746
  t.textContent = msg;
747
+ t.className = "show";
 
748
  if (kind) t.classList.add(kind);
749
+ clearTimeout(t._t);
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");
757
+ }
758
+ function hideStatus() {
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") {
782
+ el.innerHTML = "";
783
+ for (let i = 0; i < text.length; i++) {
784
+ el.innerHTML = nl2br(text.slice(0, i + 1));
785
+ scrollBottom();
786
+ await sleep(12 + Math.random() * 8);
787
+ }
788
+ } else if (mode === "human") {
789
+ el.innerHTML = "";
790
+ for (let i = 0; i < text.length; i++) {
791
+ el.innerHTML = nl2br(text.slice(0, i + 1));
792
+ scrollBottom();
793
+ const ch = text[i];
794
+ let d = 25 + Math.random() * 55;
795
+ if (ch === " ") d += Math.random() * 30;
796
+ if (".!?".includes(ch)) d += 120 + Math.random() * 180;
797
+ if (",;:".includes(ch)) d += 40 + Math.random() * 60;
798
+ if (Math.random() < .03) d += 200 + Math.random() * 300;
799
+ await sleep(d);
800
+ }
801
+ } else if (mode === "diffusion") {
802
+ el.innerHTML = `<span class="diffusion-text">${nl2br(text)}</span>`;
803
+ scrollBottom();
804
+ await sleep(80);
805
+ el.querySelector(".diffusion-text").classList.add("revealed");
806
+ } else {
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;
868
+ const vd = ver.votes_by_client && ver.votes_by_client[S.clientId] === -1;
869
+ return `<div class="vote-row">
870
+ <button class="vote-btn ${vu?"voted-up":""}" data-vote="${answerId}|${ver.id}|1">▲ ${Number(ver.votes||0)}</button>
871
+ <button class="vote-btn ${vd?"voted-down":""}" data-vote="${answerId}|${ver.id}|-1">▼</button>
872
+ </div>`;
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}">
881
+ <span class="arrow"></span> ${others.length} version${others.length>1?"s":""}
882
+ </button>
883
+ <div class="versions-panel" id="vp-${answer.id}">
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
 
900
+ function renderPropose(answerId) {
901
+ return `
902
+ <button class="action-btn" data-propose="${answerId}">Propose version</button>
903
+ <div class="propose-panel" id="pp-${answerId}">
904
+ <textarea class="propose-textarea" placeholder="Write a better version…" rows="3"></textarea>
905
+ <div class="propose-actions">
906
+ <button class="propose-submit" data-submit-proposal="${answerId}">Submit</button>
907
+ <button class="propose-cancel" data-cancel-propose="${answerId}">Cancel</button>
908
+ </div>
909
+ </div>`;
910
+ }
911
 
912
+ function renderOtherAnswers(answers) {
913
+ if (answers.length <= 1) return "";
914
+ const others = answers.slice(1);
915
+ return `
916
+ <button class="other-answers-toggle" id="otherAnswersToggle">
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>
936
+ </div>`;
 
937
  }).join("")}
938
+ </div>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
939
  }
940
 
941
+ /* ── Main render ── */
942
+ async function renderConversation(questionText, doAnimate) {
943
+ const tr = $("transcript");
944
+ const wl = $("welcome");
945
+ tr.innerHTML = "";
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>
958
+ <div class="bubble">${nl2br(q)}</div>
959
+ <div class="turn-meta">
960
  <span class="chip muted">question</span>
961
  <span>${fmtTime(S.conversation.created_at)}</span>
962
  </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;
1023
+ const [aid, vid, d] = btn.getAttribute("data-vote").split("|");
1024
+ btn.style.transform = "scale(.9)";
1025
+ const res = await callAPI("vote", {
1026
+ conversation_id: S.conversation.id,
1027
+ answer_id: aid, version_id: vid, delta: Number(d),
1028
+ });
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
  }
 
1103
  });
1104
  }
1105
 
1106
+ /* ── API ── */
1107
+ async function callAPI(action, payload = {}) {
 
 
 
 
 
1108
  const resp = await fetch("/api", {
1109
  method: "POST",
1110
+ headers: { "Content-Type": "application/json", "X-Client-Id": S.clientId },
1111
+ body: JSON.stringify({ action, client_id: S.clientId, ...payload }),
 
 
 
1112
  });
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) {
1159
+ el.style.height = "auto";
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;
1167
+ showStatus("Loading conversation…");
1168
+ const res = await callAPI("get_conversation", { conversation_id: id });
1169
+ hideStatus();
1170
  if (res.ok && res.conversation) {
1171
  S.conversation = res.conversation;
1172
  S.currentQuestion = res.conversation.question || "";
1173
+ renderConversation(S.currentQuestion, false);
1174
  }
1175
  }
1176
 
1177
+ /* ── New chat ── */
1178
+ function newChat() {
1179
  S.conversation = null;
1180
  S.currentQuestion = "";
1181
+ localStorage.removeItem("hi_last_cid");
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 ── */
1220
+ function init() {
1221
+ S.clientId = getClientId();
1222
+ $("sendBtn").onclick = submitPrompt;
1223
+ $("newChatBtn").onclick = newChat;
1224
+ $("prompt").addEventListener("input", e => autoGrow(e.target));
1225
+ $("prompt").addEventListener("keydown", e => {
1226
+ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); submitPrompt(); }
1227
+ });
1228
+ initSettings();
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();
1236
  })();
1237
  </script>