v2.5.0 模型实时获取:接口配置区「📋 查看模型」按钮,Base URL+API Key 下实时拉取模型列表(GET /models),搜索/点击填入,仍支持手动输入
This commit is contained in:
@@ -129,6 +129,34 @@ body {
|
||||
.conn-result.ok { background: rgba(34,197,139,.12); color: var(--accent2); border: 1px solid rgba(34,197,139,.3); }
|
||||
.conn-result.fail { background: rgba(255,92,108,.1); color: var(--danger); border: 1px solid rgba(255,92,108,.3); }
|
||||
|
||||
/* ── 查看模型(实时获取模型列表下拉面板) ── */
|
||||
.model-pick {
|
||||
margin-top: 8px; background: var(--panel2); border: 1px solid var(--accent);
|
||||
border-radius: 10px; overflow: hidden;
|
||||
}
|
||||
.model-pick[hidden] { display: none; }
|
||||
.model-pick-head {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
padding: 8px; border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.model-pick-head input {
|
||||
flex: 1; background: var(--bg); color: var(--text);
|
||||
border: 1px solid var(--border); border-radius: 6px;
|
||||
padding: 6px 8px; font-size: 12px; outline: none;
|
||||
}
|
||||
.model-pick-head input:focus { border-color: var(--accent); }
|
||||
.model-pick-list { max-height: 240px; overflow-y: auto; }
|
||||
.model-pick-item {
|
||||
padding: 7px 10px; font-size: 12.5px; cursor: pointer;
|
||||
font-family: var(--mono); word-break: break-all;
|
||||
border-bottom: 1px solid rgba(42,53,80,.5);
|
||||
}
|
||||
.model-pick-item:hover { background: rgba(79,140,255,.16); color: var(--accent); }
|
||||
.model-pick-item.active { background: rgba(34,197,139,.14); color: var(--accent2); }
|
||||
.model-pick-empty {
|
||||
padding: 14px 10px; color: var(--muted); font-size: 12px; text-align: center;
|
||||
}
|
||||
|
||||
/* ── 指标卡 ── */
|
||||
.metrics { display: grid; grid-template-columns: repeat(6, 1fr); gap: 12px; }
|
||||
.metric-card {
|
||||
|
||||
+13
-2
@@ -55,8 +55,19 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>模型名称</label>
|
||||
<div class="icon-input"><span class="icon">🤖</span><input id="cfg-model" placeholder="如 gpt-4o / deepseek-chat / claude-sonnet-4"></div>
|
||||
<label>模型名称 <span class="hint-inline">可直接输入,也可点击「查看模型」实时获取</span></label>
|
||||
<div class="row">
|
||||
<div class="icon-input" style="flex:1"><span class="icon">🤖</span><input id="cfg-model" placeholder="如 gpt-4o / deepseek-chat / claude-sonnet-4"></div>
|
||||
<button class="btn small" id="btn-list-models" title="实时获取此接口(Base URL + API Key)下的所有模型并选择填入">📋 查看模型</button>
|
||||
</div>
|
||||
<div class="model-pick" id="model-pick" hidden>
|
||||
<div class="model-pick-head">
|
||||
<input id="model-pick-search" placeholder="🔍 搜索模型(可直接输入模型名回车确认)" autocomplete="off">
|
||||
<button class="btn small" id="model-pick-close" title="关闭">✕</button>
|
||||
</div>
|
||||
<div class="model-pick-list" id="model-pick-list"></div>
|
||||
</div>
|
||||
<div class="conn-result" id="model-result" hidden></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>温度 Temperature <b id="tmp-val">0.7</b></label>
|
||||
|
||||
+82
-2
@@ -191,6 +191,7 @@ function loadSelectedConfig() {
|
||||
$("#cfg-temp").value = c.temperature ?? 0.7;
|
||||
$("#tmp-val").textContent = (c.temperature ?? 0.7).toFixed(1);
|
||||
updateDefaultUrlHint();
|
||||
hideModelPick();
|
||||
flash("已加载配置:" + c.name);
|
||||
}).catch(() => toast("加载失败"));
|
||||
}
|
||||
@@ -223,6 +224,81 @@ function showConn(ok, text) {
|
||||
box.textContent = text;
|
||||
}
|
||||
|
||||
/* ───────────────────────── 查看模型(实时获取接口下所有模型) ───────────────────────── */
|
||||
|
||||
function hideModelPick() {
|
||||
$("#model-pick").hidden = true;
|
||||
$("#model-result").hidden = true;
|
||||
}
|
||||
|
||||
async function listModels() {
|
||||
const cfg = currentConfig();
|
||||
hideModelPick();
|
||||
if (!cfg.base_url) { toast("请先填写 Base URL"); return; }
|
||||
if (!cfg.api_key) { toast("请先填写 API Key"); return; }
|
||||
const panel = $("#model-pick");
|
||||
panel.hidden = false;
|
||||
const listEl = $("#model-pick-list");
|
||||
listEl.innerHTML = '<div class="model-pick-empty">⏳ 正在获取模型列表(GET /models)...</div>';
|
||||
try {
|
||||
const r = await api("/api/models", "POST", cfg);
|
||||
if (!r.ok) throw new Error(r.error || "获取失败");
|
||||
const models = r.models || [];
|
||||
if (!models.length) {
|
||||
listEl.innerHTML = '<div class="model-pick-empty">⚠️ ' + esc(r.note || "接口可用,但未返回任何模型") + '</div>';
|
||||
return;
|
||||
}
|
||||
renderModelList(models);
|
||||
} catch (e) {
|
||||
panel.hidden = true;
|
||||
const box = $("#model-result");
|
||||
box.hidden = false;
|
||||
box.className = "conn-result fail";
|
||||
box.textContent = "❌ 获取模型失败:" + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
function renderModelList(models) {
|
||||
const listEl = $("#model-pick-list");
|
||||
const search = $("#model-pick-search");
|
||||
search.value = "";
|
||||
listEl.innerHTML = "";
|
||||
const fill = (m) => {
|
||||
$("#cfg-model").value = m;
|
||||
hideModelPick();
|
||||
toast("已填入模型:" + m);
|
||||
};
|
||||
const render = () => {
|
||||
const q = search.value.trim().toLowerCase();
|
||||
const cur = $("#cfg-model").value.trim();
|
||||
const filtered = q ? models.filter((m) => m.toLowerCase().includes(q)) : models;
|
||||
if (!filtered.length) {
|
||||
listEl.innerHTML = '<div class="model-pick-empty">无匹配模型,可直接在上方输入模型名</div>';
|
||||
return;
|
||||
}
|
||||
listEl.innerHTML = "";
|
||||
for (const m of filtered) {
|
||||
const item = document.createElement("div");
|
||||
item.className = "model-pick-item" + (m === cur ? " active" : "");
|
||||
item.textContent = m;
|
||||
item.title = "点击填入模型名称";
|
||||
item.addEventListener("click", () => fill(m));
|
||||
listEl.appendChild(item);
|
||||
}
|
||||
};
|
||||
search.addEventListener("input", render);
|
||||
search.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
const v = search.value.trim();
|
||||
if (v) fill(v); // 手动输入回车直接确认填入
|
||||
} else if (e.key === "Escape") {
|
||||
hideModelPick();
|
||||
}
|
||||
});
|
||||
render();
|
||||
search.focus();
|
||||
}
|
||||
|
||||
/* ───────────────────────── 测试运行 ───────────────────────── */
|
||||
|
||||
function setStatus(state, label) {
|
||||
@@ -951,13 +1027,17 @@ function flash(msg) {
|
||||
/* ───────────────────────── 事件绑定 ───────────────────────── */
|
||||
|
||||
function bind() {
|
||||
$("#cfg-provider").addEventListener("change", updateDefaultUrlHint);
|
||||
$("#cfg-provider").addEventListener("change", () => { updateDefaultUrlHint(); hideModelPick(); });
|
||||
$("#cfg-baseurl").addEventListener("input", hideModelPick);
|
||||
$("#cfg-apikey").addEventListener("input", hideModelPick);
|
||||
$("#cfg-temp").addEventListener("input", () => $("#tmp-val").textContent = parseFloat($("#cfg-temp").value).toFixed(1));
|
||||
|
||||
$("#btn-toggle-key").addEventListener("click", () => {
|
||||
const inp = $("#cfg-apikey");
|
||||
inp.type = inp.type === "password" ? "text" : "password";
|
||||
});
|
||||
$("#btn-list-models").addEventListener("click", listModels);
|
||||
$("#model-pick-close").addEventListener("click", hideModelPick);
|
||||
$("#btn-save-config").addEventListener("click", async () => {
|
||||
const r = await api("/api/configs", "POST", currentConfig());
|
||||
if (r.ok) { toast("配置已保存"); loadConfigs(); }
|
||||
@@ -1027,7 +1107,7 @@ function bind() {
|
||||
$("#dt-export-xlsx").addEventListener("click", () => { if (window.__detail) exportXlsx(window.__detail.id); });
|
||||
$("#dt-send-eval").addEventListener("click", () => { if (window.__detail) sendToEval(window.__detail.id); });
|
||||
|
||||
document.addEventListener("keydown", (e) => { if (e.key === "Escape") { closeDetail(); closeCompare(); } });
|
||||
document.addEventListener("keydown", (e) => { if (e.key === "Escape") { hideModelPick(); closeDetail(); closeCompare(); } });
|
||||
}
|
||||
|
||||
/* ───────────────────────── 初始化 ───────────────────────── */
|
||||
|
||||
Reference in New Issue
Block a user