more stuff to improve robustness
This commit is contained in:
@ -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>
|
||||
|
||||
90
frontend/src/components/ErrorBoundary.tsx
Normal file
90
frontend/src/components/ErrorBoundary.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user