Files
X-Agents/web/src/views/Knowledge.vue
DESKTOP-72TV0V4\caoxiaozhu afa3026585 feat: 增强 Knowledge 创建流程步骤验证
- 添加步骤有效性验证逻辑
- 支持跳转到已完成步骤
- 优化创建流程用户体验

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 10:47:03 +08:00

571 lines
20 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.
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { useModelSettings } from './settings/useModelSettings'
import './knowledge/knowledge.css'
// 获取已配置的模型列表
const { models, fetchModels } = useModelSettings()
// 步骤验证
const step1Valid = computed(() => !!newKbForm.value.name.trim())
const step2Valid = computed(() => !!embeddingConfig.value.modelId)
const step3Valid = computed(() => true) // Parsing - 暂时默认通过
const step4Valid = computed(() => true) // Storage - 暂时默认通过
// 获取当前步骤是否有效
const isCurrentStepValid = computed(() => {
switch (createStep.value) {
case 1: return step1Valid.value
case 2: return step2Valid.value
case 3: return step3Valid.value
case 4: return step4Valid.value
default: return false
}
})
// 步骤是否可以点击(只能点击当前步骤或已完成的步骤)
const canClickStep = (step: number) => {
if (step === 1) return true
if (step === createStep.value) return true
// 可以点击已完成的前面的步骤
if (step < createStep.value) {
switch (step) {
case 1: return step1Valid.value
case 2: return step2Valid.value
case 3: return step3Valid.value
case 4: return step4Valid.value
default: return false
}
}
return false
}
// 下一步
const nextStep = () => {
if (!isCurrentStepValid.value) {
ElMessage.warning('Please complete the current step first')
return
}
if (createStep.value < 4) {
createStep.value++
}
}
// 上一步
const prevStep = () => {
if (createStep.value > 1) {
createStep.value--
}
}
// 模拟知识库数据
const knowledgeBases = ref([
{
id: '1',
name: 'Product Documentation',
description: 'Product user manual and API docs',
document_count: 156,
chunk_count: 1248,
created_at: '2024-01-15T10:30:00Z',
status: 'ready'
},
{
id: '2',
name: 'Company Policies',
description: 'Internal company policies and procedures',
document_count: 42,
chunk_count: 320,
created_at: '2024-01-20T14:20:00Z',
status: 'ready'
},
{
id: '3',
name: 'Technical Wiki',
description: 'Engineering documentation and RFCs',
document_count: 89,
chunk_count: 756,
created_at: '2024-02-01T09:15:00Z',
status: 'processing'
},
])
const loading = ref(false)
const searchQuery = ref('')
// 筛选知识库
const filteredKnowledgeBases = () => {
if (!searchQuery.value) return knowledgeBases.value
const query = searchQuery.value.toLowerCase()
return knowledgeBases.value.filter(kb =>
kb.name.toLowerCase().includes(query) ||
kb.description.toLowerCase().includes(query)
)
}
// 新建知识库 - 分步骤
const showCreateDialog = ref(false)
const createStep = ref(1)
const newKbForm = ref({
name: '',
description: '',
})
const embeddingConfig = ref({
modelId: '',
})
const parsingConfig = ref({
enablePdf: true,
engine: 'default',
pandoc: true,
academic: false,
highRes: false,
fileSizeLimit: '5242880',
})
const openCreateDialog = () => {
createStep.value = 1
newKbForm.value = { name: '', description: '' }
embeddingConfig.value = { modelId: '' }
// 获取已配置的模型列表
fetchModels()
parsingConfig.value = {
enablePdf: true,
engine: 'default',
pandoc: true,
academic: false,
highRes: false,
fileSizeLimit: '5242880',
}
showCreateDialog.value = true
}
const cancelCreate = () => {
newKbForm.value = { name: '', description: '' }
embeddingConfig.value = { modelId: '' }
parsingConfig.value = {
enablePdf: true,
engine: 'default',
pandoc: true,
academic: false,
highRes: false,
fileSizeLimit: '5242880',
}
showCreateDialog.value = false
}
const createKnowledgeBase = () => {
knowledgeBases.value.push({
id: Date.now().toString(),
name: newKbForm.value.name,
description: newKbForm.value.description,
document_count: 0,
chunk_count: 0,
created_at: new Date().toISOString(),
status: 'ready'
})
newKbForm.value = { name: '', description: '' }
embeddingConfig.value = { modelId: '' }
showCreateDialog.value = false
ElMessage.success('Knowledge base created successfully')
}
// 编辑知识库
const showEditDialog = ref(false)
const editForm = ref({
id: '',
name: '',
description: '',
})
const openEdit = (kb: any) => {
editForm.value = {
id: kb.id,
name: kb.name,
description: kb.description
}
showEditDialog.value = true
}
const saveEdit = () => {
const kb = knowledgeBases.value.find(k => k.id === editForm.value.id)
if (kb) {
kb.name = editForm.value.name
kb.description = editForm.value.description
}
showEditDialog.value = false
ElMessage.success('Knowledge base updated successfully')
}
const cancelEdit = () => {
showEditDialog.value = false
}
// 删除知识库
const deleteKb = (id: string) => {
const index = knowledgeBases.value.findIndex(k => k.id === id)
if (index > -1) {
knowledgeBases.value.splice(index, 1)
ElMessage.success('Knowledge base deleted')
}
}
// 查看详情
const viewDetail = (kb: any) => {
ElMessage.info(`Viewing ${kb.name}`)
}
</script>
<template>
<div class="p-6 min-h-screen">
<!-- 顶部导航 -->
<div class="flex justify-between items-center mb-6">
<div class="flex items-center gap-2">
<i class="fa-solid fa-brain text-gray-400"></i>
<span class="font-medium">Knowledge Base</span>
</div>
<button @click="openCreateDialog" class="btn-primary">
<i class="fa-solid fa-plus"></i>
New Knowledge Base
</button>
</div>
<!-- 搜索 -->
<div class="flex gap-4 mb-6">
<div class="flex-1 relative">
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
<input
v-model="searchQuery"
type="text"
placeholder="Search knowledge bases by name or description..."
class="search-input w-full"
>
</div>
</div>
<!-- Knowledge Base 列表 -->
<div class="bg-dark-700 rounded-xl overflow-hidden">
<div v-if="loading" class="py-12 text-center text-gray-400">
<i class="fa-solid fa-circle-notch fa-spin text-2xl mb-2"></i>
<p>Loading...</p>
</div>
<table v-else-if="filteredKnowledgeBases().length > 0" class="w-full">
<thead class="bg-dark-600">
<tr>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Name</th>
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Documents</th>
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Chunks</th>
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Status</th>
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Created</th>
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="kb in filteredKnowledgeBases()" :key="kb.id" class="table-row">
<td class="px-5 py-4">
<div class="font-medium">{{ kb.name }}</div>
<div class="text-sm text-gray-500">{{ kb.description }}</div>
</td>
<td class="px-5 py-4 text-center">
<span class="text-primary-cyan">{{ kb.document_count }}</span>
</td>
<td class="px-5 py-4 text-center text-gray-400 text-sm">
{{ kb.chunk_count }}
</td>
<td class="px-5 py-4 text-center">
<span v-if="kb.status === 'ready'" class="bg-primary-success/20 text-primary-success px-2 py-1 rounded text-sm">
Ready
</span>
<span v-else-if="kb.status === 'processing'" class="bg-primary-warning/20 text-primary-warning px-2 py-1 rounded text-sm">
Processing
</span>
</td>
<td class="px-5 py-4 text-center text-gray-400 text-sm">
{{ kb.created_at?.split('T')[0] }}
</td>
<td class="px-5 py-4">
<div class="flex items-center justify-center gap-2">
<button
@click="viewDetail(kb)"
class="p-2 rounded-lg transition-colors hover:bg-dark-600"
title="View"
>
<i class="fa-solid fa-eye text-gray-400 hover:text-primary-danger"></i>
</button>
<button
@click="openEdit(kb)"
class="p-2 rounded-lg transition-colors hover:bg-dark-600"
title="Edit"
>
<i class="fa-solid fa-pen text-gray-400 hover:text-primary-danger"></i>
</button>
<button
@click="deleteKb(kb.id)"
class="p-2 rounded-lg transition-colors hover:bg-dark-600"
title="Delete"
>
<i class="fa-solid fa-trash text-gray-400 hover:text-primary-danger"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<!-- 空状态 -->
<div v-else class="empty-box">
<div class="empty-icon">
<i class="fa-solid fa-brain"></i>
</div>
<p class="empty-text">No knowledge bases found</p>
<p class="empty-tip">Click "New Knowledge Base" to create one</p>
</div>
</div>
<!-- 新建知识库弹窗 -->
<el-dialog
v-model="showCreateDialog"
title="创建知识库"
width="1100px"
:close-on-click-modal="false"
:show-close="false"
class="kb-dialog kb-create-dialog"
>
<div class="create-layout">
<!-- 左侧选项列表 -->
<div class="sidebar">
<div
class="menu-item"
:class="{ active: createStep === 1 }"
@click="createStep = 1"
>
<i class="fa-solid fa-layer-group menu-icon"></i>
<div class="menu-content">
<span class="menu-title">Basic Info</span>
<span class="menu-desc">Name and description</span>
</div>
<i v-if="createStep === 1" class="fa-solid fa-chevron-right menu-arrow"></i>
<i v-else-if="createStep > 1 && step1Valid" class="fa-solid fa-check-circle menu-check"></i>
</div>
<div
class="menu-item"
:class="{ active: createStep === 2, disabled: !canClickStep(2) }"
@click="canClickStep(2) && (createStep = 2)"
>
<i class="fa-solid fa-robot menu-icon"></i>
<div class="menu-content">
<span class="menu-title">Model Config</span>
<span class="menu-desc">Embedding & rerank models</span>
</div>
<i v-if="createStep === 2" class="fa-solid fa-chevron-right menu-arrow"></i>
<i v-else-if="createStep > 2 && step2Valid" class="fa-solid fa-check-circle menu-check"></i>
</div>
<div
class="menu-item"
:class="{ active: createStep === 3, disabled: !canClickStep(3) }"
@click="canClickStep(3) && (createStep = 3)"
>
<i class="fa-solid fa-file-lines menu-icon"></i>
<div class="menu-content">
<span class="menu-title">Parsing</span>
<span class="menu-desc">Document parsing settings</span>
</div>
<i v-if="createStep === 3" class="fa-solid fa-chevron-right menu-arrow"></i>
<i v-else-if="createStep > 3 && step3Valid" class="fa-solid fa-check-circle menu-check"></i>
</div>
<div
class="menu-item"
:class="{ active: createStep === 4, disabled: !canClickStep(4) }"
@click="canClickStep(4) && (createStep = 4)"
>
<i class="fa-solid fa-hard-drive menu-icon"></i>
<div class="menu-content">
<span class="menu-title">Storage</span>
<span class="menu-desc">Vector & file storage</span>
</div>
<i v-if="createStep === 4" class="fa-solid fa-chevron-right menu-arrow"></i>
<i v-else-if="createStep > 4 && step4Valid" class="fa-solid fa-check-circle menu-check"></i>
</div>
</div>
<!-- 右侧内容区 -->
<div class="content">
<!-- Step 1: Basic Info -->
<div v-if="createStep === 1" class="form-content">
<div class="section-header">
<i class="fa-solid fa-layer-group section-icon"></i>
<span class="section-title">Basic Info</span>
</div>
<el-form label-position="top" class="kb-form">
<el-form-item label="Name" required>
<el-input v-model="newKbForm.name" placeholder="Enter knowledge base name" />
</el-form-item>
<el-form-item label="Description">
<el-input v-model="newKbForm.description" type="textarea" :rows="4" placeholder="Enter knowledge base description" />
</el-form-item>
</el-form>
</div>
<!-- Step 2: Model Config -->
<div v-if="createStep === 2" class="form-content">
<div class="section-header">
<i class="fa-solid fa-robot section-icon"></i>
<span class="section-title">Model Config</span>
</div>
<el-form label-position="top" class="kb-form">
<el-form-item label="Embedding Model">
<el-select v-model="embeddingConfig.modelId" placeholder="Select a configured model" class="w-full" popper-class="dark-select-dropdown">
<el-option
v-for="model in models"
:key="model.id"
:label="model.name"
:value="model.id"
>
<div class="model-option">
<span class="model-name">{{ model.name }}</span>
<span class="model-info">{{ model.provider }} - {{ model.model }}</span>
</div>
</el-option>
</el-select>
</el-form-item>
</el-form>
</div>
<!-- Step 3: Parsing -->
<div v-if="createStep === 3" class="form-content">
<div class="section-header">
<i class="fa-solid fa-file-lines section-icon"></i>
<span class="section-title">Parsing</span>
</div>
<div class="parsing-section">
<div class="parsing-row">
<span class="parsing-label">Enable PDF Parsing</span>
<el-switch v-model="parsingConfig.enablePdf" />
</div>
<div class="parsing-row">
<span class="parsing-label">Parsing Engine</span>
<el-select v-model="parsingConfig.engine" placeholder="Select engine" class="parsing-select">
<el-option label="Default Engine" value="default" />
<el-option label="DeepDoc" value="deepdoc" />
</el-select>
</div>
<div class="parsing-divider"></div>
<div class="parsing-label">Enabled Post-processing</div>
<div class="postprocess-list">
<div class="postprocess-item" :class="{ active: parsingConfig.pandoc }">
<div class="postprocess-toggle">
<el-switch v-model="parsingConfig.pandoc" />
</div>
<div class="postprocess-info">
<span class="postprocess-title">Use Pandoc Markdown Enrichment</span>
<span class="postprocess-desc">Enable Pandoc conversion for rich formatting</span>
</div>
</div>
<div class="postprocess-item" :class="{ active: parsingConfig.academic }">
<div class="postprocess-toggle">
<el-switch v-model="parsingConfig.academic" />
</div>
<div class="postprocess-info">
<span class="postprocess-title">Use Academic Document Parsing</span>
<span class="postprocess-desc">Parse formulas, tables, and academic structures</span>
</div>
</div>
<div class="postprocess-item" :class="{ active: parsingConfig.highRes }">
<div class="postprocess-toggle">
<el-switch v-model="parsingConfig.highRes" />
</div>
<div class="postprocess-info">
<span class="postprocess-title">Use High Resolution Parsing</span>
<span class="postprocess-desc">High quality mode for complex documents</span>
</div>
</div>
</div>
<div class="parsing-divider"></div>
<div class="parsing-row">
<span class="parsing-label">File Size Limit</span>
<el-input v-model="parsingConfig.fileSizeLimit" class="parsing-input" />
</div>
</div>
</div>
<!-- Step 4: Storage -->
<div v-if="createStep === 4" class="form-content">
<div class="section-header">
<i class="fa-solid fa-hard-drive section-icon"></i>
<span class="section-title">Storage</span>
</div>
<el-form label-position="top" class="kb-form">
<el-form-item label="Storage Type">
<el-select placeholder="Select storage type" class="w-full">
<el-option label="Local" value="local" />
<el-option label="MinIO" value="minio" />
<el-option label="S3" value="s3" />
</el-select>
</el-form-item>
</el-form>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<div class="footer-left">
<span class="step-hint">Step {{ createStep }} of 4</span>
</div>
<div class="footer-right">
<el-button @click="cancelCreate" class="cancel-btn">Cancel</el-button>
<el-button v-if="createStep > 1" @click="prevStep" class="prev-btn">
<i class="fa-solid fa-arrow-left"></i>
Previous
</el-button>
<el-button v-if="createStep < 4" :disabled="!isCurrentStepValid" @click="nextStep" class="next-btn">
Next
<i class="fa-solid fa-arrow-right"></i>
</el-button>
<el-button v-if="createStep === 4" :disabled="!isCurrentStepValid" @click="createKnowledgeBase" class="confirm-btn">
<i class="fa-solid fa-plus"></i>
Create
</el-button>
</div>
</div>
</template>
</el-dialog>
<!-- 编辑知识库弹窗 -->
<el-dialog
v-model="showEditDialog"
title="Edit Knowledge Base"
width="500px"
:close-on-click-modal="false"
class="kb-dialog"
>
<p class="dialog-desc">Edit knowledge base information</p>
<el-form label-position="top" class="kb-form">
<el-form-item label="Name">
<el-input v-model="editForm.name" placeholder="Enter knowledge base name" />
</el-form-item>
<el-form-item label="Description">
<el-input v-model="editForm.description" type="textarea" :rows="3" placeholder="Enter description" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button text @click="cancelEdit">Cancel</el-button>
<el-button type="primary" @click="saveEdit">Save</el-button>
</div>
</template>
</el-dialog>
</div>
</template>