mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-05-06 20:32:47 +08:00
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:
@@ -1,18 +1,21 @@
|
||||
"""
|
||||
配置路由模块
|
||||
"""
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from app.core.security import verify_auth_token
|
||||
from app.log.logger import get_config_routes_logger, Logger
|
||||
from app.log.logger import Logger, get_config_routes_logger
|
||||
from app.service.config.config_service import ConfigService
|
||||
|
||||
router = APIRouter(prefix="/api/config", tags=["config"])
|
||||
|
||||
logger = get_config_routes_logger()
|
||||
|
||||
|
||||
@router.get("", response_model=Dict[str, Any])
|
||||
async def get_config(request: Request):
|
||||
auth_token = request.cookies.get("auth_token")
|
||||
@@ -49,3 +52,23 @@ async def reset_config(request: Request):
|
||||
return await ConfigService.reset_config()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/ui/models")
|
||||
async def get_ui_models(request: Request):
|
||||
auth_token_cookie = request.cookies.get("auth_token")
|
||||
if not auth_token_cookie or not verify_auth_token(auth_token_cookie):
|
||||
logger.warning("Unauthorized access attempt to /api/config/ui/models")
|
||||
raise HTTPException(status_code=403, detail="Not authenticated")
|
||||
|
||||
try:
|
||||
models = await ConfigService.fetch_ui_models()
|
||||
return models
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in /ui/models endpoint: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"An unexpected error occurred while fetching UI models: {str(e)}",
|
||||
)
|
||||
|
||||
@@ -1,41 +1,49 @@
|
||||
"""
|
||||
配置服务模块
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from dotenv import find_dotenv, load_dotenv
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import insert, update
|
||||
|
||||
from app.config.config import Settings as ConfigSettings
|
||||
from app.config.config import settings
|
||||
from app.database.connection import database
|
||||
from app.database.models import Settings
|
||||
from app.config.config import Settings as ConfigSettings
|
||||
from app.database.services import get_all_settings
|
||||
from app.service.key.key_manager import get_key_manager_instance, reset_key_manager_instance
|
||||
from app.log.logger import get_config_routes_logger
|
||||
from app.service.key.key_manager import (
|
||||
get_key_manager_instance,
|
||||
reset_key_manager_instance,
|
||||
)
|
||||
from app.service.model.model_service import ModelService
|
||||
|
||||
logger = get_config_routes_logger()
|
||||
|
||||
|
||||
class ConfigService:
|
||||
"""配置服务类,用于管理应用程序配置"""
|
||||
|
||||
|
||||
@staticmethod
|
||||
async def get_config() -> Dict[str, Any]:
|
||||
return settings.model_dump()
|
||||
|
||||
|
||||
@staticmethod
|
||||
async def update_config(config_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
for key, value in config_data.items():
|
||||
if hasattr(settings, key):
|
||||
setattr(settings, key, value)
|
||||
logger.debug(f"Updated setting in memory: {key}")
|
||||
|
||||
logger.debug(f"Updated setting in memory: {key}")
|
||||
|
||||
# 获取现有设置
|
||||
existing_settings_raw: List[Dict[str, Any]] = await get_all_settings()
|
||||
existing_settings_map: Dict[str, Dict[str, Any]] = {s['key']: s for s in existing_settings_raw}
|
||||
existing_settings_map: Dict[str, Dict[str, Any]] = {
|
||||
s["key"]: s for s in existing_settings_raw
|
||||
}
|
||||
existing_keys = set(existing_settings_map.keys())
|
||||
|
||||
settings_to_update: List[Dict[str, Any]] = []
|
||||
@@ -47,7 +55,7 @@ class ConfigService:
|
||||
# 处理不同类型的值
|
||||
if isinstance(value, list):
|
||||
db_value = json.dumps(value)
|
||||
elif isinstance(value, dict): # 新增对 dict 类型的处理
|
||||
elif isinstance(value, dict): # 新增对 dict 类型的处理
|
||||
db_value = json.dumps(value)
|
||||
elif isinstance(value, bool):
|
||||
db_value = str(value).lower()
|
||||
@@ -55,24 +63,26 @@ class ConfigService:
|
||||
db_value = str(value)
|
||||
|
||||
# 仅当值发生变化时才更新
|
||||
if key in existing_keys and existing_settings_map[key]['value'] == db_value:
|
||||
continue
|
||||
if key in existing_keys and existing_settings_map[key]["value"] == db_value:
|
||||
continue
|
||||
|
||||
description = f"{key}配置项"
|
||||
description = f"{key}配置项"
|
||||
|
||||
data = {
|
||||
'key': key,
|
||||
'value': db_value,
|
||||
'description': description,
|
||||
'updated_at': now
|
||||
"key": key,
|
||||
"value": db_value,
|
||||
"description": description,
|
||||
"updated_at": now,
|
||||
}
|
||||
|
||||
if key in existing_keys:
|
||||
# Preserve original description if not explicitly provided
|
||||
data['description'] = existing_settings_map[key].get('description', description)
|
||||
data["description"] = existing_settings_map[key].get(
|
||||
"description", description
|
||||
)
|
||||
settings_to_update.append(data)
|
||||
else:
|
||||
data['created_at'] = now
|
||||
data["created_at"] = now
|
||||
settings_to_insert.append(data)
|
||||
|
||||
# 在事务中执行批量插入和更新
|
||||
@@ -82,17 +92,19 @@ class ConfigService:
|
||||
if settings_to_insert:
|
||||
query_insert = insert(Settings).values(settings_to_insert)
|
||||
await database.execute(query=query_insert)
|
||||
logger.info(f"Bulk inserted {len(settings_to_insert)} settings.")
|
||||
logger.info(
|
||||
f"Bulk inserted {len(settings_to_insert)} settings."
|
||||
)
|
||||
|
||||
if settings_to_update:
|
||||
for setting_data in settings_to_update:
|
||||
query_update = (
|
||||
update(Settings)
|
||||
.where(Settings.key == setting_data['key'])
|
||||
.where(Settings.key == setting_data["key"])
|
||||
.values(
|
||||
value=setting_data['value'],
|
||||
description=setting_data['description'],
|
||||
updated_at=setting_data['updated_at']
|
||||
value=setting_data["value"],
|
||||
description=setting_data["description"],
|
||||
updated_at=setting_data["updated_at"],
|
||||
)
|
||||
)
|
||||
await database.execute(query=query_update)
|
||||
@@ -112,7 +124,7 @@ class ConfigService:
|
||||
# For now, we log the error and continue
|
||||
|
||||
return await ConfigService.get_config()
|
||||
|
||||
|
||||
@staticmethod
|
||||
async def reset_config() -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -124,7 +136,9 @@ class ConfigService:
|
||||
"""
|
||||
# 1. 重新加载配置对象,它应该处理环境变量和 .env 的优先级
|
||||
_reload_settings()
|
||||
logger.info("Settings object reloaded, prioritizing system environment variables then .env file.")
|
||||
logger.info(
|
||||
"Settings object reloaded, prioritizing system environment variables then .env file."
|
||||
)
|
||||
|
||||
# 2. 重置并重新初始化 KeyManager
|
||||
try:
|
||||
@@ -140,6 +154,36 @@ class ConfigService:
|
||||
# 3. 返回更新后的配置
|
||||
return await ConfigService.get_config()
|
||||
|
||||
@staticmethod
|
||||
async def fetch_ui_models() -> List[Dict[str, Any]]:
|
||||
"""获取用于UI显示的模型列表"""
|
||||
try:
|
||||
key_manager = await get_key_manager_instance()
|
||||
model_service = ModelService()
|
||||
|
||||
api_key = await key_manager.get_first_valid_key()
|
||||
if not api_key:
|
||||
logger.error("No valid API keys available to fetch model list for UI.")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="No valid API keys available to fetch model list.",
|
||||
)
|
||||
|
||||
models = await model_service.get_gemini_openai_models(api_key)
|
||||
return models
|
||||
except HTTPException as e:
|
||||
# Re-raise HTTPExceptions directly if they are already specific
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to fetch models for UI in ConfigService: {e}", exc_info=True
|
||||
)
|
||||
# Raise a generic HTTPException for other errors
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to fetch models for UI: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
# 重新加载配置的函数
|
||||
def _reload_settings():
|
||||
"""重新加载环境变量并更新配置"""
|
||||
@@ -147,4 +191,4 @@ def _reload_settings():
|
||||
load_dotenv(find_dotenv(), override=True)
|
||||
# 更新现有 settings 对象的属性,而不是新建实例
|
||||
for key, value in ConfigSettings().model_dump().items():
|
||||
setattr(settings, key, value)
|
||||
setattr(settings, key, value)
|
||||
|
||||
@@ -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 --
|
||||
|
||||
@@ -557,13 +557,23 @@ endblock %} {% block head_extra_styles %}
|
||||
<label for="TEST_MODEL" class="block font-semibold mb-2 text-gray-700"
|
||||
>测试模型</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="TEST_MODEL"
|
||||
name="TEST_MODEL"
|
||||
placeholder="gemini-1.5-flash"
|
||||
class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 form-input-themed"
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
id="TEST_MODEL"
|
||||
name="TEST_MODEL"
|
||||
placeholder="gemini-1.5-flash"
|
||||
class="flex-grow px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 form-input-themed"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
title="选择模型"
|
||||
class="model-helper-trigger-btn p-2 rounded-md text-violet-300 hover:bg-violet-700 transition-colors"
|
||||
data-target-input-id="TEST_MODEL"
|
||||
>
|
||||
<i class="fas fa-list-ul"></i>
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-gray-500 mt-1 block">用于测试API密钥的模型</small>
|
||||
</div>
|
||||
|
||||
@@ -577,7 +587,15 @@ endblock %} {% block head_extra_styles %}
|
||||
<div class="array-container" id="IMAGE_MODELS_container">
|
||||
<!-- 数组项将在这里动态添加 -->
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<div class="flex justify-end gap-2 mt-2">
|
||||
<button
|
||||
type="button"
|
||||
title="从列表选择模型添加到下方"
|
||||
class="model-helper-trigger-btn p-2 rounded-md text-violet-300 hover:bg-violet-700 transition-colors"
|
||||
data-target-array-key="IMAGE_MODELS"
|
||||
>
|
||||
<i class="fas fa-list-ul"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2"
|
||||
@@ -599,7 +617,15 @@ endblock %} {% block head_extra_styles %}
|
||||
<div class="array-container" id="SEARCH_MODELS_container">
|
||||
<!-- 数组项将在这里动态添加 -->
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<div class="flex justify-end gap-2 mt-2">
|
||||
<button
|
||||
type="button"
|
||||
title="从列表选择模型添加到下方"
|
||||
class="model-helper-trigger-btn p-2 rounded-md text-violet-300 hover:bg-violet-700 transition-colors"
|
||||
data-target-array-key="SEARCH_MODELS"
|
||||
>
|
||||
<i class="fas fa-list-ul"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2"
|
||||
@@ -621,7 +647,15 @@ endblock %} {% block head_extra_styles %}
|
||||
<div class="array-container" id="FILTERED_MODELS_container">
|
||||
<!-- 数组项将在这里动态添加 -->
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<div class="flex justify-end gap-2 mt-2">
|
||||
<button
|
||||
type="button"
|
||||
title="从列表选择模型添加到下方"
|
||||
class="model-helper-trigger-btn p-2 rounded-md text-violet-300 hover:bg-violet-700 transition-colors"
|
||||
data-target-array-key="FILTERED_MODELS"
|
||||
>
|
||||
<i class="fas fa-list-ul"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2"
|
||||
@@ -708,7 +742,15 @@ endblock %} {% block head_extra_styles %}
|
||||
<div class="array-container" id="THINKING_MODELS_container">
|
||||
<!-- 数组项将在这里动态添加 -->
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<div class="flex justify-end gap-2 mt-2">
|
||||
<button
|
||||
type="button"
|
||||
title="从列表选择模型添加到下方"
|
||||
class="model-helper-trigger-btn p-2 rounded-md text-violet-300 hover:bg-violet-700 transition-colors"
|
||||
data-target-array-key="THINKING_MODELS"
|
||||
>
|
||||
<i class="fas fa-list-ul"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2"
|
||||
@@ -829,13 +871,23 @@ endblock %} {% block head_extra_styles %}
|
||||
class="block font-semibold mb-2 text-gray-700"
|
||||
>图像生成模型</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="CREATE_IMAGE_MODEL"
|
||||
name="CREATE_IMAGE_MODEL"
|
||||
placeholder="imagen-3.0-generate-002"
|
||||
class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 form-input-themed"
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
id="CREATE_IMAGE_MODEL"
|
||||
name="CREATE_IMAGE_MODEL"
|
||||
placeholder="imagen-3.0-generate-002"
|
||||
class="flex-grow px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 form-input-themed"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
title="选择模型"
|
||||
class="model-helper-trigger-btn p-2 rounded-md text-violet-300 hover:bg-violet-700 transition-colors"
|
||||
data-target-input-id="CREATE_IMAGE_MODEL"
|
||||
>
|
||||
<i class="fas fa-list-ul"></i>
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-gray-500 mt-1 block">用于图像生成的模型</small>
|
||||
</div>
|
||||
|
||||
@@ -1505,6 +1557,55 @@ endblock %} {% block head_extra_styles %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model Helper Modal -->
|
||||
<div id="modelHelperModal" class="modal">
|
||||
<div
|
||||
class="w-full max-w-lg mx-auto rounded-2xl shadow-2xl overflow-hidden animate-fade-in"
|
||||
style="
|
||||
background-color: rgba(70, 50, 150, 0.95);
|
||||
color: #ffffff;
|
||||
border: 1px solid rgba(120, 100, 200, 0.4);
|
||||
"
|
||||
>
|
||||
<div class="p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 id="modelHelperTitle" class="text-xl font-bold text-gray-100">
|
||||
选择模型
|
||||
</h2>
|
||||
<button
|
||||
id="closeModelHelperModalBtn"
|
||||
class="text-gray-300 hover:text-gray-100 text-xl"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="modelHelperSearchInput"
|
||||
placeholder="搜索模型..."
|
||||
class="w-full px-4 py-3 mb-4 rounded-lg border font-mono text-sm form-input-themed"
|
||||
/>
|
||||
<div
|
||||
id="modelHelperListContainer"
|
||||
class="array-container"
|
||||
style="max-height: 300px; overflow-y: auto"
|
||||
>
|
||||
<!-- Model items will be populated here -->
|
||||
<p class="text-gray-400 text-sm italic">正在加载模型列表...</p>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3 mt-6">
|
||||
<button
|
||||
type="button"
|
||||
id="cancelModelHelperBtn"
|
||||
class="bg-gray-500 bg-opacity-50 hover:bg-opacity-70 text-gray-200 px-6 py-2 rounded-lg font-medium transition"
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %} {% block body_scripts %}
|
||||
<script src="/static/js/config_editor.js"></script>
|
||||
<!-- 增强下拉框样式和交互性 -->
|
||||
|
||||
Reference in New Issue
Block a user