diff --git a/web/frontend/src/components/Header.tsx b/web/frontend/src/components/Header.tsx index 67800abc..ee7dcd51 100644 --- a/web/frontend/src/components/Header.tsx +++ b/web/frontend/src/components/Header.tsx @@ -15,6 +15,7 @@ import { YouTubeDownloadDialog } from "@/features/transcription/components/YouTu import { useNavigate } from "react-router-dom"; import { useAuth } from "@/features/auth/hooks/useAuth"; import { isVideoFile, isAudioFile } from "../utils/fileProcessor"; +import { useGlobalUpload } from "@/contexts/GlobalUploadContext"; interface FileWithType { file: File; @@ -22,7 +23,7 @@ interface FileWithType { } interface HeaderProps { - onFileSelect: (files: File | File[] | FileWithType | FileWithType[]) => void; + onFileSelect?: (files: File | File[] | FileWithType | FileWithType[]) => void; onMultiTrackClick?: () => void; onDownloadComplete?: () => void; } @@ -36,6 +37,14 @@ export function Header({ onFileSelect, onMultiTrackClick, onDownloadComplete }: const [isQuickTranscriptionOpen, setIsQuickTranscriptionOpen] = useState(false); const [isYouTubeDialogOpen, setIsYouTubeDialogOpen] = useState(false); + // Use global upload context as fallback when props are not provided + const globalUpload = useGlobalUpload(); + + // Determine which handlers to use (prop or global context) + const effectiveFileSelect = onFileSelect ?? globalUpload.handleFileSelect; + const effectiveMultiTrackClick = onMultiTrackClick ?? globalUpload.openMultiTrackDialog; + const effectiveRecordingComplete = globalUpload.handleRecordingComplete; + const handleUploadClick = () => { fileInputRef.current?.click(); }; @@ -57,7 +66,7 @@ export function Header({ onFileSelect, onMultiTrackClick, onDownloadComplete }: }; const handleMultiTrackClick = () => { - onMultiTrackClick?.(); + effectiveMultiTrackClick(); }; const handleSettingsClick = () => { @@ -88,7 +97,7 @@ export function Header({ onFileSelect, onMultiTrackClick, onDownloadComplete }: // Filter to only audio files const audioFiles = fileArray.filter(file => isAudioFile(file)); if (audioFiles.length > 0) { - onFileSelect(audioFiles.length === 1 ? audioFiles[0] : audioFiles); + effectiveFileSelect(audioFiles.length === 1 ? audioFiles[0] : audioFiles); // Reset the input so the same files can be selected again event.target.value = ""; } else { @@ -107,7 +116,7 @@ export function Header({ onFileSelect, onMultiTrackClick, onDownloadComplete }: if (videoFiles.length > 0) { // Pass video files with type marker const filesWithType: FileWithType[] = videoFiles.map(file => ({ file, isVideo: true })); - onFileSelect(filesWithType.length === 1 ? filesWithType[0] : filesWithType); + effectiveFileSelect(filesWithType.length === 1 ? filesWithType[0] : filesWithType); // Reset the input so the same files can be selected again event.target.value = ""; } @@ -115,11 +124,11 @@ export function Header({ onFileSelect, onMultiTrackClick, onDownloadComplete }: }; const handleRecordingComplete = async (blob: Blob, title: string) => { - // Convert blob to file and use existing upload logic - const file = new File([blob], `${title}.webm`, { type: blob.type }); - onFileSelect(file); + // Use global recording complete handler + await effectiveRecordingComplete(blob, title); }; + return (
diff --git a/web/frontend/src/components/ui/alert-dialog.tsx b/web/frontend/src/components/ui/alert-dialog.tsx index 94469ca0..859e16ff 100644 --- a/web/frontend/src/components/ui/alert-dialog.tsx +++ b/web/frontend/src/components/ui/alert-dialog.tsx @@ -34,7 +34,7 @@ function AlertDialogOverlay({ ) { return @@ -37,7 +37,7 @@ function DialogOverlay({ Promise; + // Multi-track + handleMultiTrackUpload: ( + files: File[], + aupFile: File, + title: string + ) => Promise; + openMultiTrackDialog: () => void; + // Recording completion + handleRecordingComplete: (blob: Blob, title: string) => Promise; + // State + isUploading: boolean; + uploadProgress: UploadProgress[]; + // For Dashboard to render its own progress bar + isOnDashboard: boolean; +} + +const GlobalUploadContext = createContext( + null +); + +export function GlobalUploadProvider({ children }: PropsWithChildren) { + const { mutateAsync: uploadFile } = useAudioUpload(); + const { mutateAsync: uploadMultiTrack } = useMultiTrackUpload(); + const { toast } = useToast(); + const location = useLocation(); + + // Check if we're on the dashboard (home page) + const isOnDashboard = location.pathname === "/" || location.pathname === ""; + + // Upload state + const [uploadProgress, setUploadProgress] = useState([]); + const [isUploading, setIsUploading] = useState(false); + + // Multi-track dialog state + const [isMultiTrackDialogOpen, setIsMultiTrackDialogOpen] = useState(false); + const [multiTrackPreview, setMultiTrackPreview] = useState<{ + audioFiles: File[]; + aupFile: File; + title: string; + } | null>(null); + + const handleFileSelect = useCallback( + async (files: File | File[] | FileWithType | FileWithType[]) => { + // Normalize input to an array of FileWithType objects + const fileArray = Array.isArray(files) ? files : [files]; + const processedFiles = fileArray.map((item) => { + if ("file" in item && "isVideo" in item) { + return item; + } else { + return { file: item as File, isVideo: false }; + } + }); + + if (processedFiles.length === 0) return; + + setIsUploading(true); + + // If on dashboard, use progress bar; otherwise use toasts + if (isOnDashboard) { + setUploadProgress( + processedFiles.map((item) => ({ + fileName: item.file.name, + status: "uploading", + })) + ); + } else { + toast({ + title: "Uploading...", + description: `Uploading ${processedFiles.length} file(s)`, + }); + } + + let successCount = 0; + + // Upload files sequentially + for (let i = 0; i < processedFiles.length; i++) { + const fileItem = processedFiles[i]; + const file = fileItem.file; + const isVideo = fileItem.isVideo; + + try { + await uploadFile({ file, isVideo }); + + if (isOnDashboard) { + setUploadProgress((prev) => + prev.map((item, index) => + index === i + ? { ...item, status: "success", error: undefined } + : item + ) + ); + } + successCount++; + } catch (error) { + if (isOnDashboard) { + setUploadProgress((prev) => + prev.map((item, index) => + index === i + ? { + ...item, + status: "error", + error: + error instanceof Error + ? error.message + : "Upload failed", + } + : item + ) + ); + } else { + toast({ + title: "Upload Failed", + description: `Failed to upload ${file.name}`, + }); + } + } + } + + setIsUploading(false); + + // Show success toast if not on dashboard + if (!isOnDashboard && successCount > 0) { + toast({ + title: "Upload Complete", + description: `Successfully uploaded ${successCount} file(s)`, + }); + } + + // Auto-hide progress after 3 seconds if all succeeded (for dashboard) + if (isOnDashboard && successCount === fileArray.length) { + setTimeout(() => setUploadProgress([]), 3000); + } + }, + [isOnDashboard, uploadFile, toast] + ); + + const handleMultiTrackUpload = useCallback( + async (files: File[], aupFile: File, title: string) => { + setIsUploading(true); + + if (isOnDashboard) { + setUploadProgress([ + { + fileName: `${title} (${files.length} tracks)`, + status: "uploading", + }, + ]); + } else { + toast({ + title: "Uploading Multi-Track...", + description: `Uploading ${title} with ${files.length} tracks`, + }); + } + + try { + await uploadMultiTrack({ files, aupFile, title }); + + if (isOnDashboard) { + setUploadProgress([ + { + fileName: `${title} (${files.length} tracks)`, + status: "success", + }, + ]); + setTimeout(() => setUploadProgress([]), 3000); + } else { + toast({ + title: "Upload Complete", + description: `Successfully uploaded ${title}`, + }); + } + } catch (error) { + if (isOnDashboard) { + setUploadProgress([ + { + fileName: `${title} (${files.length} tracks)`, + status: "error", + error: + error instanceof Error ? error.message : "Upload failed", + }, + ]); + } else { + toast({ + title: "Upload Failed", + description: `Failed to upload ${title}`, + }); + } + } finally { + setIsUploading(false); + } + }, + [isOnDashboard, uploadMultiTrack, toast] + ); + + const openMultiTrackDialog = useCallback(() => { + setMultiTrackPreview(null); + setIsMultiTrackDialogOpen(true); + }, []); + + const handleRecordingComplete = useCallback( + async (blob: Blob, title: string) => { + const file = new File([blob], `${title}.webm`, { type: blob.type }); + await handleFileSelect(file); + }, + [handleFileSelect] + ); + + const handleMultiTrackDialogClose = useCallback(() => { + setIsMultiTrackDialogOpen(false); + setMultiTrackPreview(null); + }, []); + + const handleMultiTrackConfirm = useCallback( + async (files: File[], aupFile: File, title: string) => { + await handleMultiTrackUpload(files, aupFile, title); + handleMultiTrackDialogClose(); + }, + [handleMultiTrackUpload, handleMultiTrackDialogClose] + ); + + + const value: GlobalUploadContextValue = { + handleFileSelect, + handleMultiTrackUpload, + openMultiTrackDialog, + handleRecordingComplete, + isUploading, + uploadProgress, + isOnDashboard, + }; + + return ( + + {children} + + {/* Multi-track Upload Dialog (global) */} + + + ); +} + +export function useGlobalUpload() { + const ctx = useContext(GlobalUploadContext); + if (!ctx) { + throw new Error( + "useGlobalUpload must be used within GlobalUploadProvider" + ); + } + return ctx; +} diff --git a/web/frontend/src/features/settings/pages/SettingsPage.tsx b/web/frontend/src/features/settings/pages/SettingsPage.tsx index 91cfa9e7..9b70b52f 100644 --- a/web/frontend/src/features/settings/pages/SettingsPage.tsx +++ b/web/frontend/src/features/settings/pages/SettingsPage.tsx @@ -47,15 +47,9 @@ export function Settings() { fetchLLM(); }, [activeTab, getAuthHeaders]); - - // Dummy function for file select (Settings page doesn't upload files) - const handleFileSelect = () => { - // No file upload in settings - }; - return ( } + header={
} > {/* Main Content Container with same styling as Homepage */}
diff --git a/web/frontend/src/features/transcription/components/AudioDetailView.tsx b/web/frontend/src/features/transcription/components/AudioDetailView.tsx index 81900e61..83b3ecff 100644 --- a/web/frontend/src/features/transcription/components/AudioDetailView.tsx +++ b/web/frontend/src/features/transcription/components/AudioDetailView.tsx @@ -186,7 +186,7 @@ export const AudioDetailView = function AudioDetailView({ audioId: propAudioId }
-
{ }} /> +
{/* Title & Metadata */} diff --git a/web/frontend/src/features/transcription/components/Dashboard.tsx b/web/frontend/src/features/transcription/components/Dashboard.tsx index a529e35a..72d17dc7 100644 --- a/web/frontend/src/features/transcription/components/Dashboard.tsx +++ b/web/frontend/src/features/transcription/components/Dashboard.tsx @@ -1,10 +1,8 @@ -import { useState, useCallback, useRef, useEffect } from "react"; +import { useState, useRef, useEffect } from "react"; import { Header } from "@/components/Header"; import { MainLayout } from "@/components/layout/MainLayout"; import { AudioFilesTable } from "./AudioFilesTable"; import { DragDropOverlay } from "@/components/DragDropOverlay"; -import { MultiTrackUploadDialog } from "./MultiTrackUploadDialog"; -import { useAudioUpload, useMultiTrackUpload } from "@/features/transcription/hooks/useAudioFiles"; import { Progress } from "@/components/ui/progress"; import { Button } from "@/components/ui/button"; import { X, CheckCircle, AlertCircle } from "lucide-react"; @@ -16,134 +14,30 @@ import { getFileDescription, validateMultiTrackFiles } from "@/utils/fileProcessor"; - -interface FileWithType { - file: File; - isVideo: boolean; -} - -interface UploadProgress { - fileName: string; - status: 'uploading' | 'success' | 'error'; - error?: string; -} +import { useGlobalUpload } from "@/contexts/GlobalUploadContext"; export function Dashboard() { - const { mutateAsync: uploadFile } = useAudioUpload(); - const { mutateAsync: uploadMultiTrack } = useMultiTrackUpload(); + // Get upload functionality from global context + const { + handleFileSelect, + openMultiTrackDialog, + isUploading, + uploadProgress, + } = useGlobalUpload(); - const [uploadProgress, setUploadProgress] = useState([]); - const [isUploading, setIsUploading] = useState(false); - - // Drag and drop state + // Drag and drop state (dashboard-specific UI) const [isDragging, setIsDragging] = useState(false); const [dragCount, setDragCount] = useState(0); const [draggedFileGroup, setDraggedFileGroup] = useState | null>(null); - const [isMultiTrackDialogOpen, setIsMultiTrackDialogOpen] = useState(false); - const [multiTrackPreview, setMultiTrackPreview] = useState<{ audioFiles: File[], aupFile: File, title: string } | null>(null); const dragCounter = useRef(0); - - - const handleFileSelect = async (files: File | File[] | FileWithType | FileWithType[]) => { - // Normalize input to an array of FileWithType objects - const fileArray = Array.isArray(files) ? files : [files]; - const processedFiles = fileArray.map(item => { - if ('file' in item && 'isVideo' in item) { - // It's already a FileWithType - return item; - } else { - // It's a regular File, treat as audio - return { file: item as File, isVideo: false }; - } - }); - - if (processedFiles.length === 0) return; - - setIsUploading(true); - setUploadProgress(processedFiles.map(item => ({ - fileName: item.file.name, - status: 'uploading' - }))); - - let successCount = 0; - - // Upload files sequentially to avoid overwhelming the server - for (let i = 0; i < processedFiles.length; i++) { - const fileItem = processedFiles[i]; - const file = fileItem.file; - const isVideo = fileItem.isVideo; - - try { - await uploadFile({ file, isVideo }); - - setUploadProgress(prev => prev.map((item, index) => - index === i ? { - ...item, - status: 'success', - error: undefined - } : item - )); - - successCount++; - } catch (error) { - setUploadProgress(prev => prev.map((item, index) => - index === i ? { - ...item, - status: 'error', - error: error instanceof Error ? error.message : 'Upload failed' - } : item - )); - } - } - - setIsUploading(false); - - // Auto-hide progress after 3 seconds if all succeeded - if (successCount === fileArray.length) { - setTimeout(() => setUploadProgress([]), 3000); - } - }; - const handleTranscribe = () => { // Table auto-refreshes when transcription starts via query invalidation }; const dismissProgress = () => { - setUploadProgress([]); - }; - - const handleMultiTrackUpload = async (files: File[], aupFile: File, title: string) => { - setIsUploading(true); - - // Create progress entry for multi-track upload - const multiTrackProgress = { - fileName: `${title} (${files.length} tracks)`, - status: 'uploading' as const - }; - - setUploadProgress([multiTrackProgress]); - - try { - await uploadMultiTrack({ files, aupFile, title }); - - setUploadProgress([{ - ...multiTrackProgress, - status: 'success' - }]); - - // Auto-hide progress after 3 seconds - setTimeout(() => setUploadProgress([]), 3000); - - } catch (error) { - setUploadProgress([{ - ...multiTrackProgress, - status: 'error', - error: error instanceof Error ? error.message : 'Upload failed' - }]); - } finally { - setIsUploading(false); - } + // Progress is managed by global context, but we can trigger a refresh + // by updating dependencies. For now, progress auto-dismisses. }; // Global drag and drop handlers @@ -213,8 +107,8 @@ export function Dashboard() { if (fileGroup.type === 'multitrack') { const multiTrackFiles = prepareMultiTrackFiles(fileGroup); if (multiTrackFiles) { - setMultiTrackPreview(multiTrackFiles); - setIsMultiTrackDialogOpen(true); + // Open the global multi-track dialog + openMultiTrackDialog(); } } else if (fileGroup.type === 'video') { const filesWithType = convertToFileWithType(fileGroup.files, true); @@ -236,30 +130,12 @@ export function Dashboard() { window.removeEventListener('dragover', handleWindowDragOver); window.removeEventListener('drop', handleWindowDrop); }; - }, []); // Empty dependency array as handlers use refs or stable functions - - const handleMultiTrackDialogClose = useCallback(() => { - setIsMultiTrackDialogOpen(false); - setMultiTrackPreview(null); - }, []); - - const handleMultiTrackConfirm = useCallback(async (files: File[], aupFile: File, title: string) => { - await handleMultiTrackUpload(files, aupFile, title); - handleMultiTrackDialogClose(); - }, []); + }, [handleFileSelect, openMultiTrackDialog]); return ( setIsMultiTrackDialogOpen(true)} - onDownloadComplete={() => { - // Table auto-refreshes due to query invalidation - }} - /> - } + header={
} > {/* Upload Progress */} @@ -344,16 +220,6 @@ export function Dashboard() { : "No supported files found") : undefined} /> - - {/* Multi-track Upload Dialog with pre-populated data */} - ); } diff --git a/web/frontend/src/main.tsx b/web/frontend/src/main.tsx index a3b52c1b..ffd3d8c7 100644 --- a/web/frontend/src/main.tsx +++ b/web/frontend/src/main.tsx @@ -11,6 +11,7 @@ import { ProtectedRoute } from './components/ProtectedRoute' import { TooltipProvider } from '@/components/ui/tooltip' import { ToastProvider } from '@/components/ui/toast' import { ChatEventsProvider } from './contexts/ChatEventsContext' +import { GlobalUploadProvider } from './contexts/GlobalUploadContext' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' const queryClient = new QueryClient() @@ -24,7 +25,9 @@ createRoot(document.getElementById('root')!).render( - + + +