feat(config): 添加模型助手功能以选择和管理模型

本次提交主要包含以下更改:

1. **后端更新**:
   - 在 `app/service/config/config_service.py` 中新增 `fetch_ui_models` 方法,用于获取可用于 UI 的模型列表,并处理相关的错误情况。
   - 在 `app/router/config_routes.py` 中新增 `/ui/models` 路由,提供模型列表的 API 接口,并添加身份验证逻辑。

2. **前端更新**:
   - 在 `app/static/js/config_editor.js` 中实现模型助手的功能,包括模型列表的加载、搜索和选择。
   - 在 `app/templates/config_editor.html` 中添加模型助手的模态框和相关的 UI 元素,允许用户从列表中选择模型。

这些更改旨在增强用户体验,使用户能够更方便地选择和管理模型,提高配置界面的交互性和功能性。
This commit is contained in:
snaily
2025-05-08 19:48:03 +08:00
parent 30bf666a57
commit f1f568afca
4 changed files with 405 additions and 44 deletions
+193
View File
@@ -31,6 +31,23 @@ const bulkDeleteProxyInput = document.getElementById("bulkDeleteProxyInput");
const resetConfirmModal = document.getElementById("resetConfirmModal");
const configForm = document.getElementById("configForm"); // Added for frequent use
// Model Helper Modal Elements
const modelHelperModal = document.getElementById("modelHelperModal");
const modelHelperTitleElement = document.getElementById("modelHelperTitle");
const modelHelperSearchInput = document.getElementById(
"modelHelperSearchInput"
);
const modelHelperListContainer = document.getElementById(
"modelHelperListContainer"
);
const closeModelHelperModalBtn = document.getElementById(
"closeModelHelperModalBtn"
);
const cancelModelHelperBtn = document.getElementById("cancelModelHelperBtn");
let cachedModelsList = null;
let currentModelHelperTarget = null; // { type: 'input'/'array', target: elementOrIdOrKey }
// Modal Control Functions
function openModal(modalElement) {
if (modalElement) {
@@ -227,6 +244,7 @@ document.addEventListener("DOMContentLoaded", function () {
bulkDeleteApiKeyModal,
proxyModal,
bulkDeleteProxyModal,
modelHelperModal,
];
modals.forEach((modal) => {
if (event.target === modal) {
@@ -353,6 +371,44 @@ document.addEventListener("DOMContentLoaded", function () {
}
initializeSensitiveFields(); // Initialize sensitive field handling
// Model Helper Modal Event Listeners
if (closeModelHelperModalBtn) {
closeModelHelperModalBtn.addEventListener("click", () =>
closeModal(modelHelperModal)
);
}
if (cancelModelHelperBtn) {
cancelModelHelperBtn.addEventListener("click", () =>
closeModal(modelHelperModal)
);
}
if (modelHelperSearchInput) {
modelHelperSearchInput.addEventListener("input", () =>
renderModelsInModal()
);
}
// Add event listeners to all model helper trigger buttons
const modelHelperTriggerBtns = document.querySelectorAll(
".model-helper-trigger-btn"
);
modelHelperTriggerBtns.forEach((btn) => {
btn.addEventListener("click", () => {
const targetInputId = btn.dataset.targetInputId;
const targetArrayKey = btn.dataset.targetArrayKey;
if (targetInputId) {
currentModelHelperTarget = {
type: "input",
target: document.getElementById(targetInputId),
};
} else if (targetArrayKey) {
currentModelHelperTarget = { type: "array", targetKey: targetArrayKey };
}
openModelHelperModal();
});
});
}); // <-- DOMContentLoaded end
/**
@@ -1719,3 +1775,140 @@ function addSafetySettingItem(category = "", threshold = "") {
container.appendChild(settingItem);
}
// --- Model Helper Functions ---
async function fetchModels() {
if (cachedModelsList) {
return cachedModelsList;
}
try {
showNotification("正在从 /api/config/ui/models 加载模型列表...", "info");
const response = await fetch("/api/config/ui/models");
if (!response.ok) {
const errorData = await response.text();
throw new Error(`HTTP error ${response.status}: ${errorData}`);
}
const responseData = await response.json(); // Changed variable name to responseData
// The backend returns an object like: { object: "list", data: [{id: "m1"}, {id: "m2"}], success: true }
if (
responseData &&
responseData.success &&
Array.isArray(responseData.data)
) {
cachedModelsList = responseData.data; // Use responseData.data
showNotification("模型列表加载成功", "success");
return cachedModelsList;
} else {
console.error("Invalid model list format received:", responseData);
throw new Error("模型列表格式无效或请求未成功");
}
} catch (error) {
console.error("加载模型列表失败:", error);
showNotification(`加载模型列表失败: ${error.message}`, "error");
cachedModelsList = []; // Avoid repeated fetches on error for this session, or set to null to retry
return [];
}
}
function renderModelsInModal() {
if (!modelHelperListContainer) return;
if (!cachedModelsList) {
modelHelperListContainer.innerHTML =
'<p class="text-gray-400 text-sm italic">模型列表尚未加载。</p>';
return;
}
const searchTerm = modelHelperSearchInput.value.toLowerCase();
const filteredModels = cachedModelsList.filter((model) =>
model.id.toLowerCase().includes(searchTerm)
);
modelHelperListContainer.innerHTML = ""; // Clear previous items
if (filteredModels.length === 0) {
modelHelperListContainer.innerHTML =
'<p class="text-gray-400 text-sm italic">未找到匹配的模型。</p>';
return;
}
filteredModels.forEach((model) => {
const modelItemElement = document.createElement("button");
modelItemElement.type = "button";
modelItemElement.textContent = model.id;
modelItemElement.className =
"block w-full text-left px-4 py-2 rounded-md hover:bg-violet-700 focus:bg-violet-700 focus:outline-none transition-colors text-gray-200";
// Add any other classes for styling, e.g., from existing modals or array items
modelItemElement.addEventListener("click", () =>
handleModelSelection(model.id)
);
modelHelperListContainer.appendChild(modelItemElement);
});
}
async function openModelHelperModal() {
if (!currentModelHelperTarget) {
console.error("Model helper target not set.");
showNotification("无法打开模型助手:目标未设置", "error");
return;
}
await fetchModels(); // Ensure models are loaded
renderModelsInModal(); // Render them (handles empty/error cases internally)
if (modelHelperTitleElement) {
if (
currentModelHelperTarget.type === "input" &&
currentModelHelperTarget.target
) {
const label = document.querySelector(
`label[for="${currentModelHelperTarget.target.id}"]`
);
modelHelperTitleElement.textContent = label
? `为 "${label.textContent.trim()}" 选择模型`
: "选择模型";
} else if (currentModelHelperTarget.type === "array") {
modelHelperTitleElement.textContent = `${currentModelHelperTarget.targetKey} 添加模型`;
} else {
modelHelperTitleElement.textContent = "选择模型";
}
}
if (modelHelperSearchInput) modelHelperSearchInput.value = ""; // Clear search on open
if (modelHelperModal) openModal(modelHelperModal);
}
function handleModelSelection(selectedModelId) {
if (!currentModelHelperTarget) return;
if (
currentModelHelperTarget.type === "input" &&
currentModelHelperTarget.target
) {
const inputElement = currentModelHelperTarget.target;
inputElement.value = selectedModelId;
// If the input is a sensitive field, dispatch focusout to trigger masking behavior if needed
if (inputElement.classList.contains(SENSITIVE_INPUT_CLASS)) {
const event = new Event("focusout", { bubbles: true, cancelable: true });
inputElement.dispatchEvent(event);
}
// Dispatch input event for any other listeners
inputElement.dispatchEvent(new Event("input", { bubbles: true }));
} else if (
currentModelHelperTarget.type === "array" &&
currentModelHelperTarget.targetKey
) {
const modelId = addArrayItemWithValue(
currentModelHelperTarget.targetKey,
selectedModelId
);
if (currentModelHelperTarget.targetKey === "THINKING_MODELS" && modelId) {
// Automatically add corresponding budget map item with default budget 0
createAndAppendBudgetMapItem(selectedModelId, 0, modelId);
}
}
if (modelHelperModal) closeModal(modelHelperModal);
currentModelHelperTarget = null; // Reset target
}
// -- End Model Helper Functions --