Refine AgentFeed UI and local workflow

This commit is contained in:
peter 2026-06-11 14:01:16 +01:00
parent 3179f832fc
commit ba9f5eb168
29 changed files with 2986 additions and 69 deletions

View file

@ -3,14 +3,13 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Daybook</title>
<title>AgentFeed</title>
<link rel="stylesheet" href="/static/daybook.css">
</head>
<body>
<header class="topbar">
<div class="brandline">
<h1>Daybook</h1>
<p>Agent Artifacts</p>
<h1>AgentFeed</h1>
</div>
<div class="toolbar">
<form method="get" class="filter">
@ -44,12 +43,16 @@
</div>
</header>
<main class="feed">
<main class="feed"
data-selected-day="{{ selected_day|date:'Y-m-d' }}"
data-selected-persona="{{ selected_persona }}"
data-selected-kind="{{ selected_kind }}"
data-title-query="{{ title_query }}">
<aside class="day-sidebar" aria-label="Days">
<h2>Days</h2>
<nav class="day-list">
{% for day in days %}
<a class="day-link {% if day == selected_day %}selected{% endif %}" href="?day={{ day|date:'Y-m-d' }}{% if selected_persona %}&amp;persona={{ selected_persona|urlencode }}{% endif %}{% if selected_kind %}&amp;kind={{ selected_kind|urlencode }}{% endif %}{% if title_query %}&amp;q={{ title_query|urlencode }}{% endif %}&amp;view={{ selected_view }}">
<a class="day-link {% if day == selected_day %}selected{% endif %}" href="?day={{ day|date:'Y-m-d' }}{% if selected_persona %}&amp;persona={{ selected_persona|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>{{ day|date:"D" }}{% if day == today %}<em class="today-badge">Today</em>{% endif %}</span>
<strong>{{ day|date:"Y-m-d" }}</strong>
</a>
@ -65,6 +68,10 @@
<span>{{ artifacts|length }} artifact{{ artifacts|length|pluralize }}</span>
</div>
<button class="new-artifacts-banner" type="button" hidden data-new-artifacts-banner>
<strong data-new-artifacts-count>0</strong> new artifact<span data-new-artifacts-plural>s</span> available
</button>
<div class="artifact-list">
{% for artifact in artifacts %}
<a class="artifact-row {% if selected_artifact.id == artifact.id %}selected{% endif %}"
@ -74,18 +81,16 @@
data-kind="{{ artifact.kind }}"
data-rendered-url="{% url 'artifact' artifact.id %}"
data-raw-url="{% url 'artifact' artifact.id %}?raw=1">
<span class="kind-icon kind-{{ artifact.kind }}" aria-hidden="true">
{% if artifact.kind == "image" %}IMG{% elif artifact.kind == "pdf" %}PDF{% elif artifact.kind == "markdown" %}MD{% elif artifact.kind == "deck" %}DECK{% elif artifact.kind == "binary" %}BIN{% else %}HTML{% endif %}
</span>
<time class="artifact-time" datetime="{{ artifact.created_at|date:'c' }}">{{ artifact.created_at|date:"H:i" }}</time>
<span class="artifact-row-main">
<strong>{{ artifact.created_at|date:"H:i" }}</strong>
<span>{{ artifact.title }}</span>
</span>
<span class="kind-icon kind-{{ artifact.kind }}" aria-hidden="true">{% if artifact.kind == "image" %}IMG{% elif artifact.kind == "pdf" %}PDF{% elif artifact.kind == "markdown" %}MD{% elif artifact.kind == "deck" %}DECK{% elif artifact.kind == "binary" %}BIN{% else %}HTML{% endif %}</span>
</a>
{% empty %}
<div class="empty compact">
<h3>No artifacts</h3>
<p>This day is ready for new agent output.</p>
<p>No artifacts exist for this day yet.</p>
</div>
{% endfor %}
</div>
@ -124,9 +129,13 @@
</footer>
{% else %}
<div class="empty preview-empty">
<h2>Select an artifact</h2>
<p>Choose an item from the middle column to preview it here.</p>
<p>POST to <code>/api/artifacts</code> with <code>Authorization: Bearer dev-token</code>.</p>
{% 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 %}
</section>
@ -179,6 +188,11 @@
const previewPane = document.querySelector(".preview-pane");
const rows = Array.from(document.querySelectorAll(".artifact-row"));
const dayLinks = Array.from(document.querySelectorAll(".day-link"));
const initialArtifactIds = new Set(rows.map((row) => row.dataset.artifactId));
const newArtifactsBanner = document.querySelector("[data-new-artifacts-banner]");
const newArtifactsCount = document.querySelector("[data-new-artifacts-count]");
const newArtifactsPlural = document.querySelector("[data-new-artifacts-plural]");
const escapeHtml = (value) => value.replace(/[&<>"]/g, (character) => ({
"&": "&amp;",
@ -200,11 +214,21 @@
}, 1200);
};
const selectedRow = () => document.querySelector(".artifact-row.selected");
let navigationMode = selectedRow() ? "artifacts" : "days";
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;
};
@ -224,6 +248,7 @@
};
const renderPreview = (row, pushState = true) => {
navigationMode = "artifacts";
rows.forEach((candidate) => candidate.classList.toggle("selected", candidate === row));
const source = urlFor(row, selectedView);
const title = escapeHtml(row.dataset.title);
@ -246,6 +271,18 @@
if (pushState) window.history.pushState({}, "", pageUrlFor(row, selectedView));
};
const clearPreview = () => {
navigationMode = "days";
if (document.activeElement && document.activeElement.classList.contains("artifact-row")) {
document.activeElement.blur();
}
rows.forEach((candidate) => candidate.classList.remove("selected"));
previewPane.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 currentDay = document.querySelector(".day-link.selected");
if (currentDay) currentDay.focus({preventScroll: true});
};
rows.forEach((row) => {
row.addEventListener("click", (event) => {
event.preventDefault();
@ -253,6 +290,119 @@
});
});
const moveSelection = (direction) => {
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 moveDay = (direction) => {
if (!dayLinks.length) return;
navigationMode = "days";
const current = document.querySelector(".day-link.selected");
const currentIndex = current ? dayLinks.indexOf(current) : 0;
const nextIndex = Math.max(0, Math.min(dayLinks.length - 1, currentIndex + direction));
const nextDay = dayLinks[nextIndex];
if (nextDay && nextDay !== current) window.location.href = nextDay.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();
moveDay(1);
} else if (event.altKey && event.key === "ArrowUp") {
event.preventDefault();
moveDay(-1);
} else if (event.altKey) {
return;
} else if (event.key === "ArrowDown") {
event.preventDefault();
if (navigationMode === "days") {
moveDay(1);
} else {
moveSelection(1);
}
} else if (event.key === "ArrowUp") {
event.preventDefault();
if (navigationMode === "days") {
moveDay(-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") {
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 = "days";
const currentDay = document.querySelector(".day-link.selected");
if (currentDay) currentDay.focus({preventScroll: true});
}
}
});
const artifactListUrl = () => {
const url = new URL("/api/artifacts", window.location.origin);
const params = url.searchParams;
if (feed.dataset.selectedDay) params.set("day", feed.dataset.selectedDay);
if (feed.dataset.selectedPersona) params.set("persona", feed.dataset.selectedPersona);
if (feed.dataset.selectedKind) params.set("kind", feed.dataset.selectedKind);
if (feed.dataset.titleQuery) params.set("q", feed.dataset.titleQuery);
return url;
};
const showNewArtifactsBanner = (count) => {
if (!newArtifactsBanner || count < 1) return;
newArtifactsCount.textContent = String(count);
newArtifactsPlural.hidden = count === 1;
newArtifactsBanner.hidden = false;
};
const checkForNewArtifacts = async () => {
try {
const response = await fetch(artifactListUrl(), {headers: {Accept: "application/json"}});
if (!response.ok) return;
const artifacts = await response.json();
const unseenCount = artifacts.filter((artifact) => !initialArtifactIds.has(artifact.id)).length;
showNewArtifactsBanner(unseenCount);
} catch {
// Polling is opportunistic; leave the current feed untouched on errors.
}
};
if (newArtifactsBanner) {
newArtifactsBanner.addEventListener("click", () => window.location.reload());
window.setInterval(checkForNewArtifacts, 5000);
}
previewPane.addEventListener("click", (event) => {
const copyButton = event.target.closest("[data-copy-url]");
if (copyButton) {