ai-robot-core/ai-service-admin/src/views/admin/embedding/index.vue

495 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="embedding-config-page">
<div class="page-header">
<div class="header-content">
<div class="title-section">
<h1 class="page-title">嵌入模型配置</h1>
<p class="page-desc">配置和管理系统使用的嵌入模型支持多种提供者切换配置修改后需保存才能生效</p>
</div>
<div class="header-actions" v-if="currentConfig.updated_at">
<div class="update-info">
<el-icon class="update-icon"><Clock /></el-icon>
<span>上次更新: {{ formatDate(currentConfig.updated_at) }}</span>
</div>
</div>
</div>
</div>
<el-row :gutter="24" v-loading="pageLoading" element-loading-text="加载中...">
<el-col :xs="24" :sm="24" :md="12" :lg="12">
<el-card shadow="hover" class="config-card">
<template #header>
<div class="card-header">
<div class="header-left">
<div class="icon-wrapper">
<el-icon><Setting /></el-icon>
</div>
<span class="header-title">模型配置</span>
</div>
</div>
</template>
<div class="card-content">
<div class="provider-select-section">
<div class="section-label">
<el-icon><Connection /></el-icon>
<span>选择提供者</span>
</div>
<EmbeddingProviderSelect
v-model="currentConfig.provider"
:providers="providers"
:loading="providersLoading"
placeholder="请选择嵌入模型提供者"
@change="handleProviderChange"
/>
<transition name="fade">
<div v-if="currentProvider" class="provider-info">
<el-icon class="info-icon"><InfoFilled /></el-icon>
<span class="info-text">{{ currentProvider.description }}</span>
</div>
</transition>
</div>
<el-divider />
<transition name="slide-fade" mode="out-in">
<div v-if="currentConfig.provider" key="form" class="config-form-section">
<EmbeddingConfigForm
ref="configFormRef"
:schema="configSchema"
v-model="currentConfig.config"
label-width="140px"
/>
</div>
<el-empty v-else key="empty" description="请先选择一个嵌入模型提供者" :image-size="120">
<template #image>
<div class="empty-icon">
<el-icon><Box /></el-icon>
</div>
</template>
</el-empty>
</transition>
</div>
<template #footer>
<div class="card-footer">
<el-button size="large" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>
重置
</el-button>
<el-button type="primary" size="large" :loading="saving" @click="handleSave">
<el-icon><Check /></el-icon>
保存配置
</el-button>
</div>
</template>
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :md="12" :lg="12">
<div class="right-column">
<EmbeddingTestPanel
:config="{ provider: currentConfig.provider, config: currentConfig.config }"
/>
<el-card shadow="hover" class="formats-card">
<template #header>
<div class="card-header">
<div class="header-left">
<div class="icon-wrapper">
<el-icon><Document /></el-icon>
</div>
<span class="header-title">支持的文档格式</span>
</div>
</div>
</template>
<SupportedFormats />
</el-card>
</div>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Setting, Connection, InfoFilled, Box, RefreshLeft, Check, Clock, Document } from '@element-plus/icons-vue'
import { useEmbeddingStore } from '@/stores/embedding'
import EmbeddingProviderSelect from '@/components/embedding/EmbeddingProviderSelect.vue'
import EmbeddingConfigForm from '@/components/embedding/EmbeddingConfigForm.vue'
import EmbeddingTestPanel from '@/components/embedding/EmbeddingTestPanel.vue'
import SupportedFormats from '@/components/embedding/SupportedFormats.vue'
const embeddingStore = useEmbeddingStore()
const configFormRef = ref<InstanceType<typeof EmbeddingConfigForm>>()
const saving = ref(false)
const pageLoading = ref(false)
const providers = computed(() => embeddingStore.providers)
const currentConfig = computed(() => embeddingStore.currentConfig)
const currentProvider = computed(() => embeddingStore.currentProvider)
const configSchema = computed(() => embeddingStore.configSchema)
const providersLoading = computed(() => embeddingStore.providersLoading)
const formatDate = (dateStr: string) => {
if (!dateStr) return ''
const date = new Date(dateStr)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
const handleProviderChange = (provider: any) => {
if (provider) {
embeddingStore.setProvider(provider.name)
}
}
const handleSave = async () => {
if (!currentConfig.value.provider) {
ElMessage.warning('请先选择嵌入模型提供者')
return
}
try {
const valid = await configFormRef.value?.validate()
if (!valid) {
return
}
} catch (error) {
ElMessage.warning('请检查配置表单中的必填项')
return
}
saving.value = true
try {
const response: any = await embeddingStore.saveCurrentConfig()
ElMessage.success('配置保存成功')
if (response?.warning || response?.requires_reindex) {
ElMessageBox.alert(
response.warning || '嵌入模型已更改,请重新上传文档以确保检索效果正常。',
'重要提示',
{
confirmButtonText: '我知道了',
type: 'warning',
}
)
}
} catch (error) {
ElMessage.error('配置保存失败')
} finally {
saving.value = false
}
}
const handleReset = async () => {
try {
await ElMessageBox.confirm('确定要重置配置吗?将恢复为当前保存的配置。', '确认重置', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await embeddingStore.loadConfig()
ElMessage.success('配置已重置')
} catch (error) {
}
}
const initPage = async () => {
pageLoading.value = true
try {
await Promise.all([
embeddingStore.loadProviders(),
embeddingStore.loadConfig(),
embeddingStore.loadFormats()
])
} catch (error) {
ElMessage.error('初始化页面失败')
} finally {
pageLoading.value = false
}
}
onMounted(() => {
initPage()
})
</script>
<style scoped>
.embedding-config-page {
padding: 24px;
min-height: calc(100vh - 60px);
}
.page-header {
margin-bottom: 24px;
animation: slideDown 0.4s ease-out;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.header-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 20px;
flex-wrap: wrap;
}
.title-section {
flex: 1;
min-width: 300px;
}
.page-title {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.5px;
}
.page-desc {
margin: 0;
font-size: 14px;
color: var(--text-secondary);
line-height: 1.6;
}
.header-actions {
display: flex;
align-items: center;
}
.update-info {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background-color: var(--bg-tertiary);
border-radius: 8px;
font-size: 13px;
color: var(--text-secondary);
}
.update-icon {
font-size: 14px;
color: var(--text-tertiary);
}
.config-card {
animation: fadeInUp 0.5s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0;
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.icon-wrapper {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--primary-lighter);
border-radius: 10px;
color: var(--primary-color);
font-size: 18px;
}
.header-title {
font-size: 15px;
font-weight: 600;
color: var(--text-primary);
}
.card-content {
padding: 8px 0;
}
.provider-select-section {
margin-bottom: 16px;
}
.section-label {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
}
.section-label .el-icon {
color: var(--primary-color);
}
.provider-info {
display: flex;
align-items: flex-start;
gap: 10px;
margin-top: 14px;
padding: 14px 16px;
background-color: var(--bg-tertiary);
border-radius: 10px;
font-size: 13px;
color: var(--text-secondary);
line-height: 1.6;
border-left: 3px solid var(--primary-color);
}
.info-icon {
margin-top: 2px;
color: var(--primary-color);
font-size: 16px;
}
.info-text {
flex: 1;
}
.config-form-section {
max-height: 400px;
overflow-y: auto;
padding-right: 8px;
}
.config-form-section::-webkit-scrollbar {
width: 6px;
}
.config-form-section::-webkit-scrollbar-track {
background: var(--bg-tertiary);
border-radius: 3px;
}
.config-form-section::-webkit-scrollbar-thumb {
background: var(--text-tertiary);
border-radius: 3px;
}
.config-form-section::-webkit-scrollbar-thumb:hover {
background: var(--text-secondary);
}
.card-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding-top: 8px;
}
.right-column {
display: flex;
flex-direction: column;
gap: 24px;
}
.formats-card {
animation: fadeInUp 0.6s ease-out;
}
.empty-icon {
width: 100px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--bg-tertiary);
border-radius: 50%;
margin: 0 auto;
}
.empty-icon .el-icon {
font-size: 48px;
color: var(--text-tertiary);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.25s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.slide-fade-enter-active {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.slide-fade-leave-active {
transition: all 0.2s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter-from {
opacity: 0;
transform: translateX(-16px);
}
.slide-fade-leave-to {
opacity: 0;
transform: translateX(16px);
}
@media (max-width: 768px) {
.embedding-config-page {
padding: 16px;
}
.page-title {
font-size: 20px;
}
.header-content {
flex-direction: column;
}
.title-section {
min-width: 100%;
}
.config-form-section {
max-height: 300px;
}
}
</style>