From 4579159c0a102ea228a511a675e2b8607f980d9b Mon Sep 17 00:00:00 2001 From: MerCry Date: Wed, 25 Feb 2026 14:06:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E5=89=8D=E7=AB=AF=E6=B5=85?= =?UTF-8?q?=E8=89=B2=E8=B0=83=E9=A3=8E=E6=A0=BC=E4=BC=98=E5=8C=96=E4=B8=8E?= =?UTF-8?q?=E4=B8=8B=E6=8B=89=E6=A1=86=E6=98=BE=E7=A4=BA=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 建立全局浅色调样式系统,统一配色风格 - 优化导航栏设计,添加品牌标识 - 修复下拉框被遮挡问题,添加 teleported 和 popper 配置 - 优化 LLM 选择器中当前配置标签的显示 - 重构控制台页面,采用白色卡片风格 - 统一所有页面的视觉风格,提升用户体验 --- ai-service-admin/src/App.vue | 232 +++++- ai-service-admin/src/api/llm.ts | 10 +- .../src/components/common/ConfigForm.vue | 219 ++++++ .../src/components/common/ProviderSelect.vue | 83 +++ .../src/components/common/TestPanel.vue | 523 +++++++++++++ .../embedding/EmbeddingProviderSelect.vue | 9 +- .../src/components/rag/AIResponseViewer.vue | 351 +++++++++ .../src/components/rag/LLMSelector.vue | 120 +++ .../src/components/rag/StreamOutput.vue | 299 ++++++++ ai-service-admin/src/main.ts | 1 + ai-service-admin/src/router/index.ts | 6 + ai-service-admin/src/stores/llm.ts | 161 ++++ ai-service-admin/src/stores/rag.ts | 126 ++++ ai-service-admin/src/styles/main.css | 486 ++++++++++++ ai-service-admin/src/types/llm.ts | 3 +- ai-service-admin/src/utils/request.ts | 2 +- .../src/views/admin/embedding/index.vue | 263 +++---- .../src/views/admin/llm/index.vue | 470 ++++++++++++ .../src/views/dashboard/index.vue | 693 +++++++++++++++++- ai-service-admin/src/views/kb/index.vue | 191 ++++- ai-service-admin/src/views/rag-lab/index.vue | 439 +++++++++-- ai-service/app/api/admin/dashboard.py | 122 ++- ai-service/app/api/admin/llm.py | 26 +- ai-service/app/models/entities.py | 7 + ai-service/app/services/llm/factory.py | 251 +++++-- docs/progress/ai-service-admin-progress.md | 108 ++- spec/ai-service-admin/tasks.md | 64 +- 27 files changed, 4830 insertions(+), 435 deletions(-) create mode 100644 ai-service-admin/src/components/common/ConfigForm.vue create mode 100644 ai-service-admin/src/components/common/ProviderSelect.vue create mode 100644 ai-service-admin/src/components/common/TestPanel.vue create mode 100644 ai-service-admin/src/components/rag/AIResponseViewer.vue create mode 100644 ai-service-admin/src/components/rag/LLMSelector.vue create mode 100644 ai-service-admin/src/components/rag/StreamOutput.vue create mode 100644 ai-service-admin/src/stores/llm.ts create mode 100644 ai-service-admin/src/stores/rag.ts create mode 100644 ai-service-admin/src/styles/main.css create mode 100644 ai-service-admin/src/views/admin/llm/index.vue diff --git a/ai-service-admin/src/App.vue b/ai-service-admin/src/App.vue index a76a199..28e82e1 100644 --- a/ai-service-admin/src/App.vue +++ b/ai-service-admin/src/App.vue @@ -1,26 +1,63 @@ @@ -28,28 +65,167 @@ import { ref, computed } from 'vue' import { useRoute } from 'vue-router' import { useTenantStore } from '@/stores/tenant' +import { Odometer, FolderOpened, Cpu, Monitor, Connection, ChatDotSquare } from '@element-plus/icons-vue' const route = useRoute() const tenantStore = useTenantStore() -const activeIndex = computed(() => route.path) const currentTenantId = ref(tenantStore.currentTenantId) +const isActive = (path: string) => { + return route.path === path || route.path.startsWith(path + '/') +} + const handleTenantChange = (val: string) => { tenantStore.setTenant(val) } diff --git a/ai-service-admin/src/api/llm.ts b/ai-service-admin/src/api/llm.ts index 73b04f5..b474dcc 100644 --- a/ai-service-admin/src/api/llm.ts +++ b/ai-service-admin/src/api/llm.ts @@ -11,21 +11,21 @@ import type { export function getLLMProviders(): Promise { return request({ - url: '/llm/providers', + url: '/admin/llm/providers', method: 'get' }) } export function getLLMConfig(): Promise { return request({ - url: '/llm/config', + url: '/admin/llm/config', method: 'get' }) } -export function saveLLMConfig(data: LLMConfigUpdate): Promise { +export function updateLLMConfig(data: LLMConfigUpdate): Promise { return request({ - url: '/llm/config', + url: '/admin/llm/config', method: 'put', data }) @@ -33,7 +33,7 @@ export function saveLLMConfig(data: LLMConfigUpdate): Promise { return request({ - url: '/llm/test', + url: '/admin/llm/test', method: 'post', data }) diff --git a/ai-service-admin/src/components/common/ConfigForm.vue b/ai-service-admin/src/components/common/ConfigForm.vue new file mode 100644 index 0000000..569e4af --- /dev/null +++ b/ai-service-admin/src/components/common/ConfigForm.vue @@ -0,0 +1,219 @@ + + + + + diff --git a/ai-service-admin/src/components/common/ProviderSelect.vue b/ai-service-admin/src/components/common/ProviderSelect.vue new file mode 100644 index 0000000..6120441 --- /dev/null +++ b/ai-service-admin/src/components/common/ProviderSelect.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/ai-service-admin/src/components/common/TestPanel.vue b/ai-service-admin/src/components/common/TestPanel.vue new file mode 100644 index 0000000..c3a0d08 --- /dev/null +++ b/ai-service-admin/src/components/common/TestPanel.vue @@ -0,0 +1,523 @@ + + + + + diff --git a/ai-service-admin/src/components/embedding/EmbeddingProviderSelect.vue b/ai-service-admin/src/components/embedding/EmbeddingProviderSelect.vue index 5ccb24b..66a27eb 100644 --- a/ai-service-admin/src/components/embedding/EmbeddingProviderSelect.vue +++ b/ai-service-admin/src/components/embedding/EmbeddingProviderSelect.vue @@ -5,6 +5,8 @@ :placeholder="placeholder" :disabled="disabled" :clearable="clearable" + :teleported="true" + :popper-options="{ modifiers: [{ name: 'flip', enabled: true }, { name: 'preventOverflow', enabled: true }] }" @update:model-value="handleChange" > { .provider-option { display: flex; flex-direction: column; - line-height: 1.4; + line-height: 1.5; + padding: 4px 0; } .provider-name { font-weight: 500; + color: var(--text-primary); } .provider-desc { font-size: 12px; - color: var(--el-text-color-secondary); + color: var(--text-secondary); margin-top: 2px; + line-height: 1.4; } diff --git a/ai-service-admin/src/components/rag/AIResponseViewer.vue b/ai-service-admin/src/components/rag/AIResponseViewer.vue new file mode 100644 index 0000000..a9960a5 --- /dev/null +++ b/ai-service-admin/src/components/rag/AIResponseViewer.vue @@ -0,0 +1,351 @@ + + + + + diff --git a/ai-service-admin/src/components/rag/LLMSelector.vue b/ai-service-admin/src/components/rag/LLMSelector.vue new file mode 100644 index 0000000..7ffe06c --- /dev/null +++ b/ai-service-admin/src/components/rag/LLMSelector.vue @@ -0,0 +1,120 @@ + + + + + + + diff --git a/ai-service-admin/src/components/rag/StreamOutput.vue b/ai-service-admin/src/components/rag/StreamOutput.vue new file mode 100644 index 0000000..e06f03c --- /dev/null +++ b/ai-service-admin/src/components/rag/StreamOutput.vue @@ -0,0 +1,299 @@ + + + + + diff --git a/ai-service-admin/src/main.ts b/ai-service-admin/src/main.ts index ab1c387..5aff922 100644 --- a/ai-service-admin/src/main.ts +++ b/ai-service-admin/src/main.ts @@ -2,6 +2,7 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' +import './styles/main.css' import App from './App.vue' import router from './router' diff --git a/ai-service-admin/src/router/index.ts b/ai-service-admin/src/router/index.ts index 6863b47..c4ffa87 100644 --- a/ai-service-admin/src/router/index.ts +++ b/ai-service-admin/src/router/index.ts @@ -34,6 +34,12 @@ const routes: Array = [ name: 'EmbeddingConfig', component: () => import('@/views/admin/embedding/index.vue'), meta: { title: '嵌入模型配置' } + }, + { + path: '/admin/llm', + name: 'LLMConfig', + component: () => import('@/views/admin/llm/index.vue'), + meta: { title: 'LLM 模型配置' } } ] diff --git a/ai-service-admin/src/stores/llm.ts b/ai-service-admin/src/stores/llm.ts new file mode 100644 index 0000000..656ab29 --- /dev/null +++ b/ai-service-admin/src/stores/llm.ts @@ -0,0 +1,161 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { + getLLMProviders, + getLLMConfig, + updateLLMConfig, + testLLM, + type LLMProviderInfo, + type LLMConfig, + type LLMConfigUpdate, + type LLMTestResult +} from '@/api/llm' + +export const useLLMStore = defineStore('llm', () => { + const providers = ref([]) + const currentConfig = ref({ + provider: '', + config: {} + }) + const loading = ref(false) + const providersLoading = ref(false) + const testResult = ref(null) + const testLoading = ref(false) + + const currentProvider = computed(() => { + return providers.value.find(p => p.name === currentConfig.value.provider) + }) + + const configSchema = computed(() => { + return currentProvider.value?.config_schema || { properties: {} } + }) + + const loadProviders = async () => { + providersLoading.value = true + try { + const res: any = await getLLMProviders() + providers.value = res?.providers || res?.data?.providers || [] + } catch (error) { + console.error('Failed to load LLM providers:', error) + throw error + } finally { + providersLoading.value = false + } + } + + const loadConfig = async () => { + loading.value = true + try { + const res: any = await getLLMConfig() + const config = res?.data || res + if (config) { + currentConfig.value = { + provider: config.provider || '', + config: config.config || {}, + updated_at: config.updated_at + } + } + } catch (error) { + console.error('Failed to load LLM config:', error) + throw error + } finally { + loading.value = false + } + } + + const saveCurrentConfig = async () => { + loading.value = true + try { + const updateData: LLMConfigUpdate = { + provider: currentConfig.value.provider, + config: currentConfig.value.config + } + await updateLLMConfig(updateData) + } catch (error) { + console.error('Failed to save LLM config:', error) + throw error + } finally { + loading.value = false + } + } + + const runTest = async (testPrompt?: string): Promise => { + testLoading.value = true + testResult.value = null + try { + const result = await testLLM({ + test_prompt: testPrompt, + provider: currentConfig.value.provider, + config: currentConfig.value.config + }) + testResult.value = result + return result + } catch (error: any) { + const errorResult: LLMTestResult = { + success: false, + error: error?.message || '连接测试失败' + } + testResult.value = errorResult + return errorResult + } finally { + testLoading.value = false + } + } + + const setProvider = (providerName: string) => { + currentConfig.value.provider = providerName + const provider = providers.value.find(p => p.name === providerName) + if (provider?.config_schema?.properties) { + const newConfig: Record = {} + Object.entries(provider.config_schema.properties).forEach(([key, field]: [string, any]) => { + if (field.default !== undefined) { + newConfig[key] = field.default + } else { + switch (field.type) { + case 'string': + newConfig[key] = '' + break + case 'integer': + case 'number': + newConfig[key] = field.minimum ?? 0 + break + case 'boolean': + newConfig[key] = false + break + default: + newConfig[key] = null + } + } + }) + currentConfig.value.config = newConfig + } else { + currentConfig.value.config = {} + } + } + + const updateConfigValue = (key: string, value: any) => { + currentConfig.value.config[key] = value + } + + const clearTestResult = () => { + testResult.value = null + } + + return { + providers, + currentConfig, + loading, + providersLoading, + testResult, + testLoading, + currentProvider, + configSchema, + loadProviders, + loadConfig, + saveCurrentConfig, + runTest, + setProvider, + updateConfigValue, + clearTestResult + } +}) diff --git a/ai-service-admin/src/stores/rag.ts b/ai-service-admin/src/stores/rag.ts new file mode 100644 index 0000000..c9a9b5d --- /dev/null +++ b/ai-service-admin/src/stores/rag.ts @@ -0,0 +1,126 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { + runRagExperiment, + createSSEConnection, + type AIResponse, + type RetrievalResult, + type RagExperimentRequest, + type RagExperimentResult +} from '@/api/rag' + +export const useRagStore = defineStore('rag', () => { + const retrievalResults = ref([]) + const finalPrompt = ref('') + const aiResponse = ref(null) + const totalLatencyMs = ref(0) + + const loading = ref(false) + const streaming = ref(false) + const streamContent = ref('') + const streamError = ref(null) + + const hasResults = computed(() => retrievalResults.value.length > 0 || aiResponse.value !== null) + + const abortStream = ref<(() => void) | null>(null) + + const runExperiment = async (params: RagExperimentRequest) => { + loading.value = true + streamError.value = null + + try { + const result: RagExperimentResult = await runRagExperiment(params) + + retrievalResults.value = result.retrieval_results || [] + finalPrompt.value = result.final_prompt || '' + aiResponse.value = result.ai_response || null + totalLatencyMs.value = result.total_latency_ms || 0 + + return result + } catch (error: any) { + streamError.value = error?.message || '实验运行失败' + throw error + } finally { + loading.value = false + } + } + + const startStream = (params: RagExperimentRequest) => { + streaming.value = true + streamContent.value = '' + streamError.value = null + aiResponse.value = null + + abortStream.value = createSSEConnection( + '/admin/rag/experiments/stream', + params, + (data: string) => { + try { + const parsed = JSON.parse(data) + + if (parsed.type === 'content') { + streamContent.value += parsed.content || '' + } else if (parsed.type === 'retrieval') { + retrievalResults.value = parsed.results || [] + } else if (parsed.type === 'prompt') { + finalPrompt.value = parsed.prompt || '' + } else if (parsed.type === 'complete') { + aiResponse.value = { + content: streamContent.value, + prompt_tokens: parsed.prompt_tokens, + completion_tokens: parsed.completion_tokens, + total_tokens: parsed.total_tokens, + latency_ms: parsed.latency_ms, + model: parsed.model + } + totalLatencyMs.value = parsed.total_latency_ms || 0 + } else if (parsed.type === 'error') { + streamError.value = parsed.message || '流式输出错误' + } + } catch { + streamContent.value += data + } + }, + (error: Error) => { + streaming.value = false + streamError.value = error.message + }, + () => { + streaming.value = false + } + ) + } + + const stopStream = () => { + if (abortStream.value) { + abortStream.value() + abortStream.value = null + } + streaming.value = false + } + + const clearResults = () => { + retrievalResults.value = [] + finalPrompt.value = '' + aiResponse.value = null + totalLatencyMs.value = 0 + streamContent.value = '' + streamError.value = null + } + + return { + retrievalResults, + finalPrompt, + aiResponse, + totalLatencyMs, + loading, + streaming, + streamContent, + streamError, + hasResults, + runExperiment, + startStream, + stopStream, + clearResults + } +}) diff --git a/ai-service-admin/src/styles/main.css b/ai-service-admin/src/styles/main.css new file mode 100644 index 0000000..09883c7 --- /dev/null +++ b/ai-service-admin/src/styles/main.css @@ -0,0 +1,486 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=DM+Sans:wght@400;500;600;700&display=swap'); + +:root { + --primary-color: #4F7CFF; + --primary-light: #6B91FF; + --primary-lighter: #E8EEFF; + --primary-dark: #3A5FD9; + + --secondary-color: #6366F1; + --secondary-light: #818CF8; + + --accent-color: #10B981; + --accent-light: #34D399; + + --warning-color: #F59E0B; + --danger-color: #EF4444; + --success-color: #10B981; + --info-color: #3B82F6; + + --bg-primary: #F8FAFC; + --bg-secondary: #FFFFFF; + --bg-tertiary: #F1F5F9; + --bg-hover: #F1F5F9; + + --text-primary: #1E293B; + --text-secondary: #64748B; + --text-tertiary: #94A3B8; + --text-placeholder: #CBD5E1; + + --border-color: #E2E8F0; + --border-light: #F1F5F9; + + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -4px rgba(0, 0, 0, 0.05); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.05); + + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 14px; + --radius-xl: 20px; + + --transition-fast: 0.15s ease; + --transition-normal: 0.25s ease; + --transition-slow: 0.35s ease; + + --font-sans: 'DM Sans', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + --font-mono: 'JetBrains Mono', 'Fira Code', 'SF Mono', Consolas, monospace; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body { + font-family: var(--font-sans); + font-size: 14px; + line-height: 1.6; + color: var(--text-primary); + background-color: var(--bg-primary); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.el-menu { + font-family: var(--font-sans) !important; + border-bottom: 1px solid var(--border-color) !important; + background-color: var(--bg-secondary) !important; +} + +.el-menu-item { + font-weight: 500 !important; + transition: all var(--transition-fast) !important; +} + +.el-menu-item:hover { + background-color: var(--bg-hover) !important; +} + +.el-menu-item.is-active { + color: var(--primary-color) !important; + border-bottom-color: var(--primary-color) !important; + background-color: var(--primary-lighter) !important; +} + +.el-card { + border-radius: var(--radius-lg) !important; + border: 1px solid var(--border-color) !important; + box-shadow: var(--shadow-sm) !important; + transition: all var(--transition-normal) !important; + background-color: var(--bg-secondary) !important; +} + +.el-card:hover { + box-shadow: var(--shadow-md) !important; +} + +.el-card__header { + border-bottom: 1px solid var(--border-light) !important; + padding: 16px 20px !important; + font-weight: 600 !important; + color: var(--text-primary) !important; +} + +.el-card__body { + padding: 20px !important; +} + +.el-button--primary { + background-color: var(--primary-color) !important; + border-color: var(--primary-color) !important; + font-weight: 500 !important; + transition: all var(--transition-fast) !important; +} + +.el-button--primary:hover { + background-color: var(--primary-light) !important; + border-color: var(--primary-light) !important; + transform: translateY(-1px); + box-shadow: var(--shadow-md) !important; +} + +.el-button--default { + font-weight: 500 !important; + border-color: var(--border-color) !important; + transition: all var(--transition-fast) !important; +} + +.el-button--default:hover { + border-color: var(--primary-color) !important; + color: var(--primary-color) !important; + background-color: var(--primary-lighter) !important; +} + +.el-input__wrapper { + border-radius: var(--radius-md) !important; + box-shadow: none !important; + border: 1px solid var(--border-color) !important; + transition: all var(--transition-fast) !important; +} + +.el-input__wrapper:hover { + border-color: var(--primary-light) !important; +} + +.el-input__wrapper.is-focus { + border-color: var(--primary-color) !important; + box-shadow: 0 0 0 3px var(--primary-lighter) !important; +} + +.el-select { + --el-select-border-color-hover: var(--primary-light) !important; +} + +.el-select .el-input__wrapper { + border-radius: var(--radius-md) !important; +} + +.el-select-dropdown { + border-radius: var(--radius-lg) !important; + border: 1px solid var(--border-color) !important; + box-shadow: var(--shadow-xl) !important; + margin-top: 8px !important; +} + +.el-select-dropdown__wrap { + max-height: 320px !important; +} + +.el-select-dropdown__item { + padding: 10px 16px !important; + line-height: 1.5 !important; + transition: all var(--transition-fast) !important; +} + +.el-select-dropdown__item.hover, +.el-select-dropdown__item:hover { + background-color: var(--primary-lighter) !important; + color: var(--primary-color) !important; +} + +.el-select-dropdown__item.is-selected { + background-color: var(--primary-lighter) !important; + color: var(--primary-color) !important; + font-weight: 600 !important; +} + +.el-tag { + border-radius: var(--radius-sm) !important; + font-weight: 500 !important; + border: none !important; +} + +.el-tag--success { + background-color: #D1FAE5 !important; + color: #059669 !important; +} + +.el-tag--warning { + background-color: #FEF3C7 !important; + color: #D97706 !important; +} + +.el-tag--danger { + background-color: #FEE2E2 !important; + color: #DC2626 !important; +} + +.el-tag--info { + background-color: #E0E7FF !important; + color: #4F46E5 !important; +} + +.el-table { + border-radius: var(--radius-lg) !important; + overflow: hidden !important; +} + +.el-table th.el-table__cell { + background-color: var(--bg-tertiary) !important; + font-weight: 600 !important; + color: var(--text-secondary) !important; +} + +.el-table td.el-table__cell { + border-bottom: 1px solid var(--border-light) !important; +} + +.el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell { + background-color: var(--bg-tertiary) !important; +} + +.el-table__row:hover > td.el-table__cell { + background-color: var(--primary-lighter) !important; +} + +.el-tabs__item { + font-weight: 500 !important; + transition: all var(--transition-fast) !important; +} + +.el-tabs__item.is-active { + color: var(--primary-color) !important; +} + +.el-tabs__active-bar { + background-color: var(--primary-color) !important; +} + +.el-dialog { + border-radius: var(--radius-xl) !important; + overflow: hidden !important; +} + +.el-dialog__header { + padding: 20px 24px !important; + border-bottom: 1px solid var(--border-light) !important; +} + +.el-dialog__title { + font-weight: 600 !important; + font-size: 18px !important; +} + +.el-dialog__body { + padding: 24px !important; +} + +.el-form-item__label { + font-weight: 500 !important; + color: var(--text-secondary) !important; +} + +.el-slider__bar { + background-color: var(--primary-color) !important; +} + +.el-slider__button { + border-color: var(--primary-color) !important; +} + +.el-switch.is-checked .el-switch__core { + background-color: var(--primary-color) !important; + border-color: var(--primary-color) !important; +} + +.el-alert { + border-radius: var(--radius-md) !important; + border: none !important; +} + +.el-alert--info { + background-color: #EFF6FF !important; +} + +.el-alert--success { + background-color: #ECFDF5 !important; +} + +.el-alert--warning { + background-color: #FFFBEB !important; +} + +.el-alert--error { + background-color: #FEF2F2 !important; +} + +.el-divider { + border-color: var(--border-light) !important; +} + +.el-empty__description { + color: var(--text-tertiary) !important; +} + +.el-loading-mask { + border-radius: var(--radius-lg) !important; +} + +.el-descriptions { + border-radius: var(--radius-md) !important; + overflow: hidden !important; +} + +.el-descriptions__label { + background-color: var(--bg-tertiary) !important; + font-weight: 500 !important; +} + +.fade-in-up { + animation: fadeInUp 0.5s ease-out forwards; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.slide-in-left { + animation: slideInLeft 0.4s ease-out forwards; +} + +@keyframes slideInLeft { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.scale-in { + animation: scaleIn 0.3s ease-out forwards; +} + +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +.page-container { + padding: 24px; + min-height: calc(100vh - 60px); + background-color: var(--bg-primary); +} + +.page-header { + margin-bottom: 24px; +} + +.page-title { + font-size: 24px; + font-weight: 700; + color: var(--text-primary); + margin: 0 0 8px 0; + letter-spacing: -0.5px; +} + +.page-desc { + font-size: 14px; + color: var(--text-secondary); + margin: 0; + line-height: 1.6; +} + +.card-icon { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-md); + font-size: 18px; +} + +.card-icon.primary { + background-color: var(--primary-lighter); + color: var(--primary-color); +} + +.card-icon.success { + background-color: #D1FAE5; + color: #059669; +} + +.card-icon.warning { + background-color: #FEF3C7; + color: #D97706; +} + +.card-icon.info { + background-color: #E0E7FF; + color: #4F46E5; +} + +.stat-card { + position: relative; + overflow: hidden; +} + +.stat-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); + opacity: 0; + transition: opacity var(--transition-fast); +} + +.stat-card:hover::before { + opacity: 1; +} + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--bg-tertiary); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb { + background: var(--text-tertiary); + border-radius: 4px; + transition: background var(--transition-fast); +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary); +} + +::selection { + background-color: var(--primary-lighter); + color: var(--primary-dark); +} + +@media (max-width: 768px) { + .page-container { + padding: 16px; + } + + .page-title { + font-size: 20px; + } +} diff --git a/ai-service-admin/src/types/llm.ts b/ai-service-admin/src/types/llm.ts index 50889df..e64bec2 100644 --- a/ai-service-admin/src/types/llm.ts +++ b/ai-service-admin/src/types/llm.ts @@ -29,7 +29,8 @@ export interface LLMTestResult { export interface LLMTestRequest { test_prompt?: string - config?: LLMConfigUpdate + provider?: string + config?: Record } export interface LLMProvidersResponse { diff --git a/ai-service-admin/src/utils/request.ts b/ai-service-admin/src/utils/request.ts index a3b7f10..7d64d24 100644 --- a/ai-service-admin/src/utils/request.ts +++ b/ai-service-admin/src/utils/request.ts @@ -5,7 +5,7 @@ import { useTenantStore } from '@/stores/tenant' // 创建 axios 实例 const service = axios.create({ baseURL: import.meta.env.VITE_APP_BASE_API || '/api', - timeout: 10000 + timeout: 60000 }) // 请求拦截器 diff --git a/ai-service-admin/src/views/admin/embedding/index.vue b/ai-service-admin/src/views/admin/embedding/index.vue index d3f6d6a..e38ad24 100644 --- a/ai-service-admin/src/views/admin/embedding/index.vue +++ b/ai-service-admin/src/views/admin/embedding/index.vue @@ -6,95 +6,91 @@

嵌入模型配置

配置和管理系统使用的嵌入模型,支持多种提供者切换。配置修改后需保存才能生效。

-
- - - 上次更新: {{ formatDate(currentConfig.updated_at) }} - +
+
+ + 上次更新: {{ formatDate(currentConfig.updated_at) }} +
-
- - -
-
- - - -
- - {{ currentProvider.description }} -
-
+
+
+ - - - - -
- + + +
+ + {{ currentProvider.description }}
- - -
- - -
+ + + + +
+ + +
-
- -
+