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

@ -1,12 +1,13 @@
import { useState, useCallback } from 'react';
import { useEditorStore } from '../store/editorStore';
import { Terminal, ChevronDown, ChevronUp, Play, Wifi } from 'lucide-react';
import { Terminal, ChevronDown, ChevronUp, Play, Wifi, AlertTriangle } from 'lucide-react';
export default function DevPanel() {
const [open, setOpen] = useState(false);
const [pathInput, setPathInput] = useState('');
const [testResult, setTestResult] = useState<string | null>(null);
const [testing, setTesting] = useState(false);
const [showResetConfirm, setShowResetConfirm] = useState(false);
const { backendUrl, videoPath, loadVideo } = useEditorStore();
@ -121,6 +122,37 @@ export default function DevPanel() {
{testResult}
</pre>
)}
{/* Danger Zone */}
<div className="space-y-1">
<div className="text-[#ef4444] uppercase tracking-wider text-[9px]">Danger Zone</div>
{!showResetConfirm ? (
<button
onClick={() => setShowResetConfirm(true)}
className="w-full px-2 py-1.5 rounded border border-red-500/40 text-red-400 hover:bg-red-500/10 text-xs flex items-center justify-center gap-1.5"
>
<AlertTriangle className="w-3 h-3" />
Reset Editor State
</button>
) : (
<div className="bg-[#1e1020] border border-red-500/40 rounded p-2 space-y-1.5">
<p className="text-[#fca5a5] text-[10px]">This will clear all editor data and reload the page. Unsaved changes will be lost.</p>
<div className="flex gap-1">
<button
onClick={() => setShowResetConfirm(false)}
className="flex-1 px-2 py-1 rounded text-[10px] text-[#6b7280] hover:text-white hover:bg-[#2a2d3e]"
>
Cancel
</button>
<button
onClick={() => { useEditorStore.getState().reset(); window.location.reload(); }}
className="flex-1 px-2 py-1 rounded text-[10px] border border-red-500/40 text-red-400 hover:bg-red-500/10"
>
Confirm Reset
</button>
</div>
</div>
)}
</div>
</div>
)}
</div>

View File

@ -0,0 +1,90 @@
import { Component, type ReactNode } from 'react';
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
export default class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error('ErrorBoundary caught:', error, info.componentStack);
try {
window.electronAPI?.logError?.(error.message, error.stack || '', info.componentStack || '');
} catch {}
}
handleReload = () => {
window.location.reload();
};
handleReset = () => {
try {
localStorage.clear();
sessionStorage.clear();
} catch {}
window.location.reload();
};
render() {
if (this.state.hasError) {
return (
<div className="h-screen flex flex-col items-center justify-center gap-6 bg-editor-bg px-6">
<div className="flex flex-col items-center gap-3 max-w-md text-center">
<div className="w-12 h-12 rounded-full bg-red-500/20 flex items-center justify-center">
<svg className="w-6 h-6 text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<h2 className="text-lg font-semibold text-editor-text">Something went wrong</h2>
<p className="text-xs text-editor-text-muted leading-relaxed">
An unexpected error occurred. Your work may still be recoverable.
</p>
</div>
{this.state.error && (
<details className="max-w-md w-full">
<summary className="text-xs text-editor-text-muted cursor-pointer hover:text-editor-text">
Error details
</summary>
<pre className="mt-2 p-3 rounded bg-editor-surface border border-editor-border text-[10px] text-red-300 overflow-auto max-h-32 whitespace-pre-wrap">
{this.state.error.message}
{'\n'}
{this.state.error.stack}
</pre>
</details>
)}
<div className="flex flex-col items-center gap-2">
<button
onClick={this.handleReload}
className="px-4 py-2 bg-editor-accent hover:bg-editor-accent-hover rounded-lg text-sm font-medium transition-colors"
>
Reload App
</button>
<button
onClick={this.handleReset}
className="text-xs text-editor-text-muted hover:text-editor-text underline transition-colors"
>
Reset & Clear All Data
</button>
</div>
</div>
);
}
return this.props.children;
}
}

View File

@ -2,6 +2,7 @@ import { useState, useCallback } from 'react';
import { useEditorStore } from '../store/editorStore';
import { Download, Loader2, Zap, Cog, Info, Volume2, FileText, ZoomIn, Video, Music } from 'lucide-react';
import type { ExportOptions } from '../types/project';
import { assert } from '../lib/assert';
export default function ExportDialog() {
const { videoPath, words, cutRanges, muteRanges, gainRanges, speedRanges, globalGainDb, isExporting, exportProgress, backendUrl, setExporting, getKeepSegments, additionalClips, backgroundMusic } =
@ -136,6 +137,7 @@ export default function ExportDialog() {
setExportError(null);
try {
const keepSegments = getKeepSegments();
assert(words.length > 0, 'handleExport: words is empty before building keep segments');
const deletedSet = getDeletedSet();
// Map frontend camelCase gain/speed fields to backend snake_case

View File

@ -3,6 +3,7 @@ import { useEditorStore } from '../store/editorStore';
import { useLicenseStore } from '../store/licenseStore';
import { Virtuoso } from 'react-virtuoso';
import { Scissors, VolumeX, SlidersHorizontal, Gauge, RotateCcw, Search, ChevronUp, ChevronDown, X, RefreshCw } from 'lucide-react';
import { assert } from '../lib/assert';
interface TranscriptEditorProps {
cutMode: boolean;
@ -206,6 +207,8 @@ export default function TranscriptEditor({
const handleMouseUp = useCallback(() => {
if (zoneDragStart.current !== null && zoneDragRange) {
assert(zoneDragRange.start >= 0 && zoneDragRange.start < words.length, 'handleMouseUp: zoneDragRange.start out of bounds');
assert(zoneDragRange.end >= 0 && zoneDragRange.end < words.length, 'handleMouseUp: zoneDragRange.end out of bounds');
const startWord = words[zoneDragRange.start];
const endWord = words[zoneDragRange.end];
if (startWord && endWord && canEdit) {
@ -269,6 +272,7 @@ export default function TranscriptEditor({
// Snapshot indices and word timings before the async gap
const sorted = [...selectedWordIndices].sort((a, b) => a - b);
assert(sorted[0] >= 0 && sorted[sorted.length - 1] < words.length, 'handleReTranscribe: sorted indices out of bounds');
const startWord = words[sorted[0]];
const endWord = words[sorted[sorted.length - 1]];
if (!startWord || !endWord) {
@ -336,6 +340,8 @@ export default function TranscriptEditor({
const cutSelectedWords = useCallback(() => {
if (selectedWordIndices.length === 0) return;
const sorted = [...selectedWordIndices].sort((a, b) => a - b);
assert(sorted[0] >= 0 && sorted[0] < words.length, 'cutSelectedWords: sorted[0] out of bounds');
assert(sorted[sorted.length - 1] >= 0 && sorted[sorted.length - 1] < words.length, 'cutSelectedWords: sorted[last] out of bounds');
const startTime = words[sorted[0]].start;
const endTime = words[sorted[sorted.length - 1]].end;
addCutRange(startTime, endTime);

View File

@ -3,6 +3,7 @@ import { useEditorStore } from '../store/editorStore';
import { useLicenseStore } from '../store/licenseStore';
import { AlertTriangle } from 'lucide-react';
import { extractThumbnails } from '../lib/thumbnails';
import { assert } from '../lib/assert';
const RULER_H = 20; // px reserved at top of canvas for the time ruler
const COLLAPSED_CUT_DISPLAY_SECONDS = 0.08;
@ -821,6 +822,7 @@ export default function WaveformTimeline({
}, [displayDuration, setCurrentTime, timelineSegments]);
const clientXToTime = useCallback((clientX: number): number => {
assert(headCanvasRef.current !== null, 'clientXToTime: headCanvasRef.current is null');
const canvas = headCanvasRef.current;
const dur = waveformDataRef.current?.duration;
if (!canvas || !dur) return 0;
@ -1122,6 +1124,8 @@ export default function WaveformTimeline({
setIsDragging(false);
if (selectionStartRef.current !== null && selectionEndRef.current !== null) {
assert(selectionStartRef.current !== null, 'handleMouseDown: selectionStartRef is null');
assert(selectionEndRef.current !== null, 'handleMouseDown: selectionEndRef is null');
const start = Math.min(selectionStartRef.current, selectionEndRef.current);
const end = Math.max(selectionStartRef.current, selectionEndRef.current);
const minDuration = 0.01;