diff --git a/src/components/Folder/index.tsx b/src/components/Folder/index.tsx index af8742643..a160eb641 100644 --- a/src/components/Folder/index.tsx +++ b/src/components/Folder/index.tsx @@ -41,6 +41,68 @@ import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; import { ZoomControls } from './ZoomControls'; +const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg']; +const AUDIO_EXTENSIONS = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma']; +const VIDEO_EXTENSIONS = ['mp4', 'webm', 'mov', 'avi', 'mkv', 'flv', 'wmv']; + +type FileTypeTarget = { + name?: string; + path?: string; + type?: string; +}; +const loggedFileTypeWarnings = new Set(); + +function getExt(value?: string) { + if (!value) return ''; + const normalized = value.split(/[?#]/)[0]; + const lastSegment = normalized.split('/').pop() || normalized; + if (!lastSegment.includes('.')) return ''; + return lastSegment.split('.').pop()?.toLowerCase() || ''; +} + +function getFileType(file: FileTypeTarget) { + const extFromNameOrPath = getExt(file.name) || getExt(file.path); + const normalizedType = (file.type || '').replace(/^\./, '').toLowerCase(); + const fileId = file.path || file.name || 'unknown-file'; + + if (!extFromNameOrPath && normalizedType) { + const key = `missing-ext|${fileId}|${normalizedType}`; + if (!loggedFileTypeWarnings.has(key)) { + loggedFileTypeWarnings.add(key); + console.warn( + `[Folder getFileType] extension missing in name/path, file.type fallback disabled: ${fileId} (type=${normalizedType})` + ); + } + } + + if ( + extFromNameOrPath && + normalizedType && + normalizedType !== 'folder' && + extFromNameOrPath !== normalizedType + ) { + const key = `mismatch|${fileId}|${extFromNameOrPath}|${normalizedType}`; + if (!loggedFileTypeWarnings.has(key)) { + loggedFileTypeWarnings.add(key); + console.warn( + `[Folder getFileType] extension/type mismatch for ${fileId}: inferred=${extFromNameOrPath}, type=${normalizedType}` + ); + } + } + + return extFromNameOrPath; +} + +function isImageFile(file: FileTypeTarget) { + return IMAGE_EXTENSIONS.includes(getFileType(file)); +} +function isAudioFile(file: FileTypeTarget) { + return AUDIO_EXTENSIONS.includes(getFileType(file)); +} +function isVideoFile(file: FileTypeTarget) { + return VIDEO_EXTENSIONS.includes(getFileType(file)); +} + // Type definitions interface FileTreeNode { name: string; @@ -244,6 +306,14 @@ export default function Folder({ data: _data }: { data?: Agent }) { return; } + // For audio/video files, skip open-file — loaders handle reading themselves + if (isAudioFile(file) || isVideoFile(file)) { + setSelectedFile({ ...file }); + chatStore.setSelectedFile(chatStore.activeTaskId as string, file); + setLoading(false); + return; + } + // all other files call open-file interface, the backend handles download and parsing window.ipcRenderer .invoke('open-file', file.type, file.path, isShowSourceCode) @@ -657,15 +727,15 @@ export default function Folder({ data: _data }: { data?: Agent }) {

- ) : [ - 'png', - 'jpg', - 'jpeg', - 'gif', - 'bmp', - 'webp', - 'svg', - ].includes(selectedFile.type.toLowerCase()) ? ( + ) : isAudioFile(selectedFile) ? ( +
+ +
+ ) : isVideoFile(selectedFile) ? ( +
+ +
+ ) : isImageFile(selectedFile) ? (
@@ -724,6 +794,75 @@ function ImageLoader({ selectedFile }: { selectedFile: FileInfo }) { ); } +function AudioLoader({ selectedFile }: { selectedFile: FileInfo }) { + const [src, setSrc] = useState(''); + + useEffect(() => { + let cancelled = false; + setSrc(''); + if (selectedFile.isRemote) { + setSrc(selectedFile.content || selectedFile.path); + return; + } + window.electronAPI + .readFileAsDataUrl(selectedFile.path) + .then((dataUrl: string) => { + if (!cancelled) setSrc(dataUrl); + }) + .catch((err: any) => { + if (cancelled) return; + console.error('Audio load error:', err); + setSrc(''); + }); + return () => { + cancelled = true; + }; + }, [selectedFile]); + + return ( +
+

+ {selectedFile.name} +

+ +
+ ); +} + +function VideoLoader({ selectedFile }: { selectedFile: FileInfo }) { + const [src, setSrc] = useState(''); + + useEffect(() => { + let cancelled = false; + setSrc(''); + if (selectedFile.isRemote) { + setSrc(selectedFile.content || selectedFile.path); + return; + } + window.electronAPI + .readFileAsDataUrl(selectedFile.path) + .then((dataUrl: string) => { + if (!cancelled) setSrc(dataUrl); + }) + .catch((err: any) => { + if (cancelled) return; + console.error('Video load error:', err); + setSrc(''); + }); + return () => { + cancelled = true; + }; + }, [selectedFile]); + + return ( + + ); +} + // Helper function to get directory path from file path function getDirPath(filePath: string): string { const normalizedPath = filePath.replace(/\\/g, '/');