import { computed, onBeforeUnmount, ref, watch } from 'vue' import { buildSelectedFileCards } from './workbenchAiComposerModel.js' import { fetchReceiptFolderAsset } from '../../services/receiptFolder.js' import { inferPreviewKindFromBlob, inferPreviewKindFromFile, isInlinePreviewUrl, isTemporaryPreviewUrl, resolveDocumentPreviewAsset } from '../../utils/documentPreviewAssets.js' function normalizePreviewText(value) { return String(value ?? '').replace(/\s+/g, ' ').trim() } function formatFileSize(size) { const bytes = Number(size || 0) if (!Number.isFinite(bytes) || bytes <= 0) { return '-' } if (bytes < 1024 * 1024) { return `${Math.max(1, Math.round(bytes / 1024))} KB` } return `${(bytes / 1024 / 1024).toFixed(1)} MB` } function formatFileTime(timestamp) { const value = Number(timestamp || 0) if (!Number.isFinite(value) || value <= 0) { return '-' } return new Date(value).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) } function normalizePreviewField(field = {}) { const value = normalizePreviewText(field.value ?? field.text) if (!value) { return null } return { label: normalizePreviewText(field.label || field.key || field.name) || '识别字段', value } } function resolveSourceKind(sourceUrl, rawFile = {}) { const type = normalizePreviewText(rawFile?.type).toLowerCase() const name = normalizePreviewText(rawFile?.name).toLowerCase() if (!sourceUrl) { return 'unsupported' } if (sourceUrl.startsWith('data:image/') || type.startsWith('image/') || /\.(png|jpe?g|webp|gif|bmp|svg)$/.test(name)) { return 'image' } if (type === 'application/pdf' || /\.pdf$/.test(name)) { return 'pdf' } return 'unsupported' } function createObjectUrl(rawFile) { if (!rawFile || typeof URL === 'undefined' || typeof URL.createObjectURL !== 'function') { return '' } return URL.createObjectURL(rawFile) } export function useWorkbenchAiFilePreview({ attachmentFlow, conversationStarted, scrollInlineConversationToBottom, selectedFiles }) { const filePreviewState = ref({ open: false, key: '', objectUrl: '', objectKind: '', objectSource: '', loading: false }) const selectedFileCards = computed(() => buildSelectedFileCards( selectedFiles.value, (file) => attachmentFlow.resolveAiModeReceiptRecognitionState(file) ).map((card, index) => ({ ...card, ocrState: attachmentFlow.resolveAiModeReceiptRecognitionState(selectedFiles.value[index]) }))) function clearFilePreviewObjectUrl() { const objectUrl = filePreviewState.value.objectUrl if (objectUrl && objectUrl.startsWith('blob:') && typeof URL !== 'undefined') { URL.revokeObjectURL(objectUrl) } } function findSelectedFile(fileKey) { const index = selectedFileCards.value.findIndex((file) => file.key === fileKey) if (index < 0) { return null } return { card: selectedFileCards.value[index], index, rawFile: selectedFiles.value[index] } } function resolveTargetDocument(target) { if (!target?.rawFile) { return null } const recognitionState = attachmentFlow.resolveAiModeReceiptRecognitionState(target.rawFile) || target.card.ocrState || null return recognitionState?.document || null } function resolveRemotePreviewAsset(target) { const asset = resolveDocumentPreviewAsset(resolveTargetDocument(target)) if (!asset?.url || isInlinePreviewUrl(asset.url) || isTemporaryPreviewUrl(asset.url)) { return null } return asset } async function loadRemotePreviewAsset(fileKey) { const target = findSelectedFile(fileKey) const asset = resolveRemotePreviewAsset(target) if (!target?.rawFile || !asset?.url) { return } try { const blob = await fetchReceiptFolderAsset(asset.url) const objectUrl = createObjectUrl(blob) const objectKind = inferPreviewKindFromBlob(blob) || asset.kind if (!objectUrl || !['image', 'pdf'].includes(objectKind)) { return } if (!filePreviewState.value.open || filePreviewState.value.key !== fileKey) { URL.revokeObjectURL(objectUrl) return } clearFilePreviewObjectUrl() filePreviewState.value = { ...filePreviewState.value, objectUrl, objectKind, objectSource: asset.source, loading: false } } catch (error) { console.warn('AI mode remote attachment preview unavailable:', error) if (!filePreviewState.value.open || filePreviewState.value.key !== fileKey) { return } clearFilePreviewObjectUrl() filePreviewState.value = { ...filePreviewState.value, objectUrl: createObjectUrl(target.rawFile), objectKind: inferPreviewKindFromFile(target.rawFile), objectSource: 'file', loading: false } } } function openAiModeFilePreview(fileKey) { const target = findSelectedFile(fileKey) if (!target?.rawFile) { return } clearFilePreviewObjectUrl() const remoteAsset = resolveRemotePreviewAsset(target) filePreviewState.value = { open: true, key: fileKey, objectUrl: remoteAsset ? '' : createObjectUrl(target.rawFile), objectKind: remoteAsset ? '' : inferPreviewKindFromFile(target.rawFile), objectSource: remoteAsset ? remoteAsset.source : 'file', loading: Boolean(remoteAsset) } if (remoteAsset) { void loadRemotePreviewAsset(fileKey) } } function closeAiModeFilePreview() { clearFilePreviewObjectUrl() filePreviewState.value = { open: false, key: '', objectUrl: '', objectKind: '', objectSource: '', loading: false } } const activeAiModeFilePreview = computed(() => { if (!filePreviewState.value.open || !filePreviewState.value.key) { return null } const target = findSelectedFile(filePreviewState.value.key) if (!target) { return null } const rawFile = target.rawFile const recognitionState = attachmentFlow.resolveAiModeReceiptRecognitionState(rawFile) || target.card.ocrState || null const document = recognitionState?.document || null const documentFields = Array.isArray(document?.document_fields) ? document.document_fields : document?.fields || [] const ocrFields = documentFields.map((field) => normalizePreviewField(field)).filter(Boolean) const documentPreviewAsset = resolveDocumentPreviewAsset(document) const inlinePreviewAvailable = documentPreviewAsset?.url && isInlinePreviewUrl(documentPreviewAsset.url) const sourceUrl = inlinePreviewAvailable ? documentPreviewAsset.url : filePreviewState.value.objectUrl const sourceKind = inlinePreviewAvailable ? documentPreviewAsset.kind : filePreviewState.value.loading ? 'loading' : filePreviewState.value.objectKind || resolveSourceKind(sourceUrl, rawFile) const documentTypeLabel = normalizePreviewText( document?.document_type_label || document?.scene_label || document?.document_type || target.card.typeLabel ) return { open: true, key: target.card.key, name: target.card.name, sourceKind, sourceUrl, documentTypeLabel: documentTypeLabel || '待识别', recognitionStatus: recognitionState?.status || 'idle', recognitionStatusLabel: recognitionState?.label || '等待智能录入识别', recognitionStatusTitle: recognitionState?.title || '', fileInfoRows: [ { label: '文件类型', value: target.card.typeLabel }, { label: '文件大小', value: formatFileSize(rawFile?.size) }, { label: '上传时间', value: formatFileTime(rawFile?.lastModified) } ], ocrFields, ocrSummary: normalizePreviewText(document?.summary), rawText: normalizePreviewText(document?.text).slice(0, 600) } }) watch(selectedFiles, (files, previousFiles = []) => { attachmentFlow.primeAiModeReceiptContext(files) const fileCountChanged = files.length !== previousFiles.length if (conversationStarted.value && fileCountChanged) { scrollInlineConversationToBottom({ force: true }) } if (filePreviewState.value.open && !findSelectedFile(filePreviewState.value.key)) { closeAiModeFilePreview() } }, { flush: 'sync' }) onBeforeUnmount(() => { closeAiModeFilePreview() }) return { activeAiModeFilePreview, closeAiModeFilePreview, openAiModeFilePreview, selectedFileCards } }