Spaces:
Running
Running
| <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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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> | |