Files
ai-image/web/records.html
T

260 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>生成记录 · ai-image</title>
<link rel="stylesheet" href="styles.css" />
<script src="api.js"></script>
<script src="components.js"></script>
</head>
<body data-page="records">
<aside class="app-nav">
<div class="app-brand">
<span class="brand-mark" aria-hidden="true"></span>
<span class="brand-text"><b>ai-image</b><small>绘图聚合控制台</small></span>
</div>
<nav class="nav-list">
<p class="nav-group">工作台</p>
<a class="nav-item" data-nav="create" href="create.html"><span class="nav-ico"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 3-1.9 5.8L4 10.7l6.1 1.9L12 18.4l1.9-5.8 6.1-1.9-6.1-1.9Z"/><path d="M5 3v4"/><path d="M19 17v4"/><path d="M3 5h4"/><path d="M17 19h4"/></svg></span>出图</a>
<a class="nav-item" data-nav="gallery" href="index.html"><span class="nav-ico"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg></span>画廊</a>
<a class="nav-item" data-nav="records" href="records.html"><span class="nav-ico"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/></svg></span>生成记录</a>
<p class="nav-group">配置</p>
<a class="nav-item" data-nav="providers" href="providers.html"><span class="nav-ico"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 22v-5"/><path d="M9 8V2"/><path d="M15 8V2"/><path d="M18 8v3a6 6 0 0 1-12 0V8Z"/></svg></span>供应商</a>
<a class="nav-item" data-nav="models" href="models.html"><span class="nav-ico"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z"/><path d="m7 16.5-4.74-2.85"/><path d="m7 16.5 5-3"/><path d="M7 16.5v5.17"/><path d="M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z"/><path d="m17 16.5-5-3"/><path d="m17 16.5 4.74-2.85"/><path d="M17 16.5v5.17"/><path d="M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z"/><path d="M12 8 7.26 5.15"/><path d="m12 8 4.74-2.85"/><path d="M12 13.5V8"/></svg></span>模型</a>
<p class="nav-group">Agent</p>
<a class="nav-item" data-nav="skills" href="skills.html"><span class="nav-ico"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg></span>SKILL.md</a>
</nav>
<div class="nav-foot">
<span class="dot dot-ok"></span>服务在线 <span class="mono">:8765</span>
</div>
</aside>
<main class="app-content">
<header class="top-bar">
<div class="crumb">
<span class="crumb-sec">工作台</span>
<span class="crumb-sep">/</span>
<h1>生成记录</h1>
</div>
<div class="top-actions"><span class="tag">共 <span id="topRecTotal">—</span> 条</span><a class="btn btn-secondary btn-sm" href="index.html">看画廊</a></div>
</header>
<section class="page-body">
<div class="toolbar">
<div class="chip-group" id="statusChips">
<button class="chip active" data-status="all">全部</button>
<button class="chip" data-status="success">成功</button>
<button class="chip" data-status="failed">失败</button>
</div>
<select id="provFilter" style="width:200px">
<option value="all">全部供应商</option>
</select>
<div class="input-group" style="max-width:280px;flex:1">
<input type="search" id="searchInput" placeholder="搜索 prompt / 模型 / 调用方" />
</div>
<span class="spacer"></span>
<span class="dim mono" id="recCount">—</span>
</div>
<div class="grid grid-4" style="margin-bottom:16px">
<div class="card stat">
<div class="k">总调用</div>
<div class="v" id="statTotal">—</div>
<div class="sub">全部记录</div>
</div>
<div class="card stat">
<div class="k">成功率</div>
<div class="v" id="statOk">—</div>
<div class="sub">success / total</div>
</div>
<div class="card stat">
<div class="k">平均耗时</div>
<div class="v" id="statDur">—</div>
<div class="sub">端到端</div>
</div>
<div class="card stat">
<div class="k">产出图片</div>
<div class="v" id="statFiles">—</div>
<div class="sub">落盘文件数</div>
</div>
</div>
<div class="card">
<div class="card-head">
<div>
<h3>生成记录</h3>
<div class="sub">点击任意行查看完整参数与错误信息</div>
</div>
</div>
<div class="card-body tight">
<div class="table-wrap">
<table class="tbl">
<thead>
<tr>
<th>任务 ID</th>
<th>供应商 / 模型</th>
<th>Prompt</th>
<th>尺寸</th>
<th>调用方</th>
<th>耗时</th>
<th>状态</th>
<th>时间</th>
</tr>
</thead>
<tbody id="recRows"></tbody>
</table>
</div>
<div class="empty hidden" id="recEmpty">
<span class="ico" data-icon="history"></span>
<h4>没有匹配的记录</h4>
<p>调整筛选条件,或调用 /api/generate 产生一条新记录。</p>
</div>
<div class="pager">
<span id="pagerInfo">—</span>
<div class="pager-btns">
<button class="btn btn-secondary btn-sm" id="prevBtn"><span class="ico" data-icon="chevronLeft"></span></button>
<button class="btn btn-secondary btn-sm" id="nextBtn"><span class="ico" data-icon="chevronRight"></span></button>
</div>
</div>
</div>
</div>
<div class="modal-mask" id="detailModal">
<div class="modal wide">
<div class="modal-head">
<h3 id="dTitle">任务详情</h3>
<button class="icon-btn" data-close="detailModal"><span class="ico" data-icon="x"></span></button>
</div>
<div class="modal-body" id="dBody"></div>
<div class="modal-foot">
<button class="btn btn-secondary btn-sm" data-close="detailModal">关闭</button>
</div>
</div>
</div>
<script>
(function () {
const $ = (s) => document.querySelector(s);
let records = [];
let providers = [];
let visible = [];
let page = 0;
const PER = 6;
function applyFilter() {
const st = $("#statusChips .chip.active").dataset.status;
const pv = $("#provFilter").value;
const q = $("#searchInput").value.trim().toLowerCase();
visible = records.filter((r) => {
if (st !== "all" && r.status !== st) return false;
if (pv !== "all" && r.provider_id !== pv) return false;
if (q && !((r.prompt + r.model + r.profile_name + r.id).toLowerCase().includes(q))) return false;
return true;
});
page = 0;
render();
}
function render() {
const tb = $("#recRows");
tb.innerHTML = "";
$("#recEmpty").classList.toggle("hidden", visible.length !== 0);
const slice = visible.slice(page * PER, page * PER + PER);
slice.forEach((r) => {
const tr = document.createElement("tr");
tr.className = "row-click";
tr.dataset.id = r.id;
tr.innerHTML =
'<td class="mono">' + esc(r.id) + "</td>" +
'<td><b>' + esc(r.provider_name) + '</b><div class="dim mono" style="font-size:11px">' + esc(r.model) + "</div></td>" +
'<td style="max-width:320px"><div style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(r.prompt) + "</div></td>" +
'<td class="mono dim">' + esc(r.size) + "</td>" +
'<td><span class="tag">' + esc(r.profile_name) + "</span></td>" +
'<td class="mono dim">' + fmtDuration(r.duration_ms) + "</td>" +
"<td>" + (r.status === "success" ? '<span class="tag tag-success">成功</span>' : '<span class="tag tag-danger">失败</span>') + "</td>" +
'<td class="dim mono nowrap">' + esc(r.created_at) + "</td>";
tb.appendChild(tr);
});
const total = visible.length;
const pages = Math.max(1, Math.ceil(total / PER));
$("#pagerInfo").textContent = total ? "第 " + (page + 1) + " / " + pages + " 页 · 共 " + total + " 条" : "共 0 条";
$("#prevBtn").disabled = page === 0;
$("#nextBtn").disabled = page >= pages - 1;
$("#recCount").textContent = visible.length + " / " + records.length + " 条";
const topTotal = document.getElementById("topRecTotal");
if (topTotal) topTotal.textContent = records.length;
}
function openDetail(id) {
const r = records.find((x) => x.id === id);
if (!r) return;
$("#dTitle").textContent = "任务详情 · " + r.id;
$("#dBody").innerHTML =
'<div class="grid grid-2" style="margin-bottom:14px">' +
'<div><div class="dim" style="font-size:11px">供应商</div><b>' + esc(r.provider_name) + "</b></div>" +
'<div><div class="dim" style="font-size:11px">模型</div><b class="mono">' + esc(r.model) + "</b></div>" +
'<div><div class="dim" style="font-size:11px">调用方</div><b>' + esc(r.profile_name) + "</b></div>" +
'<div><div class="dim" style="font-size:11px">时间</div><b class="mono">' + esc(r.created_at) + "</b></div>" +
'<div><div class="dim" style="font-size:11px">尺寸</div><b class="mono">' + esc(r.size) + "</b></div>" +
'<div><div class="dim" style="font-size:11px">比例 / 数量</div><b class="mono">' + esc(r.ratio) + " · n=" + r.n + "</b></div>" +
"</div>" +
'<div class="dim" style="font-size:11px;margin-bottom:4px">Prompt</div>' +
'<div class="code-box" style="white-space:pre-wrap;margin-bottom:14px">' + esc(r.prompt) + "</div>" +
(r.status === "failed"
? '<div class="dim" style="font-size:11px;margin-bottom:4px">错误</div><div class="code-box" style="color:#ea8f8b;border-color:rgba(217,100,95,.4)">' + esc(r.error) + "</div>"
: '<div class="dim" style="font-size:11px;margin-bottom:4px">产出文件</div><div class="code-box">' + (r.files.length ? r.files.map(esc).join("\n") : "—") + "</div>") +
'<div class="dim" style="font-size:11px;margin-top:14px;margin-bottom:4px">耗时</div>' +
'<div class="mono">' + fmtDuration(r.duration_ms) + "</div>";
openModal("detailModal");
}
$("#recRows").addEventListener("click", (e) => {
const tr = e.target.closest("tr.row-click");
if (tr) openDetail(tr.dataset.id);
});
$("#statusChips").addEventListener("click", (e) => {
const c = e.target.closest(".chip");
if (!c) return;
$("#statusChips .chip.active").classList.remove("active");
c.classList.add("active");
applyFilter();
});
$("#provFilter").addEventListener("change", applyFilter);
$("#searchInput").addEventListener("input", applyFilter);
$("#prevBtn").addEventListener("click", () => { if (page > 0) { page--; render(); } });
$("#nextBtn").addEventListener("click", () => {
const pages = Math.max(1, Math.ceil(visible.length / PER));
if (page < pages - 1) { page++; render(); }
});
document.addEventListener("click", (e) => {
const c = e.target.closest("[data-close]");
if (c) closeModal(c.dataset.close);
});
async function load() {
providers = await window.API.listProviders();
records = await window.API.listGenerations();
$("#provFilter").innerHTML = '<option value="all">全部供应商</option>' +
providers.map((p) => '<option value="' + esc(p.id) + '">' + esc(p.name) + "</option>").join("");
const ok = records.filter((r) => r.status === "success").length;
$("#statTotal").textContent = records.length;
$("#statOk").textContent = records.length ? Math.round((ok / records.length) * 100) + "%" : "—";
$("#statDur").textContent = records.length ? fmtDuration(Math.round(records.reduce((s, r) => s + (r.duration_ms || 0), 0) / records.length)) : "—";
$("#statFiles").textContent = records.reduce((s, r) => s + r.files.length, 0);
applyFilter();
}
paintIcons();
runTask(load);
})();
</script>
</section>
</main>
</body>
</html>