more stuff to improve robustness

This commit is contained in:
2026-05-06 14:25:23 -06:00
parent 9a301fe2a2
commit 4004312994
13 changed files with 505 additions and 54 deletions

View File

@ -44,37 +44,38 @@ const LAST_MEDIA_PATH_KEY = 'talkedit:lastMediaPath';
type Panel = 'ai' | 'settings' | 'export' | 'silence' | 'zones' | 'markers' | 'music' | 'append' | 'help' | null;
export default function App() {
const {
projectFilePath,
videoPath,
exportedAudioPath,
words,
segments,
cutRanges,
muteRanges,
gainRanges,
speedRanges,
globalGainDb,
silenceTrimGroups,
transcriptionModel,
language,
isTranscribing,
transcriptionStatus,
markInTime,
markOutTime,
loadVideo,
setProjectFilePath,
setBackendUrl,
clearMarkRange,
setTranscription,
setTranscriptionModel,
setTranscribing,
selectedWordIndices,
addCutRange,
addMuteRange,
addGainRange,
addSpeedRange,
} = useEditorStore();
const {
projectFilePath,
videoPath,
exportedAudioPath,
words,
segments,
cutRanges,
muteRanges,
gainRanges,
speedRanges,
globalGainDb,
silenceTrimGroups,
transcriptionModel,
language,
isTranscribing,
transcriptionStatus,
markInTime,
markOutTime,
loadVideo,
setProjectFilePath,
setBackendUrl,
clearMarkRange,
setTranscription,
setTranscriptionModel,
setTranscribing,
selectedWordIndices,
addCutRange,
addMuteRange,
addGainRange,
addSpeedRange,
backendUrl,
} = useEditorStore();
const [activePanel, setActivePanel] = useState<Panel>(null);
const [projectName, setProjectName] = useState<string | null>(null);
@ -142,6 +143,10 @@ export default function App() {
const [pendingProceedAction, setPendingProceedAction] = useState<(() => Promise<void>) | null>(null);
const [lastSavedSignature, setLastSavedSignature] = useState<string | null>(null);
const [showFileMenu, setShowFileMenu] = useState(false);
const [showRecoveryDialog, setShowRecoveryDialog] = useState(false);
const [recoveryData, setRecoveryData] = useState<any>(null);
const [recoveryMinutesAgo, setRecoveryMinutesAgo] = useState(0);
const [backendDown, setBackendDown] = useState(false);
const canEdit = useLicenseStore((s) => s.canEdit);
const licenseStatus = useLicenseStore((s) => s.status);
const setShowLicenseDialog = useLicenseStore((s) => s.setShowDialog);
@ -180,7 +185,10 @@ export default function App() {
const hasUnsavedChanges = Boolean(projectSignature) && projectSignature !== lastSavedSignature;
const loadProjectFromData = (data: any) => {
useEditorStore.getState().loadProject(data);
const removedCount = useEditorStore.getState().loadProject(data);
if (removedCount > 0) {
window.alert(`${removedCount} invalid zones were removed from the loaded project.`);
}
const loadedSignature = JSON.stringify({
videoPath: data.videoPath,
exportedAudioPath: data.exportedAudioPath ?? null,
@ -211,6 +219,18 @@ export default function App() {
useEffect(() => {
useLicenseStore.getState().checkStatus();
window.electronAPI?.readAutosave().then((data) => {
if (data) {
try {
const parsed = JSON.parse(data);
const savedAt = parsed.savedAt;
const minutesAgo = savedAt ? Math.round((Date.now() - savedAt) / 60000) : 0;
setRecoveryData(parsed);
setRecoveryMinutesAgo(minutesAgo);
setShowRecoveryDialog(true);
} catch {}
}
});
}, []);
// Handle Escape key to exit timeline zone modes and close menus
@ -248,6 +268,36 @@ export default function App() {
sessionStorage.removeItem(LAST_MEDIA_PATH_KEY);
}, [videoPath]);
useEffect(() => {
if (!videoPath) return;
const interval = setInterval(() => {
const state = useEditorStore.getState();
const hasData = state.words.length > 0 || state.cutRanges.length > 0 || state.muteRanges.length > 0 || state.gainRanges.length > 0 || state.speedRanges.length > 0;
if (!hasData) return;
const autosaveData = {
savedAt: Date.now(),
videoPath: state.videoPath,
words: state.words,
segments: state.segments,
cutRanges: state.cutRanges,
muteRanges: state.muteRanges,
gainRanges: state.gainRanges,
speedRanges: state.speedRanges,
globalGainDb: state.globalGainDb,
silenceTrimGroups: state.silenceTrimGroups,
transcriptionModel: state.transcriptionModel,
language: state.language,
markInTime: state.markInTime,
markOutTime: state.markOutTime,
timelineMarkers: state.timelineMarkers,
};
window.electronAPI.writeAutosave(JSON.stringify(autosaveData));
}, 60000);
return () => clearInterval(interval);
}, [videoPath]);
const handleLoadProject = async () => {
await runGuarded(async () => {
try {
@ -404,6 +454,26 @@ export default function App() {
setPendingProceedAction(null);
};
const handleRecoverAutosave = () => {
if (!recoveryData) return;
const data = recoveryData;
const removedCount = useEditorStore.getState().loadProject(data);
if (removedCount > 0) {
window.alert(`${removedCount} invalid zones were removed from the loaded project.`);
}
if (data.markInTime != null) useEditorStore.getState().setMarkInTime(data.markInTime);
if (data.markOutTime != null) useEditorStore.getState().setMarkOutTime(data.markOutTime);
window.electronAPI.deleteAutosave();
setShowRecoveryDialog(false);
setRecoveryData(null);
};
const handleDismissRecovery = () => {
window.electronAPI.deleteAutosave();
setShowRecoveryDialog(false);
setRecoveryData(null);
};
const togglePanel = (panel: Panel) => {
setActivePanel((prev) => (prev === panel ? null : panel));
};
@ -606,6 +676,21 @@ export default function App() {
);
}
// Health check timer
useEffect(() => {
const checkHealth = async () => {
try {
const res = await fetch(`${backendUrl}/health`);
setBackendDown(!res.ok);
} catch {
setBackendDown(true);
}
};
checkHealth();
const interval = setInterval(checkHealth, 30000);
return () => clearInterval(interval);
}, [videoPath, backendUrl]);
return (
<div className="h-screen flex flex-col bg-editor-bg overflow-hidden">
{/* Top bar */}
@ -1009,6 +1094,44 @@ export default function App() {
</div>
</div>
)}
{showRecoveryDialog && (
<div
className="fixed inset-0 z-[80] flex items-center justify-center bg-black/60 px-4"
onClick={handleDismissRecovery}
>
<div
className="w-full max-w-md rounded-xl border border-editor-border bg-editor-bg p-4 space-y-3"
onClick={(e) => e.stopPropagation()}
>
<h3 className="text-sm font-semibold">Recover unsaved work?</h3>
<p className="text-xs text-editor-text-muted leading-relaxed">
TalkEdit recovered a project from {recoveryMinutesAgo} minute{recoveryMinutesAgo !== 1 ? 's' : ''} ago.
</p>
<div className="flex items-center justify-end gap-2 pt-1">
<button
onClick={handleDismissRecovery}
className="px-3 py-1.5 rounded-md text-xs text-editor-text-muted hover:text-editor-text hover:bg-editor-surface"
>
Dismiss
</button>
<button
onClick={handleRecoverAutosave}
className="px-3 py-1.5 rounded-md text-xs bg-editor-accent hover:bg-editor-accent-hover text-white"
>
Recover
</button>
</div>
</div>
</div>
)}
{backendDown && (
<div className="fixed bottom-0 left-0 right-0 z-[90] flex items-center justify-center gap-2 px-4 py-2 bg-amber-500/15 border-t border-amber-500/30 text-amber-400 text-xs font-medium">
<AlertTriangle className="w-3.5 h-3.5" />
Backend disconnected — retrying...
</div>
)}
</div>
);
}