Browse Source

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

YunaiV 5 months ago
parent
commit
aeb59de673

+ 142 - 123
src/views/ai/knowledge/document/create/SplitStep.vue

@@ -1,66 +1,71 @@
 <template>
   <div class="document-segment">
+    <!-- 上部分段设置部分 -->
     <div class="mb-20px">
-      <el-alert
-        title="文档分段说明"
-        type="info"
-        description="系统会自动将文档内容分割成多个段落,您可以根据需要调整分段方式和内容。"
-        show-icon
-        :closable="false"
-      />
-    </div>
-
-    <div class="mb-20px flex justify-between items-center">
-      <div class="text-16px font-bold">分段设置</div>
-      <div>
-        <el-button type="primary" @click="handleAutoSegment">自动分段</el-button>
-        <el-button @click="handleAddSegment">添加段落</el-button>
+      <div class="mb-20px flex justify-between items-center">
+        <div class="text-16px font-bold flex items-center">
+          分段设置
+          <el-tooltip
+            content="系统会自动将文档内容分割成多个段落,您可以根据需要调整分段方式和内容。"
+            placement="top"
+          >
+            <el-icon class="ml-5px text-gray-400"><Warning /></el-icon>
+          </el-tooltip>
+        </div>
+        <div>
+          <el-button type="primary" plain size="small" @click="handleAutoSegment">
+            预览分段
+          </el-button>
+        </div>
       </div>
-    </div>
 
-    <div class="segment-settings mb-20px">
-      <el-form :model="segmentSettings" label-width="120px">
-        <el-form-item label="分段方式">
-          <el-radio-group v-model="segmentSettings.type">
-            <el-radio :label="1">按段落分割</el-radio>
-            <el-radio :label="2">按字数分割</el-radio>
-            <el-radio :label="3">按标题分割</el-radio>
-          </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 class="segment-settings mb-20px">
+        <el-form :model="segmentSettings" label-width="120px">
+          <el-form-item label="最大 Token 数">
+            <el-input-number v-model="modelData.segmentMaxTokens" :min="100" :max="2000" />
+          </el-form-item>
+        </el-form>
+      </div>
     </div>
 
-    <div class="segment-list">
-      <div class="text-16px font-bold mb-10px">段落列表 ({{ modelData.segments.length }})</div>
-
-      <el-empty v-if="modelData.segments.length === 0" description="暂无段落数据" />
+    <!-- 下部文件预览部分 -->
+    <div class="mb-10px">
+      <div class="text-16px font-bold mb-10px">分段预览</div>
+
+      <!-- 文件选择器 -->
+      <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">
-          <el-collapse-item
-            v-for="(segment, index) in modelData.segments"
-            :key="index"
-            :title="`段落 ${index + 1}`"
-            :name="index"
-          >
-            <div class="segment-content">
-              <el-input
-                v-model="segment.content"
-                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 class="file-preview bg-gray-50 p-15px rounded-md">
+        <template v-if="currentFile">
+          <div v-for="(chunk, index) in currentFile.chunks" :key="index" class="mb-10px">
+            <div class="text-gray-500 text-12px mb-5px"
+              >Chunk-{{ index + 1 }} · {{ chunk.characters }} characters</div
+            >
+            <div class="bg-white p-10px rounded-md">{{ chunk.content }}</div>
+          </div>
+        </template>
+        <el-empty v-else description="暂无预览内容" />
       </div>
     </div>
 
@@ -73,7 +78,8 @@
 </template>
 
 <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({
   modelValue: {
@@ -94,52 +100,84 @@ const modelData = computed({
 })
 
 // 分段设置
-const segmentSettings = ref({
-  type: 1, // 1: 按段落, 2: 按字数, 3: 按标题
-  maxChars: 1000
-})
+const segmentSettings = ref({})
+
+// 模拟上传的文件数据
+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 = () => {
   // 根据文档类型和分段设置进行自动分段
   // 这里只是模拟实现,实际需要根据文档内容进行分析
 
+  // 确保 segments 存在
+  if (!modelData.value.segments) {
+    modelData.value.segments = []
+  }
+
   // 清空现有段落
   modelData.value.segments = []
 
   // 模拟生成段落
   if (modelData.value.documentType === 'text' && modelData.value.content) {
-    // 文本类型,直接按段落或字数分割
+    // 文本类型,按Token数分割
     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) {
-      // 按段落分割
-      const paragraphs = content.split(/\n\s*\n/)
-      paragraphs.forEach((paragraph) => {
-        if (paragraph.trim()) {
-          modelData.value.segments.push({
-            content: paragraph.trim(),
-            order: modelData.value.segments.length + 1
-          })
-        }
+      modelData.value.segments.push({
+        content: segment,
+        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 {
     // 其他类型文档,模拟生成5个段落
@@ -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 = () => {
 // 表单校验
 const validate = () => {
   return new Promise((resolve, reject) => {
-    if (modelData.value.segments.length === 0) {
-      reject(new Error('请至少添加一个段落'))
+    // 确保 segments 存在
+    if (!modelData.value.segments || modelData.value.segments.length === 0) {
+      reject(new Error('请先进行预览分段'))
     } else {
-      // 检查是否有空段落
-      const emptySegment = modelData.value.segments.find((segment) => !segment.content.trim())
-      if (emptySegment) {
-        reject(new Error('存在空段落,请填写内容或删除'))
-      } else {
-        resolve(true)
-      }
+      resolve(true)
     }
   })
 }
@@ -218,9 +227,14 @@ defineExpose({
 
 // 初始化
 onMounted(() => {
-  // 如果已有段落数据,默认展开第一个
-  if (modelData.value.segments && modelData.value.segments.length > 0) {
-    activeSegments.value = [0]
+  // 确保 segments 存在
+  if (!modelData.value.segments) {
+    modelData.value.segments = []
+  }
+
+  // 确保 segmentMaxTokens 存在
+  if (!modelData.value.segmentMaxTokens) {
+    modelData.value.segmentMaxTokens = 500
   }
 })
 </script>
@@ -230,5 +244,10 @@ onMounted(() => {
   .segment-content {
     padding: 10px;
   }
+
+  .file-preview {
+    max-height: 600px;
+    overflow-y: auto;
+  }
 }
 </style>

+ 9 - 7
src/views/ai/knowledge/document/create/UploadStep.vue

@@ -2,7 +2,9 @@
   <el-form ref="formRef" :model="modelData" label-width="0" class="mt-20px">
     <el-form-item class="mb-20px">
       <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
             ref="uploadRef"
             class="upload-demo"
@@ -35,18 +37,18 @@
 
         <div
           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
             v-for="(file, index) in modelData.list"
             :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">
-              <el-icon class="mr-8px text-[#909399]"><document /></el-icon>
-              <span class="text-[14px] text-[#606266] break-all">{{ file.name }}</span>
+              <el-icon class="mr-8px text-[#409eff]"><document /></el-icon>
+              <span class="text-[13px] text-[#303133] break-all">{{ file.name }}</span>
             </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-button>
           </div>
@@ -67,7 +69,7 @@
 
 <script lang="ts" setup>
 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 { useUpload } from '@/components/UploadFile/src/useUpload'
 import { generateAcceptedFileTypes } from '@/utils'