feat: add LLM配置 management page (profiles view)

This commit is contained in:
2026-06-16 16:25:20 +08:00
parent e329f59139
commit dc8baf8662
5 changed files with 261 additions and 2 deletions

View File

@@ -265,3 +265,39 @@ table.group-table td { border-bottom: 1px solid #f1f5f9; font-variant-numeric: t
.sidebar { width: 64px; } .sidebar { width: 64px; }
.brand-sub, .nav-item span:not(.nav-ico), .sidebar-foot span:last-child { display: none; } .brand-sub, .nav-item span:not(.nav-ico), .sidebar-foot span:last-child { display: none; }
} }
/* ---------- LLM 配置管理页 ---------- */
.profile-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
.profile-card {
background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
padding: 16px; box-shadow: var(--shadow);
}
.profile-card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.profile-card-name { font-size: 15px; font-weight: 600; }
.profile-card-actions { display: flex; gap: 6px; }
.profile-card-field { font-size: 12px; color: var(--slate); margin-top: 4px; }
.field-label { font-weight: 600; color: var(--ink); }
/* Form */
.profile-form { display: flex; flex-direction: column; gap: 12px; margin-top: 14px; max-width: 560px; }
.form-row { display: flex; flex-direction: column; gap: 4px; }
.form-label { font-size: 13px; font-weight: 600; }
.req { color: var(--bad); }
.form-input {
border: 1px solid var(--line); border-radius: 6px; padding: 8px 10px;
font-size: 13px; font-family: inherit; width: 100%;
}
.form-input:focus { outline: none; border-color: var(--petrol); }
.form-input-sm { max-width: 120px; }
.form-actions { display: flex; gap: 10px; align-items: center; margin-top: 4px; }
.form-error { font-size: 12px; color: var(--bad); }
.btn-sm { padding: 4px 10px; font-size: 12px; }
.btn-danger { color: var(--bad); border-color: var(--bad); }
.btn-danger:hover { background: #fee2e2; }
/* ---------- LLM 角色配置面板 ---------- */
.llm-assignment-panel { border-left: 3px solid var(--petrol); }
.llm-role-rows { display: flex; flex-direction: column; gap: 10px; }
.llm-role-row { display: flex; align-items: center; gap: 14px; }
.llm-role-label { font-size: 13px; font-weight: 600; min-width: 180px; color: var(--ink); }
.llm-role-select { min-width: 240px; }

View File

@@ -25,6 +25,9 @@
<button class="nav-item" data-view="report" data-requires-run="1"> <button class="nav-item" data-view="report" data-requires-run="1">
<span class="nav-ico"></span><span>报告详情</span> <span class="nav-ico"></span><span>报告详情</span>
</button> </button>
<button class="nav-item" data-view="profiles">
<span class="nav-ico"></span><span>LLM 配置</span>
</button>
</nav> </nav>
<div class="sidebar-foot"> <div class="sidebar-foot">
<span class="dot" id="health-dot"></span> <span class="dot" id="health-dot"></span>
@@ -59,6 +62,33 @@
<span class="selected-scenario muted" id="selected-scenario">未选择场景</span> <span class="selected-scenario muted" id="selected-scenario">未选择场景</span>
</div> </div>
</div> </div>
<!-- LLM 角色配置面板(选中场景后显示) -->
<div class="panel llm-assignment-panel" id="llm-assignment-panel" hidden>
<h2>LLM 角色配置 <span class="muted" style="font-size:13px;font-weight:400">(可选)</span></h2>
<p class="muted" style="margin-bottom:14px">为不同任务角色选择已保存的 LLM 配置,留空则使用场景文件中的原始配置。</p>
<div class="llm-role-rows">
<div class="llm-role-row">
<label class="llm-role-label">评测打分 Judge LLM</label>
<select class="select llm-role-select" id="role-judge">
<option value="">— 使用场景原始配置 —</option>
</select>
</div>
<div class="llm-role-row">
<label class="llm-role-label">生成答案 Answer LLM</label>
<select class="select llm-role-select" id="role-answer">
<option value="">— 使用场景原始配置 —</option>
</select>
</div>
<div class="llm-role-row">
<label class="llm-role-label">生成题库 Dataset LLM</label>
<select class="select llm-role-select" id="role-dataset">
<option value="">— 使用场景原始配置 —</option>
</select>
</div>
</div>
</div>
<div class="panel" id="task-panel" hidden> <div class="panel" id="task-panel" hidden>
<div class="task-head"> <div class="task-head">
<h2>评估进度</h2> <h2>评估进度</h2>
@@ -107,11 +137,62 @@
<div class="lowest-table" id="lowest-table"></div> <div class="lowest-table" id="lowest-table"></div>
</div> </div>
</section> </section>
<!-- LLM 配置视图 -->
<section class="view" id="view-profiles" hidden>
<div class="panel">
<div class="panel-head">
<h2>LLM 配置管理</h2>
<button class="btn btn-primary" id="add-profile-btn"> 新建配置</button>
</div>
<p class="muted">保存常用 LLM 连接参数,在运行评估时按角色选择。</p>
</div>
<!-- 新建 / 编辑表单(默认隐藏) -->
<div class="panel" id="profile-form-panel" hidden>
<h2 id="profile-form-title">新建 LLM 配置</h2>
<div class="profile-form">
<input type="hidden" id="edit-profile-id" />
<div class="form-row">
<label class="form-label">配置名称 <span class="req">*</span></label>
<input class="form-input" id="pf-name" placeholder="例DeepSeek Flash内网" />
</div>
<div class="form-row">
<label class="form-label">模型名称 <span class="req">*</span></label>
<input class="form-input" id="pf-model" placeholder="例deepseek-v4-flash" />
</div>
<div class="form-row">
<label class="form-label">Base URL <span class="req">*</span></label>
<input class="form-input" id="pf-base-url" placeholder="例http://6.86.80.4:30080/v1" />
</div>
<div class="form-row">
<label class="form-label">API Key <span class="req">*</span></label>
<input class="form-input" id="pf-api-key" type="password" placeholder="sk-…" />
</div>
<div class="form-row">
<label class="form-label">超时(秒)</label>
<input class="form-input form-input-sm" id="pf-timeout" type="number" value="30" min="5" max="300" />
</div>
<div class="form-actions">
<button class="btn btn-primary" id="save-profile-btn">保存</button>
<button class="btn" id="cancel-profile-btn">取消</button>
<span class="form-error muted" id="profile-form-error"></span>
</div>
</div>
</div>
<div id="profile-cards" class="profile-grid"></div>
<div class="empty" id="profiles-empty" hidden>
<p>尚未添加任何 LLM 配置。</p>
<p class="muted">点击「新建配置」添加第一个。</p>
</div>
</section>
</main> </main>
</div> </div>
<script src="/static/js/api.js"></script> <script src="/static/js/api.js"></script>
<script src="/static/js/report.js"></script> <script src="/static/js/report.js"></script>
<script src="/static/js/profiles.js"></script>
<script src="/static/js/runner.js"></script> <script src="/static/js/runner.js"></script>
<script src="/static/js/app.js"></script> <script src="/static/js/app.js"></script>
</body> </body>

View File

@@ -43,4 +43,26 @@ const API = {
return API.post("/api/evaluations", { scenario_path: scenarioPath }); return API.post("/api/evaluations", { scenario_path: scenarioPath });
}, },
taskStatus(taskId) { return API.get(`/api/evaluations/${encodeURIComponent(taskId)}`); }, taskStatus(taskId) { return API.get(`/api/evaluations/${encodeURIComponent(taskId)}`); },
// LLM Profile API
profiles() { return API.get("/api/llm-profiles"); },
createProfile(body) { return API.post("/api/llm-profiles", body); },
updateProfile(id, body) {
return fetch(`/api/llm-profiles/${encodeURIComponent(id)}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
}).then(async r => {
if (!r.ok) { const d = await API._extractError(r); throw new Error(d); }
return r.json();
});
},
deleteProfile(id) {
return fetch(`/api/llm-profiles/${encodeURIComponent(id)}`, { method: "DELETE" })
.then(async r => {
if (!r.ok) { const d = await API._extractError(r); throw new Error(d); }
return r.json();
});
},
applyProfiles(body) { return API.post("/api/llm-profiles/apply", body); },
}; };

View File

@@ -2,8 +2,8 @@
const App = { const App = {
currentRunId: null, currentRunId: null,
views: ["runs", "new", "report"], views: ["runs", "new", "report", "profiles"],
titles: { runs: "运行列表", new: "新建评估", report: "报告详情" }, titles: { runs: "运行列表", new: "新建评估", report: "报告详情", profiles: "LLM 配置" },
// 初始化:绑定导航、加载首屏、启动健康检查。 // 初始化:绑定导航、加载首屏、启动健康检查。
init() { init() {
@@ -13,6 +13,7 @@ const App = {
document.getElementById("refresh-btn").addEventListener("click", () => App.refreshCurrent()); document.getElementById("refresh-btn").addEventListener("click", () => App.refreshCurrent());
Runner.init(); Runner.init();
Profiles.init();
App.switchView("runs"); App.switchView("runs");
App.checkHealth(); App.checkHealth();
setInterval(App.checkHealth, 15000); setInterval(App.checkHealth, 15000);
@@ -36,6 +37,7 @@ const App = {
if (view === "runs") App.loadRuns(); if (view === "runs") App.loadRuns();
if (view === "new") Runner.loadScenarios(); if (view === "new") Runner.loadScenarios();
if (view === "report") Report.render(App.currentRunId); if (view === "report") Report.render(App.currentRunId);
if (view === "profiles") Profiles.load();
}, },
// 刷新当前视图的数据。 // 刷新当前视图的数据。

View File

@@ -0,0 +1,118 @@
// profiles.js — LLM 配置管理页面逻辑
const Profiles = {
_data: [],
// 初始化:绑定按钮事件
init() {
document.getElementById("add-profile-btn").addEventListener("click", () => Profiles.showForm());
document.getElementById("save-profile-btn").addEventListener("click", () => Profiles.save());
document.getElementById("cancel-profile-btn").addEventListener("click", () => Profiles.hideForm());
},
// 加载并渲染 Profile 列表
async load() {
const grid = document.getElementById("profile-cards");
const empty = document.getElementById("profiles-empty");
grid.innerHTML = '<p class="muted">加载中…</p>';
try {
const data = await API.profiles();
Profiles._data = data.profiles || [];
grid.innerHTML = "";
if (Profiles._data.length === 0) {
empty.hidden = false;
} else {
empty.hidden = true;
Profiles._data.forEach(p => grid.appendChild(Profiles.renderCard(p)));
}
} catch (err) {
grid.innerHTML = `<p class="muted">加载失败:${App.escape(err.message)}</p>`;
}
},
// 渲染单个 Profile 卡片
renderCard(p) {
const card = document.createElement("div");
card.className = "profile-card";
card.dataset.id = p.profile_id;
card.innerHTML = `
<div class="profile-card-head">
<div class="profile-card-name">${App.escape(p.name)}</div>
<div class="profile-card-actions">
<button class="btn btn-sm" data-action="edit">编辑</button>
<button class="btn btn-sm btn-danger" data-action="delete">删除</button>
</div>
</div>
<div class="profile-card-field"><span class="field-label">模型</span> <code>${App.escape(p.model)}</code></div>
<div class="profile-card-field"><span class="field-label">Base URL</span> <code>${App.escape(p.base_url)}</code></div>
<div class="profile-card-field"><span class="field-label">超时</span> ${p.timeout_seconds}s</div>
`;
card.querySelector("[data-action=edit]").addEventListener("click", () => Profiles.showForm(p));
card.querySelector("[data-action=delete]").addEventListener("click", () => Profiles.remove(p.profile_id, p.name));
return card;
},
// 显示新建或编辑表单
showForm(profile = null) {
const panel = document.getElementById("profile-form-panel");
const title = document.getElementById("profile-form-title");
panel.hidden = false;
title.textContent = profile ? "编辑 LLM 配置" : "新建 LLM 配置";
document.getElementById("edit-profile-id").value = profile ? profile.profile_id : "";
document.getElementById("pf-name").value = profile ? profile.name : "";
document.getElementById("pf-model").value = profile ? profile.model : "";
document.getElementById("pf-base-url").value = profile ? profile.base_url : "";
document.getElementById("pf-api-key").value = profile ? profile.api_key : "";
document.getElementById("pf-timeout").value = profile ? profile.timeout_seconds : 30;
document.getElementById("profile-form-error").textContent = "";
panel.scrollIntoView({ behavior: "smooth", block: "start" });
},
hideForm() {
document.getElementById("profile-form-panel").hidden = true;
},
// 保存(新建 or 更新)
async save() {
const id = document.getElementById("edit-profile-id").value;
const body = {
name: document.getElementById("pf-name").value.trim(),
model: document.getElementById("pf-model").value.trim(),
base_url: document.getElementById("pf-base-url").value.trim(),
api_key: document.getElementById("pf-api-key").value.trim(),
timeout_seconds: parseInt(document.getElementById("pf-timeout").value, 10) || 30,
};
const errEl = document.getElementById("profile-form-error");
if (!body.name || !body.model || !body.base_url || !body.api_key) {
errEl.textContent = "请填写所有必填字段名称、模型、Base URL、API Key";
return;
}
try {
if (id) {
await API.updateProfile(id, body);
} else {
await API.createProfile(body);
}
Profiles.hideForm();
await Profiles.load();
} catch (err) {
errEl.textContent = `保存失败:${err.message}`;
}
},
// 删除 Profile
async remove(profileId, name) {
if (!confirm(`确认删除配置「${name}」?`)) return;
try {
await API.deleteProfile(profileId);
await Profiles.load();
} catch (err) {
alert(`删除失败:${err.message}`);
}
},
// 获取当前已加载的 profiles供 runner.js 使用)
getAll() {
return Profiles._data;
},
};