diff --git a/web/frontend/src/features/transcription/components/AudioDetailView.tsx b/web/frontend/src/features/transcription/components/AudioDetailView.tsx index 26af51cb..4c79cab2 100644 --- a/web/frontend/src/features/transcription/components/AudioDetailView.tsx +++ b/web/frontend/src/features/transcription/components/AudioDetailView.tsx @@ -1,6 +1,6 @@ import { useRef, useState, useEffect, useCallback } from "react"; import { useParams, useNavigate } from "react-router-dom"; -import { MoreVertical, Edit2, Activity, FileText, Bot, Check, Loader2 } from "lucide-react"; +import { MoreVertical, Edit2, Activity, FileText, Bot, Check, Loader2, List, AlignLeft, ArrowDownCircle, StickyNote, Users, MessageCircle, FileImage, FileJson } from "lucide-react"; import { Header } from "@/components/Header"; import { Button } from "@/components/ui/button"; @@ -12,6 +12,8 @@ import { cn } from "@/lib/utils"; // Custom Hooks import { useAudioDetail, useUpdateTitle, useTranscript } from "@/features/transcription/hooks/useAudioDetail"; +import { useSpeakerMappings } from "@/features/transcription/hooks/useTranscriptionSpeakers"; +import { useTranscriptDownload } from "@/features/transcription/hooks/useTranscriptDownload"; // Sub-components import { TranscriptSection } from "./audio-detail/TranscriptSection"; @@ -38,6 +40,14 @@ export const AudioDetailView = function AudioDetailView({ audioId: propAudioId } const [isEditingTitle, setIsEditingTitle] = useState(false); const [newTitle, setNewTitle] = useState(""); + // Lifted Transcript State + const [transcriptMode, setTranscriptMode] = useState<"compact" | "expanded">("compact"); + const [autoScrollEnabled, setAutoScrollEnabled] = useState(true); + const [notesOpen, setNotesOpen] = useState(false); + const [speakerRenameOpen, setSpeakerRenameOpen] = useState(false); + const [downloadDialogOpen, setDownloadDialogOpen] = useState(false); + const [downloadFormat, setDownloadFormat] = useState<'txt' | 'json'>('txt'); + // Dialog States const [executionDialogOpen, setExecutionDialogOpen] = useState(false); const [logsDialogOpen, setLogsDialogOpen] = useState(false); @@ -46,6 +56,25 @@ export const AudioDetailView = function AudioDetailView({ audioId: propAudioId } // Data Fetching const { data: audioFile, isLoading, error } = useAudioDetail(audioId || ""); const { mutate: updateTitle } = useUpdateTitle(audioId || ""); + // Fetch transcript & speakers here to support menu actions + const { data: transcript } = useTranscript(audioId || "", true); + const { data: speakerMappings = {} } = useSpeakerMappings(audioId || "", true); + + // Download Logic + const { downloadSRT } = useTranscriptDownload(); + + // Helpers + const getDetectedSpeakers = useCallback(() => { + if (!transcript?.segments) return []; + const speakers = new Set(); + transcript.segments.forEach((segment: any) => { + if (segment.speaker) speakers.add(segment.speaker); + }); + return Array.from(speakers).sort(); + }, [transcript]); + + const hasSpeakers = audioFile?.diarization || audioFile?.parameters?.diarize || audioFile?.is_multi_track || false; + const detectedSpeakers = getDetectedSpeakers(); // Effects useEffect(() => { @@ -111,16 +140,17 @@ export const AudioDetailView = function AudioDetailView({ audioId: propAudioId } {/* 1. Header Redesign: - Using shared Header component for consistency + - Padding matches Dashboard (px-4 sm:px-8) */} -
+
{ }} // No file upload in detail view />
{/* Main Content */} -
-
+
+
{/* 2. Metadata Section (New): @@ -189,18 +219,64 @@ export const AudioDetailView = function AudioDetailView({ audioId: propAudioId } - Recording Options + View Options - setExecutionDialogOpen(true)} className="rounded-[8px] cursor-pointer text-[var(--text-secondary)] focus:text-[var(--text-primary)] focus:bg-[var(--bg-main)]/80"> - Execution Info + setTranscriptMode(transcriptMode === 'compact' ? 'expanded' : 'compact')} className="rounded-[8px] cursor-pointer"> + {transcriptMode === 'compact' ? : } + {transcriptMode === 'compact' ? 'Timeline View' : 'Compact View'} - setLogsDialogOpen(true)} className="rounded-[8px] cursor-pointer text-[var(--text-secondary)] focus:text-[var(--text-primary)] focus:bg-[var(--bg-main)]/80"> - View Logs + setAutoScrollEnabled(!autoScrollEnabled)} className="rounded-[8px] cursor-pointer"> + + Auto Scroll {autoScrollEnabled ? 'On' : 'Off'} + setNotesOpen(!notesOpen)} className="rounded-[8px] cursor-pointer"> + + Notes + + + + + Actions + + {hasSpeakers && detectedSpeakers.length > 0 && ( + setSpeakerRenameOpen(true)} className="rounded-[8px] cursor-pointer"> + Rename Speakers + + )} + navigate(`/audio/${audioId}/chat`)} className="rounded-[8px] cursor-pointer"> + Chat with Audio + setSummaryDialogOpen(true)} className="rounded-[8px] cursor-pointer text-[var(--brand-solid)] focus:text-[var(--brand-solid)] focus:bg-[var(--brand-light)]"> AI Summary + + + + + Downloads + + transcript && downloadSRT(transcript, audioFile?.title || 'transcript', speakerMappings)} className="rounded-[8px] cursor-pointer"> + Download SRT + + { setDownloadFormat('txt'); setDownloadDialogOpen(true); }} className="rounded-[8px] cursor-pointer"> + Download Text + + { setDownloadFormat('json'); setDownloadDialogOpen(true); }} className="rounded-[8px] cursor-pointer"> + Download JSON + + + + + + System + + setExecutionDialogOpen(true)} className="rounded-[8px] cursor-pointer"> + Execution Info + + setLogsDialogOpen(true)} className="rounded-[8px] cursor-pointer"> + View Logs +
@@ -236,10 +312,19 @@ export const AudioDetailView = function AudioDetailView({ audioId: propAudioId } audioId={audioId} currentTime={currentTime} onSeek={handleSeek} - onOpenExecutionInfo={() => setExecutionDialogOpen(true)} - onOpenLogs={() => setLogsDialogOpen(true)} - onOpenSummarize={() => setSummaryDialogOpen(true)} - llmReady={true} + + // Pass down lifted state & setters + transcript={transcript} + speakerMappings={speakerMappings} + transcriptMode={transcriptMode} + autoScrollEnabled={autoScrollEnabled} + notesOpen={notesOpen} + setNotesOpen={setNotesOpen} + speakerRenameOpen={speakerRenameOpen} + setSpeakerRenameOpen={setSpeakerRenameOpen} + downloadDialogOpen={downloadDialogOpen} + setDownloadDialogOpen={setDownloadDialogOpen} + downloadFormat={downloadFormat} />
@@ -266,9 +351,9 @@ export const AudioDetailView = function AudioDetailView({ audioId: propAudioId } }; // Wrapper to handle transcript word index calculation without polluting main view -function TranscriptSectionWrapper({ audioId, currentTime, ...props }: any) { - const { data: transcript } = useTranscript(audioId, true); - +// Wrapper to handle word index calc +function TranscriptSectionWrapper({ audioId, currentTime, transcript, ...props }: any) { + // If transcript not passed (loading?), handle it let currentWordIndex = null; if (transcript?.word_segments) { // Simple linear find for now. @@ -281,6 +366,7 @@ function TranscriptSectionWrapper({ audioId, currentTime, ...props }: any) { audioId={audioId} currentTime={currentTime} currentWordIndex={currentWordIndex} + transcript={transcript} {...props} /> ); diff --git a/web/frontend/src/features/transcription/components/audio-detail/TranscriptSection.tsx b/web/frontend/src/features/transcription/components/audio-detail/TranscriptSection.tsx index 466ed1a4..f1e4dc9d 100644 --- a/web/frontend/src/features/transcription/components/audio-detail/TranscriptSection.tsx +++ b/web/frontend/src/features/transcription/components/audio-detail/TranscriptSection.tsx @@ -1,13 +1,8 @@ -import { useState, useRef, useEffect } from "react"; +import { useRef, useEffect } from "react"; import { createPortal } from "react-dom"; import { TranscriptView } from "@/components/transcript/TranscriptView"; -import { TranscriptToolbar } from "@/components/transcript/TranscriptToolbar"; import { useNotes, useCreateNote, useUpdateNote, useDeleteNote } from "@/features/transcription/hooks/useTranscriptionNotes"; -import { useSpeakerMappings } from "@/features/transcription/hooks/useTranscriptionSpeakers"; -import { useTranscript, useAudioDetail } from "@/features/transcription/hooks/useAudioDetail"; import { useTranscriptSelection } from "@/features/transcription/hooks/useTranscriptSelection"; -import { useTranscriptDownload } from "@/features/transcription/hooks/useTranscriptDownload"; -import { useNavigate } from "react-router-dom"; import { DownloadDialog } from "./DownloadDialog"; import SpeakerRenameDialog from "./SpeakerRenameDialog"; import { NotesSidebar } from "./NotesSidebar"; @@ -15,50 +10,50 @@ import { TranscriptSelectionMenu } from "./TranscriptSelectionMenu"; import { NoteEditorDialog } from "./NoteEditorDialog"; import { useIsMobile } from "@/hooks/use-mobile"; import { X, StickyNote } from "lucide-react"; +import type { Transcript } from "@/features/transcription/hooks/useAudioDetail"; interface TranscriptSectionProps { audioId: string; currentWordIndex: number | null; onSeek: (time: number) => void; - onOpenExecutionInfo: () => void; - onOpenLogs: () => void; - onOpenSummarize: () => void; - llmReady: boolean | null; + // Lifted State Props + transcript: Transcript | undefined; + speakerMappings: Record; + transcriptMode: 'compact' | 'expanded'; + autoScrollEnabled: boolean; + notesOpen: boolean; + setNotesOpen: (open: boolean) => void; + speakerRenameOpen: boolean; + setSpeakerRenameOpen: (open: boolean) => void; + downloadDialogOpen: boolean; + setDownloadDialogOpen: (open: boolean) => void; + downloadFormat: 'txt' | 'json'; } export function TranscriptSection({ audioId, currentWordIndex, onSeek, - onOpenExecutionInfo, - onOpenLogs, - onOpenSummarize, - llmReady + transcript, + speakerMappings, + transcriptMode, + autoScrollEnabled, + notesOpen, + setNotesOpen, + speakerRenameOpen, + setSpeakerRenameOpen, + downloadDialogOpen, + setDownloadDialogOpen, + downloadFormat }: TranscriptSectionProps) { - const navigate = useNavigate(); const isMobile = useIsMobile(); // Data hooks - const { data: transcript } = useTranscript(audioId, true); - const { data: audioFile } = useAudioDetail(audioId); + // const { data: audioFile } = useAudioDetail(audioId); // Unused const { data: notes = [] } = useNotes(audioId); const { mutate: createNote } = useCreateNote(audioId); const { mutateAsync: updateNote } = useUpdateNote(audioId); const { mutateAsync: deleteNote } = useDeleteNote(audioId); - const { data: speakerMappings = {} } = useSpeakerMappings(audioId, true); - - // Download Logic - const { downloadSRT } = useTranscriptDownload(); - - // Local state - const [transcriptMode, setTranscriptMode] = useState<"compact" | "expanded">("compact"); - const [autoScrollEnabled, setAutoScrollEnabled] = useState(true); - const [notesOpen, setNotesOpen] = useState(false); - const [speakerRenameOpen, setSpeakerRenameOpen] = useState(false); - - // Download Dialog State - const [downloadDialogOpen, setDownloadDialogOpen] = useState(false); - const [downloadFormat, setDownloadFormat] = useState<'txt' | 'json'>('txt'); // Refs const transcriptRef = useRef(null); @@ -117,18 +112,16 @@ export function TranscriptSection({ // Helpers const getDetectedSpeakers = () => { + // Safe check for segments array if (!transcript?.segments) return []; const speakers = new Set(); - transcript.segments.forEach(segment => { + // Using any for segment here if strict typing is an issue, or typed via Transcript + transcript.segments.forEach((segment: any) => { if (segment.speaker) speakers.add(segment.speaker); }); return Array.from(speakers).sort(); }; - const hasSpeakers = () => { - return audioFile?.diarization || audioFile?.parameters?.diarize || audioFile?.is_multi_track || false; - }; - const handleSaveNote = (content: string) => { if (pendingSelection) { createNote({ @@ -154,140 +147,120 @@ export function TranscriptSection({ if (!transcript) return null; return ( -
- {/* Sticky Toolbar */} -
-
- setSpeakerRenameOpen(true)} - onOpenSummarize={onOpenSummarize} - llmReady={llmReady} - onDownloadSRT={() => downloadSRT(transcript, audioFile?.title || 'transcript', speakerMappings)} - onDownloadTXT={() => { setDownloadFormat('txt'); setDownloadDialogOpen(true); }} - onDownloadJSON={() => { setDownloadFormat('json'); setDownloadDialogOpen(true); }} - onOpenChat={() => navigate(`/audio/${audioId}/chat`)} - /> -
-
+
+
+ {/* + TOOLBAR REMOVED -> Moved to Context Menu + */} - {/* Transcript Content - Systematic Typography */} -
-
-
- + {/* Transcript Content - Systematic Typography */} +
+
+
+ +
-
- {/* Download Dialog */} - + {/* Download Dialog */} + - {/* Speaker Rename Dialog */} - { }} - /> + {/* Speaker Rename Dialog */} + { }} + /> - {/* Portals */} - {createPortal( - <> - {/* Selection Menu Bubble (Glass) */} -
- + {/* Selection Menu Bubble (Glass) */} +
+ +
+ + {/* Note Editor Dialog */} + -
- {/* Note Editor Dialog */} - + {/* Backdrop for menu/editor */} + {(showSelectionMenu || showEditor) && ( +
{ + if (showSelectionMenu && !showEditor) { + closeEditor(); + } + }} + /> + )} - {/* Backdrop for menu/editor */} - {(showSelectionMenu || showEditor) && ( -
{ - if (showSelectionMenu && !showEditor) { - closeEditor(); - } - }} - /> - )} - - {/* Notes Sidebar - Premium Drawer */} - {notesOpen && ( -
-
-
-

- - Notes - - {notes.length} - -

- -
-
- updateNote({ id, content })} - onDelete={(id) => deleteNote(id)} - onJumpTo={(t) => { - onSeek(t); - if (isMobile) setNotesOpen(false); - }} - /> + {/* Notes Sidebar - Premium Drawer */} + {notesOpen && ( +
+
+
+

+ + Notes + + {notes.length} + +

+ +
+
+ updateNote({ id, content })} + onDelete={(id) => deleteNote(id)} + onJumpTo={(t) => { + onSeek(t); + if (isMobile) setNotesOpen(false); + }} + /> +
-
- )} - , - document.body - )} + )} + , + document.body + )} +
); }