Files
ai-image/web/models.html
T

213 lines
11 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="models">
<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 tag-info">仅启用模型对 Agent 可见</span><a class="btn btn-secondary btn-sm" href="providers.html">供应商</a></div>
</header>
<section class="page-body">
<div class="toolbar">
<div class="chip-group" id="providerChips">
<button class="chip active" data-provider="all">全部供应商</button>
</div>
<div class="input-group" style="max-width:280px;flex:1">
<input type="search" id="searchInput" placeholder="搜索模型 ID / 展示名" />
</div>
<span class="spacer"></span>
<span class="dim mono" id="modelCount">—</span>
<button class="btn btn-secondary btn-sm" id="enableAllBtn">批量启用</button>
<button class="btn btn-secondary btn-sm" id="disableAllBtn">批量停用</button>
<button class="btn btn-secondary btn-sm" id="refreshBtn"><span class="ico" data-icon="refresh"></span>刷新</button>
</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="statEnabled">—</div>
<div class="sub">对 Agent 可见</div>
</div>
<div class="card stat">
<div class="k">支持图生图</div>
<div class="v" id="statImageInput">—</div>
<div class="sub">可接收参考图</div>
</div>
<div class="card stat">
<div class="k">最近发现</div>
<div class="v" id="statLatest" style="font-size:18px">—</div>
<div class="sub">上游模型同步时间</div>
</div>
</div>
<div class="card">
<div class="card-head">
<div>
<h3>模型清单</h3>
<div class="sub">仅启用的模型会出现在 REST / MCP 的可用列表中</div>
</div>
</div>
<div class="card-body tight">
<div class="table-wrap">
<table class="tbl">
<thead>
<tr>
<th>模型 ID</th>
<th>展示名</th>
<th>供应商</th>
<th>尺寸能力</th>
<th>图生图</th>
<th>发现时间</th>
<th style="width:120px">启用</th>
</tr>
</thead>
<tbody id="modelRows"></tbody>
</table>
</div>
<div class="empty hidden" id="modelEmpty">
<span class="ico" data-icon="boxes"></span>
<h4>没有匹配的模型</h4>
<p>先到供应商页拉取上游模型,或放宽筛选条件。</p>
<a class="btn btn-secondary btn-sm" href="providers.html">去拉取模型</a>
</div>
</div>
</div>
<script>
(function () {
const $ = (s) => document.querySelector(s);
let models = [];
let providers = [];
let visible = [];
function providerName(id) {
const p = providers.find((x) => x.id === id);
return p ? p.name : id;
}
function applyFilter() {
const pid = $("#providerChips .chip.active").dataset.provider;
const q = $("#searchInput").value.trim().toLowerCase();
visible = models.filter((m) => {
if (pid !== "all" && m.provider_id !== pid) return false;
if (q && !((m.model_id + m.display_name + providerName(m.provider_id)).toLowerCase().includes(q))) return false;
return true;
});
render();
}
function render() {
const tb = $("#modelRows");
tb.innerHTML = "";
$("#modelEmpty").classList.toggle("hidden", visible.length !== 0);
visible.forEach((m) => {
const tr = document.createElement("tr");
tr.innerHTML =
'<td class="mono">' + esc(m.model_id) + (m.note ? '<div class="dim" style="font-size:11px">' + esc(m.note) + "</div>" : "") + "</td>" +
"<td>" + esc(m.display_name) + "</td>" +
'<td><span class="tag">' + esc(providerName(m.provider_id)) + "</span></td>" +
'<td class="mono dim">' + esc(m.size_hint) + "</td>" +
"<td>" + (m.supports_image_input ? '<span class="tag tag-success">支持</span>' : '<span class="tag">不支持</span>') + "</td>" +
'<td class="dim mono">' + esc(m.discovered_at) + "</td>" +
'<td><label class="switch"><input type="checkbox" ' + (m.enabled ? "checked" : "") + ' data-toggle="' + esc(m.id) + '" /><span class="track"></span></label></td>';
tb.appendChild(tr);
});
$("#modelCount").textContent = visible.length + " / " + models.length + " 个模型";
$("#statTotal").textContent = models.length;
$("#statEnabled").textContent = models.filter((m) => m.enabled).length;
$("#statImageInput").textContent = models.filter((m) => m.supports_image_input).length;
const dates = models.map((m) => m.discovered_at).filter((d) => d && d !== "—").sort();
$("#statLatest").textContent = dates.length ? dates[dates.length - 1] : "—";
}
$("#providerChips").addEventListener("click", (e) => {
const c = e.target.closest(".chip");
if (!c) return;
$("#providerChips .chip.active").classList.remove("active");
c.classList.add("active");
applyFilter();
});
$("#searchInput").addEventListener("input", applyFilter);
$("#modelRows").addEventListener("change", (e) => runTask(async () => {
const t = e.target.closest("input[data-toggle]");
if (!t) return;
try {
models = await window.API.toggleModel(t.dataset.toggle, t.checked);
toast(t.checked ? "模型已启用" : "模型已停用", "info");
} finally {
applyFilter();
}
}));
async function bulk(enabled) {
const ids = visible.filter((m) => m.enabled !== enabled).map((m) => m.id);
if (!ids.length) { toast("没有需要变更的模型", "info"); return; }
models = await window.API.setModelsEnabled(ids, enabled);
toast(enabled ? "已启用 " + ids.length + " 个模型" : "已停用 " + ids.length + " 个模型");
applyFilter();
}
$("#enableAllBtn").addEventListener("click", (event) => runTask(() => bulk(true), event.currentTarget));
$("#disableAllBtn").addEventListener("click", (event) => runTask(() => bulk(false), event.currentTarget));
$("#refreshBtn").addEventListener("click", (event) => runTask(async () => {
await load();
toast("已刷新");
}, event.currentTarget));
async function load() {
providers = await window.API.listProviders();
models = await window.API.listModels();
$("#providerChips").innerHTML = '<button class="chip active" data-provider="all">全部供应商</button>' +
providers.map((p) => '<button class="chip" data-provider="' + esc(p.id) + '">' + esc(p.name) + "</button>").join("");
applyFilter();
}
paintIcons();
runTask(load);
})();
</script>
</section>
</main>
</body>
</html>