【功能新增】AI:知识库文档上传:50%,SplitStep 基本完成

This commit is contained in:
YunaiV 2025-03-01 08:45:58 +08:00
parent aeb59de673
commit da91896419
3 changed files with 120 additions and 154 deletions

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="document-segment"> <div>
<!-- 上部分段设置部分 --> <!-- 上部分段设置部分 -->
<div class="mb-20px"> <div class="mb-20px">
<div class="mb-20px flex justify-between items-center"> <div class="mb-20px flex justify-between items-center">
@ -9,7 +9,7 @@
content="系统会自动将文档内容分割成多个段落,您可以根据需要调整分段方式和内容。" content="系统会自动将文档内容分割成多个段落,您可以根据需要调整分段方式和内容。"
placement="top" placement="top"
> >
<el-icon class="ml-5px text-gray-400"><Warning /></el-icon> <Icon icon="ep:warning" class="ml-5px text-gray-400" />
</el-tooltip> </el-tooltip>
</div> </div>
<div> <div>
@ -20,7 +20,7 @@
</div> </div>
<div class="segment-settings mb-20px"> <div class="segment-settings mb-20px">
<el-form :model="segmentSettings" label-width="120px"> <el-form label-width="120px">
<el-form-item label="最大 Token 数"> <el-form-item label="最大 Token 数">
<el-input-number v-model="modelData.segmentMaxTokens" :min="100" :max="2000" /> <el-input-number v-model="modelData.segmentMaxTokens" :min="100" :max="2000" />
</el-form-item> </el-form-item>
@ -34,16 +34,16 @@
<!-- 文件选择器 --> <!-- 文件选择器 -->
<div class="file-selector mb-10px"> <div class="file-selector mb-10px">
<el-dropdown v-if="uploadedFiles.length > 0" trigger="click"> <el-dropdown v-if="modelData.list && modelData.list.length > 0" trigger="click">
<div class="flex items-center cursor-pointer"> <div class="flex items-center cursor-pointer">
<el-icon class="text-danger mr-5px"><Document /></el-icon> <Icon icon="ep:document" class="text-danger mr-5px" />
<span>{{ currentFile.name }}</span> <span>{{ currentFile?.name || '请选择文件' }}</span>
<el-icon class="ml-5px"><ArrowDown /></el-icon> <Icon icon="ep:arrow-down" class="ml-5px" />
</div> </div>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item <el-dropdown-item
v-for="(file, index) in uploadedFiles" v-for="(file, index) in modelData.list"
:key="index" :key="index"
@click="selectFile(index)" @click="selectFile(index)"
> >
@ -56,13 +56,20 @@
</div> </div>
<!-- 文件内容预览 --> <!-- 文件内容预览 -->
<div class="file-preview bg-gray-50 p-15px rounded-md"> <div class="file-preview bg-gray-50 p-15px rounded-md max-h-600px overflow-y-auto">
<template v-if="currentFile"> <div v-if="splitLoading" class="flex justify-center items-center py-20px">
<div v-for="(chunk, index) in currentFile.chunks" :key="index" class="mb-10px"> <Icon icon="ep:loading" class="is-loading" />
<div class="text-gray-500 text-12px mb-5px" <span class="ml-10px">正在加载分段内容...</span>
>Chunk-{{ index + 1 }} · {{ chunk.characters }} characters</div </div>
<template
v-else-if="currentFile && currentFile.segments && currentFile.segments.length > 0"
> >
<div class="bg-white p-10px rounded-md">{{ chunk.content }}</div> <div v-for="(segment, index) in currentFile.segments" :key="index" class="mb-10px">
<div class="text-gray-500 text-12px mb-5px">
分片-{{ index + 1 }} · {{ segment.contentLength || 0 }} 字符数 ·
{{ segment.tokens || 0 }} Token
</div>
<div class="bg-white p-10px rounded-md">{{ segment.content }}</div>
</div> </div>
</template> </template>
<el-empty v-else description="暂无预览内容" /> <el-empty v-else description="暂无预览内容" />
@ -79,7 +86,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { PropType, ref, computed, inject, onMounted, getCurrentInstance } from 'vue' import { PropType, ref, computed, inject, onMounted, getCurrentInstance } from 'vue'
import { Document, ArrowDown, Warning } from '@element-plus/icons-vue' // TODO @icon import { Icon } from '@/components/Icon'
import { KnowledgeSegmentApi } from '@/api/ai/knowledge/segment'
import { useMessage } from '@/hooks/web/useMessage'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@ -89,165 +98,99 @@ const props = defineProps({
}) })
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const message = useMessage() //
const parent = inject('parent', null) //
//
const parent = inject('parent', null)
//
const modelData = computed({ const modelData = computed({
get: () => props.modelValue, get: () => props.modelValue,
set: (val) => emit('update:modelValue', val) set: (val) => emit('update:modelValue', val)
}) }) //
// const splitLoading = ref(false) //
const segmentSettings = ref({}) const currentFile = ref<any>(null) //
// /** 选择文件 */
const uploadedFiles = ref([ const selectFile = async (index: number) => {
{ currentFile.value = modelData.value.list[index]
name: '项目说明文档.pdf', await splitContent(currentFile.value)
type: 'pdf',
chunks: [
{
characters: 120,
content:
'项目说明文档 - 智能知识库系统 本项目旨在构建一个智能知识库系统,能够对各类文档进行智能分析、分类和检索,提高企业知识管理效率。'
},
{
characters: 180,
content:
'系统架构前端采用Vue3+Element Plus构建用户界面后端采用Spring Boot微服务架构数据存储使用MySQL和Elasticsearch文档解析使用Apache Tika向量检索使用Milvus。'
},
{
characters: 150,
content:
'核心功能1. 文档上传与解析支持多种格式文档上传自动提取文本内容。2. 智能分段根据语义自动将文档分割成合适的段落。3. 向量化存储:将文本转换为向量存储,支持语义检索。'
},
{
characters: 160,
content:
'4. 智能检索支持关键词和语义检索快速找到相关内容。5. 知识图谱自动构建领域知识图谱展示知识间关联。6. 权限管理细粒度的文档访问权限控制。7. 操作日志:记录用户操作,支持审计追踪。'
},
{
characters: 130,
content:
'技术特点1. 高性能采用分布式架构支持横向扩展。2. 高可用关键组件冗余部署确保系统稳定性。3. 安全性:数据传输加密,存储加密,多层次安全防护。'
}
]
},
{
name: '项目说明文档.pdf',
type: 'pdf',
chunks: []
}
])
//
const currentFile = ref(uploadedFiles.value[0] || null)
//
const selectFile = (index) => {
currentFile.value = uploadedFiles.value[index]
} }
// /** 获取文件分段内容 */
const handleAutoSegment = () => { const splitContent = async (file: any) => {
// if (!file || !file.url) {
// message.warning('文件 URL 不存在')
return
// segments
if (!modelData.value.segments) {
modelData.value.segments = []
} }
// splitLoading.value = true
modelData.value.segments = [] try {
const data = await KnowledgeSegmentApi.splitContent(file.url, modelData.value.segmentMaxTokens) // Token
// file.segments = data
if (modelData.value.documentType === 'text' && modelData.value.content) { } catch (error) {
// Token console.error('获取分段内容失败:', file, error)
const content = modelData.value.content message.error('获取分段内容失败')
const maxChars = Math.floor(modelData.value.segmentMaxTokens / 2) // 1token2 } finally {
let remaining = content splitLoading.value = false
while (remaining.length > 0) {
const segment = remaining.substring(0, maxChars)
remaining = remaining.substring(maxChars)
modelData.value.segments.push({
content: segment,
order: modelData.value.segments.length + 1
})
}
} else {
// 5
for (let i = 0; i < 5; i++) {
modelData.value.segments.push({
content: `这是第 ${i + 1} 个自动生成的段落,实际内容将根据文档解析结果填充。`,
order: i + 1
})
}
} }
} }
// /** 处理预览分段 */
const handleAutoSegment = async () => {
//
if (!currentFile.value && modelData.value.list && modelData.value.list.length > 0) {
currentFile.value = modelData.value.list[0]
}
//
if (!currentFile.value) {
message.warning('请先选择文件')
return
}
//
await splitContent(currentFile.value)
}
/** 上一步按钮处理 */
const handlePrevStep = () => { const handlePrevStep = () => {
// goToPrevStep
const parentEl = parent || getCurrentInstance()?.parent const parentEl = parent || getCurrentInstance()?.parent
if (parentEl && typeof parentEl.exposed?.goToPrevStep === 'function') { if (parentEl && typeof parentEl.exposed?.goToPrevStep === 'function') {
parentEl.exposed.goToPrevStep() parentEl.exposed.goToPrevStep()
} }
} }
// /** 下一步按钮处理 */
const handleNextStep = () => { const handleNextStep = () => {
// goToNextStep
const parentEl = parent || getCurrentInstance()?.parent const parentEl = parent || getCurrentInstance()?.parent
if (parentEl && typeof parentEl.exposed?.goToNextStep === 'function') { if (parentEl && typeof parentEl.exposed?.goToNextStep === 'function') {
parentEl.exposed.goToNextStep() parentEl.exposed.goToNextStep()
} }
} }
// /** 组件激活时自动调用分段接口 TODO 芋艿:需要看下 */
const validate = () => { const activated = async () => {
return new Promise((resolve, reject) => { if (!currentFile.value && modelData.value.list && modelData.value.list.length > 0) {
// segments currentFile.value = modelData.value.list[0] //
if (!modelData.value.segments || modelData.value.segments.length === 0) {
reject(new Error('请先进行预览分段'))
} else {
resolve(true)
}
})
} }
// if (currentFile.value) {
defineExpose({ await splitContent(currentFile.value) //
validate }
})
//
onMounted(() => {
// segments
if (!modelData.value.segments) {
modelData.value.segments = []
} }
/** 初始化 */
onMounted(async () => {
// segmentMaxTokens // segmentMaxTokens
if (!modelData.value.segmentMaxTokens) { if (!modelData.value.segmentMaxTokens) {
modelData.value.segmentMaxTokens = 500 modelData.value.segmentMaxTokens = 500
} }
//
if (!currentFile.value && modelData.value.list && modelData.value.list.length > 0) {
currentFile.value = modelData.value.list[0]
}
//
if (currentFile.value) {
await splitContent(currentFile.value)
}
}) })
</script> </script>
<style lang="scss" scoped>
.document-segment {
.segment-content {
padding: 10px;
}
.file-preview {
max-height: 600px;
overflow-y: auto;
}
}
</style>

View File

@ -23,11 +23,11 @@
:accept="acceptedFileTypes" :accept="acceptedFileTypes"
> >
<div class="flex flex-col items-center justify-center py-20px"> <div class="flex flex-col items-center justify-center py-20px">
<el-icon class="text-[48px] text-[#c0c4cc] mb-10px"><upload-filled /></el-icon> <Icon icon="ep:upload-filled" class="text-[48px] text-[#c0c4cc] mb-10px" />
<div class="el-upload__text text-[16px] text-[#606266]" <div class="el-upload__text text-[16px] text-[#606266]">
>拖拽文件至此或者 拖拽文件至此或者
<em class="text-[#409eff] not-italic cursor-pointer">选择文件</em></div <em class="text-[#409eff] not-italic cursor-pointer">选择文件</em>
> </div>
<div class="el-upload__tip mt-10px text-[#909399] text-[12px]"> <div class="el-upload__tip mt-10px text-[#909399] text-[12px]">
已支持 {{ supportedFileTypes.join('、') }}每个文件不超过 {{ maxFileSize }} MB 已支持 {{ supportedFileTypes.join('、') }}每个文件不超过 {{ maxFileSize }} MB
</div> </div>
@ -45,11 +45,11 @@
class="flex justify-between items-center py-4px px-12px border-l-4 border-l-[#409eff] rounded-sm shadow-sm hover:bg-[#ecf5ff] transition-all duration-300" class="flex justify-between items-center py-4px px-12px border-l-4 border-l-[#409eff] rounded-sm shadow-sm hover:bg-[#ecf5ff] transition-all duration-300"
> >
<div class="flex items-center"> <div class="flex items-center">
<el-icon class="mr-8px text-[#409eff]"><document /></el-icon> <Icon icon="ep:document" class="mr-8px text-[#409eff]" />
<span class="text-[13px] text-[#303133] break-all">{{ file.name }}</span> <span class="text-[13px] text-[#303133] break-all">{{ file.name }}</span>
</div> </div>
<el-button type="danger" link @click="removeFile(index)" class="ml-2"> <el-button type="danger" link @click="removeFile(index)" class="ml-2">
<el-icon><delete /></el-icon> <Icon icon="ep:delete" />
</el-button> </el-button>
</div> </div>
</div> </div>
@ -69,10 +69,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { PropType, ref, computed, inject, getCurrentInstance, onMounted } from 'vue' import { PropType, ref, computed, inject, getCurrentInstance, onMounted } from 'vue'
import { Document, Delete } from '@element-plus/icons-vue' // TODO @
import { useMessage } from '@/hooks/web/useMessage' import { useMessage } from '@/hooks/web/useMessage'
import { useUpload } from '@/components/UploadFile/src/useUpload' import { useUpload } from '@/components/UploadFile/src/useUpload'
import { generateAcceptedFileTypes } from '@/utils' import { generateAcceptedFileTypes } from '@/utils'
import { Icon } from '@/components/Icon'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@ -173,8 +173,8 @@ const beforeUpload = (file) => {
* @param file 上传的文件 * @param file 上传的文件
*/ */
const handleUploadSuccess = (response, file) => { const handleUploadSuccess = (response, file) => {
if (response && response.data) {
// //
if (response && response.data) {
ensureListExists() ensureListExists()
emit('update:modelValue', { emit('update:modelValue', {
...props.modelValue, ...props.modelValue,

View File

@ -89,19 +89,42 @@ const steps = [{ title: '上传文档' }, { title: '文档分段' }, { title: '
// //
const formData = ref({ const formData = ref({
id: undefined, knowlegeId: undefined, //
list: [], // id: undefined, // (documentId)
segmentMaxTokens: 500, // token
list: [] as Array<{
name: string
url: string
segments: Array<{
content?: string
contentLength?: number
tokens?: number
}>
}>, //
status: 0 // 0: 稿, 1: , 2: status: 0 // 0: 稿, 1: , 2:
}) })
/** 初始化数据 */ /** 初始化数据 */
const initData = async () => { const initData = async () => {
// TODO @knowlegeId
const documentId = route.params.id as string const documentId = route.params.id as string
if (documentId) { if (documentId) {
// //
// API // API
// formData.value = await DocumentApi.getDocument(documentId) // formData.value = await DocumentApi.getDocument(documentId)
} }
// TODO @便
if (false) {
formData.value.list = [
{
name: '项目说明文档.pdf',
url: 'https://static.iocoder.cn/README_yudao.md',
segments: []
}
]
goToNextStep()
}
} }
/** 切换到下一步 */ /** 切换到下一步 */