// ClawdDotNet — Analysis › Prompt History (#660) // // Recorded prompts and responses of every run, searchable over the full transcript, plus // how long they are kept: an instance default (14 days unless changed) and an override per // agent - "never" for an agent that handles secrets, "forever" or many days to debug one. // // Transcripts are foreign text and only ever inserted via textContent. import { api } from "../modules/api.js"; import { erstelleElement, erstelleKarte, formatiereDatumZeit, formatiereTokens, zeigeMeldung } from "../modules/dom.js"; import { navigiere } from "../modules/router.js"; import { t } from "../modules/i18n.js"; import { readFilters, applyFilters, rangeOf, loadFacets, buildFilterBar } from "../modules/usage-filters.js"; import { statusBadge, sourceLabel } from "./usage-runs.js"; const PATH = "/prompt-history"; const PAGE_SIZE = 25; const NEVER = 0; const FOREVER = -1; export async function renderPromptHistory() { const container = document.getElementById("ansicht-inhalt"); if (!container) return; const filters = readFilters(); const layout = erstelleElement("div", { klasse: "verbrauch-ansicht" }); layout.append( erstelleElement("h1", { text: t("history.title") }), erstelleElement("p", { klasse: "hinweis", text: t("history.intro") })); const facets = await loadFacets(filters); layout.append(buildFilterBar(filters, facets, (next) => applyFilters(PATH, next), ["range", "agent", "q"])); const list = erstelleElement("div", {}, erstelleElement("div", { klasse: "hinweis-zentriert", text: t("common.loading") })); const settings = erstelleElement("div", {}); layout.append(list, settings); container.replaceChildren(layout); await Promise.all([loadList(list, filters), loadSettings(settings)]); } async function loadList(target, filters) { const { from, to } = rangeOf(filters); const query = new URLSearchParams({ from, to, offset: String(filters.offset || 0), limit: String(PAGE_SIZE) }); if (filters.agent) query.set("agent", filters.agent); if (filters.q) query.set("q", filters.q); let page; try { page = await api(`/prompt-history?${query}`); } catch (err) { target.replaceChildren(erstelleKarte(t("history.unavailable"), [erstelleElement("p", { klasse: "gedaempft", text: err.message })])); return; } if (page.items.length === 0) { target.replaceChildren(erstelleKarte(t("history.empty.title"), [ erstelleElement("p", { klasse: "gedaempft", text: filters.q ? t("history.empty.search") : t("history.empty.text") }) ])); return; } const items = page.items.map((item) => { const open = () => navigiere(`#${PATH}/${encodeURIComponent(item.runId)}`); const card = erstelleElement("div", { klasse: "verlauf-eintrag klickbar", tabindex: "0", onclick: open }); card.addEventListener("keydown", (event) => { if (event.key === "Enter") open(); }); card.append( erstelleElement("div", { klasse: "verlauf-kopf" }, erstelleElement("strong", { text: item.agentId }), erstelleElement("span", { klasse: "gedaempft klein", text: `${formatiereDatumZeit(item.startedAt)} · ${sourceLabel(item.source)} · ${item.model}` }), statusBadge(item.status)), erstelleElement("div", { klasse: "verlauf-zeile" }, erstelleElement("span", { klasse: "verlauf-marke", text: t("history.prompt") }), erstelleElement("span", { klasse: "verlauf-text", text: item.prompt || "—" })), erstelleElement("div", { klasse: "verlauf-zeile" }, erstelleElement("span", { klasse: "verlauf-marke", text: t("history.response") }), erstelleElement("span", { klasse: "verlauf-text", text: item.response || "—" }))); if (item.truncated) { card.append(erstelleElement("div", { klasse: "gedaempft klein", text: t("history.truncated", { chars: formatiereTokens(item.sizeChars) }) })); } return card; }); const offset = filters.offset || 0; const pager = erstelleElement("div", { klasse: "blaettern" }, erstelleElement("button", { type: "button", klasse: "schaltflaeche flach klein", text: t("common.previous"), disabled: offset <= 0, onclick: () => applyFilters(PATH, { ...filters, offset: Math.max(0, offset - PAGE_SIZE) }) }), erstelleElement("span", { klasse: "gedaempft", text: t("runs.page", { from: offset + 1, to: offset + page.items.length, total: page.total }) }), erstelleElement("button", { type: "button", klasse: "schaltflaeche flach klein", text: t("common.next"), disabled: offset + PAGE_SIZE >= page.total, onclick: () => applyFilters(PATH, { ...filters, offset: offset + PAGE_SIZE }) })); target.replaceChildren(erstelleKarte(t("history.list", { n: page.total }), [...items, pager])); } // ─── Retention settings ─── function modeOf(days) { if (days === null || days === undefined) return "inherit"; if (days === NEVER) return "never"; if (days === FOREVER) return "forever"; return "days"; } /** A mode select plus a day count that is only shown for "days". */ function retentionInput(days, allowInherit) { const modes = allowInherit ? ["inherit", "never", "days", "forever"] : ["never", "days", "forever"]; const mode = erstelleElement("select", { "aria-label": t("history.settings.mode") }); for (const m of modes) mode.append(erstelleElement("option", { value: m, text: t(`history.settings.mode.${m}`) })); mode.value = modeOf(days); const count = erstelleElement("input", { type: "number", min: "1", step: "1", value: days > 0 ? String(days) : "14", "aria-label": t("history.settings.days") }); const unit = erstelleElement("span", { klasse: "gedaempft klein", text: t("history.settings.daysUnit") }); const dayBox = erstelleElement("span", { klasse: "gruppe-horizontal" }, count, unit); const sync = () => { dayBox.hidden = mode.value !== "days"; }; mode.addEventListener("change", sync); sync(); return { element: erstelleElement("span", { klasse: "gruppe-horizontal aufbewahrung" }, mode, dayBox), value() { switch (mode.value) { case "inherit": return null; case "never": return NEVER; case "forever": return FOREVER; default: return Math.max(1, Math.round(Number(count.value) || 14)); } } }; } function describe(days) { if (days === NEVER) return t("history.settings.effective.never"); if (days === FOREVER) return t("history.settings.effective.forever"); return t("history.settings.effective.days", { n: days }); } async function loadSettings(target) { let settings; try { settings = await api("/prompt-history/settings"); } catch (err) { target.replaceChildren(); return; } const instance = retentionInput(settings.defaultRetentionDays, false); const rows = []; const table = erstelleElement("table", { klasse: "daten-tabelle" }); const head = erstelleElement("tr", {}); for (const label of ["history.settings.agent", "history.settings.retention", "history.settings.effective"]) { head.append(erstelleElement("th", { text: t(label) })); } table.append(head); for (const agent of settings.agents) { const input = retentionInput(agent.retentionDays, true); rows.push({ agentId: agent.agentId, input, before: agent.retentionDays ?? null }); table.append(erstelleElement("tr", {}, erstelleElement("td", { text: agent.displayName }), erstelleElement("td", {}, input.element), erstelleElement("td", { klasse: "gedaempft", text: describe(agent.effectiveRetentionDays) }))); } const save = erstelleElement("button", { type: "button", klasse: "schaltflaeche primaer klein", text: t("history.settings.save"), onclick: async () => { const body = { defaultRetentionDays: instance.value(), agents: [] }; for (const row of rows) { const value = row.input.value(); if (value !== row.before) body.agents.push({ agentId: row.agentId, retentionDays: value }); } save.disabled = true; try { const result = await api("/prompt-history/settings", { method: "PUT", body }); zeigeMeldung(result?.message ?? t("history.settings.saved"), "erfolg"); await loadSettings(target); } catch (err) { zeigeMeldung(t("history.settings.failed", { error: err.message }), "gefahr"); save.disabled = false; } } }); target.replaceChildren(erstelleKarte(t("history.settings.title"), [ erstelleElement("p", { klasse: "hinweis", text: t("history.settings.intro") }), erstelleElement("div", { klasse: "gruppe-horizontal" }, erstelleElement("strong", { text: t("history.settings.default") }), instance.element), erstelleElement("div", { klasse: "tabelle-scroll" }, table), save ])); }