# LLM Config Table UI Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 将 Settings 页面的 LLM 模型配置从卡片列表改为表格行内编辑形式 **Architecture:** 使用 Vue 3 Composition API,在 SettingsView.vue 内实现 4 个 LLM 类型区的表格组件,每种类型独立成区,支持行内展开编辑、测试连接、保存操作。 **Tech Stack:** Vue 3, TypeScript, Lucide Icons --- ## File Structure ``` frontend/src/ ├── views/ │ └── SettingsView.vue # 重构:表格行内编辑 UI(所有逻辑内聚在此文件) └── components/settings/ └── LLMTableRow.vue # 表格行组件(抽取以保持 SettingsView.vue 简洁) backend/app/ ├── routers/settings.py # 确认测试 API 存在 └── services/settings_service.py # 确认无需修改 ``` --- ## Task 1: 验证后端测试 API **Files:** - Modify: `backend/app/routers/settings.py` - Modify: `backend/app/services/settings_service.py` - [ ] **Step 1: 确认 `/api/settings/llm/test` 端点存在** 检查 `backend/app/routers/settings.py` 中是否有 `POST /api/settings/llm/test` 路由。 - [ ] **Step 2: 确认 `test_llm_connection` 函数存在** 检查 `backend/app/services/settings_service.py` 中是否有 `test_llm_connection` 函数。 - [ ] **Step 3: 提交** ```bash git log --oneline -1 # 如果确认后端无需修改: echo "Backend API already supports the new UI" # 如果发现问题需要修复: # git add backend/app/routers/settings.py # git commit -m "fix(settings): ensure test LLM API exists" ``` --- ## Task 2: 创建 LLMTableRow 组件 **Files:** - Create: `frontend/src/components/settings/LLMTableRow.vue` - Modify: `frontend/src/views/SettingsView.vue` - [ ] **Step 1: 创建 LLMTableRow.vue 组件** ```vue {{ model.name || '未命名' }} {{ model.provider }} {{ model.model || '-' }} {{ statusConfig.icon }} {{ statusConfig.label }} // PROVIDER OpenAI Claude Ollama DeepSeek Custom // MODEL // BASE URL // API KEY 测试连接 保存 取消 ``` - [ ] **Step 2: 在 SettingsView.vue 中引入 LLMTableRow** ```typescript import LLMTableRow from '@/components/settings/LLMTableRow.vue' ``` - [ ] **Step 3: 提交** ```bash git add frontend/src/components/settings/LLMTableRow.vue git commit -m "feat(settings): create LLMTableRow component" ``` --- ## Task 3: 重构 SettingsView.vue 主体结构 **Files:** - Modify: `frontend/src/views/SettingsView.vue` - [ ] **Step 1: 清理原有 LLM 配置相关代码** 删除原有的 `llmConfig` 卡片列表 UI(template 中的 `.model-list`、`.model-item` 等部分),保留 profile 和 scheduler 配置部分。 - [ ] **Step 2: 添加 LLM 配置状态管理** ```typescript // LLM 配置 const llmConfig = ref({ chat: [], vlm: [], embedding: [], rerank: [] }) // 原始配置(用于比较变更) const originalLlmConfig = ref({ chat: [], vlm: [], embedding: [], rerank: [] }) // 展开的行 const expandedRow = ref(null) // 'chat-0', 'vlm-0' 等 // 当前正在编辑的模型快照(用于取消时恢复) const editingSnapshot = ref<{ type: string; index: number; data: LLMModelConfig } | null>(null) // 必填警告 const showRequiredWarning = computed(() => { return llmConfig.value.chat.length === 0 || llmConfig.value.embedding.length === 0 || llmConfig.value.rerank.length === 0 }) // 行标识 function getRowKey(type: string, index: number): string { return `${type}-${index}` } // 切换行展开 function toggleRow(type: string, index: number, model: LLMModelConfig) { const key = getRowKey(type, index) if (expandedRow.value === key) { expandedRow.value = null editingSnapshot.value = null } else { // 保存快照用于取消 editingSnapshot.value = { type, index, data: JSON.parse(JSON.stringify(model)) } expandedRow.value = key } } // 取消编辑 function cancelEdit(type: string, index: number) { if (editingSnapshot.value && editingSnapshot.value.type === type && editingSnapshot.value.index === index) { // 恢复原始数据 llmConfig.value[type as keyof LLMConfig]![index] = editingSnapshot.value.data } expandedRow.value = null editingSnapshot.value = null } ``` - [ ] **Step 3: 实现添加模型** ```typescript function addModel(type: string) { if (!llmConfig.value[type as keyof LLMConfig]) { llmConfig.value[type as keyof LLMConfig] = [] } // embedding/rerank 最多 1 个 if ((type === 'embedding' || type === 'rerank') && llmConfig.value[type as keyof LLMConfig]!.length >= 1) { showToast(`${type === 'embedding' ? 'Embedding' : 'Rerank'} 最多配置 1 个`, 'error') return } const newModel: LLMModelConfig = { name: `${type.toUpperCase()}-${Date.now()}`, provider: 'openai', model: type === 'chat' ? 'gpt-4o' : type === 'vlm' ? 'gpt-4o' : type === 'embedding' ? 'text-embedding-3-small' : 'bge-reranker-v2', base_url: 'https://api.openai.com/v1', api_key: '', enabled: false } llmConfig.value[type as keyof LLMConfig]!.push(newModel) // 自动展开新添加的行 const newIndex = llmConfig.value[type as keyof LLMConfig]!.length - 1 expandedRow.value = getRowKey(type, newIndex) editingSnapshot.value = { type, index: newIndex, data: JSON.parse(JSON.stringify(newModel)) } } ``` - [ ] **Step 4: 实现删除模型** ```typescript function removeModel(type: string, index: number) { // embedding/rerank 为知识库必填,至少保留 1 个 if ((type === 'embedding' || type === 'rerank') && llmConfig.value[type as keyof LLMConfig]!.length <= 1) { showToast(`${type === 'embedding' ? 'Embedding' : 'Rerank'} 为知识库必填,至少保留 1 个`, 'error') return } llmConfig.value[type as keyof LLMConfig]!.splice(index, 1) expandedRow.value = null editingSnapshot.value = null } ``` - [ ] **Step 5: 实现更新模型** ```typescript function updateModel(type: string, index: number, model: LLMModelConfig) { llmConfig.value[type as keyof LLMConfig]![index] = model } ``` - [ ] **Step 6: 实现测试连接** ```typescript async function testModel(type: string, index: number, model: LLMModelConfig) { try { const res = await settingsApi.testLLM({ type: type as any, ...model }) if (res.data.success) { // 测试通过,标记为可用 llmConfig.value[type as keyof LLMConfig]![index].enabled = true showToast('连接成功') } else { llmConfig.value[type as keyof LLMConfig]![index].enabled = false showToast(`连接失败: ${res.data.error}`, 'error') } } catch (e) { llmConfig.value[type as keyof LLMConfig]![index].enabled = false showToast('测试连接失败', 'error') } } ``` - [ ] **Step 7: 实现保存模型** ```typescript async function saveModel(type: string, index: number) { const key = getRowKey(type, index) savingModel.value = key try { await settingsApi.updateLLM(llmConfig.value) originalLlmConfig.value = JSON.parse(JSON.stringify(llmConfig.value)) expandedRow.value = null editingSnapshot.value = null showToast('保存成功') } catch (e: unknown) { const msg = (e as { response?: { data?: { detail?: string } } })?.response?.data?.detail || '保存失败' showToast(msg, 'error') } finally { savingModel.value = null } } ``` - [ ] **Step 8: 编写 template 的 LLM Config Section** 在 Profile section 之后,Scheduler section 之前添加: ```html // LLM CONFIGURATION ⚠ chat / embedding / rerank 为知识库必填,请确保已配置 CHAT 添加 updateModel('chat', index, m)" @delete="removeModel('chat', index)" @test="(m) => testModel('chat', index, m)" /> 暂无 chat 模型配置 VLM (可选) 添加 updateModel('vlm', index, m)" @delete="removeModel('vlm', index)" @test="(m) => testModel('vlm', index, m)" /> 暂无 vlm 模型配置 EMBEDDING (知识库) 添加 updateModel('embedding', index, m)" @delete="removeModel('embedding', index)" @test="(m) => testModel('embedding', index, m)" /> 暂无 embedding 模型配置 RERANK (知识库) 添加 updateModel('rerank', index, m)" @delete="removeModel('rerank', index)" @test="(m) => testModel('rerank', index, m)" /> 暂无 rerank 模型配置 ``` - [ ] **Step 9: 添加相关样式** ```css /* LLM Type Section */ .llm-type-section { margin-bottom: 20px; } .llm-type-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; } .llm-type-title { font-family: var(--font-display); font-size: 11px; letter-spacing: 0.15em; color: var(--accent-cyan); } .optional-tag { font-size: 9px; color: var(--text-dim); letter-spacing: 0.1em; } .required-tag { font-size: 9px; color: var(--accent-red); letter-spacing: 0.1em; } /* Warning Bar */ .warning-bar { padding: 10px 14px; background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); border-radius: var(--radius-sm); color: var(--accent-red); font-family: var(--font-mono); font-size: 11px; margin-bottom: 16px; } /* Model Table */ .model-table { /* 表格容器 */ } /* Empty State */ .empty-state { padding: 20px; text-align: center; color: var(--text-dim); font-family: var(--font-mono); font-size: 11px; border: 1px dashed var(--border-dim); border-radius: var(--radius-sm); } ``` - [ ] **Step 10: 更新 loadSettings 函数** 确保 LLM 配置正确加载: ```typescript async function loadSettings() { loading.value = true try { const res = await settingsApi.get() profile.value = { email: res.data.profile.email, full_name: res.data.profile.full_name || '', created_at: res.data.profile.created_at } originalProfile.value = { ...profile.value } // 加载 LLM 配置 if (res.data.llm_config) { llmConfig.value = { chat: res.data.llm_config.chat || [], vlm: res.data.llm_config.vlm || [], embedding: res.data.llm_config.embedding || [], rerank: res.data.llm_config.rerank || [] } } else { llmConfig.value = { chat: [], vlm: [], embedding: [], rerank: [] } } originalLlmConfig.value = JSON.parse(JSON.stringify(llmConfig.value)) if (res.data.scheduler_config && Object.keys(res.data.scheduler_config).length > 0) { schedulerConfig.value = res.data.scheduler_config as SchedulerConfig } originalSchedulerConfig.value = JSON.parse(JSON.stringify(schedulerConfig.value)) } catch (e) { console.error('加载设置失败', e) showToast('加载设置失败', 'error') } finally { loading.value = false } } ``` - [ ] **Step 11: 注册组件** ```typescript import LLMTableRow from '@/components/settings/LLMTableRow.vue' // 在 components 中注册 components: { LLMTableRow } ``` - [ ] **Step 12: 提交** ```bash git add frontend/src/views/SettingsView.vue git commit -m "feat(settings): refactor LLM config to table inline-edit UI" ``` --- ## Task 4: 测试和验证 **Files:** - Test: `frontend/src/views/SettingsView.vue` - [ ] **Step 1: 手动测试流程** 1. 打开 Settings 页面 2. 确认 chat/embedding/rerank 必填警告(如果为空) 3. 添加新模型,点击 [+] 按钮 4. 填写模型信息,点击"测试连接" 5. 测试通过后,"保存"按钮可用 6. 保存成功,刷新页面确认数据持久化 7. 点击"取消"验证表单数据恢复 - [ ] **Step 2: 提交** ```bash git add -A git commit -m "feat(settings): complete LLM config table UI implementation" ```