Files
X-Agents/web/src/views/Knowledge.vue
DESKTOP-72TV0V4\caoxiaozhu 3cc1461be2 feat: 增强 Knowledge 页面功能
- 优化知识库创建流程
- 添加更多知识库配置选项
- 改进样式和交互

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

714 lines
24 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, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { useModelSettings } from './settings/useModelSettings'
import './knowledge/knowledge.css'
// 获取已配置的模型列表
const { models, fetchModels } = useModelSettings()
// 页面加载时获取模型列表
onMounted(() => {
fetchModels()
})
// 筛选 LLM (chat) 模型
const llmModels = computed(() => {
return models.value.filter((m: any) => m.model_type === 'chat')
})
// 筛选 Embedding 模型
const embeddingModels = computed(() => {
return models.value.filter((m: any) => m.model_type === 'embedding')
})
// 步骤验证
const step1Valid = computed(() => !!newKbForm.value.name.trim())
const step2Valid = computed(() => !!modelConfig.value.llmModelId && !!modelConfig.value.embeddingModelId)
const step3Valid = computed(() => {
// Parsing - 如果选择 docling则需要填写 URL
if (parsingConfig.value.engine === 'docling') {
return !!parsingConfig.value.doclingUrl.trim()
}
return true
})
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 showFileUploadDialog = ref(false)
const selectedKnowledge = ref<any>(null)
const fileFilter = ref('all') // all, parsed, parsing, failed
const selectedFile = ref<any>(null) // 当前选中的文件
const newKbForm = ref({
name: '',
description: '',
})
// 模型配置
const modelConfig = ref({
llmModelId: '',
embeddingModelId: '',
})
const parsingConfig = ref({
enablePdf: true,
engine: 'markitdown',
doclingUrl: '',
pandoc: true,
academic: false,
highRes: false,
fileSizeLimit: '5242880',
})
const openCreateDialog = () => {
createStep.value = 1
newKbForm.value = { name: '', description: '' }
modelConfig.value = { llmModelId: '', embeddingModelId: '' }
parsingConfig.value = {
enablePdf: true,
engine: 'markitdown',
doclingUrl: '',
pandoc: true,
academic: false,
highRes: false,
fileSizeLimit: '5242880',
}
showCreateDialog.value = true
}
const cancelCreate = () => {
newKbForm.value = { name: '', description: '' }
modelConfig.value = { llmModelId: '', embeddingModelId: '' }
parsingConfig.value = {
enablePdf: true,
engine: 'markitdown',
doclingUrl: '',
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: '' }
modelConfig.value = { llmModelId: '', embeddingModelId: '' }
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 enterKnowledge = (kb: any) => {
selectedKnowledge.value = kb
showFileUploadDialog.value = true
}
</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="enterKnowledge(kb)"
class="p-2 rounded-lg transition-colors hover:bg-dark-600"
title="Enter"
>
<i class="fa-solid fa-door-open text-gray-400 hover:text-primary-orange"></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="Create Knowledge Base"
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="LLM Model">
<el-select v-model="modelConfig.llmModelId" placeholder="Select a chat model" class="w-full" popper-class="dark-select-dropdown">
<el-option
v-for="model in llmModels"
: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-item label="Embedding Model">
<el-select v-model="modelConfig.embeddingModelId" placeholder="Select an embedding model" class="w-full" popper-class="dark-select-dropdown">
<el-option
v-for="model in embeddingModels"
: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">Parsing Engine</span>
<el-select v-model="parsingConfig.engine" placeholder="Select engine" class="parsing-select" popper-class="dark-select-dropdown">
<el-option label="MarkItDown" value="markitdown" />
<el-option label="Docling" value="docling" />
</el-select>
</div>
<!-- Docling URL 配置 -->
<div v-if="parsingConfig.engine === 'docling'" class="parsing-row" style="margin-top: 12px; align-items: center;">
<span class="parsing-label" style="flex: 1;">Docling URL</span>
<input
v-model="parsingConfig.doclingUrl"
type="text"
placeholder="http://localhost:8080"
class="input-field"
style="width: 350px;"
>
</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>
<!-- 文件上传弹窗 -->
<el-dialog
v-model="showFileUploadDialog"
:title="selectedKnowledge?.name || 'Knowledge Base'"
width="95vw"
top="20px"
:close-on-click-modal="false"
class="kb-dialog file-upload-dialog"
>
<div class="file-upload-layout">
<!-- 顶部导航 -->
<div class="file-header">
<button class="back-btn" @click="showFileUploadDialog = false">
<i class="fa-solid fa-arrow-left"></i>
</button>
<h2 class="file-title">{{ selectedKnowledge?.name || 'Knowledge Base' }}</h2>
<button class="btn-primary">
<i class="fa-solid fa-upload"></i>
上传文档
</button>
</div>
<!-- 标签栏 -->
<div class="file-tabs">
<div
class="file-tab"
:class="{ active: fileFilter === 'all' }"
@click="fileFilter = 'all'"
>
全部
</div>
<div
class="file-tab"
:class="{ active: fileFilter === 'parsed' }"
@click="fileFilter = 'parsed'"
>
已解析
</div>
<div
class="file-tab"
:class="{ active: fileFilter === 'parsing' }"
@click="fileFilter = 'parsing'"
>
解析中
</div>
<div
class="file-tab"
:class="{ active: fileFilter === 'failed' }"
@click="fileFilter = 'failed'"
>
解析失败
</div>
</div>
<!-- 内容区 - 左侧文件列表 + 右侧预览 -->
<div class="file-main">
<!-- 左侧文件列表 -->
<div class="file-list">
<!-- 模拟文件项 -->
<div
class="file-item"
:class="{ selected: selectedFile === i }"
v-for="i in 8"
:key="i"
@click="selectedFile = i"
>
<div class="file-item-icon">
<i class="fa-solid fa-file-pdf"></i>
</div>
<div class="file-item-info">
<div class="file-item-name">产品手册_v2.0.pdf</div>
<div class="file-item-meta">
<span>2.4 MB</span>
<span>2024-01-15</span>
</div>
</div>
<div class="file-item-status success">
<i class="fa-solid fa-check-circle"></i>
</div>
</div>
</div>
<!-- 右侧预览面板 -->
<div class="file-preview">
<div class="preview-header">
<div class="preview-header-left">
<i class="fa-solid fa-file-pdf"></i>
<span class="preview-filename">产品手册_v2.0.pdf</span>
</div>
<div class="preview-header-info">
<span class="info-tag">2.4 MB</span>
<span class="info-tag">2024-01-15 10:30</span>
<span class="info-tag success">解析成功</span>
<span class="info-tag">156 个切片</span>
</div>
</div>
<div class="preview-content">
<!-- PDF文件预览 -->
<div class="pdf-preview">
<iframe
src="https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
class="pdf-iframe"
></iframe>
</div>
<div class="preview-pagination">
<button class="page-btn" disabled>
<i class="fa-solid fa-chevron-left"></i>
</button>
<span class="page-info">1 / 3</span>
<button class="page-btn">
<i class="fa-solid fa-chevron-right"></i>
</button>
</div>
</div>
<div class="preview-actions">
<button class="action-btn delete">
<i class="fa-solid fa-trash"></i>
删除
</button>
<button class="action-btn reparse">
<i class="fa-solid fa-rotate"></i>
重新解析
</button>
<button class="action-btn confirm">
<i class="fa-solid fa-check"></i>
确认
</button>
</div>
</div>
</div>
</div>
</el-dialog>
</div>
</template>