Answer / templates /index.html
wop's picture
Update templates/index.html
26d901b verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="/templates/logo.png" />
<title>{{ app_title }}</title>
<style>
:root {
--bg: #0b1020;
--panel: #121a2f;
--panel-2: #1a2440;
--line: #2d3a5f;
--text: #edf2ff;
--muted: #9aa7c7;
--accent: #ff875b;
--accent-soft: #40211a;
--good: #7ad79a;
--good-soft: #163326;
--warn: #ffcb6b;
--warn-soft: #3a2f14;
--bad: #ff8f8f;
--shadow: 0 22px 48px rgba(0, 0, 0, 0.38);
--radius: 18px;
--font: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
--mono: Consolas, "SFMono-Regular", monospace;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; min-height: 100%; }
body {
font-family: var(--font);
color: var(--text);
background:
radial-gradient(circle at top right, rgba(255, 135, 91, 0.16), transparent 22%),
radial-gradient(circle at top left, rgba(89, 122, 255, 0.16), transparent 18%),
linear-gradient(180deg, #11182c 0%, var(--bg) 100%);
}
a { color: inherit; }
button, input, textarea, select { font: inherit; }
.shell {
max-width: 1180px;
margin: 0 auto;
padding: 24px 18px 40px;
}
.topbar {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: center;
padding: 18px 20px;
border: 1px solid var(--line);
border-radius: 24px;
background: rgba(18, 26, 47, 0.88);
backdrop-filter: blur(12px);
box-shadow: var(--shadow);
}
.brand {
display: flex;
gap: 14px;
align-items: center;
min-width: 0;
}
.brand img {
width: 42px;
height: 42px;
border-radius: 12px;
border: 1px solid var(--line);
object-fit: cover;
background: #0f1628;
}
.brand h1 {
margin: 0;
font-size: 1.15rem;
letter-spacing: -0.02em;
}
.brand p {
margin: 4px 0 0;
color: var(--muted);
font-size: 0.93rem;
}
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px solid var(--line);
border-radius: 999px;
padding: 7px 12px;
font-size: 0.82rem;
color: var(--muted);
background: rgba(26, 36, 64, 0.78);
white-space: nowrap;
}
.view {
margin-top: 20px;
display: none;
}
.view.active { display: block; }
.panel {
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--panel);
box-shadow: var(--shadow);
}
.list-layout {
display: grid;
gap: 18px;
}
.toolbar {
display: grid;
gap: 16px;
padding: 18px;
}
.toolbar-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
justify-content: space-between;
}
.filters {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.word-filter-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.word-input {
border: 1px solid var(--line);
border-radius: 14px;
padding: 9px 12px;
background: #0e1528;
color: var(--text);
width: 80px;
}
.word-input::placeholder { color: #7381a6; }
.pill {
border: 1px solid var(--line);
background: #18233d;
color: var(--muted);
border-radius: 999px;
padding: 9px 14px;
cursor: pointer;
}
.pill.active {
background: var(--accent);
color: #fff;
border-color: var(--accent);
}
.search-form {
display: flex;
flex-wrap: wrap;
gap: 10px;
width: 100%;
}
.search-form input {
flex: 1 1 280px;
border: 1px solid var(--line);
border-radius: 14px;
padding: 12px 14px;
background: #0e1528;
color: var(--text);
min-width: 0;
}
.search-form input::placeholder,
.answer-form textarea::placeholder,
.proposal-form textarea::placeholder {
color: #7381a6;
}
.btn {
border: 1px solid var(--line);
background: #18233d;
color: var(--text);
border-radius: 14px;
padding: 11px 16px;
cursor: pointer;
transition: transform 120ms ease, background 120ms ease;
}
.btn:hover { transform: translateY(-1px); }
.btn.primary {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.meta-line {
color: var(--muted);
font-size: 0.9rem;
}
.queue-list {
display: grid;
gap: 14px;
padding: 18px;
}
.queue-item {
display: grid;
gap: 10px;
padding: 16px;
border: 1px solid var(--line);
border-radius: 16px;
background: #161f37;
text-decoration: none;
transition: border-color 120ms ease, transform 120ms ease;
}
.queue-item:hover {
border-color: var(--accent);
transform: translateY(-1px);
}
.queue-head {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: flex-start;
}
.queue-head h2 {
margin: 0;
font-size: 1.06rem;
line-height: 1.4;
}
.queue-tags {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.tag {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 5px 10px;
font-size: 0.78rem;
border: 1px solid var(--line);
color: var(--muted);
background: #18233d;
}
.tag.unanswered {
border-color: #7c6321;
background: var(--warn-soft);
color: var(--warn);
}
.tag.answered {
border-color: #2e6846;
background: var(--good-soft);
color: var(--good);
}
.preview {
color: var(--muted);
line-height: 1.55;
white-space: pre-wrap;
word-break: break-word;
}
.pager {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
padding: 0 18px 18px;
flex-wrap: wrap;
}
.pager-actions {
display: flex;
gap: 10px;
}
.empty {
padding: 28px 18px 34px;
text-align: center;
color: var(--muted);
}
.detail-layout {
display: grid;
gap: 18px;
}
.detail-header {
padding: 18px;
display: grid;
gap: 12px;
}
.back-link {
color: var(--accent);
text-decoration: none;
font-weight: 600;
}
.question-box {
padding: 20px;
border: 1px solid var(--line);
border-radius: 18px;
background: #161f37;
}
.question-box h2 {
margin: 0 0 10px;
font-size: 1.35rem;
line-height: 1.35;
}
.answer-form,
.answer-card,
.version-card {
padding: 18px;
border: 1px solid var(--line);
border-radius: 18px;
background: #161f37;
}
.answer-form textarea,
.proposal-form textarea {
width: 100%;
min-height: 120px;
border: 1px solid var(--line);
border-radius: 14px;
padding: 12px 14px;
resize: vertical;
background: #0e1528;
color: var(--text);
}
.section-title {
margin: 0;
font-size: 1.05rem;
}
.section-subtitle {
margin: 4px 0 0;
color: var(--muted);
font-size: 0.9rem;
}
.stack {
display: grid;
gap: 14px;
}
.answer-card {
display: grid;
gap: 14px;
}
.answer-top {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: flex-start;
flex-wrap: wrap;
}
.answer-text,
.version-text {
line-height: 1.65;
white-space: pre-wrap;
word-break: break-word;
}
.vote-row {
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
}
.vote-btn {
border: 1px solid var(--line);
background: #18233d;
border-radius: 12px;
padding: 8px 12px;
cursor: pointer;
color: var(--text);
}
.vote-btn.active-up {
border-color: #469966;
background: var(--good-soft);
color: var(--good);
}
.vote-btn.active-down {
border-color: #a45454;
background: #3e2023;
color: var(--bad);
}
.proposal-form {
display: grid;
gap: 10px;
padding-top: 6px;
}
.proposal-actions,
.form-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.muted {
color: var(--muted);
font-size: 0.9rem;
}
.toast {
position: fixed;
right: 18px;
bottom: 18px;
padding: 12px 14px;
border-radius: 14px;
color: #fff;
background: rgba(10, 14, 26, 0.95);
box-shadow: var(--shadow);
opacity: 0;
transform: translateY(10px);
pointer-events: none;
transition: opacity 150ms ease, transform 150ms ease;
max-width: min(340px, calc(100vw - 36px));
}
.toast.show {
opacity: 1;
transform: translateY(0);
}
@media (max-width: 760px) {
.shell { padding: 16px 12px 28px; }
.topbar { padding: 14px; }
.queue-head,
.answer-top,
.toolbar-row,
.pager {
display: grid;
}
.search-form { grid-template-columns: 1fr; }
.btn, .pill { width: 100%; text-align: center; }
.pager-actions { width: 100%; }
.pager-actions .btn { flex: 1; }
.word-filter-row { flex-direction: column; align-items: stretch; }
.word-input { width: 100%; }
}
</style>
</head>
<body>
<div class="shell">
<header class="topbar">
<div class="brand">
<img src="/templates/logo.png" alt="Logo" />
<div>
<h1>{{ app_title }} Queue</h1>
<p>For unanswered questions, answer versions, and voting.</p>
</div>
</div>
<div class="badge" id="statusBadge">Queue workspace</div>
</header>
<main>
<section id="listView" class="view">
<div class="list-layout">
<div class="panel toolbar">
<div class="toolbar-row">
<div>
<h2 class="section-title">Question queue</h2>
<p class="section-subtitle">Search stored questions and human answers with lightweight text matching.</p>
</div>
<div class="meta-line" id="listSummary"></div>
</div>
<div class="filters" id="statusFilters"></div>
<div class="filters" id="sortFilters"></div>
<div class="word-filter-row">
<span class="muted" style="align-self:center;white-space:nowrap">Question words:</span>
<input id="minWordsInput" type="number" class="word-input" placeholder="Min" min="0" />
<span class="muted" style="align-self:center"></span>
<input id="maxWordsInput" type="number" class="word-input" placeholder="Max" min="0" />
<button class="btn" type="button" id="applyWordsBtn">Apply</button>
<button class="btn" type="button" id="clearWordsBtn">Clear</button>
</div>
<form id="searchForm" class="search-form">
<input id="searchInput" type="search" placeholder="Search questions, answers, or versions" />
<button class="btn primary" type="submit">Search</button>
<button class="btn" type="button" id="clearSearchBtn">Clear</button>
</form>
</div>
<div class="panel">
<div id="queueList" class="queue-list"></div>
<div id="emptyState" class="empty" hidden>No questions match this filter yet.</div>
<div class="pager">
<div class="meta-line" id="pagerInfo"></div>
<div class="pager-actions">
<button class="btn" id="prevPageBtn" type="button">Previous</button>
<button class="btn" id="nextPageBtn" type="button">Next</button>
</div>
</div>
</div>
</div>
</section>
<section id="detailView" class="view">
<div class="detail-layout">
<div class="panel detail-header">
<a id="backLink" class="back-link" href="/">← Back to question list</a>
<div class="question-box">
<div class="queue-tags" id="detailTags"></div>
<h2 id="detailQuestion"></h2>
<div class="meta-line" id="detailMeta"></div>
</div>
</div>
<div class="panel" style="padding:18px;">
<div class="answer-form stack">
<div>
<h3 class="section-title">Add an answer</h3>
<p class="section-subtitle">Write the first answer or add another answer version path for the community to vote on.</p>
</div>
<textarea id="newAnswerText" placeholder="Write a human answer for this question"></textarea>
<div class="form-actions">
<button class="btn primary" id="submitAnswerBtn" type="button">Save answer</button>
</div>
</div>
</div>
<div class="stack" id="answersMount"></div>
</div>
</section>
</main>
</div>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script>
window.__INIT__ = {{ init_json|safe }};
</script>
<script>
(() => {
const S = {
clientId: null,
filters: { status: "unanswered", q: "", page: 1, page_size: 20, sort: "newest", min_words: 0, max_words: 0 },
list: null,
detail: null,
};
const $ = (id) => document.getElementById(id);
const esc = (value) => String(value ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
function nl2br(value) {
return esc(value).replace(/\n/g, "<br>");
}
function trimText(value) {
return String(value || "").trim();
}
function fmtDate(value) {
if (!value) return "unknown time";
const d = new Date(value);
if (Number.isNaN(d.getTime())) return value;
return d.toLocaleString();
}
function getClientId() {
const key = "hi_queue_client_id";
let id = localStorage.getItem(key);
if (!id) {
id = Math.random().toString(36).slice(2) + Date.now().toString(36);
localStorage.setItem(key, id);
}
return id;
}
function showToast(message) {
const toast = $("toast");
toast.textContent = message;
toast.classList.add("show");
clearTimeout(showToast._timer);
showToast._timer = setTimeout(() => toast.classList.remove("show"), 2400);
}
function buildQuery(extra = {}) {
const params = new URLSearchParams();
const merged = {
status: S.filters.status,
q: S.filters.q,
page: S.filters.page,
sort: S.filters.sort,
min_words: S.filters.min_words,
max_words: S.filters.max_words,
...extra,
};
if (merged.status) params.set("status", merged.status);
if (merged.q) params.set("q", merged.q);
if (merged.page && Number(merged.page) > 1) params.set("page", String(merged.page));
if (merged.conversation_id) params.set("conversation_id", merged.conversation_id);
if (merged.sort && merged.sort !== "newest") params.set("sort", merged.sort);
if (Number(merged.min_words) > 0) params.set("min_words", String(merged.min_words));
if (Number(merged.max_words) > 0) params.set("max_words", String(merged.max_words));
return params.toString();
}
function pushListUrl() {
const qs = buildQuery();
history.replaceState({}, "", qs ? `/?${qs}` : "/");
}
function pushDetailUrl(conversationId) {
const qs = buildQuery({ conversation_id: conversationId });
history.pushState({}, "", `/?${qs}`);
}
async function callAPI(action, payload = {}) {
const resp = await fetch("/api", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Client-Id": S.clientId,
},
body: JSON.stringify({ action, client_id: S.clientId, ...payload }),
});
return resp.json();
}
function activeVersion(answer) {
const versions = Array.isArray(answer?.versions) ? answer.versions : [];
if (!versions.length) return null;
const byId = versions.find((v) => v.id === answer.active_version);
if (byId) return byId;
return [...versions].sort((a, b) => {
const voteDiff = Number(b.votes || 0) - Number(a.votes || 0);
if (voteDiff !== 0) return voteDiff;
return String(b.created_at || "").localeCompare(String(a.created_at || ""));
})[0];
}
function sortedAnswers(conversation) {
return [...(conversation?.answers || [])].sort((a, b) => {
const av = activeVersion(a);
const bv = activeVersion(b);
const voteDiff = Number(bv?.votes || 0) - Number(av?.votes || 0);
if (voteDiff !== 0) return voteDiff;
return String(b.created_at || "").localeCompare(String(a.created_at || ""));
});
}
function renderStatusFilters() {
const filters = [
["unanswered", "Unanswered"],
["answered", "Answered"],
["all", "All"],
];
$("statusFilters").innerHTML = filters.map(([value, label]) => `
<button class="pill ${S.filters.status === value ? "active" : ""}" type="button" data-status="${value}">${label}</button>
`).join("");
document.querySelectorAll("[data-status]").forEach((button) => {
button.onclick = () => {
S.filters.status = button.getAttribute("data-status");
S.filters.page = 1;
loadList();
};
});
}
function renderSortFilters() {
const sorts = [
["newest", "Newest"],
["oldest", "Oldest"],
["most_votes", "Most Votes"],
["most_answers", "Most Answers"],
["longest_answer", "Longest Answer"],
["shortest_answer", "Shortest Answer"],
];
$("sortFilters").innerHTML = sorts.map(([value, label]) => `
<button class="pill ${S.filters.sort === value ? "active" : ""}" type="button" data-sort="${value}">${label}</button>
`).join("");
document.querySelectorAll("[data-sort]").forEach((button) => {
button.onclick = () => {
S.filters.sort = button.getAttribute("data-sort");
S.filters.page = 1;
loadList();
};
});
}
function renderList() {
const list = S.list || { items: [], total: 0, page: 1, page_size: 20, has_next: false, has_prev: false };
$("listSummary").textContent = `${list.total} question${list.total === 1 ? "" : "s"} in queue`;
$("searchInput").value = S.filters.q || "";
$("minWordsInput").value = S.filters.min_words > 0 ? S.filters.min_words : "";
$("maxWordsInput").value = S.filters.max_words > 0 ? S.filters.max_words : "";
const items = Array.isArray(list.items) ? list.items : [];
$("queueList").innerHTML = items.map((item) => {
const answered = item.has_answers;
const detailHref = `/?${buildQuery({ conversation_id: item.id })}`;
const preview = answered ? (item.best_answer_preview || "") : "No answer yet. Open this question to write the first answer.";
return `
<a class="queue-item" href="${detailHref}" data-open-detail="${item.id}">
<div class="queue-head">
<h2>${esc(item.question || "Untitled question")}</h2>
<div class="queue-tags">
<span class="tag ${answered ? "answered" : "unanswered"}">${answered ? "Answered" : "Unanswered"}</span>
<span class="tag">${Number(item.answer_count || 0)} answer${Number(item.answer_count || 0) === 1 ? "" : "s"}</span>
</div>
</div>
<div class="preview">${nl2br(preview)}</div>
<div class="meta-line">Updated ${fmtDate(item.updated_at || item.created_at)}${answered && item.best_answer ? ` · best answer votes ${Number(item.best_answer.votes || 0)}` : ""}</div>
</a>
`;
}).join("");
$("emptyState").hidden = items.length > 0;
$("pagerInfo").textContent = `Page ${list.page} · showing ${items.length} of ${list.total}`;
$("prevPageBtn").disabled = !list.has_prev;
$("nextPageBtn").disabled = !list.has_next;
document.querySelectorAll("[data-open-detail]").forEach((link) => {
link.onclick = async (event) => {
event.preventDefault();
await openDetail(link.getAttribute("data-open-detail"));
};
});
}
function renderDetail() {
const conversation = S.detail;
if (!conversation) return;
const answers = sortedAnswers(conversation);
const hasAnswers = answers.length > 0;
$("detailQuestion").textContent = conversation.question || "Untitled question";
$("detailMeta").textContent = `Created ${fmtDate(conversation.created_at)} · Updated ${fmtDate(conversation.updated_at)} · ${answers.length} answer${answers.length === 1 ? "" : "s"}`;
$("detailTags").innerHTML = `
<span class="tag ${hasAnswers ? "answered" : "unanswered"}">${hasAnswers ? "Answered" : "Unanswered"}</span>
<span class="tag">Question ID ${esc(conversation.id)}</span>
`;
$("backLink").href = `/?${buildQuery()}`;
$("newAnswerText").value = "";
$("answersMount").innerHTML = answers.length
? answers.map((answer, index) => renderAnswerCard(answer, index)).join("")
: `<div class="panel empty">No answers yet. This question is ready for its first human answer.</div>`;
bindDetailHandlers();
}
function renderAnswerCard(answer, index) {
const current = activeVersion(answer);
const versions = [...(answer.versions || [])].sort((a, b) => {
const voteDiff = Number(b.votes || 0) - Number(a.votes || 0);
if (voteDiff !== 0) return voteDiff;
return String(b.created_at || "").localeCompare(String(a.created_at || ""));
});
const currentHtml = current ? `
<div class="answer-text">${nl2br(current.text || "")}</div>
<div class="vote-row">
${renderVoteButton(answer.id, current.id, 1, `▲ ${Number(current.votes || 0)}`, current)}
${renderVoteButton(answer.id, current.id, -1, "▼", current)}
</div>
` : `<div class="muted">This answer has no active version yet.</div>`;
const versionCards = versions.map((version) => `
<div class="version-card">
<div class="answer-top">
<div>
<strong>${esc(version.author || "Anonymous")}</strong>
<div class="muted">${fmtDate(version.created_at)}${version.id === answer.active_version ? " · active version" : ""}</div>
</div>
<div class="queue-tags">
<span class="tag">Votes ${Number(version.votes || 0)}</span>
${version.id === answer.active_version ? '<span class="tag answered">Active</span>' : ""}
</div>
</div>
<div class="version-text">${nl2br(version.text || "")}</div>
<div class="vote-row">
${renderVoteButton(answer.id, version.id, 1, `▲ ${Number(version.votes || 0)}`, version)}
${renderVoteButton(answer.id, version.id, -1, "▼", version)}
</div>
</div>
`).join("");
return `
<div class="panel" id="answer-${answer.id}">
<div class="answer-card">
<div class="answer-top">
<div>
<h3 class="section-title">Answer ${index + 1}</h3>
<p class="section-subtitle">Current community-selected version shown first. Historical versions stay searchable and voteable.</p>
</div>
<div class="queue-tags">
<span class="tag">${Number(answer.versions?.length || 0)} version${Number(answer.versions?.length || 0) === 1 ? "" : "s"}</span>
</div>
</div>
${currentHtml}
<div class="proposal-form">
<label for="proposal-${answer.id}"><strong>Propose a new version</strong></label>
<textarea id="proposal-${answer.id}" placeholder="Write a better version for this answer"></textarea>
<div class="proposal-actions">
<button class="btn primary" type="button" data-propose="${answer.id}">Save version</button>
</div>
</div>
<div class="stack">${versionCards}</div>
</div>
</div>
`;
}
function renderVoteButton(answerId, versionId, delta, label, version) {
const voteMap = version?.votes_by_client || {};
const current = Number(voteMap[S.clientId] || 0);
const cls = delta === 1 ? (current === 1 ? "active-up" : "") : (current === -1 ? "active-down" : "");
return `<button class="vote-btn ${cls}" type="button" data-vote="${answerId}|${versionId}|${delta}">${label}</button>`;
}
function showView(name) {
$("listView").classList.toggle("active", name === "list");
$("detailView").classList.toggle("active", name === "detail");
$("statusBadge").textContent = name === "detail" ? "Question detail" : "Queue workspace";
}
async function loadList() {
const res = await callAPI("list_questions", {
status: S.filters.status,
q: S.filters.q,
page: S.filters.page,
sort: S.filters.sort,
min_words: S.filters.min_words || 0,
max_words: S.filters.max_words || 0,
});
if (!res.ok) {
showToast(res.error || "Could not load question list");
return;
}
S.list = res;
renderStatusFilters();
renderSortFilters();
renderList();
showView("list");
pushListUrl();
}
async function loadDetail(conversationId, pushUrl = false) {
const res = await callAPI("get_question_detail", { conversation_id: conversationId });
if (!res.ok) {
showToast(res.error || "Question not found");
return false;
}
S.detail = res.conversation;
renderDetail();
showView("detail");
if (pushUrl) pushDetailUrl(conversationId);
return true;
}
async function openDetail(conversationId) {
await loadDetail(conversationId, true);
}
function bindListHandlers() {
$("searchForm").addEventListener("submit", async (event) => {
event.preventDefault();
S.filters.q = trimText($("searchInput").value);
S.filters.page = 1;
await loadList();
});
$("clearSearchBtn").onclick = async () => {
$("searchInput").value = "";
S.filters.q = "";
S.filters.page = 1;
await loadList();
};
$("applyWordsBtn").onclick = async () => {
S.filters.min_words = Number($("minWordsInput").value) || 0;
S.filters.max_words = Number($("maxWordsInput").value) || 0;
S.filters.page = 1;
await loadList();
};
$("clearWordsBtn").onclick = async () => {
$("minWordsInput").value = "";
$("maxWordsInput").value = "";
S.filters.min_words = 0;
S.filters.max_words = 0;
S.filters.page = 1;
await loadList();
};
$("prevPageBtn").onclick = async () => {
if (!S.list?.has_prev) return;
S.filters.page = Math.max(1, Number(S.filters.page || 1) - 1);
await loadList();
};
$("nextPageBtn").onclick = async () => {
if (!S.list?.has_next) return;
S.filters.page = Number(S.filters.page || 1) + 1;
await loadList();
};
}
function bindDetailHandlers() {
$("submitAnswerBtn").onclick = async () => {
const text = trimText($("newAnswerText").value);
if (!text) {
showToast("Write an answer first");
return;
}
const res = await callAPI("add_answer", {
conversation_id: S.detail.id,
text,
});
if (!res.ok) {
showToast(res.error || "Could not save answer");
return;
}
S.detail = res.conversation;
renderDetail();
showToast("Answer saved");
if (S.filters.status === "unanswered") {
await loadList();
showView("detail");
}
};
document.querySelectorAll("[data-propose]").forEach((button) => {
button.onclick = async () => {
const answerId = button.getAttribute("data-propose");
const box = $("proposal-" + answerId);
const text = trimText(box?.value);
if (!text) {
showToast("Write a version first");
return;
}
const res = await callAPI("propose_version", {
conversation_id: S.detail.id,
answer_id: answerId,
text,
});
if (!res.ok) {
showToast(res.error || "Could not save version");
return;
}
S.detail = res.conversation;
renderDetail();
showToast("Version saved");
};
});
document.querySelectorAll("[data-vote]").forEach((button) => {
button.onclick = async () => {
const [answerId, versionId, delta] = button.getAttribute("data-vote").split("|");
const res = await callAPI("vote_version", {
conversation_id: S.detail.id,
answer_id: answerId,
version_id: versionId,
delta: Number(delta),
});
if (!res.ok) {
showToast(res.error || "Vote failed");
return;
}
S.detail = res.conversation;
renderDetail();
showToast("Vote saved");
};
});
}
function initFromServer() {
const init = window.__INIT__ || {};
S.clientId = init.client_id || getClientId();
S.filters = {
...S.filters,
...(init.filters || {}),
sort: (init.filters || {}).sort || "newest",
min_words: Number((init.filters || {}).min_words || 0),
max_words: Number((init.filters || {}).max_words || 0),
};
S.list = init.list || null;
S.detail = init.detail || null;
renderStatusFilters();
renderSortFilters();
renderList();
bindListHandlers();
if (S.detail) {
renderDetail();
showView("detail");
} else {
showView("list");
}
}
window.addEventListener("popstate", async () => {
const params = new URLSearchParams(window.location.search);
S.filters.status = params.get("status") || "unanswered";
S.filters.q = params.get("q") || "";
S.filters.page = Number(params.get("page") || 1);
S.filters.sort = params.get("sort") || "newest";
S.filters.min_words = Number(params.get("min_words") || 0);
S.filters.max_words = Number(params.get("max_words") || 0);
const conversationId = params.get("conversation_id");
await loadList();
if (conversationId) {
await loadDetail(conversationId, false);
}
});
initFromServer();
})();
</script>
</body>
</html>