diff --git a/web/src/views/Knowledge.vue b/web/src/views/Knowledge.vue index 3f2b202..691e579 100644 --- a/web/src/views/Knowledge.vue +++ b/web/src/views/Knowledge.vue @@ -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([]) +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(null) const fileFilter = ref('all') // all, parsed, parsing, failed -const selectedFile = ref(null) // 当前选中的文件 +const selectedFile = ref(null) // 当前选中的文件ID +const selectedDocument = ref(null) // 当前选中的文档详情 +const knowledgeDocuments = ref([]) // 知识库文档列表 +const loadingDocuments = ref(false) +const fileInput = ref(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') + } +}