Skip to content
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 89 additions & 9 deletions src/components/Folder/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,23 @@ 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'];

function getExt(name: string) {
return name.split('.').pop()?.toLowerCase() || '';
}
function isImageFile(name: string) {
return IMAGE_EXTENSIONS.includes(getExt(name));
}
function isAudioFile(name: string) {
return AUDIO_EXTENSIONS.includes(getExt(name));
}
function isVideoFile(name: string) {
return VIDEO_EXTENSIONS.includes(getExt(name));
}

// Type definitions
interface FileTreeNode {
name: string;
Expand Down Expand Up @@ -244,6 +261,14 @@ export default function Folder({ data: _data }: { data?: Agent }) {
return;
}

// For audio/video files, skip open-file — loaders handle reading themselves
if (isAudioFile(file.name) || isVideoFile(file.name)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

compared with using file.name, would using file type be more reliable?

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)
Expand Down Expand Up @@ -657,15 +682,15 @@ export default function Folder({ data: _data }: { data?: Agent }) {
</p>
</div>
</div>
) : [
'png',
'jpg',
'jpeg',
'gif',
'bmp',
'webp',
'svg',
].includes(selectedFile.type.toLowerCase()) ? (
) : isAudioFile(selectedFile.name) ? (
<div className="flex h-full items-center justify-center">
<AudioLoader selectedFile={selectedFile} />
</div>
) : isVideoFile(selectedFile.name) ? (
<div className="flex h-full items-center justify-center">
<VideoLoader selectedFile={selectedFile} />
</div>
) : isImageFile(selectedFile.name) ? (
<div className="flex h-full items-center justify-center">
<ImageLoader selectedFile={selectedFile} />
</div>
Expand Down Expand Up @@ -724,6 +749,61 @@ function ImageLoader({ selectedFile }: { selectedFile: FileInfo }) {
);
}

function AudioLoader({ selectedFile }: { selectedFile: FileInfo }) {
const [src, setSrc] = useState('');

useEffect(() => {
setSrc('');
if (selectedFile.isRemote) {
setSrc(selectedFile.content || selectedFile.path);
return;
}
window.electronAPI
.readFileAsDataUrl(selectedFile.path)
.then(setSrc)
.catch((err: any) => {
console.error('Audio load error:', err);
setSrc('');
});
}, [selectedFile]);

return (
<div className="flex w-full flex-col items-center gap-4 px-8">
<p className="text-sm font-medium text-text-primary">
{selectedFile.name}
</p>
<audio controls src={src} className="w-full">
Your browser does not support audio playback.
</audio>
</div>
);
}

function VideoLoader({ selectedFile }: { selectedFile: FileInfo }) {
const [src, setSrc] = useState('');

useEffect(() => {
setSrc('');
if (selectedFile.isRemote) {
setSrc(selectedFile.content || selectedFile.path);
return;
}
window.electronAPI
.readFileAsDataUrl(selectedFile.path)
.then(setSrc)
.catch((err: any) => {
console.error('Video load error:', err);
setSrc('');
});
}, [selectedFile]);

return (
<video controls src={src} className="max-h-full max-w-full object-contain">
Your browser does not support video playback.
</video>
);
}

// Helper function to get directory path from file path
function getDirPath(filePath: string): string {
const normalizedPath = filePath.replace(/\\/g, '/');
Expand Down