Commit aeb59de6 by YunaiV

【功能新增】AI:知识库文档上传:40%,SplitStep 初始化

parent 7cd6a5d9
<template> <template>
<div class="document-segment"> <div class="document-segment">
<!-- 上部分段设置部分 -->
<div class="mb-20px"> <div class="mb-20px">
<el-alert <div class="mb-20px flex justify-between items-center">
title="文档分段说明" <div class="text-16px font-bold flex items-center">
type="info" 分段设置
description="系统会自动将文档内容分割成多个段落,您可以根据需要调整分段方式和内容。" <el-tooltip
show-icon content="系统会自动将文档内容分割成多个段落,您可以根据需要调整分段方式和内容。"
:closable="false" placement="top"
/> >
</div> <el-icon class="ml-5px text-gray-400"><Warning /></el-icon>
</el-tooltip>
<div class="mb-20px flex justify-between items-center"> </div>
<div class="text-16px font-bold">分段设置</div> <div>
<div> <el-button type="primary" plain size="small" @click="handleAutoSegment">
<el-button type="primary" @click="handleAutoSegment">自动分段</el-button> 预览分段
<el-button @click="handleAddSegment">添加段落</el-button> </el-button>
</div>
</div> </div>
</div>
<div class="segment-settings mb-20px"> <div class="segment-settings mb-20px">
<el-form :model="segmentSettings" label-width="120px"> <el-form :model="segmentSettings" label-width="120px">
<el-form-item label="分段方式"> <el-form-item label="最大 Token 数">
<el-radio-group v-model="segmentSettings.type"> <el-input-number v-model="modelData.segmentMaxTokens" :min="100" :max="2000" />
<el-radio :label="1">按段落分割</el-radio> </el-form-item>
<el-radio :label="2">按字数分割</el-radio> </el-form>
<el-radio :label="3">按标题分割</el-radio> </div>
</el-radio-group>
</el-form-item>
<el-form-item label="最大字数" v-if="segmentSettings.type === 2">
<el-input-number v-model="segmentSettings.maxChars" :min="100" :max="5000" />
</el-form-item>
</el-form>
</div> </div>
<div class="segment-list"> <!-- 下部文件预览部分 -->
<div class="text-16px font-bold mb-10px">段落列表 ({{ modelData.segments.length }})</div> <div class="mb-10px">
<div class="text-16px font-bold mb-10px">分段预览</div>
<el-empty v-if="modelData.segments.length === 0" description="暂无段落数据" />
<!-- 文件选择器 -->
<div class="file-selector mb-10px">
<el-dropdown v-if="uploadedFiles.length > 0" trigger="click">
<div class="flex items-center cursor-pointer">
<el-icon class="text-danger mr-5px"><Document /></el-icon>
<span>{{ currentFile.name }}</span>
<el-icon class="ml-5px"><ArrowDown /></el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="(file, index) in uploadedFiles"
:key="index"
@click="selectFile(index)"
>
{{ file.name }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<div v-else class="text-gray-400">暂无上传文件</div>
</div>
<div v-else> <!-- 文件内容预览 -->
<el-collapse v-model="activeSegments"> <div class="file-preview bg-gray-50 p-15px rounded-md">
<el-collapse-item <template v-if="currentFile">
v-for="(segment, index) in modelData.segments" <div v-for="(chunk, index) in currentFile.chunks" :key="index" class="mb-10px">
:key="index" <div class="text-gray-500 text-12px mb-5px"
:title="`段落 ${index + 1}`" >Chunk-{{ index + 1 }} · {{ chunk.characters }} characters</div
:name="index" >
> <div class="bg-white p-10px rounded-md">{{ chunk.content }}</div>
<div class="segment-content"> </div>
<el-input </template>
v-model="segment.content" <el-empty v-else description="暂无预览内容" />
type="textarea"
:rows="5"
placeholder="段落内容"
/>
<div class="mt-10px flex justify-end">
<el-button type="danger" size="small" @click="handleDeleteSegment(index)">
删除段落
</el-button>
</div>
</div>
</el-collapse-item>
</el-collapse>
</div> </div>
</div> </div>
...@@ -73,7 +78,8 @@ ...@@ -73,7 +78,8 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { PropType } from 'vue' import { PropType, ref, computed, inject, onMounted, getCurrentInstance } from 'vue'
import { Document, ArrowDown, Warning } from '@element-plus/icons-vue' // TODO @芋艿:icon 的处理
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
...@@ -94,52 +100,84 @@ const modelData = computed({ ...@@ -94,52 +100,84 @@ const modelData = computed({
}) })
// 分段设置 // 分段设置
const segmentSettings = ref({ const segmentSettings = ref({})
type: 1, // 1: 按段落, 2: 按字数, 3: 按标题
maxChars: 1000 // 模拟上传的文件数据
}) const uploadedFiles = ref([
{
name: '项目说明文档.pdf',
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 activeSegments = ref([0]) const selectFile = (index) => {
currentFile.value = uploadedFiles.value[index]
}
// 自动分段 // 自动分段
const handleAutoSegment = () => { const handleAutoSegment = () => {
// 根据文档类型和分段设置进行自动分段 // 根据文档类型和分段设置进行自动分段
// 这里只是模拟实现,实际需要根据文档内容进行分析 // 这里只是模拟实现,实际需要根据文档内容进行分析
// 确保 segments 存在
if (!modelData.value.segments) {
modelData.value.segments = []
}
// 清空现有段落 // 清空现有段落
modelData.value.segments = [] modelData.value.segments = []
// 模拟生成段落 // 模拟生成段落
if (modelData.value.documentType === 'text' && modelData.value.content) { if (modelData.value.documentType === 'text' && modelData.value.content) {
// 文本类型,直接按段落或字数分割 // 文本类型,按Token数分割
const content = modelData.value.content const content = modelData.value.content
const maxChars = Math.floor(modelData.value.segmentMaxTokens / 2) // 简单估算:1个token约等于2个字符
let remaining = content
while (remaining.length > 0) {
const segment = remaining.substring(0, maxChars)
remaining = remaining.substring(maxChars)
if (segmentSettings.value.type === 1) { modelData.value.segments.push({
// 按段落分割 content: segment,
const paragraphs = content.split(/\n\s*\n/) order: modelData.value.segments.length + 1
paragraphs.forEach((paragraph) => {
if (paragraph.trim()) {
modelData.value.segments.push({
content: paragraph.trim(),
order: modelData.value.segments.length + 1
})
}
}) })
} else if (segmentSettings.value.type === 2) {
// 按字数分割
const maxChars = segmentSettings.value.maxChars
let remaining = content
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 { } else {
// 其他类型文档,模拟生成5个段落 // 其他类型文档,模拟生成5个段落
...@@ -150,30 +188,6 @@ const handleAutoSegment = () => { ...@@ -150,30 +188,6 @@ const handleAutoSegment = () => {
}) })
} }
} }
// 默认展开第一个段落
activeSegments.value = [0]
}
// 添加段落
const handleAddSegment = () => {
modelData.value.segments.push({
content: '',
order: modelData.value.segments.length + 1
})
// 展开新添加的段落
activeSegments.value = [modelData.value.segments.length - 1]
}
// 删除段落
const handleDeleteSegment = (index) => {
modelData.value.segments.splice(index, 1)
// 更新段落顺序
modelData.value.segments.forEach((segment, idx) => {
segment.order = idx + 1
})
} }
// 上一步按钮处理 // 上一步按钮处理
...@@ -197,16 +211,11 @@ const handleNextStep = () => { ...@@ -197,16 +211,11 @@ const handleNextStep = () => {
// 表单校验 // 表单校验
const validate = () => { const validate = () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (modelData.value.segments.length === 0) { // 确保 segments 存在
reject(new Error('请至少添加一个段落')) if (!modelData.value.segments || modelData.value.segments.length === 0) {
reject(new Error('请先进行预览分段'))
} else { } else {
// 检查是否有空段落 resolve(true)
const emptySegment = modelData.value.segments.find((segment) => !segment.content.trim())
if (emptySegment) {
reject(new Error('存在空段落,请填写内容或删除'))
} else {
resolve(true)
}
} }
}) })
} }
...@@ -218,9 +227,14 @@ defineExpose({ ...@@ -218,9 +227,14 @@ defineExpose({
// 初始化 // 初始化
onMounted(() => { onMounted(() => {
// 如果已有段落数据,默认展开第一个 // 确保 segments 存在
if (modelData.value.segments && modelData.value.segments.length > 0) { if (!modelData.value.segments) {
activeSegments.value = [0] modelData.value.segments = []
}
// 确保 segmentMaxTokens 存在
if (!modelData.value.segmentMaxTokens) {
modelData.value.segmentMaxTokens = 500
} }
}) })
</script> </script>
...@@ -230,5 +244,10 @@ onMounted(() => { ...@@ -230,5 +244,10 @@ onMounted(() => {
.segment-content { .segment-content {
padding: 10px; padding: 10px;
} }
.file-preview {
max-height: 600px;
overflow-y: auto;
}
} }
</style> </style>
...@@ -2,7 +2,9 @@ ...@@ -2,7 +2,9 @@
<el-form ref="formRef" :model="modelData" label-width="0" class="mt-20px"> <el-form ref="formRef" :model="modelData" label-width="0" class="mt-20px">
<el-form-item class="mb-20px"> <el-form-item class="mb-20px">
<div class="w-full"> <div class="w-full">
<div class="w-full border-2 border-[#dcdfe6] rounded-md text-center hover:border-[#409eff]"> <div
class="w-full border-2 border-dashed border-[#dcdfe6] rounded-md p-20px text-center hover:border-[#409eff]"
>
<el-upload <el-upload
ref="uploadRef" ref="uploadRef"
class="upload-demo" class="upload-demo"
...@@ -35,18 +37,18 @@ ...@@ -35,18 +37,18 @@
<div <div
v-if="modelData.list && modelData.list.length > 0" v-if="modelData.list && modelData.list.length > 0"
class="mt-15px grid grid-cols-1 gap-3" class="mt-15px grid grid-cols-1 gap-2"
> >
<div <div
v-for="(file, index) in modelData.list" v-for="(file, index) in modelData.list"
:key="index" :key="index"
class="flex justify-between items-center p-10px border-2 border-[#c0c4cc] rounded-md shadow-sm hover:border-[#409eff] transition-colors 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-[#909399]"><document /></el-icon> <el-icon class="mr-8px text-[#409eff]"><document /></el-icon>
<span class="text-[14px] text-[#606266] 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)"> <el-button type="danger" link @click="removeFile(index)" class="ml-2">
<el-icon><delete /></el-icon> <el-icon><delete /></el-icon>
</el-button> </el-button>
</div> </div>
...@@ -67,7 +69,7 @@ ...@@ -67,7 +69,7 @@
<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 { UploadFilled, Document, Delete } from '@element-plus/icons-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'
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment