feat: 完善 Knowledge 前端页面
- 增强知识库列表和详情展示 - 添加文档上传和管理功能 - 优化创建和编辑流程 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,16 +2,32 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useModelSettings } from './settings/useModelSettings'
|
||||
import { fetchKnowledgeBases, createKnowledgeBase as apiCreateKnowledgeBase, deleteKnowledgeBase as apiDeleteKnowledgeBase, fetchKnowledgeDocuments } from './knowledge/useKnowledge'
|
||||
import './knowledge/knowledge.css'
|
||||
|
||||
// 获取已配置的模型列表
|
||||
const { models, fetchModels } = useModelSettings()
|
||||
|
||||
// 页面加载时获取模型列表
|
||||
onMounted(() => {
|
||||
// 页面加载时获取数据
|
||||
onMounted(async () => {
|
||||
fetchModels()
|
||||
await fetchKbList()
|
||||
})
|
||||
|
||||
// 获取知识库列表
|
||||
const knowledgeBases = ref<any[]>([])
|
||||
const loadingKb = ref(false)
|
||||
|
||||
const fetchKbList = async () => {
|
||||
loadingKb.value = true
|
||||
try {
|
||||
const data = await fetchKnowledgeBases()
|
||||
knowledgeBases.value = data
|
||||
} finally {
|
||||
loadingKb.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 筛选 LLM (chat) 模型
|
||||
const llmModels = computed(() => {
|
||||
return models.value.filter((m: any) => m.model_type === 'chat')
|
||||
@@ -80,37 +96,6 @@ const prevStep = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟知识库数据
|
||||
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('')
|
||||
|
||||
@@ -132,7 +117,17 @@ 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 selectedFile = ref<any>(null) // 当前选中的文件ID
|
||||
const selectedDocument = ref<any>(null) // 当前选中的文档详情
|
||||
const knowledgeDocuments = ref<any[]>([]) // 知识库文档列表
|
||||
const loadingDocuments = ref(false)
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
const uploading = ref(false)
|
||||
const previewUrl = ref('') // 文档预览URL
|
||||
const loadingPreview = ref(false)
|
||||
const previewPage = ref(1) // 当前页码
|
||||
const previewTotalPages = ref(1) // 总页数
|
||||
|
||||
const newKbForm = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
@@ -153,6 +148,11 @@ const parsingConfig = ref({
|
||||
fileSizeLimit: '5242880',
|
||||
})
|
||||
|
||||
// Storage 配置
|
||||
const storageConfig = ref({
|
||||
type: 'local',
|
||||
})
|
||||
|
||||
const openCreateDialog = () => {
|
||||
createStep.value = 1
|
||||
newKbForm.value = { name: '', description: '' }
|
||||
@@ -166,6 +166,7 @@ const openCreateDialog = () => {
|
||||
highRes: false,
|
||||
fileSizeLimit: '5242880',
|
||||
}
|
||||
storageConfig.value = { type: 'local' }
|
||||
showCreateDialog.value = true
|
||||
}
|
||||
|
||||
@@ -181,23 +182,42 @@ const cancelCreate = () => {
|
||||
highRes: false,
|
||||
fileSizeLimit: '5242880',
|
||||
}
|
||||
storageConfig.value = { type: 'local' }
|
||||
showCreateDialog.value = false
|
||||
}
|
||||
|
||||
const createKnowledgeBase = () => {
|
||||
knowledgeBases.value.push({
|
||||
id: Date.now().toString(),
|
||||
const createKnowledgeBase = async () => {
|
||||
const result = await apiCreateKnowledgeBase({
|
||||
name: newKbForm.value.name,
|
||||
description: newKbForm.value.description,
|
||||
document_count: 0,
|
||||
chunk_count: 0,
|
||||
created_at: new Date().toISOString(),
|
||||
status: 'ready'
|
||||
llm_model_id: modelConfig.value.llmModelId,
|
||||
embedding_model_id: modelConfig.value.embeddingModelId,
|
||||
parsing_config: {
|
||||
engine: parsingConfig.value.engine,
|
||||
docling_url: parsingConfig.value.engine === 'docling' ? parsingConfig.value.doclingUrl : undefined,
|
||||
enable_pdf: parsingConfig.value.enablePdf,
|
||||
pandoc: parsingConfig.value.pandoc,
|
||||
}
|
||||
})
|
||||
newKbForm.value = { name: '', description: '' }
|
||||
modelConfig.value = { llmModelId: '', embeddingModelId: '' }
|
||||
showCreateDialog.value = false
|
||||
ElMessage.success('Knowledge base created successfully')
|
||||
|
||||
if (result.success) {
|
||||
await fetchKbList()
|
||||
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
|
||||
ElMessage.success('Knowledge base created successfully')
|
||||
} else {
|
||||
ElMessage.error(result.message || 'Failed to create knowledge base')
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑知识库
|
||||
@@ -232,19 +252,217 @@ const cancelEdit = () => {
|
||||
}
|
||||
|
||||
// 删除知识库
|
||||
const deleteKb = (id: string) => {
|
||||
const index = knowledgeBases.value.findIndex(k => k.id === id)
|
||||
if (index > -1) {
|
||||
knowledgeBases.value.splice(index, 1)
|
||||
const deleteKb = async (id: string) => {
|
||||
const result = await apiDeleteKnowledgeBase(id)
|
||||
if (result.success) {
|
||||
await fetchKbList()
|
||||
ElMessage.success('Knowledge base deleted')
|
||||
} else {
|
||||
ElMessage.error(result.message || 'Failed to delete knowledge base')
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数:格式化文件大小
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (!bytes) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
// 辅助函数:格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
if (!dateStr) return ''
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleDateString('zh-CN')
|
||||
}
|
||||
|
||||
// 辅助函数:获取状态图标
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'parsed':
|
||||
return 'fa-solid fa-check-circle'
|
||||
case 'parsing':
|
||||
return 'fa-solid fa-spinner fa-spin'
|
||||
case 'failed':
|
||||
return 'fa-solid fa-circle-xmark'
|
||||
default:
|
||||
return 'fa-solid fa-clock'
|
||||
}
|
||||
}
|
||||
|
||||
// 进入知识库(上传文档界面)
|
||||
const enterKnowledge = (kb: any) => {
|
||||
const enterKnowledge = async (kb: any) => {
|
||||
selectedKnowledge.value = kb
|
||||
selectedFile.value = null
|
||||
previewUrl.value = ''
|
||||
|
||||
// 获取文档列表
|
||||
loadingDocuments.value = true
|
||||
try {
|
||||
const docs = await fetchKnowledgeDocuments(kb.id, fileFilter.value)
|
||||
console.log('Fetched documents:', docs)
|
||||
knowledgeDocuments.value = docs
|
||||
|
||||
// 自动选中第一个文档
|
||||
if (docs && docs.length > 0) {
|
||||
console.log('First doc:', docs[0])
|
||||
await selectDocument(docs[0])
|
||||
}
|
||||
} finally {
|
||||
loadingDocuments.value = false
|
||||
}
|
||||
|
||||
showFileUploadDialog.value = true
|
||||
}
|
||||
|
||||
// 切换文件过滤标签时重新获取文档列表
|
||||
const changeFileFilter = async (filter: string) => {
|
||||
fileFilter.value = filter
|
||||
if (selectedKnowledge.value) {
|
||||
loadingDocuments.value = true
|
||||
try {
|
||||
const docs = await fetchKnowledgeDocuments(selectedKnowledge.value.id, filter)
|
||||
knowledgeDocuments.value = docs
|
||||
} finally {
|
||||
loadingDocuments.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 选择文档
|
||||
const selectDocument = async (doc: any) => {
|
||||
selectedFile.value = doc.id
|
||||
selectedDocument.value = doc
|
||||
previewUrl.value = ''
|
||||
previewPage.value = 1
|
||||
previewTotalPages.value = 1
|
||||
|
||||
// 尝试从多个字段获取文件URL
|
||||
const fileUrl = doc.file_url || doc.fileUrl || doc.url || doc.FileURL
|
||||
if (fileUrl) {
|
||||
previewUrl.value = fileUrl
|
||||
} else if (selectedKnowledge.value && doc.status === 'parsed') {
|
||||
// 获取文档预览
|
||||
loadingPreview.value = true
|
||||
try {
|
||||
const { getDocumentPreview } = await import('./knowledge/useKnowledge')
|
||||
const result = await getDocumentPreview(selectedKnowledge.value.id, doc.id)
|
||||
if (result.success && result.data?.content) {
|
||||
previewUrl.value = result.data.content
|
||||
previewTotalPages.value = result.data.total_pages || 1
|
||||
previewPage.value = result.data.current_page || 1
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get preview:', error)
|
||||
} finally {
|
||||
loadingPreview.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 翻页
|
||||
const changePreviewPage = async (page: number) => {
|
||||
if (!selectedKnowledge.value || !selectedFile.value || page < 1 || page > previewTotalPages.value) return
|
||||
|
||||
loadingPreview.value = true
|
||||
previewPage.value = page
|
||||
try {
|
||||
const { getDocumentPreview } = await import('./knowledge/useKnowledge')
|
||||
const result = await getDocumentPreview(selectedKnowledge.value.id, selectedFile.value, page)
|
||||
if (result.success && result.data?.content) {
|
||||
previewUrl.value = result.data.content
|
||||
previewTotalPages.value = result.data.total_pages || 1
|
||||
previewPage.value = result.data.current_page || page
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get preview:', error)
|
||||
} finally {
|
||||
loadingPreview.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 触发文件上传
|
||||
const triggerFileUpload = () => {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
// 处理文件选择
|
||||
const handleFileSelect = async (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
const file = target.files?.[0]
|
||||
if (!file || !selectedKnowledge.value) return
|
||||
|
||||
uploading.value = true
|
||||
try {
|
||||
const { uploadDocument } = await import('./knowledge/useKnowledge')
|
||||
const result = await uploadDocument(selectedKnowledge.value.id, file)
|
||||
|
||||
if (result.success) {
|
||||
ElMessage.success('File uploaded successfully')
|
||||
|
||||
// 后端返回 result.url 在顶层,result.document 里有 file_url
|
||||
const fileUrl = result.url || result.document?.file_url
|
||||
// 添加到文档列表
|
||||
const newDoc = result.document || {
|
||||
id: result.id,
|
||||
name: file.name,
|
||||
file_size: file.size,
|
||||
status: 'parsing',
|
||||
chunk_count: 0,
|
||||
uploaded_at: new Date().toISOString(),
|
||||
file_url: fileUrl
|
||||
}
|
||||
// 如果返回了file_url,添加到列表开头
|
||||
if (fileUrl) {
|
||||
previewUrl.value = fileUrl
|
||||
// 设置选中的文档信息
|
||||
selectedFile.value = result.id
|
||||
selectedDocument.value = newDoc
|
||||
// 添加到文档列表
|
||||
knowledgeDocuments.value = [newDoc, ...knowledgeDocuments.value]
|
||||
} else {
|
||||
// 刷新文档列表
|
||||
await changeFileFilter(fileFilter.value)
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(result.message || 'Failed to upload file')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('Failed to upload file')
|
||||
} finally {
|
||||
uploading.value = false
|
||||
// 清空 input 以便可以再次选择相同文件
|
||||
target.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 删除文档
|
||||
const deleteDocument = async (docId: string) => {
|
||||
if (!selectedKnowledge.value) return
|
||||
|
||||
try {
|
||||
const { deleteDocument: apiDeleteDocument } = await import('./knowledge/useKnowledge')
|
||||
const result = await apiDeleteDocument(selectedKnowledge.value.id, docId)
|
||||
|
||||
if (result.success) {
|
||||
ElMessage.success('Document deleted')
|
||||
// 如果删除的是当前选中的文件,清除选中状态
|
||||
if (selectedFile.value === docId) {
|
||||
selectedFile.value = null
|
||||
selectedDocument.value = null
|
||||
previewUrl.value = ''
|
||||
}
|
||||
// 刷新文档列表
|
||||
await changeFileFilter(fileFilter.value)
|
||||
} else {
|
||||
ElMessage.error(result.message || 'Failed to delete document')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('Failed to delete document')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -515,7 +733,7 @@ const enterKnowledge = (kb: any) => {
|
||||
</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-select v-model="storageConfig.type" placeholder="Select storage type" class="w-full" popper-class="dark-select-dropdown">
|
||||
<el-option label="Local" value="local" />
|
||||
<el-option label="MinIO" value="minio" />
|
||||
<el-option label="S3" value="s3" />
|
||||
@@ -580,8 +798,8 @@ const enterKnowledge = (kb: any) => {
|
||||
<!-- 文件上传弹窗 -->
|
||||
<el-dialog
|
||||
v-model="showFileUploadDialog"
|
||||
:title="selectedKnowledge?.name || 'Knowledge Base'"
|
||||
width="95vw"
|
||||
title=" "
|
||||
width="calc(100vw - 40px)"
|
||||
top="20px"
|
||||
:close-on-click-modal="false"
|
||||
class="kb-dialog file-upload-dialog"
|
||||
@@ -593,9 +811,16 @@ const enterKnowledge = (kb: any) => {
|
||||
<i class="fa-solid fa-arrow-left"></i>
|
||||
</button>
|
||||
<h2 class="file-title">{{ selectedKnowledge?.name || 'Knowledge Base' }}</h2>
|
||||
<button class="btn-primary">
|
||||
<input
|
||||
type="file"
|
||||
ref="fileInput"
|
||||
style="display: none"
|
||||
accept=".pdf,.doc,.docx,.txt,.md"
|
||||
@change="handleFileSelect"
|
||||
/>
|
||||
<button class="btn-primary" @click="triggerFileUpload">
|
||||
<i class="fa-solid fa-upload"></i>
|
||||
上传文档
|
||||
Upload
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -604,30 +829,30 @@ const enterKnowledge = (kb: any) => {
|
||||
<div
|
||||
class="file-tab"
|
||||
:class="{ active: fileFilter === 'all' }"
|
||||
@click="fileFilter = 'all'"
|
||||
@click="changeFileFilter('all')"
|
||||
>
|
||||
全部
|
||||
All
|
||||
</div>
|
||||
<div
|
||||
class="file-tab"
|
||||
:class="{ active: fileFilter === 'parsed' }"
|
||||
@click="fileFilter = 'parsed'"
|
||||
@click="changeFileFilter('parsed')"
|
||||
>
|
||||
已解析
|
||||
Parsed
|
||||
</div>
|
||||
<div
|
||||
class="file-tab"
|
||||
:class="{ active: fileFilter === 'parsing' }"
|
||||
@click="fileFilter = 'parsing'"
|
||||
@click="changeFileFilter('parsing')"
|
||||
>
|
||||
解析中
|
||||
Parsing
|
||||
</div>
|
||||
<div
|
||||
class="file-tab"
|
||||
:class="{ active: fileFilter === 'failed' }"
|
||||
@click="fileFilter = 'failed'"
|
||||
@click="changeFileFilter('failed')"
|
||||
>
|
||||
解析失败
|
||||
Failed
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -635,74 +860,98 @@ const enterKnowledge = (kb: any) => {
|
||||
<div class="file-main">
|
||||
<!-- 左侧文件列表 -->
|
||||
<div class="file-list">
|
||||
<!-- 模拟文件项 -->
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loadingDocuments" class="loading-state">
|
||||
<i class="fa-solid fa-spinner fa-spin"></i>
|
||||
<span>Loading...</span>
|
||||
</div>
|
||||
<!-- 无数据提示 -->
|
||||
<div v-else-if="knowledgeDocuments.length === 0" class="empty-state">
|
||||
<i class="fa-solid fa-folder-open"></i>
|
||||
<span>No documents yet</span>
|
||||
</div>
|
||||
<!-- 文档列表 -->
|
||||
<div
|
||||
v-else
|
||||
class="file-item"
|
||||
:class="{ selected: selectedFile === i }"
|
||||
v-for="i in 8"
|
||||
:key="i"
|
||||
@click="selectedFile = i"
|
||||
:class="{ selected: selectedFile === doc.id }"
|
||||
v-for="doc in knowledgeDocuments"
|
||||
:key="doc.id"
|
||||
@click="selectDocument(doc)"
|
||||
>
|
||||
<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-name">{{ doc.name }}</div>
|
||||
<div class="file-item-meta">
|
||||
<span>2.4 MB</span>
|
||||
<span>2024-01-15</span>
|
||||
<span>{{ formatFileSize(doc.file_size) }}</span>
|
||||
<span>{{ formatDate(doc.uploaded_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-item-status success">
|
||||
<i class="fa-solid fa-check-circle"></i>
|
||||
<div class="file-item-status" :class="doc.status">
|
||||
<span v-if="doc.status === 'failed'" class="status-text">Failed</span>
|
||||
<i v-else :class="getStatusIcon(doc.status)"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧预览面板 -->
|
||||
<div class="file-preview">
|
||||
<!-- 无选中文件时显示提示 -->
|
||||
<div v-if="!selectedFile" class="preview-empty">
|
||||
<i class="fa-solid fa-file-lines"></i>
|
||||
<span>Select a document to preview</span>
|
||||
</div>
|
||||
<!-- 有选中文件时显示预览 -->
|
||||
<template v-else>
|
||||
<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>
|
||||
<span class="preview-filename">{{ selectedDocument?.name || 'Document' }}</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>
|
||||
<span class="info-tag">{{ formatFileSize(selectedDocument?.file_size) }}</span>
|
||||
<span class="info-tag">{{ formatDate(selectedDocument?.uploaded_at) }}</span>
|
||||
<span class="info-tag" :class="selectedDocument?.status">{{ selectedDocument?.status === 'parsed' ? 'Parsed' : selectedDocument?.status === 'parsing' ? 'Parsing' : selectedDocument?.status === 'failed' ? 'Failed' : 'Pending' }}</span>
|
||||
<span class="info-tag">{{ selectedDocument?.chunk_count || 0 }} chunks</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 v-if="loadingPreview" class="preview-loading">
|
||||
<i class="fa-solid fa-spinner fa-spin"></i>
|
||||
<span>Loading preview...</span>
|
||||
</div>
|
||||
<!-- 有预览URL时显示PDF -->
|
||||
<embed
|
||||
v-else-if="previewUrl"
|
||||
type="application/pdf"
|
||||
:src="previewUrl"
|
||||
class="pdf-embed"
|
||||
/>
|
||||
<!-- 无预览时显示提示 -->
|
||||
<div v-else class="preview-no-file">
|
||||
<i class="fa-solid fa-file-pdf"></i>
|
||||
<span>Document preview not available</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="preview-actions">
|
||||
<button class="action-btn delete">
|
||||
<button class="action-btn delete" @click="deleteDocument(selectedFile)">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
删除
|
||||
Delete
|
||||
</button>
|
||||
<button class="action-btn reparse">
|
||||
<i class="fa-solid fa-rotate"></i>
|
||||
重新解析
|
||||
Reparse
|
||||
</button>
|
||||
<button class="action-btn confirm">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
确认
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -95,6 +95,27 @@
|
||||
border: 1px solid #374151;
|
||||
}
|
||||
|
||||
/* 文件上传弹窗特殊样式 - 覆盖 kb-dialog */
|
||||
.file-upload-dialog.kb-dialog :deep(.el-dialog) {
|
||||
background-color: #1f2937;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #374151;
|
||||
}
|
||||
|
||||
.file-upload-dialog.kb-dialog :deep(.el-dialog__header) {
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #374151;
|
||||
}
|
||||
|
||||
.file-upload-dialog.kb-dialog :deep(.el-dialog__title) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.file-upload-dialog.kb-dialog :deep(.el-dialog__body) {
|
||||
padding: 0;
|
||||
background-color: #0f1419;
|
||||
}
|
||||
|
||||
.kb-dialog :deep(.el-dialog__header) {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #374151;
|
||||
@@ -575,23 +596,27 @@
|
||||
|
||||
/* 文件上传弹窗布局 */
|
||||
.file-upload-dialog .el-dialog {
|
||||
position: absolute !important;
|
||||
top: 8px !important;
|
||||
left: 50% !important;
|
||||
transform: translateX(-50%) !important;
|
||||
margin-bottom: 8px !important;
|
||||
max-height: calc(100vh - 16px);
|
||||
margin: auto !important;
|
||||
top: 20px !important;
|
||||
bottom: 20px !important;
|
||||
height: auto !important;
|
||||
max-height: calc(100vh - 40px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-upload-dialog .el-dialog__body {
|
||||
padding: 0;
|
||||
height: calc(100vh - 80px);
|
||||
max-height: calc(100vh - 80px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-upload-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 85vh;
|
||||
height: 100%;
|
||||
background-color: #0f1419;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 顶部导航 */
|
||||
@@ -669,6 +694,26 @@
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
color: #6b7280;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.loading-state i,
|
||||
.empty-state i {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -744,10 +789,41 @@
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.file-item-status .status-text {
|
||||
font-size: 12px;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.file-item-status.parsing i {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.file-item-delete {
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: all 0.2s;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.file-item:hover .file-item-delete {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.file-item-delete:hover {
|
||||
background-color: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* 右侧预览面板 */
|
||||
.file-preview {
|
||||
flex: 1;
|
||||
@@ -756,6 +832,47 @@
|
||||
background-color: #121218;
|
||||
}
|
||||
|
||||
.preview-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #6b7280;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.preview-empty i {
|
||||
font-size: 48px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.preview-empty span {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.preview-loading,
|
||||
.preview-no-file {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #6b7280;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.preview-loading i,
|
||||
.preview-no-file i {
|
||||
font-size: 48px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.preview-loading span,
|
||||
.preview-no-file span {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -817,7 +934,8 @@
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
|
||||
.pdf-iframe {
|
||||
.pdf-iframe,
|
||||
.pdf-embed {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
|
||||
200
web/src/views/knowledge/useKnowledge.ts
Normal file
200
web/src/views/knowledge/useKnowledge.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
// Knowledge Base API
|
||||
|
||||
const API_BASE = 'http://localhost:8082'
|
||||
|
||||
export interface KnowledgeBase {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
llm_model_id: string
|
||||
embedding_model_id: string
|
||||
parsing_config: {
|
||||
engine: string
|
||||
docling_url?: string
|
||||
enable_pdf: boolean
|
||||
pandoc: boolean
|
||||
}
|
||||
status: string
|
||||
document_count: number
|
||||
chunk_count: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface KnowledgeDocument {
|
||||
id: string
|
||||
knowledge_base_id: string
|
||||
name: string
|
||||
file_key?: string
|
||||
file_url?: string
|
||||
file_size: number
|
||||
status: string
|
||||
chunk_count: number
|
||||
uploaded_at: string
|
||||
}
|
||||
|
||||
// 获取知识库列表
|
||||
export const fetchKnowledgeBases = async (): Promise<KnowledgeBase[]> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/knowledge/list`)
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Server error:', response.status)
|
||||
return []
|
||||
}
|
||||
|
||||
const text = await response.text()
|
||||
if (!text) {
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(text)
|
||||
if (data.success) {
|
||||
return data.data || []
|
||||
}
|
||||
console.error('API error:', data.message)
|
||||
return []
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse JSON:', text)
|
||||
return []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch knowledge bases:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// 创建知识库
|
||||
export const createKnowledgeBase = async (params: {
|
||||
name: string
|
||||
description?: string
|
||||
llm_model_id: string
|
||||
embedding_model_id: string
|
||||
parsing_config: {
|
||||
engine: string
|
||||
docling_url?: string
|
||||
enable_pdf?: boolean
|
||||
pandoc?: boolean
|
||||
}
|
||||
}): Promise<{ success: boolean; id?: string; message?: string }> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/knowledge/create`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
})
|
||||
|
||||
// Check if response is OK
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
return { success: false, message: `Server error: ${response.status} - ${errorText}` }
|
||||
}
|
||||
|
||||
const text = await response.text()
|
||||
if (!text) {
|
||||
return { success: false, message: 'Empty response from server' }
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(text)
|
||||
return data
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse JSON:', text)
|
||||
return { success: false, message: `Invalid JSON response: ${text}` }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create knowledge base:', error)
|
||||
return { success: false, message: `Network error: ${error}` }
|
||||
}
|
||||
}
|
||||
|
||||
// 删除知识库
|
||||
export const deleteKnowledgeBase = async (id: string): Promise<{ success: boolean; message?: string }> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/knowledge/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
const data = await response.json()
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('Failed to delete knowledge base:', error)
|
||||
return { success: false, message: 'Failed to delete knowledge base' }
|
||||
}
|
||||
}
|
||||
|
||||
// 获取知识库下的文档列表
|
||||
export const fetchKnowledgeDocuments = async (kbId: string, status?: string): Promise<KnowledgeDocument[]> => {
|
||||
try {
|
||||
const query = status && status !== 'all' ? `?status=${status}` : ''
|
||||
const response = await fetch(`${API_BASE}/api/knowledge/${kbId}/documents${query}`)
|
||||
const data = await response.json()
|
||||
if (data.success) {
|
||||
return data.data
|
||||
}
|
||||
throw new Error(data.message || 'Failed to fetch documents')
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch documents:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// 上传文档到知识库
|
||||
export const uploadDocument = async (kbId: string, file: File): Promise<{ success: boolean; id?: string; url?: string; document?: KnowledgeDocument; message?: string }> => {
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
const response = await fetch(`${API_BASE}/api/knowledge/${kbId}/documents`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
const data = await response.json()
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('Failed to upload document:', error)
|
||||
return { success: false, message: 'Failed to upload document' }
|
||||
}
|
||||
}
|
||||
|
||||
// 删除知识库文档
|
||||
export const deleteDocument = async (kbId: string, docId: string): Promise<{ success: boolean; message?: string }> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/knowledge/${kbId}/documents/${docId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
const data = await response.json()
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('Failed to delete document:', error)
|
||||
return { success: false, message: 'Failed to delete document' }
|
||||
}
|
||||
}
|
||||
|
||||
// 重新解析文档
|
||||
export const reparseDocument = async (kbId: string, docId: string): Promise<{ success: boolean; message?: string }> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/knowledge/${kbId}/documents/${docId}/reparse`, {
|
||||
method: 'POST',
|
||||
})
|
||||
const data = await response.json()
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('Failed to reparse document:', error)
|
||||
return { success: false, message: 'Failed to reparse document' }
|
||||
}
|
||||
}
|
||||
|
||||
// 获取文档预览内容
|
||||
export const getDocumentPreview = async (kbId: string, docId: string, page: number = 1): Promise<{ success: boolean; data?: { total_pages: number; current_page: number; content: string }; message?: string }> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/knowledge/${kbId}/documents/${docId}/preview?page=${page}`)
|
||||
const data = await response.json()
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('Failed to get document preview:', error)
|
||||
return { success: false, message: 'Failed to get document preview' }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user