Compare commits
5 Commits
99c30d9534
...
b284f395fd
| Author | SHA1 | Date | |
|---|---|---|---|
| b284f395fd | |||
| edee597d5f | |||
| c85e3e6988 | |||
| e7c1a57287 | |||
| 7bbaf67591 |
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { MessageCircle, BookOpen, Network, LayoutGrid, MessageSquare, LogOut, Cpu, Bot, Activity, CheckSquare, Settings } from 'lucide-vue-next'
|
import { MessageCircle, BookOpen, Network, LayoutGrid, MessageSquare, LogOut, Cpu, Bot, Activity, CheckSquare, Settings, Star } from 'lucide-vue-next'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -10,7 +10,7 @@ const auth = useAuthStore()
|
|||||||
const navItems = [
|
const navItems = [
|
||||||
{ name: '沟通系统', path: '/chat', icon: MessageCircle },
|
{ name: '沟通系统', path: '/chat', icon: MessageCircle },
|
||||||
{ name: '智能链路', path: '/agents', icon: Bot },
|
{ name: '智能链路', path: '/agents', icon: Bot },
|
||||||
{ name: 'Skill 市场', path: '/skills', icon: Bot },
|
{ name: '技能中心', path: '/skills', icon: Star },
|
||||||
{ name: '资料中枢', path: '/knowledge', icon: BookOpen },
|
{ name: '资料中枢', path: '/knowledge', icon: BookOpen },
|
||||||
{ name: '知识大脑', path: '/graph', icon: Network },
|
{ name: '知识大脑', path: '/graph', icon: Network },
|
||||||
{ name: '任务矩阵', path: '/kanban', icon: LayoutGrid },
|
{ name: '任务矩阵', path: '/kanban', icon: LayoutGrid },
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const emit = defineEmits<{
|
|||||||
(e: 'update', data: LLMModelConfig): void
|
(e: 'update', data: LLMModelConfig): void
|
||||||
(e: 'delete'): void
|
(e: 'delete'): void
|
||||||
(e: 'test', data: LLMModelConfig): void
|
(e: 'test', data: LLMModelConfig): void
|
||||||
|
(e: 'save', data: LLMModelConfig): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const showApiKey = ref(false)
|
const showApiKey = ref(false)
|
||||||
@@ -75,6 +76,10 @@ function onProviderChange() {
|
|||||||
<!-- 展开的详情面板 -->
|
<!-- 展开的详情面板 -->
|
||||||
<div v-if="isExpanded" class="expand-panel">
|
<div v-if="isExpanded" class="expand-panel">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>// NAME</label>
|
||||||
|
<input v-model="editingModel.name" type="text" placeholder="模型名称" />
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>// PROVIDER</label>
|
<label>// PROVIDER</label>
|
||||||
<select v-model="editingModel.provider" @change="onProviderChange">
|
<select v-model="editingModel.provider" @change="onProviderChange">
|
||||||
@@ -115,7 +120,7 @@ function onProviderChange() {
|
|||||||
<button
|
<button
|
||||||
class="save-btn"
|
class="save-btn"
|
||||||
:disabled="status !== 'available'"
|
:disabled="status !== 'available'"
|
||||||
@click="emit('update', editingModel)"
|
@click="emit('save', editingModel)"
|
||||||
>
|
>
|
||||||
保存
|
保存
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -2,20 +2,31 @@
|
|||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { settingsApi, type LLMConfig, type SchedulerConfig, type LLMModelConfig, type LLMProvider } from '@/api/settings'
|
import { settingsApi, type LLMConfig, type SchedulerConfig, type LLMModelConfig, type LLMProvider } from '@/api/settings'
|
||||||
import LLMTableRow from '@/components/settings/LLMTableRow.vue'
|
import LLMTableRow from '@/components/settings/LLMTableRow.vue'
|
||||||
import { Save, Eye, EyeOff, Play, RotateCcw, Plus, Trash2, X, Check } from 'lucide-vue-next'
|
import { Save, RotateCcw, Plus } from 'lucide-vue-next'
|
||||||
|
|
||||||
// 状态
|
// 状态
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const showApiKey = ref<Record<string, boolean>>({})
|
|
||||||
const savingModel = ref<string | null>(null) // 当前正在保存的模型 key
|
const savingModel = ref<string | null>(null) // 当前正在保存的模型 key
|
||||||
const modelSaveSuccess = ref<string | null>(null) // 刚刚保存成功的模型 key
|
|
||||||
const toast = ref<{ show: boolean; message: string; type: 'success' | 'error' }>({
|
const toast = ref<{ show: boolean; message: string; type: 'success' | 'error' }>({
|
||||||
show: false,
|
show: false,
|
||||||
message: '',
|
message: '',
|
||||||
type: 'success'
|
type: 'success'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 展开的行
|
||||||
|
const expandedRow = ref<string | null>(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
|
||||||
|
})
|
||||||
|
|
||||||
// 用户资料
|
// 用户资料
|
||||||
const profile = ref({
|
const profile = ref({
|
||||||
email: '',
|
email: '',
|
||||||
@@ -69,11 +80,6 @@ function isModelDirty(type: string, index: number): boolean {
|
|||||||
return JSON.stringify(original) !== JSON.stringify(current)
|
return JSON.stringify(original) !== JSON.stringify(current)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取模型唯一标识
|
|
||||||
function getModelKey(type: string, index: number): string {
|
|
||||||
return `${type}-${index}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建空的模型配置
|
// 创建空的模型配置
|
||||||
function createEmptyModel(type: string): LLMModelConfig {
|
function createEmptyModel(type: string): LLMModelConfig {
|
||||||
return {
|
return {
|
||||||
@@ -91,19 +97,64 @@ function addModel(type: string) {
|
|||||||
if (!llmConfig.value[type as keyof LLMConfig]) {
|
if (!llmConfig.value[type as keyof LLMConfig]) {
|
||||||
llmConfig.value[type as keyof LLMConfig] = []
|
llmConfig.value[type as keyof LLMConfig] = []
|
||||||
}
|
}
|
||||||
llmConfig.value[type as keyof LLMConfig]!.push(createEmptyModel(type))
|
// 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 = createEmptyModel(type)
|
||||||
|
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)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除模型
|
// 删除模型
|
||||||
function removeModel(type: string, index: number) {
|
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)
|
llmConfig.value[type as keyof LLMConfig]!.splice(index, 1)
|
||||||
|
expandedRow.value = null
|
||||||
|
editingSnapshot.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 复制模型
|
// 行标识
|
||||||
function duplicateModel(type: string, index: number) {
|
function getRowKey(type: string, index: number): string {
|
||||||
const model = llmConfig.value[type as keyof LLMConfig]![index]
|
return `${type}-${index}`
|
||||||
const copy = { ...model, name: `${model.name}-copy-${Date.now()}` }
|
}
|
||||||
llmConfig.value[type as keyof LLMConfig]!.push(copy)
|
|
||||||
|
// 切换行展开
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新模型
|
||||||
|
function updateModel(type: string, index: number, model: LLMModelConfig) {
|
||||||
|
llmConfig.value[type as keyof LLMConfig]![index] = model
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载设置
|
// 加载设置
|
||||||
@@ -119,16 +170,15 @@ async function loadSettings() {
|
|||||||
originalProfile.value = { ...profile.value }
|
originalProfile.value = { ...profile.value }
|
||||||
|
|
||||||
// 加载 LLM 配置
|
// 加载 LLM 配置
|
||||||
if (res.data.llm_config && Object.keys(res.data.llm_config).length > 0) {
|
if (res.data.llm_config) {
|
||||||
llmConfig.value = res.data.llm_config as LLMConfig
|
|
||||||
} else {
|
|
||||||
// 默认添加一个空配置
|
|
||||||
llmConfig.value = {
|
llmConfig.value = {
|
||||||
chat: [createEmptyModel('chat')],
|
chat: res.data.llm_config.chat || [],
|
||||||
vlm: [createEmptyModel('vlm')],
|
vlm: res.data.llm_config.vlm || [],
|
||||||
embedding: [createEmptyModel('embedding')],
|
embedding: res.data.llm_config.embedding || [],
|
||||||
rerank: [createEmptyModel('rerank')]
|
rerank: res.data.llm_config.rerank || []
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
llmConfig.value = { chat: [], vlm: [], embedding: [], rerank: [] }
|
||||||
}
|
}
|
||||||
originalLlmConfig.value = JSON.parse(JSON.stringify(llmConfig.value))
|
originalLlmConfig.value = JSON.parse(JSON.stringify(llmConfig.value))
|
||||||
|
|
||||||
@@ -180,7 +230,7 @@ async function saveLLM() {
|
|||||||
|
|
||||||
// 保存单个模型
|
// 保存单个模型
|
||||||
async function saveModel(type: string, index: number) {
|
async function saveModel(type: string, index: number) {
|
||||||
const key = getModelKey(type, index)
|
const key = getRowKey(type, index)
|
||||||
savingModel.value = key
|
savingModel.value = key
|
||||||
try {
|
try {
|
||||||
// 发送完整的配置(包含该类型的所有模型)
|
// 发送完整的配置(包含该类型的所有模型)
|
||||||
@@ -189,13 +239,10 @@ async function saveModel(type: string, index: number) {
|
|||||||
// 更新原始配置(深拷贝当前完整配置)
|
// 更新原始配置(深拷贝当前完整配置)
|
||||||
originalLlmConfig.value = JSON.parse(JSON.stringify(llmConfig.value))
|
originalLlmConfig.value = JSON.parse(JSON.stringify(llmConfig.value))
|
||||||
|
|
||||||
// 显示成功状态
|
// 关闭展开的行
|
||||||
modelSaveSuccess.value = key
|
expandedRow.value = null
|
||||||
setTimeout(() => {
|
editingSnapshot.value = null
|
||||||
if (modelSaveSuccess.value === key) {
|
showToast('保存成功')
|
||||||
modelSaveSuccess.value = null
|
|
||||||
}
|
|
||||||
}, 1500)
|
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const msg = (e as { response?: { data?: { detail?: string } } })?.response?.data?.detail || '保存失败'
|
const msg = (e as { response?: { data?: { detail?: string } } })?.response?.data?.detail || '保存失败'
|
||||||
showToast(msg, 'error')
|
showToast(msg, 'error')
|
||||||
@@ -205,15 +252,19 @@ async function saveModel(type: string, index: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 测试 LLM 连接
|
// 测试 LLM 连接
|
||||||
async function testLLM(type: string, config: LLMModelConfig) {
|
async function testModel(type: string, index: number, model: LLMModelConfig) {
|
||||||
try {
|
try {
|
||||||
const res = await settingsApi.testLLM({ type: type as any, ...config })
|
const res = await settingsApi.testLLM({ type: type as any, ...model })
|
||||||
if (res.data.success) {
|
if (res.data.success) {
|
||||||
showToast(`连接成功: ${res.data.message}`)
|
// 测试通过,标记为可用
|
||||||
|
llmConfig.value[type as keyof LLMConfig]![index].enabled = true
|
||||||
|
showToast('连接成功')
|
||||||
} else {
|
} else {
|
||||||
|
llmConfig.value[type as keyof LLMConfig]![index].enabled = false
|
||||||
showToast(`连接失败: ${res.data.error}`, 'error')
|
showToast(`连接失败: ${res.data.error}`, 'error')
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
llmConfig.value[type as keyof LLMConfig]![index].enabled = false
|
||||||
showToast('测试连接失败', 'error')
|
showToast('测试连接失败', 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -247,24 +298,6 @@ function resetScheduler() {
|
|||||||
schedulerConfig.value = JSON.parse(JSON.stringify(originalSchedulerConfig.value))
|
schedulerConfig.value = JSON.parse(JSON.stringify(originalSchedulerConfig.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provider 默认 URL
|
|
||||||
function getDefaultBaseUrl(provider: string): string {
|
|
||||||
switch (provider) {
|
|
||||||
case 'ollama': return 'http://localhost:11434'
|
|
||||||
case 'openai': return 'https://api.openai.com/v1'
|
|
||||||
case 'claude': return 'https://api.anthropic.com'
|
|
||||||
case 'deepseek': return 'https://api.deepseek.com/v1'
|
|
||||||
default: return ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provider 变化时自动填充 base_url
|
|
||||||
function onProviderChange(config: LLMModelConfig) {
|
|
||||||
if (!config.base_url || config.base_url === getDefaultBaseUrl(config.provider)) {
|
|
||||||
config.base_url = getDefaultBaseUrl(config.provider)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toast 提示
|
// Toast 提示
|
||||||
function showToast(message: string, type: 'success' | 'error' = 'success') {
|
function showToast(message: string, type: 'success' | 'error' = 'success') {
|
||||||
toast.value = { show: true, message, type }
|
toast.value = { show: true, message, type }
|
||||||
@@ -273,16 +306,6 @@ function showToast(message: string, type: 'success' | 'error' = 'success') {
|
|||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LLM 类型列表
|
|
||||||
const llmTypes = ['chat', 'vlm', 'embedding', 'rerank'] as const
|
|
||||||
|
|
||||||
// 获取模型显示名称
|
|
||||||
function getModelName(config: LLMModelConfig): string {
|
|
||||||
if (config.name) return config.name
|
|
||||||
if (config.model) return config.model
|
|
||||||
return '未命名'
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(loadSettings)
|
onMounted(loadSettings)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -344,133 +367,113 @@ onMounted(loadSettings)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- LLM Config Section -->
|
<!-- LLM Config Section -->
|
||||||
<div v-for="type in llmTypes" :key="type" class="settings-card">
|
<div class="settings-card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="card-title">{{ type.toUpperCase() }}</span>
|
<span class="card-title">// LLM CONFIGURATION</span>
|
||||||
<button class="add-btn" @click="addModel(type)">
|
|
||||||
<Plus :size="12" /> 添加模型
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 模型列表 -->
|
<!-- 必填警告 -->
|
||||||
<div v-if="llmConfig[type] && llmConfig[type]!.length > 0" class="model-list">
|
<div v-if="showRequiredWarning" class="warning-bar">
|
||||||
<div
|
⚠ chat / embedding / rerank 为知识库必填,请确保已配置
|
||||||
v-for="(model, index) in llmConfig[type]"
|
</div>
|
||||||
:key="index"
|
|
||||||
class="model-item"
|
|
||||||
:class="{ disabled: !model.enabled }"
|
|
||||||
>
|
|
||||||
<div class="model-header">
|
|
||||||
<div class="model-name-group">
|
|
||||||
<input
|
|
||||||
v-model="model.name"
|
|
||||||
type="text"
|
|
||||||
class="model-name-input"
|
|
||||||
placeholder="模型名称"
|
|
||||||
/>
|
|
||||||
<label class="model-enabled">
|
|
||||||
<input type="checkbox" v-model="model.enabled" />
|
|
||||||
<span class="checkbox-custom"></span>
|
|
||||||
启用
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="model-actions">
|
|
||||||
<button
|
|
||||||
class="icon-btn"
|
|
||||||
:class="{
|
|
||||||
'has-changes': isModelDirty(type, index),
|
|
||||||
'is-saving': savingModel === getModelKey(type, index),
|
|
||||||
'is-saved': modelSaveSuccess === getModelKey(type, index)
|
|
||||||
}"
|
|
||||||
@click="saveModel(type, index)"
|
|
||||||
:title="isModelDirty(type, index) ? '保存更改' : '无更改'"
|
|
||||||
:disabled="savingModel === getModelKey(type, index)"
|
|
||||||
>
|
|
||||||
<div v-if="savingModel === getModelKey(type, index)" class="btn-spinner-sm"></div>
|
|
||||||
<Check v-else-if="modelSaveSuccess === getModelKey(type, index)" :size="12" />
|
|
||||||
<Save v-else :size="12" />
|
|
||||||
<span v-if="isModelDirty(type, index) && modelSaveSuccess !== getModelKey(type, index)" class="unsaved-dot"></span>
|
|
||||||
</button>
|
|
||||||
<button class="icon-btn danger" @click="removeModel(type, index)" title="删除">
|
|
||||||
<Trash2 :size="12" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-row">
|
<!-- Chat Section -->
|
||||||
<div class="form-group">
|
<div class="llm-type-section">
|
||||||
<label class="form-label">// PROVIDER</label>
|
<div class="llm-type-header">
|
||||||
<select
|
<span class="llm-type-title">CHAT</span>
|
||||||
v-model="model.provider"
|
<button class="add-btn" @click="addModel('chat')">
|
||||||
class="form-select"
|
<Plus :size="12" /> 添加
|
||||||
@change="onProviderChange(model)"
|
</button>
|
||||||
>
|
</div>
|
||||||
<option value="openai">OpenAI</option>
|
<div v-if="llmConfig.chat && llmConfig.chat.length > 0" class="model-table">
|
||||||
<option value="claude">Claude</option>
|
<div v-for="(model, index) in llmConfig.chat" :key="index">
|
||||||
<option value="ollama">Ollama</option>
|
<LLMTableRow
|
||||||
<option value="deepseek">DeepSeek</option>
|
:model="model"
|
||||||
<option value="custom">Custom</option>
|
:is-expanded="expandedRow === getRowKey('chat', index)"
|
||||||
</select>
|
@toggle="toggleRow('chat', index, model)"
|
||||||
</div>
|
@update="(m) => updateModel('chat', index, m)"
|
||||||
<div class="form-group">
|
@delete="removeModel('chat', index)"
|
||||||
<label class="form-label">// MODEL</label>
|
@test="(m) => testModel('chat', index, m)"
|
||||||
<input v-model="model.model" type="text" class="form-input" placeholder="gpt-4o" />
|
@save="(m) => saveModel('chat', index)"
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">// BASE URL</label>
|
|
||||||
<input
|
|
||||||
v-model="model.base_url"
|
|
||||||
type="text"
|
|
||||||
class="form-input"
|
|
||||||
:placeholder="getDefaultBaseUrl(model.provider)"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="empty-state">暂无 chat 模型配置</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<!-- VLM Section -->
|
||||||
<label class="form-label">// API KEY</label>
|
<div class="llm-type-section">
|
||||||
<div class="input-with-toggle">
|
<div class="llm-type-header">
|
||||||
<input
|
<span class="llm-type-title">VLM <span class="optional-tag">(可选)</span></span>
|
||||||
v-model="model.api_key"
|
<button class="add-btn" @click="addModel('vlm')">
|
||||||
:type="showApiKey[`${type}-${index}`] ? 'text' : 'password'"
|
<Plus :size="12" /> 添加
|
||||||
class="form-input"
|
</button>
|
||||||
placeholder="sk-..."
|
</div>
|
||||||
/>
|
<div v-if="llmConfig.vlm && llmConfig.vlm.length > 0" class="model-table">
|
||||||
<button class="toggle-visibility" @click="showApiKey[`${type}-${index}`] = !showApiKey[`${type}-${index}`]">
|
<div v-for="(model, index) in llmConfig.vlm" :key="index">
|
||||||
<Eye v-if="!showApiKey[`${type}-${index}`]" :size="14" />
|
<LLMTableRow
|
||||||
<EyeOff v-else :size="14" />
|
:model="model"
|
||||||
</button>
|
:is-expanded="expandedRow === getRowKey('vlm', index)"
|
||||||
</div>
|
@toggle="toggleRow('vlm', index, model)"
|
||||||
</div>
|
@update="(m) => updateModel('vlm', index, m)"
|
||||||
|
@delete="removeModel('vlm', index)"
|
||||||
<div class="model-footer">
|
@test="(m) => testModel('vlm', index, m)"
|
||||||
<button class="test-btn" @click="testLLM(type, model)">
|
@save="(m) => saveModel('vlm', index)"
|
||||||
<Play :size="12" /> 测试连接
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="empty-state">暂无 vlm 模型配置</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- Embedding Section -->
|
||||||
<div v-else class="empty-state">
|
<div class="llm-type-section">
|
||||||
<span>暂无模型配置</span>
|
<div class="llm-type-header">
|
||||||
<button class="add-btn" @click="addModel(type)">
|
<span class="llm-type-title">EMBEDDING <span class="required-tag">(知识库)</span></span>
|
||||||
<Plus :size="12" /> 添加第一个模型
|
<button class="add-btn" @click="addModel('embedding')">
|
||||||
</button>
|
<Plus :size="12" /> 添加
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="llmConfig.embedding && llmConfig.embedding.length > 0" class="model-table">
|
||||||
|
<div v-for="(model, index) in llmConfig.embedding" :key="index">
|
||||||
|
<LLMTableRow
|
||||||
|
:model="model"
|
||||||
|
:is-expanded="expandedRow === getRowKey('embedding', index)"
|
||||||
|
@toggle="toggleRow('embedding', index, model)"
|
||||||
|
@update="(m) => updateModel('embedding', index, m)"
|
||||||
|
@delete="removeModel('embedding', index)"
|
||||||
|
@test="(m) => testModel('embedding', index, m)"
|
||||||
|
@save="(m) => saveModel('embedding', index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="empty-state">暂无 embedding 模型配置</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rerank Section -->
|
||||||
|
<div class="llm-type-section">
|
||||||
|
<div class="llm-type-header">
|
||||||
|
<span class="llm-type-title">RERANK <span class="required-tag">(知识库)</span></span>
|
||||||
|
<button class="add-btn" @click="addModel('rerank')">
|
||||||
|
<Plus :size="12" /> 添加
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="llmConfig.rerank && llmConfig.rerank.length > 0" class="model-table">
|
||||||
|
<div v-for="(model, index) in llmConfig.rerank" :key="index">
|
||||||
|
<LLMTableRow
|
||||||
|
:model="model"
|
||||||
|
:is-expanded="expandedRow === getRowKey('rerank', index)"
|
||||||
|
@toggle="toggleRow('rerank', index, model)"
|
||||||
|
@update="(m) => updateModel('rerank', index, m)"
|
||||||
|
@delete="removeModel('rerank', index)"
|
||||||
|
@test="(m) => testModel('rerank', index, m)"
|
||||||
|
@save="(m) => saveModel('rerank', index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="empty-state">暂无 rerank 模型配置</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
|
||||||
class="save-btn full-width"
|
|
||||||
@click="saveLLM"
|
|
||||||
:disabled="saving || !isLlmDirty"
|
|
||||||
>
|
|
||||||
<div v-if="saving" class="btn-loader"></div>
|
|
||||||
<Save v-else :size="14" />
|
|
||||||
<span>{{ saving ? 'SAVING...' : 'SAVE LLM CONFIG' }}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Scheduler Section -->
|
<!-- Scheduler Section -->
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@@ -692,169 +695,47 @@ onMounted(loadSettings)
|
|||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Model List */
|
/* LLM Type Section */
|
||||||
.model-list {
|
.llm-type-section {
|
||||||
display: flex;
|
margin-bottom: 20px;
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-item {
|
.llm-type-header {
|
||||||
background: var(--bg-card);
|
|
||||||
border: 1px solid var(--border-mid);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 14px;
|
|
||||||
transition: all var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-item.disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-item:hover {
|
|
||||||
border-color: var(--border-mid);
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-header {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-name-group {
|
.llm-type-title {
|
||||||
display: flex;
|
font-family: var(--font-display);
|
||||||
align-items: center;
|
font-size: 11px;
|
||||||
gap: 12px;
|
letter-spacing: 0.15em;
|
||||||
|
color: var(--accent-cyan);
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-name-input {
|
.optional-tag {
|
||||||
background: var(--bg-void);
|
font-size: 9px;
|
||||||
border: 1px solid var(--border-dim);
|
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);
|
border-radius: var(--radius-sm);
|
||||||
padding: 4px 8px;
|
color: var(--accent-red);
|
||||||
color: var(--text-primary);
|
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
width: 150px;
|
margin-bottom: 16px;
|
||||||
}
|
|
||||||
|
|
||||||
.model-name-input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--accent-cyan);
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-enabled {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 10px;
|
|
||||||
color: var(--text-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-enabled input {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-custom {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
border: 1px solid var(--border-mid);
|
|
||||||
border-radius: 3px;
|
|
||||||
background: var(--bg-void);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-enabled input:checked + .checkbox-custom {
|
|
||||||
background: var(--accent-cyan);
|
|
||||||
border-color: var(--accent-cyan);
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-enabled input:checked + .checkbox-custom::after {
|
|
||||||
content: '✓';
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
color: var(--bg-void);
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-btn {
|
|
||||||
width: 26px;
|
|
||||||
height: 26px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid var(--border-mid);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
color: var(--text-dim);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-btn:hover {
|
|
||||||
border-color: var(--accent-cyan);
|
|
||||||
color: var(--accent-cyan);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-btn.danger:hover {
|
|
||||||
border-color: var(--accent-red);
|
|
||||||
color: var(--accent-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 保存按钮状态 */
|
|
||||||
.icon-btn.has-changes {
|
|
||||||
border-color: var(--accent-cyan);
|
|
||||||
color: var(--accent-cyan);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-btn.is-saving {
|
|
||||||
opacity: 0.7;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-btn.is-saved {
|
|
||||||
border-color: #10b981;
|
|
||||||
color: #10b981;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unsaved-dot {
|
|
||||||
position: absolute;
|
|
||||||
top: -3px;
|
|
||||||
right: -3px;
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
background: var(--accent-red);
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-spinner-sm {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border: 2px solid var(--border-mid);
|
|
||||||
border-top-color: var(--accent-cyan);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 0.8s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-footer {
|
|
||||||
margin-top: 10px;
|
|
||||||
padding-top: 10px;
|
|
||||||
border-top: 1px solid var(--border-dim);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Empty State */
|
/* Empty State */
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ onMounted(fetchSkills)
|
|||||||
<div class="view-header">
|
<div class="view-header">
|
||||||
<div class="header-title">
|
<div class="header-title">
|
||||||
<span class="title-bracket">[</span>
|
<span class="title-bracket">[</span>
|
||||||
<span class="title-text">SKILL MANAGEMENT</span>
|
<span class="title-text">技能中心</span>
|
||||||
<span class="title-bracket">]</span>
|
<span class="title-bracket">]</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
@@ -186,7 +186,7 @@ onMounted(fetchSkills)
|
|||||||
</button>
|
</button>
|
||||||
<button class="btn-add" @click="openCreateModal">
|
<button class="btn-add" @click="openCreateModal">
|
||||||
<Plus :size="14" />
|
<Plus :size="14" />
|
||||||
<span>New Skill</span>
|
<span>新建技能</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -261,7 +261,7 @@ onMounted(fetchSkills)
|
|||||||
<div v-if="modalOpen" class="modal-overlay" @click.self="closeModal">
|
<div v-if="modalOpen" class="modal-overlay" @click.self="closeModal">
|
||||||
<div class="modal-card">
|
<div class="modal-card">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<span class="modal-title">{{ editingSkill ? '// EDIT SKILL' : '// NEW SKILL' }}</span>
|
<span class="modal-title">{{ editingSkill ? '// 编辑技能' : '// 新建技能' }}</span>
|
||||||
<button class="btn-close" @click="closeModal"><X :size="16" /></button>
|
<button class="btn-close" @click="closeModal"><X :size="16" /></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|||||||
Reference in New Issue
Block a user