Files
ai-image/web/create.html
T

388 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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="create">
<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 id="pageTitle">出图</h1>
</div>
<div class="top-actions">
<span class="tag tag-info mono">output/user/</span>
<a class="btn btn-secondary btn-sm" href="index.html">查看画廊</a>
<a class="btn btn-sm" href="providers.html">接入供应商</a>
</div>
</header>
<section class="page-body">
<div class="split">
<!-- ==================== 左:出图表单 ==================== -->
<div>
<div class="card">
<div class="card-head">
<div>
<h3>生成图片</h3>
<div class="sub">网页端出图固定写入 <code>output/user/</code>,目录不存在时自动创建</div>
</div>
<div class="top-actions">
<span class="tag" id="profileTag">profile_name = user</span>
</div>
</div>
<div class="card-body">
<div class="field" id="fieldPrompt">
<label>提示词 <span class="req">*</span></label>
<textarea id="prompt" rows="5" placeholder="描述你想要的画面,例如:a luminous floating city above a misty canyon at sunrise, cinematic realism, volumetric light"></textarea>
<div class="err-msg">请填写提示词</div>
<div class="hint">建议包含主体 + 风格 + 光线 + 画幅关键词,中英文均可</div>
</div>
<div class="grid grid-2">
<div class="field" id="fieldProvider">
<label>供应商 <span class="req">*</span></label>
<select id="provider"></select>
<div class="err-msg">请选择供应商</div>
</div>
<div class="field" id="fieldModel">
<label>模型 <span class="req">*</span></label>
<select id="model"></select>
<div class="err-msg">请选择模型</div>
</div>
</div>
<div class="grid grid-2">
<div class="field">
<label>尺寸</label>
<select id="size">
<option value="1024x1024">1024x1024(1:1)</option>
<option value="2048x2048">2048x2048(1:1 · 高清)</option>
<option value="1312x736">1312x736(16:9)</option>
<option value="1152x864">1152x864(4:3)</option>
<option value="2048x1152">2048x1152(16:9 · 高清)</option>
</select>
</div>
<div class="field">
<label>数量</label>
<select id="count">
<option value="1">1 张</option>
<option value="2">2 张</option>
<option value="4">4 张</option>
</select>
</div>
</div>
<div class="toolbar" style="margin-bottom:0">
<button class="btn btn-lg" id="generateBtn"><span class="ico" data-icon="sparkles"></span>开始生成</button>
<button class="btn btn-secondary btn-sm" id="discoverBtn"><span class="ico" data-icon="refresh"></span>拉取上游模型</button>
<span class="spacer"></span>
<span class="dim mono" id="modelCount">—</span>
</div>
</div>
</div>
<div class="card" style="margin-top:16px">
<div class="card-head">
<div>
<h3>本次结果</h3>
<div class="sub">生成成功后可在下方查看,并已自动加入 <code>output/user/</code> 画廊</div>
</div>
<div class="top-actions">
<span class="tag tag-success" id="resultStatus">尚未生成</span>
</div>
</div>
<div class="card-body">
<div class="gal-grid" id="resultGrid"></div>
<div class="empty" id="resultEmpty">
<span class="ico" data-icon="image"></span>
<h4>还没有生成结果</h4>
<p>填写提示词并选择可用模型后点击「开始生成」。</p>
</div>
</div>
</div>
</div>
<!-- ==================== 右:说明与最近产出 ==================== -->
<div>
<div class="card">
<div class="card-head">
<h3>落盘规则</h3>
</div>
<div class="card-body">
<div class="code-box"><code>output/
└── user/ ← 网页端手动出图
├── gpt-image-2-1K-0014ef2b.png
└── ...</code></div>
<ul class="md" style="padding-left:18px;margin:12px 0 0">
<li>output/ 下的<b>文件夹名 = 谁生成的</b>,网页端固定为 <code>user</code></li>
<li>Agent 调用时由调用方指定 <code>profile_name</code></li>
<li>目录不存在时后端自动创建</li>
</ul>
</div>
</div>
<div class="card" style="margin-top:16px">
<div class="card-head">
<h3>可用模型</h3>
<div class="top-actions">
<span class="tag tag-info" id="modelSource">来自实时拉取</span>
</div>
</div>
<div class="card-body">
<div class="model-list" id="modelList"></div>
<div class="empty hidden" id="modelEmpty">
<span class="ico" data-icon="alert"></span>
<h4>没找到有效的模型</h4>
<p>请先接入供应商并拉取上游模型清单。</p>
<a class="btn btn-secondary btn-sm" href="providers.html">接入供应商</a>
</div>
</div>
</div>
<div class="card" style="margin-top:16px">
<div class="card-head">
<h3>user 画廊最近产出</h3>
<div class="top-actions">
<a class="btn btn-ghost btn-sm" href="index.html">全部</a>
</div>
</div>
<div class="card-body">
<div class="gal-grid" id="recentGrid"></div>
<div class="empty hidden" id="recentEmpty">
<span class="ico" data-icon="images"></span>
<h4>output/user/ 还是空的</h4>
<p>生成一张图试试。</p>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
<script>
(function () {
const $ = (s) => document.querySelector(s);
let providers = [];
let models = [];
let recent = [];
function setError(fieldSel, on, msg) {
const f = $(fieldSel);
f.classList.toggle("has-error", on);
if (msg) f.querySelector(".err-msg").textContent = msg;
}
/* ---------------------------------------------------- 供应商 / 模型 */
function enabledModels(providerId) {
return models.filter(
(m) => m.enabled && m.kind === "image" && (!providerId || m.provider_id === providerId)
&& providers.some((p) => p.id === m.provider_id && p.enabled)
);
}
function renderProviderOptions() {
const sel = $("#provider");
sel.innerHTML = providers
.map((p) => '<option value="' + esc(p.id) + '"' + (p.enabled ? "" : " disabled") + ">" +
esc(p.name) + (p.enabled ? "" : "(已停用)") + "</option>")
.join("");
if (!providers.length) sel.innerHTML = '<option value="">(无可用供应商)</option>';
const first = providers.find((p) => p.enabled);
sel.value = first ? first.id : "";
}
function renderModelOptions() {
const pid = $("#provider").value;
const list = enabledModels(pid);
const sel = $("#model");
sel.innerHTML = list.length
? list
.map((m) => '<option value="' + esc(m.model_id) + '">' + esc(m.display_name || m.model_id) + " · " + esc(m.model_id) + "</option>")
.join("")
: '<option value="">(该供应商暂无可用模型)</option>';
$("#modelCount").textContent = list.length + " 个可用模型";
const preferred = providers.find((p) => p.id === pid)?.default_model;
if (list.some((m) => m.model_id === preferred)) sel.value = preferred;
}
function renderModelList() {
const list = enabledModels();
const box = $("#modelList");
box.innerHTML = "";
$("#modelEmpty").classList.toggle("hidden", list.length !== 0);
list.forEach((m) => {
const row = document.createElement("div");
row.className = "model-row";
const p = providers.find((x) => x.id === m.provider_id);
row.innerHTML =
'<div style="flex:1;min-width:0">' +
'<div class="mono" style="font-size:12px">' + esc(m.model_id) + "</div>" +
'<div class="hint">' + esc(p ? p.name : m.provider_id) + " · " + esc(m.size_hint || "—") + "</div>" +
"</div>" +
'<span class="tag tag-success">可用</span>';
box.appendChild(row);
});
}
async function loadProviders() {
providers = await window.API.listProviders();
renderProviderOptions();
renderModelOptions();
}
async function loadModels() {
models = await window.API.listModels();
renderModelOptions();
renderModelList();
}
/* ------------------------------------------------------ 拉取上游模型 */
$("#discoverBtn").addEventListener("click", async () => {
const pid = $("#provider").value;
if (!pid) {
toast("请先选择供应商", "err");
return;
}
const btn = $("#discoverBtn");
if (btn.disabled) return;
btn.disabled = true;
btn.classList.add("is-loading");
try {
const res = await window.API.discoverProviderModels(pid);
await loadModels();
if (!res.matched || !res.matched.length) {
toast("没找到有效的模型", "err");
$("#modelSource").textContent = "未命中模型";
return;
}
$("#modelSource").textContent = "实时拉取 · " + res.matched.length + " 个";
toast("拉取到 " + res.matched.length + " 个可用模型");
} catch (e) {
toast(e.message, "err");
} finally {
btn.classList.remove("is-loading");
btn.disabled = false;
}
});
/* ---------------------------------------------------------- 生成 */
$("#generateBtn").addEventListener("click", async () => {
const prompt = $("#prompt").value.trim();
const provider_id = $("#provider").value;
const model = $("#model").value;
const size = $("#size").value;
const n = parseInt($("#count").value, 10);
setError("#fieldPrompt", !prompt);
setError("#fieldProvider", !provider_id);
setError("#fieldModel", !model, "请选择模型");
if (!prompt || !provider_id || !model) return;
const btn = $("#generateBtn");
if (btn.disabled) return;
btn.disabled = true;
btn.classList.add("is-loading");
$("#resultStatus").textContent = "生成中…";
$("#resultStatus").className = "tag tag-warn";
try {
const res = await window.API.generateImage({
provider_id: provider_id,
model: model,
prompt: prompt,
size: size,
n: n,
});
renderResult(res);
$("#resultStatus").textContent = "生成成功 · " + res.images.length + " 张";
$("#resultStatus").className = "tag tag-success";
toast("已保存到 " + res.output_dir);
await loadRecent();
} catch (e) {
$("#resultStatus").textContent = "生成失败";
$("#resultStatus").className = "tag tag-danger";
toast(e.message, "err");
} finally {
btn.classList.remove("is-loading");
btn.disabled = false;
}
});
function renderResult(res) {
const grid = $("#resultGrid");
grid.innerHTML = "";
$("#resultEmpty").classList.toggle("hidden", res.images.length !== 0);
res.images.forEach((img) => {
const el = document.createElement("div");
el.className = "gal-item";
el.innerHTML =
'<img src="' + esc(img.url) + '" alt="' + esc(img.filename) + '" />' +
'<span class="tag gal-tag">' + esc(res.generation.model) + "</span>" +
'<div class="gal-meta"><b>' + esc(img.filename) + "</b><span>" +
fmtBytes(img.bytes) + " · " + esc(res.generation.created_at) + "</span></div>";
grid.appendChild(el);
});
}
/* ---------------------------------------------------- 最近产出 */
async function loadRecent() {
const items = await window.API.listGallery("user");
recent = items.slice(0, 6);
const grid = $("#recentGrid");
grid.innerHTML = "";
$("#recentEmpty").classList.toggle("hidden", recent.length !== 0);
recent.forEach((g) => {
const el = document.createElement("div");
el.className = "gal-item";
el.innerHTML =
'<img src="' + esc(g.url) + '" alt="' + esc(g.filename) + '" loading="lazy" />' +
'<div class="gal-meta"><b>' + esc(g.filename) + "</b><span>" + esc(g.created_at) + "</span></div>";
grid.appendChild(el);
});
}
$("#provider").addEventListener("change", renderModelOptions);
paintIcons();
runTask(async () => {
await loadProviders();
await loadModels();
await loadRecent();
});
})();
</script>
</body>
</html>