| | |
| | | <div |
| | | v-for="(file, fileIndex) in message.localUploadFiles" |
| | | :key="`${file.previewId || file.name}-${fileIndex}`" |
| | | class="message-local-file-item" |
| | | :class="['message-local-file-item', { clickable: !!file.accessUrl && !file.isImage }]" |
| | | @click="handleMessageFileClick(file)" |
| | | > |
| | | <el-image |
| | | v-if="file.isImage && file.previewUrl" |
| | |
| | | /> |
| | | <el-icon v-else class="message-local-file-icon"><Document /></el-icon> |
| | | <div class="message-local-file-meta"> |
| | | <span class="message-local-file-name">{{ file.name }}</span> |
| | | <small class="message-local-file-size">{{ formatFileSize(file.size) }}</small> |
| | | <span |
| | | :class="['message-local-file-name', { clickable: !!file.accessUrl }]" |
| | | :title="file.name" |
| | | @click.stop="openMessageAttachment(file)" |
| | | > |
| | | {{ file.name }} |
| | | </span> |
| | | <small v-if="Number(file.size) > 0" class="message-local-file-size">{{ formatFileSize(file.size) }}</small> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | const loadingSessions = ref(false) |
| | | |
| | | const isImageFileType = (fileType = '') => String(fileType || '').toLowerCase().startsWith('image/') |
| | | const imageFilePathPattern = /\.(png|jpe?g|gif|webp|bmp|svg)$/i |
| | | |
| | | const getPathnameFromFilePath = (filePath = '') => { |
| | | const rawPath = String(filePath || '').trim() |
| | | if (!rawPath) return '' |
| | | try { |
| | | const baseOrigin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost' |
| | | return new URL(rawPath, baseOrigin).pathname || '' |
| | | } catch (err) { |
| | | return rawPath.split('?')[0] |
| | | } |
| | | } |
| | | |
| | | const isImageFilePath = (filePath = '') => { |
| | | const pathname = getPathnameFromFilePath(filePath).toLowerCase() |
| | | return imageFilePathPattern.test(pathname) |
| | | } |
| | | |
| | | const getHistoryFileName = (filePath = '', index = 0) => { |
| | | const pathname = getPathnameFromFilePath(filePath) |
| | | const fileName = pathname.split('/').filter(Boolean).pop() |
| | | if (!fileName) return `file-${index + 1}` |
| | | try { |
| | | return decodeURIComponent(fileName) |
| | | } catch (err) { |
| | | return fileName |
| | | } |
| | | } |
| | | |
| | | const getImagePreviewList = (files = []) => { |
| | | if (!Array.isArray(files)) return [] |
| | |
| | | const fileType = rawFile?.type || '' |
| | | const isImage = isImageFileType(fileType) |
| | | const canCreateObjectURL = typeof URL !== 'undefined' && typeof URL.createObjectURL === 'function' |
| | | const previewUrl = isImage && rawFile && canCreateObjectURL ? URL.createObjectURL(rawFile) : '' |
| | | return { |
| | | previewId: `${rawFile?.name || 'file'}-${rawFile?.size || 0}-${rawFile?.lastModified || Date.now()}-${index}`, |
| | | name: rawFile?.name || `file-${index + 1}`, |
| | | size: rawFile?.size || 0, |
| | | type: fileType, |
| | | isImage, |
| | | previewUrl: isImage && rawFile && canCreateObjectURL ? URL.createObjectURL(rawFile) : '', |
| | | rawFile |
| | | previewUrl, |
| | | accessUrl: '', |
| | | rawFile, |
| | | isObjectUrl: !!previewUrl |
| | | } |
| | | } |
| | | |
| | | const createHistoryFileSnapshot = (filePath, memoryId = '', messageIndex = 0, fileIndex = 0) => { |
| | | const normalizedPath = String(filePath || '').trim() |
| | | if (!normalizedPath) return null |
| | | const isImage = isImageFilePath(normalizedPath) |
| | | return { |
| | | previewId: `${memoryId || 'history'}-${messageIndex}-${fileIndex}`, |
| | | name: getHistoryFileName(normalizedPath, fileIndex), |
| | | size: 0, |
| | | type: '', |
| | | isImage, |
| | | previewUrl: isImage ? normalizedPath : '', |
| | | accessUrl: normalizedPath, |
| | | rawFile: null, |
| | | isObjectUrl: false |
| | | } |
| | | } |
| | | |
| | |
| | | if (!canRevokeObjectURL) return |
| | | if (!Array.isArray(snapshots)) return |
| | | snapshots.forEach((snapshot) => { |
| | | if (snapshot?.previewUrl) { |
| | | if (snapshot?.isObjectUrl && snapshot?.previewUrl) { |
| | | URL.revokeObjectURL(snapshot.previewUrl) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const mapHistoryFilePathsToSnapshots = (filePaths = [], memoryId = '', messageIndex = 0) => { |
| | | if (!Array.isArray(filePaths)) return [] |
| | | return filePaths |
| | | .map((filePath, fileIndex) => createHistoryFileSnapshot(filePath, memoryId, messageIndex, fileIndex)) |
| | | .filter(Boolean) |
| | | } |
| | | |
| | | const openMessageAttachment = (file) => { |
| | | const accessUrl = String(file?.accessUrl || '').trim() |
| | | if (!accessUrl) return |
| | | if (typeof window === 'undefined' || typeof window.open !== 'function') return |
| | | window.open(accessUrl, '_blank', 'noopener,noreferrer') |
| | | } |
| | | |
| | | const handleMessageFileClick = (file) => { |
| | | if (!file?.accessUrl || file?.isImage) return |
| | | openMessageAttachment(file) |
| | | } |
| | | |
| | | const revokeMessageLocalFileSnapshots = (messageList = []) => { |
| | |
| | | |
| | | const messageObj = { |
| | | isUser, |
| | | content: msg.content, |
| | | content: msg.content || '', |
| | | htmlContent: '', |
| | | isTyping: false, |
| | | chartOptions: null, |
| | |
| | | type: '', |
| | | tableData: null, |
| | | payloadTreeData: null, |
| | | payloadHiddenData: null |
| | | payloadHiddenData: null, |
| | | localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : [] |
| | | } |
| | | |
| | | messages.value.push(messageObj) |
| | |
| | | } |
| | | |
| | | // 解析历史消息中的 JSON |
| | | const extracted = extractEmbeddedSuccessJson(msg.content) |
| | | const extracted = extractEmbeddedSuccessJson(msg.content || '') |
| | | if (extracted) { |
| | | applyStructuredMessageData(messageObj, extracted.data, botMsgIndex) |
| | | } |
| | | |
| | | updateOutputState(msg.content, botMsgIndex) |
| | | messageObj.htmlContent = convertStreamOutput(msg.content, botMsgIndex) |
| | | updateOutputState(msg.content || '', botMsgIndex) |
| | | messageObj.htmlContent = convertStreamOutput(msg.content || '', botMsgIndex) |
| | | } else { |
| | | messageObj.htmlContent = convertTextToHtml(msg.content) |
| | | messageObj.htmlContent = convertTextToHtml(msg.content || '') |
| | | } |
| | | }) |
| | | scrollToBottom() |
| | |
| | | border: 1px solid rgba(88, 117, 255, 0.2); |
| | | background: rgba(255, 255, 255, 0.9); |
| | | max-width: 100%; |
| | | |
| | | &.clickable { |
| | | cursor: pointer; |
| | | transition: all 0.2s ease; |
| | | |
| | | &:hover { |
| | | border-color: rgba(44, 109, 255, 0.38); |
| | | background: rgba(243, 247, 255, 0.96); |
| | | } |
| | | } |
| | | } |
| | | |
| | | .message-local-file-thumb { |
| | |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | |
| | | &.clickable { |
| | | color: $primary-blue; |
| | | cursor: pointer; |
| | | |
| | | &:hover { |
| | | text-decoration: underline; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .message-local-file-size { |