From 9d0064b84cfa738871e62af1ddcbbd93df186f74 Mon Sep 17 00:00:00 2001 From: FernandoM33 Date: Sun, 23 Aug 2026 23:07:17 -0700 Subject: [PATCH] fix: reset media-specific ai workspace --- .../scripts/smoke-clips-first-lifecycle.mjs | 75 +++++++++++++++++-- .../scripts/smoke-errors-accessibility.mjs | 2 +- frontend/src/App.tsx | 8 +- frontend/src/components/AIPanel.tsx | 17 +++-- frontend/src/store/aiStore.ts | 28 +++---- frontend/src/utils/clipWorkspace.ts | 41 +++++++++- 6 files changed, 136 insertions(+), 35 deletions(-) diff --git a/frontend/scripts/smoke-clips-first-lifecycle.mjs b/frontend/scripts/smoke-clips-first-lifecycle.mjs index 820d726..fca58d2 100644 --- a/frontend/scripts/smoke-clips-first-lifecycle.mjs +++ b/frontend/scripts/smoke-clips-first-lifecycle.mjs @@ -15,8 +15,8 @@ const aiStoreSource = readSource('../src/store/aiStore.ts'); assert.match(appSource, /label=\{editorWorkflow === 'short' \? 'Export Video' : 'Export'\}/); assert.match(appSource, /dataAction="full-video-export"/); -assert.match(appSource, /const resetClipWorkspaceForNewMedia = useCallback/); -assert.match(appSource, /useAIStore\.getState\(\)\.resetClipWorkspace\(\)/); +assert.match(appSource, /const resetMediaAIWorkspaceForNewMedia = useCallback/); +assert.match(appSource, /useAIStore\.getState\(\)\.resetMediaAIWorkspace\(\)/); assert.match(appSource, /clearClipPresentationPreview\(\)/); assert.match(appSource, /setSelectedWordIndices\(\[\]\)/); assert.match(appSource, //); -assert.match(aiStoreSource, /resetClipWorkspaceState\(state\)/); +assert.match(aiStoreSource, /resetMediaAIWorkspace: \(\) =>/); +assert.match(aiStoreSource, /resetMediaAIWorkspaceState\(state\)/); +assert.match(aiStoreSource, /restoreMediaAIWorkspaceState\(state, workspace\)/); assert.match(aiStoreSource, /clipWorkspaceEpoch: state\.clipWorkspaceEpoch \+ 1/); +assert.match(aiStoreSource, /fillerResult: null/); +assert.match(aiStoreSource, /fillerDecisions: \{\}/); +assert.match(aiStoreSource, /editPlanInstruction: ''/); +assert.match(aiStoreSource, /editPlanResult: null/); +assert.match(aiStoreSource, /editPlanDecisions: \{\}/); assert.match(aiStoreSource, /clipSuggestions: \[\]/); assert.match(aiStoreSource, /clipDrafts: \[\]/); assert.match(aiStoreSource, /clipReviewDecisions: \{\}/); assert.match(aiStoreSource, /providers: \{/); +assert.match(panelSource, /if \(!isCurrentClipWorkspace\(\)\) return;\s+setEditPlanResult\(data\);/); +assert.match(panelSource, /if \(!isCurrentClipWorkspace\(\)\) return;\s+setFillerResult\(data\);/); +assert.match(panelSource, /const result = await pollAIJob[\s\S]*if \(!isCurrentClipWorkspace\(\)\) return;/); +assert.match(panelSource, /const cancelAIJob = useCallback\([\s\S]*if \(!isCurrentClipWorkspace\(\)\) return;/); +assert.match(panelSource, /finally \{\s+if \(isCurrentClipWorkspace\(\)\) setProcessing\(false\);/); + +const openFileBody = appSource.match(/const handleOpenFile = async \(intent: WorkflowIntent = 'full-video'\) => \{([\s\S]*?)\n\s*\};\s*const handleBrowserFileChange/)?.[1] || ''; +const browserChangeBody = appSource.match(/const handleBrowserFileChange = async \(e: React\.ChangeEvent\) => \{([\s\S]*?)\n\s*\};\s*const handleBrowserDrop/)?.[1] || ''; +assert.match(openFileBody, /const path = await window\.electronAPI!\.openFile\(\);[\s\S]*if \(path\) \{[\s\S]*resetMediaAIWorkspaceForNewMedia\(\)/); +assert.match(browserChangeBody, /e\.target\.value = '';[\s\S]*if \(!file\) return;/); +assert.match(appSource, /const restoreProject = \(data: ReturnType\) => \{[\s\S]*loadProjectState\(data\)/); +assert.match(appSource, /useAIStore\.getState\(\)\.loadProjectAIState\(data\.aiWorkspace\)/); function loadTsModule(relativePath) { const source = readSource(relativePath); @@ -50,7 +68,7 @@ function loadTsModule(relativePath) { return module.exports; } -const { resetClipWorkspaceState } = loadTsModule('../src/utils/clipWorkspace.ts'); +const { resetMediaAIWorkspaceState, restoreMediaAIWorkspaceState } = loadTsModule('../src/utils/clipWorkspace.ts'); const { getProjectWorkflow } = loadTsModule('../src/utils/editorTask.ts'); const suggestion = { @@ -61,17 +79,35 @@ const suggestion = { endTime: 18, reason: 'Clear hook', }; +const fillerResult = { + fillerWords: [{ index: 1, word: 'um', start: 1, end: 1.2, reason: 'Repeated filler' }], +}; +const editPlanResult = { + summary: 'Tighten the opening', + suggestions: [{ id: 'edit-1', startWordIndex: 0, endWordIndex: 2, startTime: 0, endTime: 4, reason: 'Tighter hook' }], +}; const providerState = { providers: { ollama: { provider: 'ollama', model: 'llama3' } }, defaultProvider: 'ollama', customFillerWords: 'okay', + _keysHydrated: true, + fillerResult, + fillerDecisions: { 1: 'rejected' }, + editPlanInstruction: 'Tighten the opening', + editPlanResult, + editPlanDecisions: { 'edit-1': 'accepted' }, clipSuggestions: [suggestion], clipDrafts: [{ ...suggestion, id: 'clip-1', status: 'draft' }], clipReviewDecisions: { 'clip-0-4': 'approved' }, isProcessing: true, processingMessage: 'Finding clips...', }; -const resetState = resetClipWorkspaceState(providerState); +const resetState = resetMediaAIWorkspaceState(providerState); +assert.equal(resetState.fillerResult, null); +assert.deepEqual(resetState.fillerDecisions, {}); +assert.equal(resetState.editPlanInstruction, ''); +assert.equal(resetState.editPlanResult, null); +assert.deepEqual(resetState.editPlanDecisions, {}); assert.deepEqual(resetState.clipSuggestions, []); assert.deepEqual(resetState.clipDrafts, []); assert.deepEqual(resetState.clipReviewDecisions, {}); @@ -80,6 +116,33 @@ assert.equal(resetState.processingMessage, ''); assert.equal(resetState.providers, providerState.providers); assert.equal(resetState.defaultProvider, providerState.defaultProvider); assert.equal(resetState.customFillerWords, providerState.customFillerWords); +assert.equal(resetState._keysHydrated, providerState._keysHydrated); + +const projectWorkspace = { + customFillerWords: 'project filler', + fillerResult, + fillerDecisions: { 1: 'accepted' }, + editPlanInstruction: 'Keep the strongest explanation', + editPlanResult, + editPlanDecisions: { 'edit-1': 'rejected' }, + clipSuggestions: [suggestion], + clipDrafts: [{ ...suggestion, id: 'project-clip-1', status: 'draft' }], + clipReviewDecisions: { 'clip-0-4': 'skipped' }, +}; +const restoredState = restoreMediaAIWorkspaceState(resetState, projectWorkspace); +assert.deepEqual(restoredState.fillerResult, projectWorkspace.fillerResult); +assert.deepEqual(restoredState.fillerDecisions, projectWorkspace.fillerDecisions); +assert.equal(restoredState.editPlanInstruction, projectWorkspace.editPlanInstruction); +assert.deepEqual(restoredState.editPlanResult, projectWorkspace.editPlanResult); +assert.deepEqual(restoredState.editPlanDecisions, projectWorkspace.editPlanDecisions); +assert.deepEqual(restoredState.clipSuggestions, projectWorkspace.clipSuggestions); +assert.deepEqual(restoredState.clipDrafts, projectWorkspace.clipDrafts); +assert.deepEqual(restoredState.clipReviewDecisions, projectWorkspace.clipReviewDecisions); +assert.equal(restoredState.customFillerWords, projectWorkspace.customFillerWords); +assert.equal(restoredState.providers, providerState.providers); +assert.equal(restoredState.defaultProvider, providerState.defaultProvider); +assert.equal(restoredState.isProcessing, false); +assert.equal(restoredState.processingMessage, ''); assert.equal(getProjectWorkflow({ clipSuggestions: [suggestion] }), 'short'); assert.equal(getProjectWorkflow({ clipDrafts: [{ ...suggestion, status: 'draft' }] }), 'short'); diff --git a/frontend/scripts/smoke-errors-accessibility.mjs b/frontend/scripts/smoke-errors-accessibility.mjs index 9a146ce..6b8dfd8 100644 --- a/frontend/scripts/smoke-errors-accessibility.mjs +++ b/frontend/scripts/smoke-errors-accessibility.mjs @@ -36,7 +36,7 @@ assert.match(openFileBody, /if\s*\(path\)\s*\{[\s\S]*setEditorWorkflow\(intent\) assert.doesNotMatch(openFileBody.split("if (IS_ELECTRON)")[0], /setEditorWorkflow\(/); assert.match(browserChangeBody, /if\s*\(!file\)\s*return;[\s\S]*await uploadBrowserFile\(file, browserWorkflowIntent\)/); assert.doesNotMatch(browserChangeBody.split('if (!file) return;')[0], /setEditorWorkflow\(/); -assert.match(app, /const data = \(await res\.json\(\)\)[\s\S]*resetClipWorkspaceForNewMedia\(\)/); +assert.match(app, /const data = \(await res\.json\(\)\)[\s\S]*resetMediaAIWorkspaceForNewMedia\(\)/); assert.match(app, /Autosaved work found/); assert.match(app, /Restore autosave/); assert.match(app, /Start new transcription/); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 713f5e4..bb7d32e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -224,8 +224,8 @@ export default function App() { setActivePanel(workflow === 'short' ? 'ai' : null); }; - const resetClipWorkspaceForNewMedia = useCallback(() => { - useAIStore.getState().resetClipWorkspace(); + const resetMediaAIWorkspaceForNewMedia = useCallback(() => { + useAIStore.getState().resetMediaAIWorkspace(); const editorState = useEditorStore.getState(); editorState.clearClipPresentationPreview(); editorState.setSelectedWordIndices([]); @@ -358,7 +358,7 @@ export default function App() { if (IS_ELECTRON) { const path = await window.electronAPI!.openFile(); if (path) { - resetClipWorkspaceForNewMedia(); + resetMediaAIWorkspaceForNewMedia(); setEditorWorkflow(intent); applyWorkflowIntent(intent); const restored = await tryRestoreAutosave(path); @@ -413,7 +413,7 @@ export default function App() { } const data = (await res.json()) as { path: string; filename: string; size: number }; - resetClipWorkspaceForNewMedia(); + resetMediaAIWorkspaceForNewMedia(); setEditorWorkflow(intent); applyWorkflowIntent(intent); loadVideo(data.path); diff --git a/frontend/src/components/AIPanel.tsx b/frontend/src/components/AIPanel.tsx index 5df71c9..8ffb617 100644 --- a/frontend/src/components/AIPanel.tsx +++ b/frontend/src/components/AIPanel.tsx @@ -519,16 +519,18 @@ export default function AIPanel({ mode = 'general' }: { mode?: AIPanelMode }) { 'Edit planning', { label: 'Edit planning' }, ); + if (!isCurrentClipWorkspace()) return; setEditPlanResult(data); } catch (err) { console.error(err); setCreatorNotice({ ...getCreatorErrorPresentation('ai-action', err), onDismiss: () => setCreatorNotice(null) }); } finally { - setProcessing(false); + if (isCurrentClipWorkspace()) setProcessing(false); } }, [ defaultProvider, editPlanInstruction, + isCurrentClipWorkspace, providers, setEditPlanResult, setProcessing, @@ -565,6 +567,7 @@ export default function AIPanel({ mode = 'general' }: { mode?: AIPanelMode }) { 'AI Director', { label: 'AI Director' }, ); + if (!isCurrentClipWorkspace()) return; setEditPlanResult(data); if (data.directorClip) { if (!isCurrentClipWorkspace()) return; @@ -638,12 +641,13 @@ export default function AIPanel({ mode = 'general' }: { mode?: AIPanelMode }) { const cancelAIJob = useCallback(async () => { if (!activeAIJob || !['queued', 'running'].includes(activeAIJob.status)) return; const res = await fetch(`${backendUrl}/jobs/${activeAIJob.id}/cancel`, { method: 'POST' }); + if (!isCurrentClipWorkspace()) return; if (res.ok) { const job = (await res.json()) as AIJob; setActiveAIJob({ ...job, label: activeAIJob.label, draftId: activeAIJob.draftId }); } setProcessing(false); - }, [activeAIJob, backendUrl, setProcessing]); + }, [activeAIJob, backendUrl, isCurrentClipWorkspace, setProcessing]); const detectFillers = useCallback(async () => { if (words.length === 0) return; @@ -665,14 +669,15 @@ export default function AIPanel({ mode = 'general' }: { mode?: AIPanelMode }) { 'Filler detection', { label: 'Filler detection' }, ); + if (!isCurrentClipWorkspace()) return; setFillerResult(data); } catch (err) { console.error(err); setCreatorNotice({ ...getCreatorErrorPresentation('ai-action', err), onDismiss: () => setCreatorNotice(null) }); } finally { - setProcessing(false); + if (isCurrentClipWorkspace()) setProcessing(false); } - }, [words, defaultProvider, providers, customFillerWords, setProcessing, setFillerResult, startAIJob]); + }, [words, defaultProvider, providers, customFillerWords, isCurrentClipWorkspace, setProcessing, setFillerResult, startAIJob]); const createClips = useCallback(async () => { if (words.length === 0) return; @@ -1423,6 +1428,7 @@ export default function AIPanel({ mode = 'general' }: { mode?: AIPanelMode }) { const { job_id: jobId } = await retryRes.json(); const context = { label: activeAIJob.label, draftId: activeAIJob.draftId }; const result = await pollAIJob(jobId, activeAIJob.label, context); + if (!isCurrentClipWorkspace()) return; if (activeAIJob.kind === 'ai:filler-removal') { setFillerResult(result as FillerWordResult); @@ -1441,13 +1447,14 @@ export default function AIPanel({ mode = 'general' }: { mode?: AIPanelMode }) { console.error(err); setCreatorNotice({ ...getCreatorErrorPresentation('ai-action', err), onDismiss: () => setCreatorNotice(null) }); } finally { - setProcessing(false); + if (isCurrentClipWorkspace()) setProcessing(false); } }, [ activeAIJob, applyClipDiscoveryResult, backendUrl, clipDrafts, + isCurrentClipWorkspace, pollAIJob, setEditPlanResult, setFillerResult, diff --git a/frontend/src/store/aiStore.ts b/frontend/src/store/aiStore.ts index d9b4361..3ab84a7 100644 --- a/frontend/src/store/aiStore.ts +++ b/frontend/src/store/aiStore.ts @@ -1,7 +1,11 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import type { AIProvider, AIProviderConfig, FillerWordResult, ClipSuggestion, ClipDraft, ProjectAIWorkspace, FillerReviewDecision, EditPlanResult, EditPlanReviewDecision, ClipReviewDecision } from '../types/project'; -import { normalizeClipReviewDecisions, resetClipWorkspaceState } from '../utils/clipWorkspace'; +import { + normalizeClipReviewDecisions, + resetMediaAIWorkspaceState, + restoreMediaAIWorkspaceState, +} from '../utils/clipWorkspace'; const ENCRYPTED_KEY_PREFIX = 'scriptcut_enc_'; const LEGACY_ENCRYPTED_KEY_PREFIX = 'aive_enc_'; @@ -65,7 +69,7 @@ interface AIActions { | Record | ((current: Record) => Record), ) => void; - resetClipWorkspace: () => void; + resetMediaAIWorkspace: () => void; setProcessing: (active: boolean, message?: string) => void; loadProjectAIState: (workspace?: ProjectAIWorkspace) => void; hydrateKeys: () => Promise; @@ -173,9 +177,9 @@ export const useAIStore = create()( return { clipReviewDecisions: normalizeClipReviewDecisions(next) }; }), - resetClipWorkspace: () => + resetMediaAIWorkspace: () => set((state) => ({ - ...resetClipWorkspaceState(state), + ...resetMediaAIWorkspaceState(state), clipWorkspaceEpoch: state.clipWorkspaceEpoch + 1, })), @@ -183,20 +187,10 @@ export const useAIStore = create()( set({ isProcessing: active, processingMessage: message ?? '' }), loadProjectAIState: (workspace) => - set({ - customFillerWords: workspace?.customFillerWords ?? get().customFillerWords, - fillerResult: workspace?.fillerResult ?? null, - fillerDecisions: workspace?.fillerDecisions ?? {}, - editPlanInstruction: workspace?.editPlanInstruction ?? '', - editPlanResult: workspace?.editPlanResult ?? null, - editPlanDecisions: workspace?.editPlanDecisions ?? {}, - clipSuggestions: workspace?.clipSuggestions ?? [], - clipDrafts: workspace?.clipDrafts ?? [], - clipReviewDecisions: normalizeClipReviewDecisions(workspace?.clipReviewDecisions), + set((state) => ({ + ...restoreMediaAIWorkspaceState(state, workspace), clipWorkspaceEpoch: get().clipWorkspaceEpoch + 1, - isProcessing: false, - processingMessage: '', - }), + })), hydrateKeys: async () => { const [openaiKey, claudeKey, routerKey] = await Promise.all([ diff --git a/frontend/src/utils/clipWorkspace.ts b/frontend/src/utils/clipWorkspace.ts index 3da12a8..280a56e 100644 --- a/frontend/src/utils/clipWorkspace.ts +++ b/frontend/src/utils/clipWorkspace.ts @@ -3,19 +3,36 @@ import type { ClipDraftStatus, ClipReviewDecision, ClipSuggestion, + EditPlanResult, + EditPlanReviewDecision, + FillerReviewDecision, + FillerWordResult, + ProjectAIWorkspace, } from '../types/project'; export type ClipWorkspaceStage = 'find' | 'review' | 'prepare' | 'export'; -export function resetClipWorkspaceState; + editPlanInstruction: string; + editPlanResult: EditPlanResult | null; + editPlanDecisions: Record; clipSuggestions: ClipSuggestion[]; clipDrafts: ClipDraft[]; clipReviewDecisions: Record; isProcessing: boolean; processingMessage: string; -}>(state: T): T { +}; + +export function resetMediaAIWorkspaceState(state: T): T { return { ...state, + fillerResult: null, + fillerDecisions: {}, + editPlanInstruction: '', + editPlanResult: null, + editPlanDecisions: {}, clipSuggestions: [], clipDrafts: [], clipReviewDecisions: {}, @@ -24,6 +41,26 @@ export function resetClipWorkspaceState( + state: T, + workspace?: ProjectAIWorkspace, +): T { + return { + ...state, + customFillerWords: workspace?.customFillerWords ?? state.customFillerWords, + fillerResult: workspace?.fillerResult ?? null, + fillerDecisions: workspace?.fillerDecisions ?? {}, + editPlanInstruction: workspace?.editPlanInstruction ?? '', + editPlanResult: workspace?.editPlanResult ?? null, + editPlanDecisions: workspace?.editPlanDecisions ?? {}, + clipSuggestions: workspace?.clipSuggestions ?? [], + clipDrafts: workspace?.clipDrafts ?? [], + clipReviewDecisions: normalizeClipReviewDecisions(workspace?.clipReviewDecisions), + isProcessing: false, + processingMessage: '', + }; +} + export function readClipDiscoveryResult(result: unknown): { clips: ClipSuggestion[]; requestedCount: number;