mirror of
https://gitee.com/myxzgzs/boyue-ui-admin-vue3
synced 2025-08-08 16:32:43 +08:00
【功能新增】AI:知识库文档上传:50%,SplitStep 基本完成
This commit is contained in:
parent
aeb59de673
commit
da91896419
@ -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
|
||||||
<div class="bg-white p-10px rounded-md">{{ chunk.content }}</div>
|
v-else-if="currentFile && currentFile.segments && currentFile.segments.length > 0"
|
||||||
|
>
|
||||||
|
<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) // 简单估算:1个token约等于2个字符
|
} 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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对外暴露方法
|
|
||||||
defineExpose({
|
|
||||||
validate
|
|
||||||
})
|
|
||||||
|
|
||||||
// 初始化
|
|
||||||
onMounted(() => {
|
|
||||||
// 确保 segments 存在
|
|
||||||
if (!modelData.value.segments) {
|
|
||||||
modelData.value.segments = []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentFile.value) {
|
||||||
|
await splitContent(currentFile.value) // 如果有选中的文件,获取分段内容
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
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>
|
|
||||||
|
@ -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,
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 切换到下一步 */
|
/** 切换到下一步 */
|
||||||
|
Loading…
x
Reference in New Issue
Block a user