260 lines
14 KiB
HTML
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>
|