diff --git a/internal/gateway/ui/index.html b/internal/gateway/ui/index.html index c65158b..6775462 100644 --- a/internal/gateway/ui/index.html +++ b/internal/gateway/ui/index.html @@ -366,6 +366,22 @@ /* ---- chat page ---- */ .tab-chat{max-width:840px;margin:0 auto;display:flex;flex-direction:column;height:calc(100vh - 190px)} + + .tab-image{max-width:840px;margin:0 auto;display:flex;flex-direction:column; + height:calc(100vh - 190px)} + .img-wrap{flex:1;display:flex;flex-direction:column;min-height:0} + .img-grid{flex:1;overflow-y:auto;padding:12px 2px;display:grid; + grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:10px;align-content:start} + .img-grid .chat-empty{grid-column:1/-1} + .img-cell{border:1px solid var(--line);border-radius:10px;overflow:hidden; + background:var(--bg2)} + .img-cell img{display:block;width:100%;height:auto;background:#fff} + .img-meta{padding:6px 8px;font-size:11px;color:var(--muted); + overflow:hidden;text-overflow:ellipsis;white-space:nowrap} + .img-composer{border-top:1px solid var(--line);padding:10px 2px 12px} + .img-composer .chat-box{display:flex;gap:8px;align-items:flex-end} + .img-composer textarea{flex:1;resize:vertical;min-height:52px} + .img-composer .empty{padding:8px 0} .chat-wrap{background:var(--card);border:1px solid var(--line);border-radius:18px;box-shadow:var(--sh-lg); display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden;backdrop-filter:blur(var(--glass)) saturate(1.5)} .chat-log{flex:1;overflow-y:auto;padding:20px 20px 8px;display:flex;flex-direction:column;gap:18px} @@ -573,6 +589,21 @@ />对话 + + + + ${body} + + + `; + } + function clearImages() { + $("#it-grid").innerHTML = `
+
M
+

${esc(t("imgWelcome"))}

+

${esc(t("imgSlow"))}

+
`; + } + // Image generation is slow — measured 16s to 2min on the one upstream + // tested — so the button reports elapsed time instead of looking hung. + async function sendImage() { + if (imgBusy) return; + const prompt = ($("#it-prompt").value || "").trim(); + const model = $("#it-model").value; + if (!prompt) { + toast(t("imgNeedPrompt")); + return; + } + if (!model) { + toast(t("imgNoModels")); + return; + } + const n = Math.max(1, Math.min(4, parseInt($("#it-n").value, 10) || 1)); + const size = ($("#it-size").value || "").trim(); + imgBusy = true; + const btn = $("#it-send"); + btn.disabled = true; + const t0 = Date.now(); + const tick = setInterval(() => { + btn.textContent = t("imgWorking").replace("%d", String(Math.round((Date.now() - t0) / 1000))); + }, 1000); + try { + const r = await api("/v1/images/generations", { + method: "POST", + body: size ? { model: model, prompt: prompt, n: n, size: size } : { model: model, prompt: prompt, n: n }, + }); + const empty = $("#it-empty"); + if (empty) empty.remove(); + const grid = $("#it-grid"); + const items = Array.isArray(r.data) ? r.data : []; + items.forEach((it) => { + const src = it.url || (it.b64_json ? "data:image/png;base64," + it.b64_json : ""); + if (!src) return; + grid.insertAdjacentHTML( + "beforeend", + `
generated +
${esc(it.revised_prompt || model)}
`, + ); + }); + if (!items.length) toast(t("imgNoData")); + } catch (e) { + toast(t("imgFailed") + (e && e.message ? ": " + e.message : "")); + } finally { + clearInterval(tick); + imgBusy = false; + btn.disabled = false; + btn.textContent = t("imgGenerate"); + } + } + function updateGrow() { const ta = $("#ct-msg"); if (!ta) return; @@ -3161,7 +3385,8 @@ recsState.keyNames = keyNames || {};
${adSel}
- + +
${t("urlHint")}
@@ -3571,7 +3796,8 @@ recsState.keyNames = keyNames || {};
${adSel}
- + +
${t("urlHint")}
@@ -5814,6 +6040,7 @@ toast(t("kAutoChainGlobal")); const done = () => remountPluginElements(); if (tab === "status") return renderStatus().then(done, done); if (tab === "chat") return renderChat().then(done, done); + if (tab === "image") return renderImage().then(done, done); if (tab === "keys") return renderKeys().then(done, done); if (tab === "sort") return renderSort().then(done, done); if (tab === "sources") return renderSources().then(done, done); diff --git a/internal/gateway/ui_image_page_test.go b/internal/gateway/ui_image_page_test.go new file mode 100644 index 0000000..a1b6bfe --- /dev/null +++ b/internal/gateway/ui_image_page_test.go @@ -0,0 +1,195 @@ +package gateway + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "llmsproxy/internal/provider" +) + +// TestUI_ChatPageGroupsImageModels is a UI-contract guard for the bug that +// started this: the chat page listed image models beside chat ones, and picking +// one posted to /api/chat, which cannot serve it. +// +// The contract is asserted on the HTML source rather than by running a browser, +// because the failure this must catch is a source-level one — an