daybook/templates/artifacts/feed.html
peter d30e2436ab v0.3.0: rename app Daybook -> AgentFeed
Removes the "Daybook" name from everything user- and agent-facing, since no
agents are configured yet and this is the cheap moment to do it.

- UI brand, docs, README, CLAUDE.md -> AgentFeed.
- Ingest env vars DAYBOOK_URL/TOKEN/SOURCE/DATA_DIR/BIND_ADDR/HOST_DATA_DIR/
  KEY_PROVISIONING_API -> AGENTFEED_* (token *values* unchanged).
- Skill: skills/daybook-posting -> skills/agentfeed-posting, daybook_post.py
  -> agentfeed_post.py (+ scripts/ wrapper and its test).
- static/daybook.css -> static/agentfeed.css; localStorage daybook.* ->
  agentfeed.*; templatetag daybook_extras -> agentfeed_extras; multipart
  boundaries; json/csv viewer element ids; daybook_exception_handler ->
  agentfeed_exception_handler; compose service/container -> agentfeed.
- Key-provisioning API now OFF by default (independent of DEBUG); enable with
  AGENTFEED_KEY_PROVISIONING_API=1 on test installs only.
- Posting helper now points at Settings -> API Keys.

Deferred on purpose (a later, separate step): the Django package
`daybook_project`, the `/opt/daybook` install path, and the Forgejo repo name.
The sibling AGENT_DAYBOOK__LLM_TEST name is intentionally untouched.

Full suite: 129 passing; live server verified serving under the new name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-04 13:10:28 +01:00

655 lines
30 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AgentFeed</title>
<link rel="icon" type="image/png" href="/static/favicon.png">
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png">
<link rel="stylesheet" href="/static/agentfeed.css">
<script>
// Set the theme before first paint so dark-mode users never see a light flash.
(function () {
try {
if (localStorage.getItem("agentfeed.theme") === "dark") {
document.documentElement.setAttribute("data-theme", "dark");
}
} catch (e) {}
})();
</script>
</head>
<body>
{% include "artifacts/_topbar.html" with topbar_toolbar="artifacts/_feed_toolbar.html" %}
<main class="feed"
data-selected-source="{{ selected_source }}"
data-selected-label="{{ selected_label }}"
data-selected-kind="{{ selected_kind }}"
data-title-query="{{ title_query }}"
data-selected-view="{{ selected_view }}"
data-window-start="{{ window_start|date:'Y-m-d' }}"
data-older-count="{{ older_count }}">
<aside class="source-sidebar" aria-label="Sources">
<div class="pane-bar">
<h2>Sources</h2>
</div>
<nav class="source-list">
<a class="source-link {% if not selected_source %}selected{% endif %}" href="?{% if selected_label %}label={{ selected_label|urlencode }}&amp;{% endif %}{% if selected_kind %}kind={{ selected_kind|urlencode }}&amp;{% endif %}{% if title_query %}q={{ title_query|urlencode }}&amp;{% endif %}view={{ selected_view }}&amp;preview=none">
<span class="source-name">All sources</span>
{% if total_count %}<span class="source-count">{{ total_count }}</span>{% endif %}
</a>
{% for entry in source_counts %}
<a class="source-link {% if entry.source == selected_source %}selected{% endif %}" href="?source={{ entry.source|urlencode }}{% if selected_label %}&amp;label={{ selected_label|urlencode }}{% endif %}{% if selected_kind %}&amp;kind={{ selected_kind|urlencode }}{% endif %}{% if title_query %}&amp;q={{ title_query|urlencode }}{% endif %}&amp;view={{ selected_view }}&amp;preview=none">
<span class="source-name">{{ entry.source }}</span>
{% if entry.count %}<span class="source-count">{{ entry.count }}</span>{% endif %}
</a>
{% endfor %}
</nav>
</aside>
<div class="splitter" data-splitter="days" role="separator" aria-label="Resize sources sidebar" aria-orientation="vertical"></div>
<aside class="artifact-sidebar" aria-label="Artifact feed">
<div class="pane-bar pane-heading">
<h2>{% if selected_source %}{{ selected_source }}{% else %}All sources{% endif %}</h2>
<span>{{ artifacts|length }} artifact{{ artifacts|length|pluralize }}</span>
</div>
<div class="artifact-list{% if selected_source %} no-source-col{% endif %}" data-artifact-list>
{% if day_groups %}
{% include "artifacts/_day_groups.html" with has_more=older_count next_before=window_start|date:"Y-m-d" %}
{% else %}
<div class="empty compact">
<h3>No artifacts</h3>
<p>Nothing in the last {{ 7 }} days for these filters.</p>
</div>
{% endif %}
</div>
</aside>
<div class="splitter" data-splitter="artifacts" role="separator" aria-label="Resize artifacts sidebar" aria-orientation="vertical"></div>
<section class="preview-pane">
<header class="pane-bar preview-actions" data-actions>
<div class="preview-title" data-preview-title {% if selected_artifact %}title="{{ selected_artifact.title }}"{% endif %}>
{% if selected_artifact %}{{ selected_artifact.title }}{% else %}No artifact loaded{% endif %}
</div>
<nav class="view-toggle" aria-label="Preview mode">
<a class="{% if selected_view == 'rendered' %}selected{% endif %}{% if not selected_artifact %} disabled{% endif %}" href="{% if selected_artifact %}{{ rendered_url }}{% else %}#{% endif %}" data-view="rendered" {% if not selected_artifact %}aria-disabled="true"{% endif %}>Rendered</a>
<a class="{% if selected_view == 'raw' %}selected{% endif %}{% if not selected_artifact %} disabled{% endif %}" href="{% if selected_artifact %}{{ raw_url }}{% else %}#{% endif %}" data-view="raw" {% if not selected_artifact %}aria-disabled="true"{% endif %}>Raw</a>
</nav>
<div class="artifact-action-links">
<button type="button" class="keep-button {% if selected_artifact.is_kept %}kept{% endif %}" data-keep-button data-artifact-id="{% if selected_artifact %}{{ selected_artifact.id }}{% endif %}" {% if not selected_artifact %}disabled{% endif %}>{% if selected_artifact.is_kept %}✓ Kept{% else %}Keep{% endif %}</button>
<button type="button" class="share-button {% if selected_artifact.is_shared %}shared{% endif %}" data-share-button data-artifact-id="{% if selected_artifact %}{{ selected_artifact.id }}{% endif %}" {% if not selected_artifact %}disabled{% endif %}>{% if selected_artifact.is_shared %}🔗 Shared{% else %}Share{% endif %}</button>
<button type="button" data-details-toggle {% if not selected_artifact %}disabled{% endif %}>Details</button>
<a class="{% if not selected_artifact %}disabled{% endif %}" href="{% if selected_artifact %}{% url 'artifact' selected_artifact.id %}{% else %}#{% endif %}" target="_blank" rel="noopener" {% if not selected_artifact %}aria-disabled="true"{% endif %}>Open</a>
<a class="{% if not selected_artifact %}disabled{% endif %}" href="{% if selected_artifact %}{% url 'artifact' selected_artifact.id %}{% else %}#{% endif %}" download {% if not selected_artifact %}aria-disabled="true"{% endif %}>Download</a>
<button type="button" data-copy-url="{% if selected_artifact %}{% url 'artifact' selected_artifact.id %}{% endif %}" {% if not selected_artifact %}disabled{% endif %}>Copy Link</button>
</div>
<div class="share-link-box" data-share-link-box {% if not selected_artifact.is_shared %}hidden{% endif %}>
<input type="text" readonly data-share-link-input value="{% if selected_artifact.is_shared %}{{ request.scheme }}://{{ request.get_host }}{% url 'artifact-shared' selected_artifact.share_token %}{% endif %}">
<button type="button" data-copy-url="{% if selected_artifact.is_shared %}{{ request.scheme }}://{{ request.get_host }}{% url 'artifact-shared' selected_artifact.share_token %}{% endif %}">Copy</button>
</div>
</header>
<div class="details-panel" hidden data-details-panel></div>
<div class="preview-body">
{% if selected_artifact %}
<div class="preview-frame-wrap">
{% if selected_view == "raw" %}
<iframe class="preview-frame" src="{% url 'artifact' selected_artifact.id %}?raw=1" title="{{ selected_artifact.title }} raw source"></iframe>
{% elif selected_artifact.kind == "image" %}
<img class="preview-image" src="{% url 'artifact' selected_artifact.id %}" alt="{{ selected_artifact.title }}">
{% elif selected_artifact.kind == "binary" %}
<div class="empty preview-empty">
<h2>Binary Artifact</h2>
<p>This artifact type cannot be previewed inline yet.</p>
<p><a href="{% url 'artifact' selected_artifact.id %}">Open or download it</a>.</p>
</div>
{% else %}
<iframe class="preview-frame" src="{% url 'artifact' selected_artifact.id %}{% if selected_view == 'raw' %}?raw=1{% endif %}" title="{{ selected_artifact.title }}"></iframe>
{% endif %}
</div>
{% else %}
<div class="empty preview-empty">
{% if artifacts %}
<h2>No artifact loaded</h2>
<p>Press <kbd>Right Arrow</kbd> or select an artifact from the middle column to preview it.</p>
{% else %}
<h2>No artifacts for this day</h2>
<p>This day is ready for new agent output.</p>
{% endif %}
</div>
{% endif %}
</div>
</section>
</main>
<script>
(() => {
const feed = document.querySelector(".feed");
if (!feed) return;
let selectedView = "{{ selected_view }}";
// When a single source is selected, the row's source column is dropped
// (see .no-source-col), so the artifact sidebar needs ~7rem less width.
// The stored width is the "all sources" (wide) width; a selected source
// renders it minus this fixed offset. Dragging the splitter in either
// mode adjusts the stored width directly, so the width is settable in
// whichever mode you're in and the other mode tracks it by the offset.
const SOURCE_COL_WIDTH = 112; // 7rem, matches the dropped source column
const sourceSelected = Boolean(feed.dataset.selectedSource);
const sizes = {
days: Number(localStorage.getItem("agentfeed.daysWidth")) || 144,
artifacts: Number(localStorage.getItem("agentfeed.artifactsWidth")) || 280,
};
const applySizes = () => {
feed.style.setProperty("--days-width", `${sizes.days}px`);
const artifactsWidth = sourceSelected
? Math.max(150, sizes.artifacts - SOURCE_COL_WIDTH)
: sizes.artifacts;
feed.style.setProperty("--artifacts-width", `${artifactsWidth}px`);
};
applySizes();
// Per-pane resize bounds. The artifact list may grow much wider than the
// sources sidebar, but is still capped so it can't swallow the preview.
const sizeBounds = {
days: {min: 96, max: 320},
artifacts: {min: 200, max: 720},
};
document.querySelectorAll(".splitter").forEach((splitter) => {
splitter.addEventListener("pointerdown", (event) => {
const target = splitter.dataset.splitter;
const startX = event.clientX;
const startWidth = sizes[target];
const bounds = sizeBounds[target];
splitter.setPointerCapture(event.pointerId);
document.body.classList.add("is-resizing");
const move = (moveEvent) => {
sizes[target] = Math.max(bounds.min, Math.min(bounds.max, startWidth + moveEvent.clientX - startX));
applySizes();
};
const up = () => {
localStorage.setItem(`agentfeed.${target}Width`, String(sizes[target]));
document.body.classList.remove("is-resizing");
splitter.removeEventListener("pointermove", move);
splitter.removeEventListener("pointerup", up);
splitter.removeEventListener("pointercancel", up);
};
splitter.addEventListener("pointermove", move);
splitter.addEventListener("pointerup", up);
splitter.addEventListener("pointercancel", up);
});
});
const previewPane = document.querySelector(".preview-pane");
const previewBody = document.querySelector(".preview-body");
const artifactList = document.querySelector("[data-artifact-list]");
// Rows grow as older pages load, so always read them live rather than caching an array.
const allRows = () => Array.from(document.querySelectorAll(".artifact-row"));
const sourceLinks = Array.from(document.querySelectorAll(".source-link"));
const csrfToken = document.querySelector("[name=csrfmiddlewaretoken]")?.value || "";
const escapeHtml = (value) => value.replace(/[&<>"]/g, (character) => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
"\"": "&quot;",
}[character]));
const copyLink = async (button) => {
const absoluteUrl = new URL(button.dataset.copyUrl, window.location.origin).toString();
try {
await navigator.clipboard.writeText(absoluteUrl);
} catch {
window.prompt("Copy artifact link", absoluteUrl);
}
const original = button.textContent;
button.textContent = "Copied";
setTimeout(() => {
button.textContent = original;
}, 1200);
};
const selectedRow = () => document.querySelector(".artifact-row.selected");
let navigationMode = selectedRow() ? "artifacts" : "sources";
const urlFor = (row, view) => view === "raw" ? row.dataset.rawUrl : row.dataset.renderedUrl;
const pageUrlFor = (row, view) => {
const url = new URL(row.href, window.location.origin);
url.searchParams.set("artifact", row.dataset.artifactId);
url.searchParams.set("view", view);
url.searchParams.delete("preview");
return url;
};
const noPreviewUrl = () => {
const url = new URL(window.location.href);
url.searchParams.delete("artifact");
url.searchParams.set("preview", "none");
url.searchParams.set("view", selectedView);
return url;
};
const updateActions = (row) => {
const actions = document.querySelector("[data-actions]");
if (!actions) return;
const previewTitle = actions.querySelector("[data-preview-title]");
if (previewTitle) {
previewTitle.textContent = row.dataset.title;
previewTitle.title = row.dataset.title;
}
actions.querySelectorAll("[data-view]").forEach((link) => {
const view = link.dataset.view;
link.classList.toggle("selected", view === selectedView);
link.classList.remove("disabled");
link.removeAttribute("aria-disabled");
link.href = pageUrlFor(row, view).toString();
});
actions.querySelectorAll(".artifact-action-links a").forEach((link) => {
link.classList.remove("disabled");
link.removeAttribute("aria-disabled");
link.href = row.dataset.renderedUrl;
});
const copyButton = actions.querySelector("[data-copy-url]");
if (copyButton) {
copyButton.disabled = false;
copyButton.dataset.copyUrl = row.dataset.renderedUrl;
}
syncKeepButton(row.dataset.kept === "1");
const keepButton = actions.querySelector("[data-keep-button]");
if (keepButton) keepButton.dataset.artifactId = row.dataset.artifactId;
syncShareButton(row.dataset.shared === "1", row.dataset.shareUrl || "");
const shareButton = actions.querySelector("[data-share-button]");
if (shareButton) {
shareButton.disabled = false;
shareButton.dataset.artifactId = row.dataset.artifactId;
}
actions.querySelector("[data-details-toggle]").disabled = false;
renderDetails(row);
};
const syncKeepButton = (isKept) => {
const keepButton = document.querySelector("[data-keep-button]");
if (!keepButton) return;
keepButton.disabled = false;
keepButton.classList.toggle("kept", isKept);
keepButton.textContent = isKept ? "✓ Kept" : "Keep";
};
const syncShareButton = (isShared, shareUrl) => {
const shareButton = document.querySelector("[data-share-button]");
if (shareButton) {
shareButton.classList.toggle("shared", isShared);
shareButton.textContent = isShared ? "🔗 Shared" : "Share";
}
const box = document.querySelector("[data-share-link-box]");
if (!box) return;
box.hidden = !isShared;
const input = box.querySelector("[data-share-link-input]");
if (input) input.value = isShared ? shareUrl : "";
const copyButton = box.querySelector("[data-copy-url]");
if (copyButton) copyButton.dataset.copyUrl = isShared ? shareUrl : "";
};
const renderDetails = (row) => {
const panel = document.querySelector("[data-details-panel]");
if (!panel) return;
const d = row.dataset;
const tags = d.tags ? d.tags.split(",").map((t) => t.trim()).filter(Boolean) : [];
const rows = [
["Source", `${escapeHtml(d.source)} (${escapeHtml(d.sourceType)})`],
["Source name", escapeHtml(d.sourceName || "—")],
["Type", escapeHtml(d.kind)],
["Created", escapeHtml(d.created)],
["Size", escapeHtml(d.size)],
["Tags", tags.length ? tags.map(escapeHtml).join(", ") : "—"],
];
panel.innerHTML = `<dl class="details-grid">${rows.map(([k, v]) => `<dt>${k}</dt><dd>${v}</dd>`).join("")}</dl>`;
};
const disableActions = () => {
const actions = document.querySelector("[data-actions]");
if (!actions) return;
const previewTitle = actions.querySelector("[data-preview-title]");
if (previewTitle) {
previewTitle.textContent = "No artifact loaded";
previewTitle.removeAttribute("title");
}
actions.querySelectorAll("[data-view]").forEach((link) => {
link.classList.add("disabled");
link.setAttribute("aria-disabled", "true");
link.href = "#";
});
actions.querySelectorAll(".artifact-action-links a").forEach((link) => {
link.classList.add("disabled");
link.setAttribute("aria-disabled", "true");
link.href = "#";
});
const copyButton = actions.querySelector("[data-copy-url]");
if (copyButton) {
copyButton.disabled = true;
copyButton.dataset.copyUrl = "";
}
const keepButton = actions.querySelector("[data-keep-button]");
if (keepButton) {
keepButton.disabled = true;
keepButton.classList.remove("kept");
keepButton.textContent = "Keep";
}
const shareButton = actions.querySelector("[data-share-button]");
if (shareButton) {
shareButton.disabled = true;
shareButton.classList.remove("shared");
shareButton.textContent = "Share";
}
syncShareButton(false, "");
actions.querySelector("[data-details-toggle]").disabled = true;
const panel = document.querySelector("[data-details-panel]");
if (panel) panel.hidden = true;
};
const markRead = async (row) => {
if (!row.classList.contains("unread")) return;
row.classList.remove("unread");
decrementSourceCount(row.querySelector(".artifact-source")?.textContent.trim());
try {
await fetch(`/a/${row.dataset.artifactId}/read`, {
method: "POST",
headers: {"X-CSRFToken": csrfToken},
});
} catch {
// Non-critical: the row stays visually read until the next page load.
}
};
const decrementSourceCount = (sourceName) => {
document.querySelectorAll(".source-link").forEach((link) => {
const name = link.querySelector(".source-name")?.textContent.trim();
if (name !== sourceName && name !== "All sources") return;
const badge = link.querySelector(".source-count");
if (!badge) return;
const next = Number(badge.textContent) - 1;
if (next > 0) {
badge.textContent = String(next);
} else {
badge.remove();
}
});
};
const renderPreview = (row, pushState = true) => {
navigationMode = "artifacts";
markRead(row);
allRows().forEach((candidate) => candidate.classList.toggle("selected", candidate === row));
const source = urlFor(row, selectedView);
const title = escapeHtml(row.dataset.title);
const kind = row.dataset.kind;
const content = selectedView === "raw"
? `<iframe class="preview-frame" src="${source}" title="${title} raw source"></iframe>`
: kind === "image"
? `<img class="preview-image" src="${source}" alt="${title}">`
: kind === "binary"
? `<div class="empty preview-empty"><h2>Binary Artifact</h2><p>This artifact type cannot be previewed inline yet.</p><p><a href="${source}">Open or download it</a>.</p></div>`
: `<iframe class="preview-frame" src="${source}" title="${title}"></iframe>`;
previewBody.innerHTML = `<div class="preview-frame-wrap">${content}</div>`;
updateActions(row);
if (pushState) window.history.pushState({}, "", pageUrlFor(row, selectedView));
};
const clearPreview = () => {
navigationMode = "sources";
if (document.activeElement && document.activeElement.classList.contains("artifact-row")) {
document.activeElement.blur();
}
allRows().forEach((candidate) => candidate.classList.remove("selected"));
disableActions();
previewBody.innerHTML = `<div class="empty preview-empty"><h2>No artifact loaded</h2><p>Press <kbd>Right Arrow</kbd> or select an artifact from the middle column to preview it.</p></div>`;
window.history.pushState({}, "", noPreviewUrl());
const currentSource = document.querySelector(".source-link.selected");
if (currentSource) currentSource.focus({preventScroll: true});
};
// Delegated so rows appended by infinite scroll work without rebinding.
artifactList.addEventListener("click", (event) => {
const row = event.target.closest(".artifact-row");
if (!row) return;
event.preventDefault();
renderPreview(row);
});
const moveSelection = (direction) => {
const rows = allRows();
if (!rows.length) return;
navigationMode = "artifacts";
const current = selectedRow();
const currentIndex = current ? rows.indexOf(current) : -1;
const nextIndex = Math.max(0, Math.min(rows.length - 1, currentIndex + direction));
const nextRow = rows[nextIndex === -1 ? 0 : nextIndex];
renderPreview(nextRow);
nextRow.focus({preventScroll: true});
nextRow.scrollIntoView({block: "nearest"});
};
const moveSource = (direction) => {
if (!sourceLinks.length) return;
navigationMode = "sources";
const current = document.querySelector(".source-link.selected");
const currentIndex = current ? sourceLinks.indexOf(current) : 0;
const nextIndex = Math.max(0, Math.min(sourceLinks.length - 1, currentIndex + direction));
const nextSource = sourceLinks[nextIndex];
if (nextSource && nextSource !== current) window.location.href = nextSource.href;
};
document.addEventListener("keydown", (event) => {
if (event.ctrlKey || event.metaKey || event.shiftKey) return;
if (["INPUT", "SELECT", "TEXTAREA", "BUTTON"].includes(event.target.tagName)) return;
if (event.altKey && event.key === "ArrowDown") {
event.preventDefault();
moveSource(1);
} else if (event.altKey && event.key === "ArrowUp") {
event.preventDefault();
moveSource(-1);
} else if (event.altKey) {
return;
} else if (event.key === "ArrowDown") {
event.preventDefault();
if (navigationMode === "sources") {
moveSource(1);
} else {
moveSelection(1);
}
} else if (event.key === "ArrowUp") {
event.preventDefault();
if (navigationMode === "sources") {
moveSource(-1);
} else {
moveSelection(-1);
}
} else if (event.key === "PageDown") {
if (navigationMode === "artifacts") {
event.preventDefault();
moveSelection(5);
}
} else if (event.key === "PageUp") {
if (navigationMode === "artifacts") {
event.preventDefault();
moveSelection(-5);
}
} else if (event.key === "ArrowRight") {
const rows = allRows();
if (!selectedRow() && rows.length) {
event.preventDefault();
renderPreview(rows[0]);
rows[0].focus({preventScroll: true});
rows[0].scrollIntoView({block: "nearest"});
} else if (selectedRow()) {
navigationMode = "artifacts";
}
} else if (event.key === "ArrowLeft") {
if (selectedRow()) {
event.preventDefault();
clearPreview();
} else {
navigationMode = "sources";
const currentSource = document.querySelector(".source-link.selected");
if (currentSource) currentSource.focus({preventScroll: true});
}
}
});
// Infinite scroll: when the sentinel at the end of the list scrolls into
// view, fetch the next page of older day-groups and append them.
let loadingOlder = false;
const loadOlder = async (sentinel) => {
if (loadingOlder) return;
loadingOlder = true;
sentinel.removeAttribute("data-feed-more");
try {
const url = new URL("{% url 'feed-older' %}", window.location.origin);
const params = url.searchParams;
params.set("before", sentinel.dataset.nextBefore);
params.set("view", feed.dataset.selectedView || "rendered");
if (feed.dataset.selectedSource) params.set("source", feed.dataset.selectedSource);
if (feed.dataset.selectedLabel) params.set("label", feed.dataset.selectedLabel);
if (feed.dataset.selectedKind) params.set("kind", feed.dataset.selectedKind);
if (feed.dataset.titleQuery) params.set("q", feed.dataset.titleQuery);
const response = await fetch(url, {headers: {"X-Requested-With": "fetch"}});
if (!response.ok) return;
const markup = (await response.text()).trim();
sentinel.remove();
if (markup) {
artifactList.insertAdjacentHTML("beforeend", markup);
observeSentinel();
}
} catch {
// Leave the list as-is; the user can scroll again to retry.
} finally {
loadingOlder = false;
}
};
const olderObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
olderObserver.unobserve(entry.target);
loadOlder(entry.target);
}
});
}, {root: artifactList, rootMargin: "200px"});
const observeSentinel = () => {
const sentinel = artifactList.querySelector("[data-feed-more]");
if (sentinel) olderObserver.observe(sentinel);
};
observeSentinel();
// Populate the details panel for a server-preselected artifact so the first
// "Details" click shows content immediately.
const preselected = selectedRow();
if (preselected) renderDetails(preselected);
const toggleKeep = async (button) => {
const artifactId = button.dataset.artifactId;
if (!artifactId) return;
button.disabled = true;
try {
const response = await fetch(`/a/${artifactId}/keep`, {
method: "POST",
headers: {"X-CSRFToken": csrfToken},
});
const data = await response.json();
syncKeepButton(data.is_kept);
const row = document.querySelector(`.artifact-row[data-artifact-id="${artifactId}"]`);
if (row) {
row.dataset.kept = data.is_kept ? "1" : "0";
row.classList.toggle("kept", data.is_kept);
const badge = row.querySelector("[data-kept-badge]");
if (badge) badge.hidden = !data.is_kept;
}
} catch {
button.disabled = false;
}
};
const toggleShare = async (button) => {
const artifactId = button.dataset.artifactId;
if (!artifactId) return;
button.disabled = true;
try {
const response = await fetch(`/a/${artifactId}/share`, {
method: "POST",
headers: {"X-CSRFToken": csrfToken},
});
const data = await response.json();
syncShareButton(data.is_shared, data.share_url || "");
const row = document.querySelector(`.artifact-row[data-artifact-id="${artifactId}"]`);
if (row) {
row.dataset.shared = data.is_shared ? "1" : "0";
row.dataset.shareUrl = data.share_url || "";
row.classList.toggle("shared", data.is_shared);
const badge = row.querySelector("[data-shared-badge]");
if (badge) badge.hidden = !data.is_shared;
}
} finally {
button.disabled = false;
}
};
previewPane.addEventListener("click", (event) => {
const copyButton = event.target.closest("[data-copy-url]");
if (copyButton) {
event.preventDefault();
copyLink(copyButton);
return;
}
const keepButton = event.target.closest("[data-keep-button]");
if (keepButton && !keepButton.disabled) {
event.preventDefault();
toggleKeep(keepButton);
return;
}
const shareButton = event.target.closest("[data-share-button]");
if (shareButton && !shareButton.disabled) {
event.preventDefault();
toggleShare(shareButton);
return;
}
const detailsToggle = event.target.closest("[data-details-toggle]");
if (detailsToggle && !detailsToggle.disabled) {
event.preventDefault();
const panel = document.querySelector("[data-details-panel]");
if (panel) {
panel.hidden = !panel.hidden;
detailsToggle.classList.toggle("selected", !panel.hidden);
}
return;
}
const toggle = event.target.closest("[data-view]");
if (!toggle) return;
if (toggle.classList.contains("disabled")) {
event.preventDefault();
return;
}
const row = selectedRow();
if (!row) return;
event.preventDefault();
selectedView = toggle.dataset.view;
renderPreview(row);
});
})();
</script>
</body>
</html>