fix: 优化界面

This commit is contained in:
Daniel
2026-04-06 18:07:32 +08:00
parent b342a90f9d
commit 005a25b77a
4 changed files with 125 additions and 185 deletions

View File

@@ -18,5 +18,28 @@ RUN --mount=type=cache,target=/root/.cache/pip \
COPY . . COPY . .
# 可选:构建时自动拉取远端仓库最新代码覆盖工作目录(默认关闭)。
# 放在 COPY 之后,避免影响依赖层缓存;仅在你显式开启时才会触发网络更新。
# 用法示例:
# docker compose build \
# --build-arg GIT_AUTO_UPDATE=1 \
# --build-arg GIT_REMOTE_URL=https://github.com/you/repo.git \
# --build-arg GIT_REF=main
ARG GIT_AUTO_UPDATE=0
ARG GIT_REMOTE_URL=
ARG GIT_REF=main
RUN if [ "$GIT_AUTO_UPDATE" = "1" ] && [ -n "$GIT_REMOTE_URL" ]; then \
set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends git ca-certificates; \
rm -rf /var/lib/apt/lists/*; \
tmpdir="$(mktemp -d)"; \
git clone --depth=1 --branch "$GIT_REF" "$GIT_REMOTE_URL" "$tmpdir/repo"; \
cp -a "$tmpdir/repo/." /app/; \
rm -rf "$tmpdir"; \
else \
echo "Skip remote code update (GIT_AUTO_UPDATE=0 or GIT_REMOTE_URL empty)"; \
fi
EXPOSE 8000 EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -51,32 +51,6 @@ async function postJSON(url, body) {
return data; return data;
} }
function renderTrace(trace, headerRid) {
const wrap = $("traceWrap");
const pre = $("traceJson");
const badge = $("traceBadge");
if (!pre || !wrap) return;
if (!trace || Object.keys(trace).length === 0) {
pre.textContent = headerRid
? JSON.stringify({ request_id: headerRid, note: "响应中无 trace 字段" }, null, 2)
: "尚无数据完成一次「AI 改写」后,这里会显示请求 ID、耗时、质检与降级原因。";
if (badge) badge.textContent = "";
return;
}
const merged = { ...trace };
if (headerRid && !merged.request_id) merged.request_id = headerRid;
pre.textContent = JSON.stringify(merged, null, 2);
const mode = merged.mode || "";
if (badge) {
badge.textContent = mode === "ai" ? "AI" : mode === "fallback" ? "保底" : "";
badge.className = "trace-badge " + (mode === "ai" ? "is-ai" : mode === "fallback" ? "is-fallback" : "");
}
wrap.open = true;
}
$("rewriteBtn").addEventListener("click", async () => { $("rewriteBtn").addEventListener("click", async () => {
const sourceText = $("sourceText").value.trim(); const sourceText = $("sourceText").value.trim();
if (sourceText.length < 20) { if (sourceText.length < 20) {
@@ -99,7 +73,6 @@ $("rewriteBtn").addEventListener("click", async () => {
$("summary").value = data.summary || ""; $("summary").value = data.summary || "";
$("body").value = data.body_markdown || ""; $("body").value = data.body_markdown || "";
updateCounters(); updateCounters();
renderTrace(data.trace, data._requestId);
const tr = data.trace || {}; const tr = data.trace || {};
const modelLine = tr.model ? `模型 ${tr.model}` : ""; const modelLine = tr.model ? `模型 ${tr.model}` : "";
if (data.mode === "fallback") { if (data.mode === "fallback") {
@@ -193,32 +166,4 @@ $("imBtn").addEventListener("click", async () => {
$(id).addEventListener("input", updateCounters); $(id).addEventListener("input", updateCounters);
}); });
async function loadBackendConfig() {
const el = $("backendConfig");
if (!el) return;
try {
const res = await fetch("/api/config");
const c = await res.json();
if (!c.openai_configured) {
el.textContent =
"后端未配置 OPENAI_API_KEY改写将使用本地保底稿千问不会参与。请在 .env 中配置并重启容器。";
el.style.color = "#b42318";
return;
}
const name =
c.provider === "dashscope"
? "通义千问DashScope 兼容接口)"
: "OpenAI 兼容接口";
const host = c.base_url_host ? ` · ${c.base_url_host}` : "";
const to = c.openai_timeout_sec != null ? ` · 单轮最长等待 ${c.openai_timeout_sec}s` : "";
el.textContent = `已接入:${c.openai_model} · ${name}${host}${to}`;
el.style.color = "";
} catch (e) {
el.textContent = "无法读取 /api/config请确认服务已启动";
el.style.color = "#b42318";
}
}
loadBackendConfig();
updateCounters(); updateCounters();
renderTrace(null, "");

View File

@@ -1,11 +1,12 @@
:root { :root {
--bg: #f3f7f5; --bg: #ecfeff;
--panel: #ffffff; --panel: #ffffff;
--line: #d7e3dd; --line: #d9eef2;
--text: #1a3128; --text: #164e63;
--muted: #5e7a6f; --muted: #457386;
--accent: #18794e; --accent: #0891b2;
--accent-2: #0f5f3d; --accent-2: #0e7490;
--accent-soft: #f0fdff;
} }
* { * {
@@ -14,17 +15,18 @@
body { body {
margin: 0; margin: 0;
background: radial-gradient(circle at 10% 20%, #e6f4ec, transparent 35%), background: var(--bg);
radial-gradient(circle at 90% 80%, #dff0ff, transparent 30%),
var(--bg);
color: var(--text); color: var(--text);
font-family: "PingFang SC", "Noto Sans SC", "Microsoft YaHei", sans-serif; font-family: Inter, "PingFang SC", "Noto Sans SC", "Microsoft YaHei", sans-serif;
height: 100vh;
overflow: hidden;
} }
.topbar { .topbar {
max-width: 1280px; max-width: 1240px;
margin: 20px auto 0; height: 72px;
padding: 0 16px; margin: 0 auto;
padding: 0 20px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@@ -32,42 +34,43 @@ body {
.brand h1 { .brand h1 {
margin: 0; margin: 0;
font-size: 32px;
letter-spacing: -0.02em;
} }
.brand .muted { .brand .muted {
margin: 6px 0 0; margin: 6px 0 0;
} }
.backend-config {
margin: 8px 0 0;
line-height: 1.5;
}
.badge { .badge {
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 700;
color: #0f5f3d; color: #fafafa;
background: #eaf7f0; background: var(--accent-2);
border: 1px solid #cde6d7; border: 1px solid var(--accent-2);
padding: 5px 10px; padding: 5px 10px;
border-radius: 999px; border-radius: 999px;
} }
.layout { .layout {
max-width: 1280px; max-width: 1240px;
margin: 14px auto 24px; height: calc(100vh - 72px);
padding: 0 16px; margin: 0 auto;
padding: 0 20px;
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: minmax(320px, 42%) 1fr;
gap: 16px; gap: 12px;
overflow: hidden;
} }
.panel { .panel {
background: var(--panel); background: var(--panel);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 14px; border-radius: 12px;
padding: 18px; padding: 14px;
box-shadow: 0 8px 24px rgba(32, 84, 55, 0.07); box-shadow: 0 6px 24px rgba(8, 145, 178, 0.08);
min-height: 0;
overflow: hidden;
} }
h1, h1,
@@ -75,16 +78,25 @@ h2 {
margin-top: 0; margin-top: 0;
} }
.panel-head {
margin-bottom: 10px;
}
.panel-head h2 {
margin: 0;
font-size: 18px;
}
.muted { .muted {
color: var(--muted); color: var(--muted);
margin-top: -6px; margin-top: 2px;
} }
label { label {
display: block; display: block;
margin-top: 10px; margin-top: 8px;
margin-bottom: 6px; margin-bottom: 4px;
font-size: 14px; font-size: 13px;
font-weight: 600; font-weight: 600;
} }
@@ -105,8 +117,8 @@ button {
width: 100%; width: 100%;
border-radius: 10px; border-radius: 10px;
border: 1px solid var(--line); border: 1px solid var(--line);
padding: 10px 12px; padding: 8px 10px;
font-size: 14px; font-size: 13px;
transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease; transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
} }
@@ -118,13 +130,13 @@ textarea {
input:focus, input:focus,
textarea:focus { textarea:focus {
outline: none; outline: none;
border-color: #8ec6aa; border-color: #7dd3e7;
box-shadow: 0 0 0 3px rgba(24, 121, 78, 0.12); box-shadow: 0 0 0 3px rgba(34, 211, 238, 0.15);
} }
button { button {
cursor: pointer; cursor: pointer;
margin-top: 12px; margin-top: 8px;
font-weight: 700; font-weight: 700;
} }
@@ -142,6 +154,22 @@ button.primary:hover {
background: var(--accent-2); background: var(--accent-2);
} }
button.secondary {
background: #fff;
color: var(--text);
border-color: var(--line);
}
button.secondary:hover {
background: #f8fdff;
}
.subtle-btn {
background: #fff;
border-color: #a5dae6;
color: var(--accent-2);
}
button:disabled { button:disabled {
cursor: not-allowed; cursor: not-allowed;
opacity: 0.65; opacity: 0.65;
@@ -173,67 +201,29 @@ button:disabled {
} }
.status { .status {
min-height: 22px; min-height: 18px;
margin-top: 8px; margin-top: 6px;
color: var(--accent-2); color: var(--accent-2);
font-weight: 600; font-weight: 500;
font-size: 12px;
} }
.small { .small {
font-size: 13px; font-size: 12px;
margin: 0 0 12px; margin: 0 0 6px;
}
.flow-hint {
margin: 0 0 14px 18px;
padding: 0;
font-size: 13px;
line-height: 1.6;
}
.trace-wrap {
margin-top: 12px;
padding: 10px 12px;
border: 1px dashed var(--line);
border-radius: 10px;
background: #f9fbf9;
}
.trace-wrap summary {
cursor: pointer;
font-weight: 700;
color: var(--text);
}
.trace-badge {
margin-left: 8px;
font-size: 11px;
padding: 2px 8px;
border-radius: 999px;
font-weight: 700;
}
.trace-badge.is-ai {
background: #eaf7f0;
color: #0f5f3d;
border: 1px solid #cde6d7;
}
.trace-badge.is-fallback {
background: #fff4e6;
color: #9a3412;
border: 1px solid #fed7aa;
} }
.body-split { .body-split {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 0.9fr;
gap: 12px; gap: 10px;
align-items: stretch; align-items: stretch;
min-height: 0;
} }
.body-split textarea { .body-split textarea {
min-height: 280px; min-height: 170px;
max-height: 240px;
} }
.preview-panel { .preview-panel {
@@ -244,10 +234,10 @@ button:disabled {
.markdown-preview { .markdown-preview {
flex: 1; flex: 1;
min-height: 280px; min-height: 170px;
max-height: 480px; max-height: 240px;
overflow: auto; overflow: auto;
padding: 12px 14px; padding: 10px 12px;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 10px; border-radius: 10px;
background: #fafcfb; background: #fafcfb;
@@ -258,7 +248,7 @@ button:disabled {
.markdown-preview h2 { .markdown-preview h2 {
font-size: 1.15rem; font-size: 1.15rem;
margin: 1em 0 0.5em; margin: 1em 0 0.5em;
color: var(--accent-2); color: #111827;
} }
.markdown-preview h3 { .markdown-preview h3 {
@@ -280,23 +270,14 @@ button:disabled {
margin: 0.25em 0; margin: 0.25em 0;
} }
.trace-json {
margin: 10px 0 0;
padding: 10px;
max-height: 220px;
overflow: auto;
font-size: 11px;
line-height: 1.45;
background: #fff;
border-radius: 8px;
border: 1px solid var(--line);
white-space: pre-wrap;
word-break: break-word;
}
@media (max-width: 960px) { @media (max-width: 960px) {
body {
overflow: auto;
}
.layout { .layout {
grid-template-columns: 1fr; grid-template-columns: 1fr;
height: auto;
} }
.body-split { .body-split {

View File

@@ -10,27 +10,22 @@
<header class="topbar"> <header class="topbar">
<div class="brand"> <div class="brand">
<h1>{{ app_name }}</h1> <h1>{{ app_name }}</h1>
<p class="muted">粘贴原文 → 洗成约 <strong>5 段、500 字内</strong> 的短文(无小标题)→ 右侧预览 → 满意后发布。</p> <p class="muted">从原文到公众号草稿,一页完成改写、封面和发布。</p>
<p id="backendConfig" class="backend-config muted small" aria-live="polite"></p>
</div> </div>
<div class="badge">Beta</div> <div class="badge">Studio</div>
</header> </header>
<main class="layout"> <main class="layout">
<section class="panel input-panel"> <section class="panel input-panel">
<h2>输入与改写策略</h2> <div class="panel-head">
<ol class="flow-hint muted"> <h2>内容输入</h2>
<li>粘贴原文并设置语气/读者</li> </div>
<li>点击改写 → 右侧为短标题、摘要与<strong>五段正文</strong>(段落间空一行)</li>
<li>看「运行追踪」:<strong>模式为 AI</strong> 且模型名正确,即千问/接口已生效</li>
<li>人工改好后 →「发布到公众号草稿箱」(需配置 WECHAT_*</li>
</ol>
<div class="field-head"> <div class="field-head">
<label>原始内容</label> <label>内容</label>
<span id="sourceCount" class="meta">0 字</span> <span id="sourceCount" class="meta">0 字</span>
</div> </div>
<textarea id="sourceText" rows="14" placeholder="粘贴原文(长帖、线程、摘录均可),洗稿会围绕原文主题展开…"></textarea> <textarea id="sourceText" rows="9" placeholder="粘贴原文(长帖、线程、摘录均可),洗稿会围绕原文主题展开…"></textarea>
<div class="grid2"> <div class="grid2">
<div> <div>
@@ -62,8 +57,9 @@
</section> </section>
<section class="panel output-panel"> <section class="panel output-panel">
<h2>发布内容</h2> <div class="panel-head">
<p class="muted small">下方「运行追踪」会显示本次请求 ID、耗时、质检项与是否降级便于与容器日志对照。</p> <h2>发布内容</h2>
</div>
<label>标题</label> <label>标题</label>
<input id="title" type="text" /> <input id="title" type="text" />
@@ -72,12 +68,12 @@
<label>摘要</label> <label>摘要</label>
<span id="summaryCount" class="meta">0 字</span> <span id="summaryCount" class="meta">0 字</span>
</div> </div>
<textarea id="summary" rows="3"></textarea> <textarea id="summary" rows="2"></textarea>
<label>公众号封面(可选上传)</label> <label>公众号封面(可选上传)</label>
<div class="cover-tools"> <div class="cover-tools">
<input id="coverFile" type="file" accept="image/png,image/jpeg,image/jpg,image/webp" /> <input id="coverFile" type="file" accept="image/png,image/jpeg,image/jpg,image/webp" />
<button id="coverUploadBtn" type="button">上传封面并绑定</button> <button id="coverUploadBtn" class="subtle-btn" type="button">上传封面并绑定</button>
</div> </div>
<input id="thumbMediaId" type="text" placeholder="thumb_media_id上传后自动填充也可手动粘贴" /> <input id="thumbMediaId" type="text" placeholder="thumb_media_id上传后自动填充也可手动粘贴" />
<p id="coverHint" class="muted small">未上传时将使用后端默认封面策略。</p> <p id="coverHint" class="muted small">未上传时将使用后端默认封面策略。</p>
@@ -87,24 +83,19 @@
<span id="bodyCount" class="meta">0 字</span> <span id="bodyCount" class="meta">0 字</span>
</div> </div>
<div class="body-split"> <div class="body-split">
<textarea id="body" rows="10" placeholder="五段之间空一行;无需 # 标题"></textarea> <textarea id="body" rows="7" placeholder="五段之间空一行;无需 # 标题"></textarea>
<div class="preview-panel"> <div class="preview-panel">
<div class="field-head"> <div class="field-head">
<label>排版预览</label> <label>排版预览</label>
<span class="meta">与公众号 HTML 渲染接近</span> <span class="meta">实时同步</span>
</div> </div>
<div id="bodyPreview" class="markdown-preview"></div> <div id="bodyPreview" class="markdown-preview"></div>
</div> </div>
</div> </div>
<details id="traceWrap" class="trace-wrap">
<summary>运行追踪 <span id="traceBadge" class="trace-badge"></span></summary>
<pre id="traceJson" class="trace-json"></pre>
</details>
<div class="actions"> <div class="actions">
<button id="wechatBtn">发布到公众号草稿箱</button> <button id="wechatBtn" class="primary">发布到公众号草稿箱</button>
<button id="imBtn">发送到 IM</button> <button id="imBtn" class="secondary">发送到 IM</button>
</div> </div>
</section> </section>
</main> </main>