Refine AgentFeed UI and local workflow
This commit is contained in:
parent
3179f832fc
commit
ba9f5eb168
29 changed files with 2986 additions and 69 deletions
|
|
@ -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 %}&persona={{ selected_persona|urlencode }}{% endif %}{% if selected_kind %}&kind={{ selected_kind|urlencode }}{% endif %}{% if title_query %}&q={{ title_query|urlencode }}{% endif %}&view={{ selected_view }}">
|
||||
<a class="day-link {% if day == selected_day %}selected{% endif %}" href="?day={{ day|date:'Y-m-d' }}{% if selected_persona %}&persona={{ selected_persona|urlencode }}{% endif %}{% if selected_kind %}&kind={{ selected_kind|urlencode }}{% endif %}{% if title_query %}&q={{ title_query|urlencode }}{% endif %}&view={{ selected_view }}&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) => ({
|
||||
"&": "&",
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue