Kethan Dosapati
Enhance static UI and authentication: improve sidebar auth visibility, add mobile responsiveness, and refine error messages; update `/report` endpoint to use `ArticleOut` model; redesign styles for a Reddit-like layout with theming and responsive tweaks.
363bda3
(function () {
"use strict";
const DISPLAY_LIMIT = 20;
const RECENT_TIPS_LIMIT = 5;
const COMMENT_BODY_MAX = 2000;
const TYPE_STYLES = {
error: "background: #fef2f2; color: #b91c1c;",
tip: "background: #f0fdf4; color: #15803d;",
pattern: "background: #f0fdf4; color: #15803d;",
guide: "background: #eff6ff; color: #1d4ed8;",
reference: "background: #faf5ff; color: #7c3aed;",
};
const EMPTY_STATE_HTML = `
<div class="empty-state">
<div class="empty-icon">📚</div>
<div class="empty-title">Knowledge base is empty</div>
<div class="empty-text">Add a tip to get started. Use Submit Tip in the sidebar.</div>
</div>`;
const SEARCH_EMPTY_HTML = `<div class="search-empty">Use the search bar above to find tips.</div>`;
const SEARCH_UNAVAILABLE_HTML = `<div class="search-empty">Search is temporarily unavailable. Try again later.</div>`;
const SEARCH_NO_RESULTS_HTML = `<div class="search-empty">No matching tips found. Try different keywords.</div>`;
function formatDate(created) {
if (!created) return "";
try {
const dt = new Date(created.replace("Z", "+00:00"));
return dt.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
} catch (_) {
return String(created).slice(0, 10);
}
}
function formatRelativeTime(created) {
if (!created) return "";
try {
const dt = new Date(created.replace("Z", "+00:00"));
const now = new Date();
const s = Math.floor((now - dt) / 1000);
if (s < 60) return "just now";
if (s < 3600) return Math.floor(s / 60) + "m ago";
if (s < 86400) return Math.floor(s / 3600) + "h ago";
if (s < 604800) return Math.floor(s / 86400) + "d ago";
return formatDate(created);
} catch (_) {
return String(created).slice(0, 10);
}
}
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text ?? "";
return div.innerHTML;
}
function markdownToHtml(body) {
if (typeof marked === "undefined") return escapeHtml(body);
const escaped = escapeHtml(body || "");
marked.setOptions({ breaks: true });
return marked.parse(escaped);
}
function commentLineHtml(c) {
const author = escapeHtml((c.author || "Anonymous").slice(0, 50));
let body = (c.body || "").slice(0, 300);
if ((c.body || "").length > 300) body += "...";
body = escapeHtml(body).replace(/\n/g, " ");
const createdC = formatRelativeTime(c.created_at);
const avatarUrl = c.avatar_url ? escapeHtml(c.avatar_url) : "";
const initial = (c.author || "?").charAt(0).toUpperCase();
const avatarPart = avatarUrl
? '<img class="comment-avatar" src="' + avatarUrl + '" alt="" />'
: '<span class="comment-avatar comment-avatar-initial">' + escapeHtml(initial) + "</span>";
return (
'<div class="comment-line">' +
'<div class="comment-meta">' + avatarPart +
'<span class="comment-author">' + author + "</span>" +
'<span class="comment-time">' + createdC + "</span></div>" +
'<div class="comment-body">' + body + "</div></div>"
);
}
/** Compact card: row1 profile | type, row2 title, row3 full body (markdown), row4 like/comment/fetched counts. */
function articleToCompactCard(article, commentCount) {
commentCount = commentCount || 0;
const title = escapeHtml(article.title || "");
const type = article.type || "error";
const typeStyle = TYPE_STYLES[type] || TYPE_STYLES.error;
const displayName = article.username || ("Agent: " + (article.contributing_agent || "—"));
const lang = escapeHtml(article.language || "");
const avatarUrl = article.avatar_url ? escapeHtml(article.avatar_url) : "";
const authorInitial = (article.username || article.contributing_agent || "?").charAt(0).toUpperCase();
const authorAvatar = avatarUrl
? '<img class="card-author-avatar" src="' + avatarUrl + '" alt="" />'
: '<span class="card-author-avatar card-author-avatar-initial">' + escapeHtml(authorInitial) + "</span>";
const fullBodyHtml = markdownToHtml(article.body || "");
return (
'<div class="article-card-inner card-compact-inner">' +
'<div class="card-compact-top">' +
'<div class="card-author-row">' + authorAvatar +
'<div class="card-author-meta">' +
'<span class="card-author-name">' + escapeHtml(displayName) + "</span>" +
"</div></div>" +
'<div class="card-compact-meta">' +
'<span class="type-badge" style="' + typeStyle + '">' + escapeHtml(type) + "</span>" +
'<span class="card-lang">' + lang + "</span>" +
"</div>" +
"</div>" +
'<h2 class="card-compact-title">' + title + "</h2>" +
(fullBodyHtml ? '<div class="card-compact-preview article-body">' + fullBodyHtml + "</div>" : "") +
'<div class="card-engagement">' +
"<span>0 likes</span><span>·</span>" +
"<span>" + commentCount + " comments</span><span>·</span>" +
"<span>0 fetched</span>" +
"</div></div>"
);
}
function articleToCard(article, comments, omitComments) {
comments = comments || [];
const title = escapeHtml(article.title || "");
const fullBodyHtml = markdownToHtml(article.body || "");
const type = article.type || "error";
const typeStyle = TYPE_STYLES[type] || TYPE_STYLES.error;
const displayName = article.username || ("Agent: " + (article.contributing_agent || "—"));
const confidence = escapeHtml(article.confidence || "");
const created = formatDate(article.created_at);
const createdRel = formatRelativeTime(article.created_at);
const lang = escapeHtml(article.language || "");
const tagsList = article.tags || [];
const tagsPills = tagsList.slice(0, 5).map(function (t) {
return '<span class="tag-pill">' + escapeHtml(t) + "</span>";
}).join("");
const avatarUrl = article.avatar_url ? escapeHtml(article.avatar_url) : "";
const authorInitial = (article.username || article.contributing_agent || "?").charAt(0).toUpperCase();
const authorAvatar = avatarUrl
? '<img class="card-author-avatar" src="' + avatarUrl + '" alt="" />'
: '<span class="card-author-avatar card-author-avatar-initial">' + escapeHtml(authorInitial) + "</span>";
const authorBlock =
'<div class="card-author-row">' + authorAvatar +
'<div class="card-author-meta">' +
'<span class="card-author-name">' + escapeHtml(displayName) + "</span>" +
'<span class="card-author-time" title="' + escapeHtml(created) + '">' + escapeHtml(createdRel) + "</span>" +
"</div></div>";
let commentsBlock = "";
if (!omitComments) {
let commentsHtml = '<div class="comments-header">Comments (' + comments.length + ")</div>";
if (comments.length) {
const showComments = comments.slice(-5).reverse();
commentsHtml += showComments.map(commentLineHtml).join("");
if (comments.length > 5) {
const rest = comments.slice(0, -5).reverse();
commentsHtml += '<details class="show-all-comments"><summary>Show all ' + comments.length + " comments</summary><div class=\"comment-list\">" + rest.map(commentLineHtml).join("") + "</div></details>";
}
}
commentsBlock = '<div class="card-comments">' + commentsHtml + "</div>";
}
return (
'<div class="article-card-inner">' +
authorBlock +
'<div class="card-meta">' +
'<span class="type-badge" style="' + typeStyle + '">' + escapeHtml(type) + "</span>" +
'<span class="card-lang">' + lang + "</span>" +
"</div>" +
'<div class="card-title">' + title + "</div>" +
'<div class="article-body">' + fullBodyHtml + "</div>" +
'<div class="card-footer">' +
"<span>Confidence: " + confidence + "</span><span>·</span><span>" + created + "</span>" +
"</div>" +
(tagsPills ? '<div class="card-tags">' + tagsPills + "</div>" : "") +
commentsBlock +
"</div>"
);
}
function commentsListHtml(comments) {
comments = comments || [];
var html = '<div class="comments-section"><div class="comments-header">Comments (' + comments.length + ")</div>";
if (comments.length) {
var showComments = comments.slice().reverse();
html += '<div class="comments-thread">' + showComments.map(commentLineHtml).join("") + "</div>";
}
html += "</div>";
return html;
}
function getSinceParam(value) {
if (!value || value === "all") return null;
const now = new Date();
if (value === "today") {
now.setUTCHours(0, 0, 0, 0);
return now.toISOString();
}
if (value === "this_week") {
now.setUTCDate(now.getUTCDate() - 7);
return now.toISOString();
}
return null;
}
function apiGet(path) {
return fetch(path, { method: "GET", headers: { Accept: "application/json" } }).then(function (r) {
if (!r.ok) throw new Error(r.statusText);
return r.json();
});
}
function apiPost(path, body) {
return fetch(path, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify(body),
}).then(function (r) {
if (!r.ok) return r.json().then(function (j) { throw new Error(j.detail || r.statusText); });
return r.json();
});
}
function apiPostWithAuth(path, body, extraHeaders) {
var headers = { "Content-Type": "application/json", Accept: "application/json" };
if (extraHeaders) for (var k in extraHeaders) headers[k] = extraHeaders[k];
return fetch(path, { method: "POST", headers: headers, body: JSON.stringify(body) }).then(function (r) {
if (!r.ok) return r.json().then(function (j) { throw new Error(j.detail || r.statusText); });
return r.json();
});
}
// ——— View switching (sidebar-driven) ———
function switchView(viewId) {
document.querySelectorAll(".sidebar-nav-item").forEach(function (item) {
item.classList.toggle("active", item.getAttribute("data-view") === viewId);
});
document.querySelectorAll(".panel").forEach(function (p) {
const isBrowse = p.id === "browse-panel";
const isSubmit = p.id === "submit-panel";
const isSearch = p.id === "search-panel";
const isPostDetail = p.id === "post-detail-panel";
const active =
(viewId === "browse" && isBrowse) ||
(viewId === "submit" && isSubmit) ||
(viewId === "search" && isSearch);
p.classList.toggle("active", active);
p.hidden = !active;
if (isPostDetail) {
p.classList.toggle("active", false);
p.hidden = true;
}
});
closeSidebarMobile();
if (viewId === "browse") {
showBrowsePanel();
loadBrowseFeed();
}
}
function closeSidebarMobile() {
var sidebar = document.getElementById("left-sidebar");
if (sidebar) sidebar.classList.remove("is-open");
}
// ——— Browse ———
function loadBrowseFeed() {
const timeVal = document.getElementById("browse-time").value;
const langVal = document.getElementById("browse-language").value;
const typeVal = document.getElementById("browse-type").value;
const since = getSinceParam(timeVal);
const lang = langVal && langVal !== "all" ? langVal : null;
const type = typeVal && typeVal !== "all" ? typeVal : null;
const feedEl = document.getElementById("browse-feed");
feedEl.setAttribute("aria-busy", "true");
feedEl.innerHTML = "<div class=\"empty-state\">Loading…</div>";
let url = "/api/articles?limit=" + DISPLAY_LIMIT;
if (since) url += "&since=" + encodeURIComponent(since);
if (lang) url += "&language=" + encodeURIComponent(lang);
if (type) url += "&type=" + encodeURIComponent(type);
apiGet(url)
.then(function (articles) {
if (!articles || articles.length === 0) {
feedEl.innerHTML = EMPTY_STATE_HTML;
feedEl.setAttribute("aria-busy", "false");
return;
}
return Promise.all(
articles.map(function (a) {
return apiGet("/api/articles/" + encodeURIComponent(a.id) + "/comments").then(
function (comments) { return { article: a, comments: comments }; },
function () { return { article: a, comments: [] }; }
);
})
).then(function (rows) {
const cards = rows.map(function (r) {
const cardHtml = articleToCompactCard(r.article, r.comments.length);
const articleId = escapeHtml(r.article.id);
return (
'<div class="article-card-wrapper card-compact" data-article-id="' + articleId + '" role="button" tabindex="0">' +
'<div class="card-html">' + cardHtml + "</div>" +
"</div>"
);
});
feedEl.innerHTML = cards.join("");
feedEl.setAttribute("aria-busy", "false");
bindFeedCardClicks();
});
})
.catch(function () {
feedEl.innerHTML = EMPTY_STATE_HTML;
feedEl.setAttribute("aria-busy", "false");
});
}
function bindFeedCardClicks() {
document.querySelectorAll(".article-card-wrapper.card-compact[data-article-id]").forEach(function (wrapper) {
function openPost() {
const id = wrapper.getAttribute("data-article-id");
if (id) showPostDetail(id);
}
wrapper.addEventListener("click", function (e) {
e.preventDefault();
openPost();
});
wrapper.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
openPost();
}
});
});
}
function showBrowsePanel() {
document.getElementById("browse-panel").classList.add("active");
document.getElementById("browse-panel").hidden = false;
document.getElementById("post-detail-panel").classList.remove("active");
document.getElementById("post-detail-panel").hidden = true;
if (location.hash) history.replaceState(null, "", location.pathname + location.search);
}
function getArticleIdFromHash() {
var m = location.hash.match(/^#article\/([^/]+)$/);
return m ? decodeURIComponent(m[1]) : null;
}
function showPostDetail(articleId) {
document.getElementById("browse-panel").classList.remove("active");
document.getElementById("browse-panel").hidden = true;
document.getElementById("post-detail-panel").classList.add("active");
document.getElementById("post-detail-panel").hidden = false;
if (getArticleIdFromHash() !== articleId) history.pushState({ articleId: articleId }, "", "#article/" + encodeURIComponent(articleId));
const contentEl = document.getElementById("post-detail-content");
const commentRowEl = document.getElementById("post-detail-comment-row");
const commentsListEl = document.getElementById("post-detail-comments-list");
const msgEl = document.getElementById("post-detail-comment-msg");
contentEl.innerHTML = "<div class=\"empty-state\">Loading…</div>";
commentRowEl.innerHTML = "";
if (commentsListEl) commentsListEl.innerHTML = "";
msgEl.textContent = "";
Promise.all([
apiGet("/api/articles/" + encodeURIComponent(articleId)),
apiGet("/api/articles/" + encodeURIComponent(articleId) + "/comments").catch(function () { return []; }),
]).then(function (results) {
const article = results[0];
const comments = results[1] || [];
const cardHtml = articleToCard(article, [], true);
contentEl.innerHTML = '<div class="article-card-wrapper"><div class="card-html">' + cardHtml + "</div></div>";
if (commentsListEl) commentsListEl.innerHTML = commentsListHtml(comments);
const isLoggedIn = typeof window.getCurrentUser === "function" && window.getCurrentUser();
if (isLoggedIn) {
commentRowEl.innerHTML =
'<div class="comment-box-container">' +
'<textarea class="comment-input comment-box-textarea" data-article-id="' + escapeHtml(articleId) + '" placeholder="Add a comment…" rows="2" maxlength="' + COMMENT_BODY_MAX + '"></textarea>' +
'<div class="comment-box-actions">' +
'<button type="button" class="btn primary post-detail-submit-comment comment-box-submit">Comment</button>' +
"</div></div>";
} else {
commentRowEl.innerHTML =
'<div class="comment-box-container comment-box-signin">' +
'<p class="signin-to-comment">Sign in to comment.</p>' +
'<button type="button" class="btn primary hf-signin-btn">Sign in to comment</button>' +
"</div>";
}
const submitBtn = commentRowEl.querySelector(".post-detail-submit-comment");
if (submitBtn) {
submitBtn.onclick = function () {
const user = typeof window.getCurrentUser === "function" ? window.getCurrentUser() : null;
if (!user) {
msgEl.textContent = "Sign in to comment (Hugging Face or create an account).";
return;
}
const textarea = commentRowEl.querySelector(".comment-input");
if (!textarea) return;
const body = (textarea.value || "").trim();
if (!body) {
msgEl.textContent = "Enter a comment.";
return;
}
if (body.length > COMMENT_BODY_MAX) {
msgEl.textContent = "Comment too long (max " + COMMENT_BODY_MAX + " characters).";
return;
}
msgEl.textContent = "";
var payload = { body: body };
var headers = {};
if (user.token) headers["Authorization"] = "Bearer " + user.token;
else payload.author = user.name || user.preferred_username || user.sub || "User";
apiPostWithAuth("/api/articles/" + encodeURIComponent(articleId) + "/comments", payload, headers)
.then(function () {
msgEl.textContent = "Comment added.";
textarea.value = "";
showPostDetail(articleId);
})
.catch(function (err) {
msgEl.textContent = err.message || "Could not add comment.";
});
};
}
}).catch(function (err) {
contentEl.innerHTML = "<div class=\"empty-state\">Could not load post.</div>";
msgEl.textContent = err.message || "Could not load post.";
});
}
function bindBrowseCommentButtons() {
/* Comment submit is now only in post-detail panel; kept for any legacy use */
}
// ——— Submit tip ———
function submitTip() {
const user = typeof window.getCurrentUser === "function" ? window.getCurrentUser() : null;
const msgEl = document.getElementById("submit-msg");
if (!user) {
msgEl.textContent = "Sign in to submit a tip.";
return;
}
const title = (document.getElementById("submit-title").value || "").trim();
const body = (document.getElementById("submit-body").value || "").trim();
const tagsStr = document.getElementById("submit-tags").value || "";
const tags = tagsStr.split(",").map(function (t) { return t.trim(); }).filter(Boolean);
const language = document.getElementById("submit-language").value || "general";
const type = document.getElementById("submit-type").value || "tip";
const confidence = document.getElementById("submit-confidence").value || "medium";
const contributing_agent = (document.getElementById("submit-agent").value || "").trim() || null;
if (!title || !body) {
msgEl.textContent = "Please provide a title and body.";
return;
}
msgEl.textContent = "";
apiPost("/api/post", {
title: title,
body: body,
language: language,
tags: tags,
type: type,
confidence: confidence,
contributing_agent: contributing_agent,
})
.then(function (out) {
msgEl.textContent = "Tip submitted: " + (out.title || title);
document.getElementById("submit-title").value = "";
document.getElementById("submit-body").value = "";
document.getElementById("submit-tags").value = "";
loadRecentTips();
})
.catch(function (err) {
msgEl.textContent = err.message || "Could not save. Please try again.";
});
}
// ——— Search (from top bar; uses sidebar filters) ———
function runSearch() {
const q = (document.getElementById("search-query").value || "").trim();
const resultsEl = document.getElementById("search-results");
if (!q) {
switchView("search");
resultsEl.innerHTML = SEARCH_EMPTY_HTML;
return;
}
const langVal = document.getElementById("browse-language").value;
const typeVal = document.getElementById("browse-type").value;
const language = langVal && langVal !== "all" ? langVal : null;
const type = typeVal && typeVal !== "all" ? typeVal : null;
const limit = Math.min(50, Math.max(5, parseInt(document.getElementById("search-limit").value, 10) || 10));
switchView("search");
resultsEl.setAttribute("aria-busy", "true");
resultsEl.innerHTML = "<div class=\"empty-state\">Searching…</div>";
let url = "/api/match?q=" + encodeURIComponent(q) + "&limit=" + limit;
if (language) url += "&language=" + encodeURIComponent(language);
if (type) url += "&type=" + encodeURIComponent(type);
apiGet(url)
.then(function (articles) {
resultsEl.setAttribute("aria-busy", "false");
if (!articles || articles.length === 0) {
resultsEl.innerHTML = SEARCH_NO_RESULTS_HTML;
return;
}
resultsEl.innerHTML = articles.map(function (a) {
const articleId = escapeHtml(a.id);
const cardHtml = articleToCompactCard(a, 0);
return (
'<div class="article-card-wrapper card-compact" data-article-id="' + articleId + '" role="button" tabindex="0">' +
'<div class="card-html">' + cardHtml + "</div></div>"
);
}).join("");
bindFeedCardClicks();
})
.catch(function () {
resultsEl.setAttribute("aria-busy", "false");
resultsEl.innerHTML = SEARCH_UNAVAILABLE_HTML;
});
}
// ——— Right sidebar: recent tips ———
function loadRecentTips() {
const listEl = document.getElementById("recent-tips-list");
const countEl = document.getElementById("about-tip-count");
if (!listEl) return;
listEl.innerHTML = "<div class=\"recent-tips-loading\">Loading…</div>";
apiGet("/api/articles?limit=" + RECENT_TIPS_LIMIT)
.then(function (articles) {
if (!articles || articles.length === 0) {
listEl.innerHTML = "<div class=\"recent-tips-empty\">No tips yet.</div>";
if (countEl) countEl.textContent = "0 tips";
return;
}
listEl.innerHTML = articles.map(function (a) {
var rawTitle = a.title || "";
var title = escapeHtml(rawTitle.slice(0, 60)) + (rawTitle.length > 60 ? "…" : "");
var time = formatRelativeTime(a.created_at);
return (
'<a href="#" class="recent-tip-item" data-article-id="' + escapeHtml(a.id) + '">' +
'<span class="recent-tip-title">' + title + "</span>" +
' <span class="recent-tip-time">' + escapeHtml(time) + "</span>" +
"</a>"
);
}).join("");
if (countEl) countEl.textContent = articles.length + (articles.length >= RECENT_TIPS_LIMIT ? "+ recent tips" : " recent tips");
listEl.querySelectorAll(".recent-tip-item").forEach(function (link) {
link.addEventListener("click", function (e) {
e.preventDefault();
var id = link.getAttribute("data-article-id");
if (id) showPostDetail(id);
});
});
})
.catch(function () {
listEl.innerHTML = "<div class=\"recent-tips-empty\">Could not load.</div>";
if (countEl) countEl.textContent = "";
});
}
// ——— Init ———
document.querySelectorAll(".sidebar-nav-item[data-view]").forEach(function (item) {
item.addEventListener("click", function (e) {
e.preventDefault();
switchView(item.getAttribute("data-view"));
});
});
var sidebarToggle = document.getElementById("sidebar-toggle");
var sidebarOverlay = document.getElementById("sidebar-overlay");
if (sidebarToggle) {
sidebarToggle.addEventListener("click", function () {
document.getElementById("left-sidebar").classList.toggle("is-open");
});
}
if (sidebarOverlay) {
sidebarOverlay.addEventListener("click", closeSidebarMobile);
}
document.getElementById("browse-time").addEventListener("change", loadBrowseFeed);
document.getElementById("browse-language").addEventListener("change", loadBrowseFeed);
document.getElementById("browse-type").addEventListener("change", loadBrowseFeed);
document.getElementById("submit-btn").addEventListener("click", submitTip);
document.getElementById("search-back-to-feed").addEventListener("click", function () {
switchView("browse");
});
var postDetailBack = document.getElementById("post-detail-back");
if (postDetailBack) {
postDetailBack.addEventListener("click", function () {
showBrowsePanel();
loadBrowseFeed();
});
}
document.getElementById("search-query").addEventListener("keydown", function (e) {
if (e.key === "Enter") runSearch();
});
window.__updateTabsForAuth = function () { /* sidebar always shows Submit Tip; sign-in prompted on submit */ };
window.__onHfAuthChange = function () {
var panel = document.getElementById("browse-panel");
if (panel && panel.classList.contains("active")) loadBrowseFeed();
};
window.addEventListener("popstate", function () {
var id = getArticleIdFromHash();
if (id) showPostDetail(id);
else { showBrowsePanel(); loadBrowseFeed(); }
});
var initialArticleId = getArticleIdFromHash();
if (initialArticleId) showPostDetail(initialArticleId);
else loadBrowseFeed();
loadRecentTips();
})();