| | |
| | | > |
| | | 保存文件关联 |
| | | </el-button> |
| | | <el-button |
| | | v-if="uploadedBlobIds.length > 0" |
| | | type="text" |
| | | @click="clearUploadedFiles" |
| | | style="margin-left: 10px" |
| | | > |
| | | 清空待保存列表 |
| | | </el-button> |
| | | </div> |
| | | |
| | | <!-- 待保存的文件列表 --> |
| | | <div v-if="uploadedBlobIds.length > 0" class="uploaded-list"> |
| | | <div class="uploaded-tip"> |
| | | <el-icon style="color: #409eff"><InfoFilled /></el-icon> |
| | | <span>已上传 {{ uploadedBlobIds.length }} 个文件,请点击"保存文件关联"按钮触发向量化处理</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 文件列表与向量化状态 --> |
| | |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="chunkCount" label="切片数" width="100" align="center" /> |
| | | <el-table-column label="错误信息" width="200" show-overflow-tooltip> |
| | | <template #default="{ row }"> |
| | | <span v-if="row.vectorError" style="color: #f56c6c">{{ row.vectorError }}</span> |
| | | <span v-else style="color: #909399">-</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="createTime" label="上传时间" width="180" /> |
| | | <el-table-column label="操作" width="150" align="center"> |
| | | <template #default="{ row }"> |
| | |
| | | <div class="chat-input"> |
| | | <el-input |
| | | v-model="inputQuestion" |
| | | placeholder="请输入问题,按回车发送" |
| | | placeholder="请输入问题,按回车发送(Ctrl+Enter快捷发送)" |
| | | @keyup.enter="sendMessage" |
| | | :disabled="chatLoading" |
| | | > |
| | |
| | | <el-button @click="sendMessage" :loading="chatLoading">发送</el-button> |
| | | </template> |
| | | </el-input> |
| | | <div class="chat-actions"> |
| | | <el-button type="text" size="small" @click="clearMessages">清空对话</el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </FormDialog> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import { Search, InfoFilled } from "@element-plus/icons-vue"; |
| | | import { onMounted, ref, reactive, toRefs, getCurrentInstance, computed, watch, nextTick } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import PIMTable from "@/components/PIMTable/PIMTable.vue"; |
| | |
| | | fileList: [], |
| | | uploadedBlobIds: [], |
| | | savingFiles: false, |
| | | vectorStatusTimer: null, // 向量化状态轮询定时器 |
| | | chatDialogVisible: false, |
| | | messages: [], |
| | | inputQuestion: "", |
| | |
| | | fileList, |
| | | uploadedBlobIds, |
| | | savingFiles, |
| | | vectorStatusTimer, |
| | | chatDialogVisible, |
| | | messages, |
| | | inputQuestion, |
| | |
| | | try { |
| | | const res = await getVectorStatus(currentKnowledgeBase.value.id); |
| | | fileList.value = res.data || []; |
| | | |
| | | // 检查是否有处理中的文件,如果有则启动轮询 |
| | | const hasProcessing = res.data.some(item => item.vectorStatus === 1); |
| | | if (hasProcessing && !vectorStatusTimer.value) { |
| | | startVectorStatusPolling(); |
| | | } else if (!hasProcessing && vectorStatusTimer.value) { |
| | | stopVectorStatusPolling(); |
| | | } |
| | | } catch (error) { |
| | | console.error("加载文件列表失败:", error); |
| | | ElMessage.error("加载文件列表失败"); |
| | | } |
| | | }; |
| | | |
| | | // 开始轮询向量化状态 |
| | | const startVectorStatusPolling = () => { |
| | | vectorStatusTimer.value = setInterval(async () => { |
| | | try { |
| | | const res = await getVectorStatus(currentKnowledgeBase.value.id); |
| | | fileList.value = res.data || []; |
| | | |
| | | // 检查是否还有处理中的文件 |
| | | const hasProcessing = res.data.some(item => item.vectorStatus === 1); |
| | | if (!hasProcessing) { |
| | | stopVectorStatusPolling(); |
| | | ElMessage.success("所有文件向量化处理完成"); |
| | | } |
| | | } catch (error) { |
| | | console.error("轮询向量化状态失败:", error); |
| | | stopVectorStatusPolling(); |
| | | } |
| | | }, 3000); // 每3秒轮询一次 |
| | | }; |
| | | |
| | | // 停止轮询向量化状态 |
| | | const stopVectorStatusPolling = () => { |
| | | if (vectorStatusTimer.value) { |
| | | clearInterval(vectorStatusTimer.value); |
| | | vectorStatusTimer.value = null; |
| | | } |
| | | }; |
| | | |
| | |
| | | } |
| | | }; |
| | | |
| | | // 清空待保存的文件列表 |
| | | const clearUploadedFiles = () => { |
| | | uploadedBlobIds.value = []; |
| | | ElMessage.success("已清空待保存文件列表"); |
| | | }; |
| | | |
| | | // 删除文件 |
| | | const deleteFile = async (row) => { |
| | | try { |
| | |
| | | currentKnowledgeBase.value = null; |
| | | fileList.value = []; |
| | | uploadedBlobIds.value = []; |
| | | getList(); // 刷新主列表,更新文件数量 |
| | | stopVectorStatusPolling(); // 停止轮询 |
| | | getList(); // 刷新主列表,更新文件数量 |
| | | }; |
| | | |
| | | // ============ 知识库问答相关 ============ |
| | |
| | | const sendMessage = async () => { |
| | | if (!inputQuestion.value.trim()) { |
| | | ElMessage.warning("请输入问题"); |
| | | return; |
| | | } |
| | | |
| | | if (!currentKnowledgeBase.value?.id) { |
| | | ElMessage.error("知识库信息异常"); |
| | | return; |
| | | } |
| | | |
| | |
| | | }); |
| | | |
| | | if (!response.ok) { |
| | | throw new Error('请求失败'); |
| | | const errorText = await response.text(); |
| | | throw new Error(errorText || '请求失败'); |
| | | } |
| | | |
| | | // 处理SSE流式响应 |
| | |
| | | await nextTick(); |
| | | scrollToBottom(); |
| | | } |
| | | |
| | | // 如果AI返回空内容,显示提示 |
| | | if (!aiContent.trim()) { |
| | | messages.value[messages.value.length - 1].content = '抱歉,知识库中未找到相关内容,请尝试其他问题。'; |
| | | } |
| | | } catch (error) { |
| | | console.error("问答请求失败:", error); |
| | | ElMessage.error("问答请求失败,请稍后重试"); |
| | | ElMessage.error("问答请求失败,请稍后重试"); |
| | | messages.value.push({ |
| | | role: 'assistant', |
| | | content: '抱歉,发生了错误,请稍后重试' |
| | | content: '抱歉,发生了错误,请稍后重试' |
| | | }); |
| | | } finally { |
| | | chatLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | // 清空对话 |
| | | const clearMessages = () => { |
| | | ElMessageBox.confirm( |
| | | "确定要清空所有对话记录吗?", |
| | | "清空确认", |
| | | { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | | type: "warning" |
| | | } |
| | | ).then(() => { |
| | | messages.value = []; |
| | | memoryId.value = crypto.randomUUID(); // 重新生成会话ID |
| | | ElMessage.success("对话已清空"); |
| | | }).catch(() => { |
| | | // 用户取消 |
| | | }); |
| | | }; |
| | | |
| | | // 滚动到底部 |
| | |
| | | align-items: center; |
| | | } |
| | | |
| | | .uploaded-list { |
| | | margin-top: 16px; |
| | | padding: 12px; |
| | | background: #f0f9ff; |
| | | border-radius: 6px; |
| | | border: 1px solid #b3d8ff; |
| | | } |
| | | |
| | | .uploaded-tip { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | color: #409eff; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | /* 知识库问答样式 */ |
| | | .knowledge-chat { |
| | | display: flex; |
| | |
| | | margin-top: auto; |
| | | } |
| | | |
| | | .chat-actions { |
| | | margin-top: 8px; |
| | | text-align: right; |
| | | } |
| | | |
| | | </style> |