researcher-reports / index.html
evalstate's picture
evalstate HF Staff
Archive security release 1.2.3
bb984d4 verified
Raw
History Blame Contribute Delete
24.4 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Research Archive</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=Source+Sans+3:wght@400;600;700&display=swap" rel="stylesheet">
<script>
(() => {
const stored = localStorage.getItem("research-archive-theme");
const dark = stored ? stored === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
document.documentElement.classList.toggle("dark", dark);
})();
</script>
<style>
:root {
color-scheme: light dark;
--bg: #f9fafb;
--paper: #fff;
--paper-2: #f9fafb;
--text: #111827;
--soft: #4b5563;
--muted: #6b7280;
--line: #e5e7eb;
--accent: #b45309;
--accent-soft: #fffbeb;
--accent-line: #fde68a;
--success: #059669;
--warning: #d97706;
--danger: #dc2626;
--shadow: 0 1px 2px rgba(15, 23, 42, .04), 0 10px 32px rgba(15, 23, 42, .07);
--sans: "Source Sans 3", "Source Sans Pro", ui-sans-serif, system-ui, sans-serif;
--mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
:root.dark {
color-scheme: dark;
--bg: #0b0f19;
--paper: #101623;
--paper-2: #141c2e;
--text: #e5e7eb;
--soft: #d1d5db;
--muted: #9ca3af;
--line: #263044;
--accent: #fbbf24;
--accent-soft: rgba(245, 158, 11, .12);
--accent-line: rgba(245, 158, 11, .28);
--success: #34d399;
--warning: #fbbf24;
--danger: #f87171;
--shadow: 0 18px 50px rgba(0, 0, 0, .28);
}
* { box-sizing: border-box; }
html, body { min-height: 100%; margin: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--sans);
}
button, input { font: inherit; }
button, a { -webkit-tap-highlight-color: transparent; }
.app { height: 100vh; min-height: 640px; display: grid; grid-template-rows: auto 1fr; }
.topbar {
position: sticky;
z-index: 10;
top: 0;
display: flex;
align-items: center;
gap: 24px;
padding: 14px 28px;
border-top: 3px solid #ffd21e;
border-bottom: 1px solid var(--line);
background: color-mix(in srgb, var(--paper) 94%, transparent);
backdrop-filter: blur(14px);
}
.brand { min-width: 240px; display: flex; align-items: center; gap: 10px; }
.brand-logo { width: 36px; height: 34px; flex: none; object-fit: contain; }
.brand-name { font-size: 17px; font-weight: 700; line-height: 1; }
.brand-product { margin-top: 5px; color: var(--muted); font-family: var(--mono); font-size: 9px; letter-spacing: .09em; line-height: 1; text-transform: uppercase; }
.kicker, .label, .meta, .chip, .file-size {
font-family: var(--mono);
text-transform: uppercase;
letter-spacing: .12em;
}
.kicker { color: var(--accent); font-size: 10px; font-weight: 600; }
.search {
width: min(520px, 100%);
margin-left: auto;
padding: 11px 14px;
border: 1px solid var(--line);
border-radius: 10px;
outline: none;
background: var(--paper-2);
color: var(--text);
font-family: var(--mono);
font-family: var(--sans);
font-size: 14px;
}
.search:focus { border-color: #f59e0b; box-shadow: 0 0 0 3px rgba(245, 158, 11, .12); }
.theme-toggle {
width: 38px;
height: 38px;
flex: none;
border: 1px solid var(--line);
border-radius: 9px;
background: linear-gradient(var(--paper), var(--paper-2));
color: var(--muted);
cursor: pointer;
}
.theme-toggle:hover { border-color: var(--accent-line); color: var(--accent); }
.layout {
width: min(1800px, 100%);
margin: 0 auto;
padding: 24px 28px 36px;
display: grid;
grid-template-columns: 300px minmax(0, 1fr);
gap: 22px;
min-height: 0;
overflow: hidden;
}
.sidebar, .detail {
border: 1px solid var(--line);
border-radius: 12px;
background: var(--paper);
box-shadow: var(--shadow);
min-height: 0;
}
.sidebar { overflow: hidden; display: flex; flex-direction: column; }
.sidebar-head {
display: flex;
align-items: baseline;
justify-content: space-between;
padding: 18px;
border-bottom: 1px solid var(--line);
}
.label { color: var(--muted); font-size: 10px; }
.count { color: var(--soft); font-family: var(--mono); font-size: 12px; }
.run-list { flex: 1; min-height: 0; overflow-y: auto; padding: 8px; }
.run {
width: 100%;
padding: 14px 12px;
border: 1px solid transparent;
border-radius: 9px;
background: transparent;
color: inherit;
text-align: left;
cursor: pointer;
}
.run:hover { background: var(--paper-2); }
.run.active { border-color: var(--accent-line); background: var(--accent-soft); }
.run-top { display: flex; align-items: center; gap: 8px; }
.run-date { color: var(--muted); font-family: var(--mono); font-size: 10px; }
.status-dot { width: 7px; height: 7px; margin-left: auto; border-radius: 99px; background: var(--muted); }
.status-dot.complete { background: var(--success); }
.status-dot.markdown { background: var(--warning); }
.run-title { margin-top: 8px; font-size: 16px; font-weight: 600; line-height: 1.3; }
.run-meta { margin-top: 8px; color: var(--muted); font-family: var(--mono); font-size: 10px; }
.detail { overflow: hidden; display: flex; flex-direction: column; }
.empty {
min-height: 560px;
display: grid;
place-items: center;
padding: 40px;
color: var(--soft);
text-align: center;
}
.detail-toolbar {
min-height: 58px;
padding: 8px 18px;
display: flex;
align-items: center;
gap: 14px;
border-bottom: 1px solid var(--line);
}
.detail-context { min-width: 0; display: flex; align-items: center; gap: 10px; }
.chip {
padding: 5px 8px;
border: 1px solid var(--line);
border-radius: 7px;
color: var(--soft);
font-size: 9px;
}
.run-id {
max-width: 340px;
overflow: hidden;
color: var(--muted);
font-family: var(--mono);
font-size: 10px;
text-overflow: ellipsis;
white-space: nowrap;
}
.tabs {
display: flex;
align-self: stretch;
gap: 0;
margin-left: auto;
overflow-x: auto;
}
.tab {
padding: 10px 14px;
border: 0;
border-bottom: 2px solid transparent;
background: transparent;
color: var(--soft);
font-family: var(--mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: .08em;
cursor: pointer;
white-space: nowrap;
}
.tab.active { border-color: #f59e0b; color: var(--text); font-weight: 600; }
.toolbar-actions { display: flex; align-items: center; gap: 6px; }
.toolbar-action {
min-height: 32px;
padding: 7px 10px;
border: 1px solid var(--line);
border-radius: 7px;
background: linear-gradient(var(--paper), var(--paper-2));
color: var(--accent);
font-family: var(--mono);
font-size: 9px;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
}
.toolbar-action:hover { border-color: var(--accent-line); background: var(--accent-soft); }
.toolbar-action.copy-success { border-color: var(--success); color: var(--success); }
.toolbar-action.copy-error { border-color: var(--danger); color: var(--danger); }
.panel { flex: 1; min-height: 0; padding: 26px 32px 40px; overflow: auto; }
.panel.html-panel {
display: flex;
flex-direction: column;
padding: 16px 18px 18px;
overflow: hidden;
}
.cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
.card {
padding: 18px;
border: 1px solid var(--line);
border-radius: 10px;
background: var(--paper-2);
}
.card-value { margin-top: 10px; font-size: 28px; font-weight: 700; }
.delete-button {
display: inline-grid;
width: 38px;
height: 38px;
place-items: center;
border: 1px solid var(--line);
border-radius: 8px;
background: transparent;
color: var(--muted);
cursor: pointer;
}
.delete-button:hover { border-color: var(--danger); color: var(--danger); background: color-mix(in srgb, var(--danger) 7%, transparent); }
.delete-button svg { width: 17px; height: 17px; }
.delete-button:disabled { cursor: wait; opacity: .55; }
dialog {
width: min(460px, calc(100% - 32px));
padding: 0;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--paper);
color: var(--text);
box-shadow: 0 24px 80px rgba(0, 0, 0, .28);
}
dialog::backdrop { background: rgba(20, 18, 15, .55); backdrop-filter: blur(3px); }
.dialog-body { padding: 26px 28px 20px; }
.dialog-body h3 { margin: 10px 0; font-size: 25px; font-weight: 700; }
.dialog-body p { margin: 0; color: var(--soft); line-height: 1.55; }
.dialog-run {
display: block;
margin-top: 16px;
padding: 10px 12px;
border: 1px solid var(--line);
border-radius: 8px;
color: var(--muted);
font-family: var(--mono);
font-size: 10px;
overflow-wrap: anywhere;
}
.dialog-error { min-height: 18px; margin-top: 12px; color: var(--danger); font-family: var(--mono); font-size: 11px; }
.dialog-actions { display: flex; justify-content: flex-end; gap: 10px; padding: 16px 28px; border-top: 1px solid var(--line); background: var(--paper-2); }
.dialog-button {
padding: 9px 14px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--paper);
color: var(--text);
cursor: pointer;
}
.dialog-button.danger { border-color: var(--danger); background: var(--danger); color: white; }
.dialog-button:disabled { cursor: wait; opacity: .6; }
.markdown { max-width: 1100px; font-size: 16px; line-height: 1.68; }
.markdown h1, .markdown h2, .markdown h3 { line-height: 1.15; font-weight: 700; }
.markdown h1 { font-size: 36px; }
.markdown h2 { margin-top: 40px; font-size: 27px; }
.markdown h3 { margin-top: 28px; font-size: 20px; }
.markdown a { color: var(--accent); }
.markdown code { font-family: var(--mono); font-size: .9em; }
.markdown pre { padding: 16px; overflow: auto; border: 1px solid var(--line); border-radius: 8px; background: var(--paper-2); }
.markdown table { width: 100%; border-collapse: collapse; display: block; overflow-x: auto; }
.markdown th, .markdown td { padding: 9px 11px; border-bottom: 1px solid var(--line); text-align: left; }
.report-frame { width: 100%; min-height: 0; flex: 1; border: 1px solid var(--line); border-radius: 10px; background: white; }
.file-list { border-top: 1px solid var(--line); }
.file {
display: grid;
grid-template-columns: 80px minmax(0, 1fr) max-content;
gap: 14px;
align-items: center;
padding: 11px 4px;
border-bottom: 1px solid var(--line);
}
.file-kind { color: var(--accent); font-family: var(--mono); font-size: 10px; text-transform: uppercase; }
.file a { color: var(--text); font-family: var(--mono); font-size: 12px; text-decoration: none; overflow-wrap: anywhere; }
.file a:hover { color: var(--accent); }
.file-size { color: var(--muted); font-size: 9px; }
.loading { opacity: .6; }
@media (max-width: 900px) {
.app { height: auto; min-height: 100vh; }
.topbar { align-items: flex-start; flex-wrap: wrap; padding: 16px 18px; }
.brand { min-width: 0; }
.search { order: 3; width: 100%; }
.layout { grid-template-columns: 1fr; padding: 16px; overflow: visible; }
.run-list { max-height: 320px; flex: none; }
.cards { grid-template-columns: 1fr; }
.detail-toolbar { flex-wrap: wrap; padding: 10px 14px; gap: 6px 12px; }
.detail-context { order: 1; width: calc(100% - 48px); }
.delete-button { order: 2; margin-left: auto; }
.tabs { order: 3; width: 100%; margin-left: 0; }
.toolbar-actions { order: 4; width: 100%; }
.panel { padding-left: 20px; padding-right: 20px; }
.panel.html-panel { height: 72vh; min-height: 520px; padding: 14px; }
}
</style>
</head>
<body>
<div class="app">
<header class="topbar">
<div class="brand">
<img class="brand-logo" src="/assets/huggingface-logo.svg" alt="Hugging Face">
<div>
<div class="brand-name">Hugging Face</div>
<div class="brand-product">Research Archive</div>
</div>
</div>
<input id="search" class="search" type="search" placeholder="Search reports, dates, workspaces…">
<button id="theme-toggle" class="theme-toggle" type="button" title="Toggle color theme" aria-label="Toggle color theme"></button>
</header>
<main class="layout">
<aside class="sidebar">
<div class="sidebar-head"><span class="label">Research folders</span><span id="count" class="count"></span></div>
<div id="runs" class="run-list loading">Loading archive…</div>
</aside>
<section id="detail" class="detail">
<div class="empty"><div><div class="kicker">Research archive</div><p>Select a research folder to inspect its reports, workings, and generated assets.</p></div></div>
</section>
</main>
</div>
<dialog id="delete-dialog">
<div class="dialog-body">
<div class="kicker">Permanent deletion</div>
<h3>Delete this research report?</h3>
<p>This removes the report and all of its workings, assets, manifests, and traces from the research bucket. This cannot be undone.</p>
<span id="delete-run" class="dialog-run"></span>
<div id="delete-error" class="dialog-error" role="alert"></div>
</div>
<div class="dialog-actions">
<button id="delete-cancel" class="dialog-button" type="button">Cancel</button>
<button id="delete-confirm" class="dialog-button danger" type="button">Delete report</button>
</div>
</dialog>
<script>
const state = {
runs: [],
selected: null,
detail: null,
details: new Map(),
tab: "markdown",
query: "",
readOnly: false,
};
const $ = (id) => document.getElementById(id);
const escapeHtml = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const formatBytes = (n) => n < 1024 ? `${n} B` : n < 1048576 ? `${(n/1024).toFixed(1)} KB` : `${(n/1048576).toFixed(1)} MB`;
async function loadRuns() {
const [runsResponse, configResponse] = await Promise.all([
fetch("/api/runs"),
fetch("/api/config"),
]);
const [data, config] = await Promise.all([
runsResponse.json(),
configResponse.json(),
]);
state.readOnly = config.read_only;
state.runs = data.runs;
$("count").textContent = `${data.count} runs`;
const requested = decodeURIComponent(location.hash.slice(1));
state.selected = state.runs.some(run => run.id === requested) ? requested : state.runs[0]?.id;
renderRuns();
if (state.selected) await selectRun(state.selected, false);
}
function filteredRuns() {
const query = state.query.trim().toLowerCase();
return query ? state.runs.filter(run => `${run.title} ${run.id} ${run.date}`.toLowerCase().includes(query)) : state.runs;
}
function renderRuns() {
const runs = filteredRuns();
$("runs").classList.remove("loading");
$("runs").innerHTML = runs.length ? runs.map(run => `
<button class="run ${run.id === state.selected ? "active" : ""}" data-run="${escapeHtml(run.id)}">
<div class="run-top"><span class="run-date">${escapeHtml(run.date)}</span><span class="status-dot ${run.status}"></span></div>
<div class="run-title">${escapeHtml(run.title)}</div>
<div class="run-meta">${run.asset_count} assets · ${run.trace_count} traces</div>
</button>`).join("") : `<div class="empty">No matching research folders.</div>`;
document.querySelectorAll("[data-run]").forEach(button => button.addEventListener("click", () => selectRun(button.dataset.run)));
}
async function selectRun(id, push = true) {
state.selected = id;
state.detail = null;
if (push) location.hash = encodeURIComponent(id);
renderRuns();
const cached = state.details.get(id);
if (cached) {
state.detail = cached;
state.tab = cached.has_html ? "html" : cached.has_markdown ? "markdown" : "files";
renderDetail();
return;
}
$("detail").innerHTML = `<div class="empty loading">Loading research folder…</div>`;
const response = await fetch(`/api/runs/${encodeURIComponent(id)}`);
const detail = await response.json();
state.details.set(id, detail);
if (state.selected !== id) return;
state.detail = detail;
state.tab = state.detail.has_html ? "html" : state.detail.has_markdown ? "markdown" : "files";
renderDetail();
}
function renderDetail() {
const run = state.detail;
if (!run) return;
const tabs = ["markdown", "html", "files"];
$("detail").innerHTML = `
<div class="detail-toolbar">
<div class="detail-context">
<span class="chip">${escapeHtml(run.status)}</span><span class="label">${escapeHtml(run.date)}</span>
<span class="run-id" title="${escapeHtml(run.id)}">${escapeHtml(run.id)}</span>
</div>
<div class="tabs">${tabs.map(tab => `<button class="tab ${tab === state.tab ? "active" : ""}" data-tab="${tab}">${tab}</button>`).join("")}</div>
<div class="toolbar-actions">
${run.has_markdown ? `<button class="toolbar-action" type="button" data-copy-markdown>Copy Markdown</button>` : ""}
${run.has_html ? `<a class="toolbar-action" href="${run.html_url}" target="_blank" rel="noopener">Open in new window ↗</a><a class="toolbar-action" href="${run.html_url}?download=true">Download ↓</a>` : ""}
</div>
${state.readOnly ? "" : `<button class="delete-button" type="button" data-delete title="Delete report" aria-label="Delete ${escapeHtml(run.title)}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M4 7h16M9 7V4h6v3m3 0-1 13H7L6 7m4 4v5m4-5v5"/>
</svg>
</button>`}
</div>
<div id="panel" class="panel ${state.tab === "html" ? "html-panel" : ""}"></div>`;
document.querySelectorAll("[data-tab]").forEach(button => button.addEventListener("click", () => { state.tab = button.dataset.tab; renderDetail(); }));
document.querySelector("[data-copy-markdown]")?.addEventListener("click", event => copyMarkdown(event.currentTarget));
document.querySelector("[data-delete]")?.addEventListener("click", openDeleteDialog);
renderPanel();
}
async function copyMarkdown(button) {
const original = "Copy Markdown";
try {
let copied = false;
if (navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(state.detail.markdown);
copied = true;
} catch {}
}
if (!copied) {
const textarea = document.createElement("textarea");
try {
textarea.value = state.detail.markdown;
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.select();
copied = document.execCommand("copy");
} finally {
textarea.remove();
}
}
if (!copied) throw new Error("Copy failed");
button.textContent = "Copied ✓";
button.classList.add("copy-success");
} catch (error) {
button.textContent = "Copy failed";
button.classList.add("copy-error");
}
window.setTimeout(() => {
button.textContent = original;
button.classList.remove("copy-success", "copy-error");
}, 1800);
}
function openDeleteDialog() {
$("delete-run").textContent = `${state.detail.title}${state.detail.id}`;
$("delete-error").textContent = "";
$("delete-dialog").showModal();
}
async function deleteSelectedRun() {
const run = state.detail;
if (!run) return;
$("delete-confirm").disabled = true;
$("delete-cancel").disabled = true;
$("delete-error").textContent = "";
try {
const response = await fetch(`/api/runs/${encodeURIComponent(run.id)}`, { method: "DELETE" });
if (!response.ok) {
const body = await response.json().catch(() => ({}));
throw new Error(body.detail || "Could not delete this report.");
}
state.runs = state.runs.filter(item => item.id !== run.id);
state.details.delete(run.id);
state.selected = state.runs[0]?.id ?? null;
state.detail = null;
history.replaceState(null, "", state.selected ? `#${encodeURIComponent(state.selected)}` : location.pathname);
$("count").textContent = `${state.runs.length} runs`;
$("delete-dialog").close();
renderRuns();
if (state.selected) await selectRun(state.selected, false);
else $("detail").innerHTML = `<div class="empty"><div><div class="kicker">Research archive</div><p>No research folders remain.</p></div></div>`;
} catch (error) {
$("delete-error").textContent = error.message;
} finally {
$("delete-confirm").disabled = false;
$("delete-cancel").disabled = false;
}
}
function renderPanel() {
const run = state.detail;
$("panel").classList.toggle("html-panel", state.tab === "html");
if (state.tab === "markdown") {
$("panel").innerHTML = run.markdown_html ? `<article class="markdown">${run.markdown_html}</article>` : `<div class="empty">No Markdown report was produced.</div>`;
} else if (state.tab === "html") {
$("panel").innerHTML = run.html_url ? `<iframe class="report-frame" src="${run.html_url}" title="${escapeHtml(run.title)}" sandbox="allow-popups allow-popups-to-escape-sandbox"></iframe>` : `<div class="empty">No HTML report was produced.</div>`;
} else {
$("panel").innerHTML = `<div class="file-list">${run.files.map(file => `
<div class="file"><span class="file-kind">${escapeHtml(file.kind)}</span><a href="/files/${encodeURIComponent(run.id)}/${file.path.split("/").map(encodeURIComponent).join("/")}" target="_blank" rel="noopener noreferrer">${escapeHtml(file.path)}</a><span class="file-size">${formatBytes(file.size)}</span></div>`).join("")}</div>`;
}
}
$("search").addEventListener("input", event => { state.query = event.target.value; renderRuns(); });
$("theme-toggle").addEventListener("click", () => {
const dark = !document.documentElement.classList.contains("dark");
document.documentElement.classList.toggle("dark", dark);
localStorage.setItem("research-archive-theme", dark ? "dark" : "light");
});
$("delete-cancel").addEventListener("click", () => $("delete-dialog").close());
$("delete-confirm").addEventListener("click", deleteSelectedRun);
$("delete-dialog").addEventListener("click", event => { if (event.target === $("delete-dialog")) $("delete-dialog").close(); });
window.addEventListener("hashchange", () => { const id = decodeURIComponent(location.hash.slice(1)); if (id && id !== state.selected) selectRun(id, false); });
loadRuns().catch(error => { $("runs").textContent = "Could not load archive."; $("detail").innerHTML = `<div class="empty">${escapeHtml(error.message)}</div>`; });
</script>
</body>
</html>