import { useEffect, useState, useRef } from 'react'; import { invoke } from '@tauri-apps/api/core'; import { useEditorStore } from './store/editorStore'; import VideoPlayer from './components/VideoPlayer'; import TranscriptEditor from './components/TranscriptEditor'; import WaveformTimeline from './components/WaveformTimeline'; import AIPanel from './components/AIPanel'; import ExportDialog from './components/ExportDialog'; import SettingsPanel from './components/SettingsPanel'; import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'; import { Film, FolderOpen, Settings, Sparkles, Download, FolderSearch, FileInput, } from 'lucide-react'; const IS_ELECTRON = !!window.electronAPI; const IS_TAURI = !IS_ELECTRON && '__TAURI_INTERNALS__' in window; type Panel = 'ai' | 'settings' | 'export' | null; export default function App() { const { videoPath, words, isTranscribing, transcriptionProgress, transcriptionStatus, loadVideo, setBackendUrl, setTranscription, setTranscribing, backendUrl, } = useEditorStore(); const [activePanel, setActivePanel] = useState(null); const [manualPath, setManualPath] = useState(''); const [whisperModel, setWhisperModel] = useState('base'); const fileInputRef = useRef(null); useKeyboardShortcuts(); useEffect(() => { if (IS_ELECTRON) { window.electronAPI!.getBackendUrl().then(setBackendUrl); } else if (IS_TAURI) { invoke('get_backend_url').then(setBackendUrl).catch(console.error); } }, [setBackendUrl]); const handleLoadProject = async () => { if (!IS_ELECTRON) return; try { const projectPath = await window.electronAPI!.openProject(); if (!projectPath) return; const content = await window.electronAPI!.readFile(projectPath); const data = JSON.parse(content); useEditorStore.getState().loadProject(data); } catch (err) { console.error('Failed to load project:', err); alert(`Failed to load project: ${err}`); } }; const handleOpenFile = async () => { if (IS_ELECTRON) { const path = await window.electronAPI!.openFile(); if (path) { loadVideo(path); await transcribeVideo(path); } } else { // Browser: use the manual path input const path = manualPath.trim(); if (path) { loadVideo(path); await transcribeVideo(path); } } }; const handleManualSubmit = async (e: React.FormEvent) => { e.preventDefault(); const path = manualPath.trim(); if (!path) return; loadVideo(path); await transcribeVideo(path); }; const transcribeVideo = async (path: string) => { setTranscribing(true, 0, 'Checking model...'); try { if (!window.electronAPI?.transcribe) { throw new Error('Transcription not available'); } // Step 1: ensure model is downloaded (may take a while on first run) const modelLabel = whisperModel === 'tiny' ? '~75 MB' : whisperModel === 'base' ? '~140 MB' : '~460 MB'; setTranscribing(true, 5, `Downloading ${whisperModel} model (${modelLabel})...`); await window.electronAPI.ensureModel(whisperModel); // Step 2: run transcription setTranscribing(true, 20, 'Transcribing audio...'); const data = await window.electronAPI.transcribe(path, whisperModel); setTranscription(data); } catch (err) { console.error('Transcription error:', err); alert(`Transcription failed. Check the console for details.\n\n${err}`); } finally { setTranscribing(false); } }; const togglePanel = (panel: Panel) => setActivePanel((prev) => (prev === panel ? null : panel)); if (!videoPath) { return (

TalkEdit

Offline AI-powered video editor.

{/* Whisper model selector */}
{IS_ELECTRON ? (
) : ( /* Browser: manual path input */
Running in browser — paste the full path to your video file below.
setManualPath(e.target.value)} placeholder="C:\Videos\my-video.mp4" className="w-full pl-9 pr-3 py-2.5 bg-editor-surface border border-editor-border rounded-lg text-sm text-editor-text placeholder:text-editor-text-muted/40 focus:outline-none focus:border-editor-accent" autoFocus />

Supported: MP4, AVI, MOV, MKV, WebM, M4A

)}
); } return (
{/* Top bar */}
{videoPath.split(/[\\/]/).pop()}
} label="Open" onClick={IS_ELECTRON ? handleOpenFile : () => useEditorStore.getState().reset()} /> } label="AI" active={activePanel === 'ai'} onClick={() => togglePanel('ai')} disabled={words.length === 0} /> } label="Export" active={activePanel === 'export'} onClick={() => togglePanel('export')} disabled={words.length === 0} /> } label="Settings" active={activePanel === 'settings'} onClick={() => togglePanel('settings')} />
{/* Main content */}
{/* Left: video + transcript */}
{/* Video player */}
{/* Transcript */}
{isTranscribing ? (
{/* Animated waveform */}
{[35, 60, 45, 80, 55, 70, 40, 65, 50, 75, 40, 58].map((h, i) => (
))}

Processing audio

{transcriptionStatus || 'Please wait...'}

) : words.length > 0 ? ( ) : (
No transcript yet
)}
{/* Waveform timeline */}
{/* Right panel (AI / Export / Settings) */} {activePanel && (
{activePanel === 'ai' && } {activePanel === 'export' && } {activePanel === 'settings' && }
)}
); } function ToolbarButton({ icon, label, active, onClick, disabled, }: { icon: React.ReactNode; label: string; active?: boolean; onClick: () => void; disabled?: boolean; }) { return ( ); }