mirror of
https://github.com/rishikanthc/Scriberr.git
synced 2026-07-01 08:15:46 +00:00
fix: styling inconsistencies and move actins menu to context menu
This commit is contained in:
@@ -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<string>();
|
||||
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)
|
||||
*/}
|
||||
<div className="max-w-6xl mx-auto w-full px-6 py-6">
|
||||
<div className="max-w-6xl mx-auto w-full px-4 sm:px-8 py-6">
|
||||
<Header
|
||||
onFileSelect={() => { }} // No file upload in detail view
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 overflow-y-auto overflow-x-hidden p-6 pb-32">
|
||||
<div className="max-w-6xl mx-auto space-y-8">
|
||||
<main className="flex-1 overflow-y-auto overflow-x-hidden px-4 sm:px-8 pb-32 max-w-6xl mx-auto">
|
||||
<div className="mx-auto space-y-8">
|
||||
|
||||
{/*
|
||||
2. Metadata Section (New):
|
||||
@@ -189,18 +219,64 @@ export const AudioDetailView = function AudioDetailView({ audioId: propAudioId }
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56 glass-card rounded-[var(--radius-card)] shadow-[var(--shadow-float)] border-[var(--border-subtle)] p-1.5">
|
||||
<DropdownMenuLabel className="text-xs font-semibold text-[var(--text-tertiary)] uppercase tracking-wider px-2 py-1.5">
|
||||
Recording Options
|
||||
View Options
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => setExecutionDialogOpen(true)} className="rounded-[8px] cursor-pointer text-[var(--text-secondary)] focus:text-[var(--text-primary)] focus:bg-[var(--bg-main)]/80">
|
||||
<Activity className="mr-2 h-4 w-4 opacity-70" /> Execution Info
|
||||
<DropdownMenuItem onClick={() => setTranscriptMode(transcriptMode === 'compact' ? 'expanded' : 'compact')} className="rounded-[8px] cursor-pointer">
|
||||
{transcriptMode === 'compact' ? <List className="mr-2 h-4 w-4 opacity-70" /> : <AlignLeft className="mr-2 h-4 w-4 opacity-70" />}
|
||||
{transcriptMode === 'compact' ? 'Timeline View' : 'Compact View'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setLogsDialogOpen(true)} className="rounded-[8px] cursor-pointer text-[var(--text-secondary)] focus:text-[var(--text-primary)] focus:bg-[var(--bg-main)]/80">
|
||||
<FileText className="mr-2 h-4 w-4 opacity-70" /> View Logs
|
||||
<DropdownMenuItem onClick={() => setAutoScrollEnabled(!autoScrollEnabled)} className="rounded-[8px] cursor-pointer">
|
||||
<ArrowDownCircle className={cn("mr-2 h-4 w-4 opacity-70", autoScrollEnabled && "text-[var(--brand-solid)]")} />
|
||||
Auto Scroll {autoScrollEnabled ? 'On' : 'Off'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setNotesOpen(!notesOpen)} className="rounded-[8px] cursor-pointer">
|
||||
<StickyNote className={cn("mr-2 h-4 w-4 opacity-70", notesOpen && "text-[var(--brand-solid)]")} />
|
||||
Notes
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator className="bg-[var(--border-subtle)] my-1" />
|
||||
|
||||
<DropdownMenuLabel className="text-xs font-semibold text-[var(--text-tertiary)] uppercase tracking-wider px-2 py-1.5">
|
||||
Actions
|
||||
</DropdownMenuLabel>
|
||||
{hasSpeakers && detectedSpeakers.length > 0 && (
|
||||
<DropdownMenuItem onClick={() => setSpeakerRenameOpen(true)} className="rounded-[8px] cursor-pointer">
|
||||
<Users className="mr-2 h-4 w-4 opacity-70" /> Rename Speakers
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem onClick={() => navigate(`/audio/${audioId}/chat`)} className="rounded-[8px] cursor-pointer">
|
||||
<MessageCircle className="mr-2 h-4 w-4 opacity-70" /> Chat with Audio
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setSummaryDialogOpen(true)} className="rounded-[8px] cursor-pointer text-[var(--brand-solid)] focus:text-[var(--brand-solid)] focus:bg-[var(--brand-light)]">
|
||||
<Bot className="mr-2 h-4 w-4" /> AI Summary
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator className="bg-[var(--border-subtle)] my-1" />
|
||||
|
||||
<DropdownMenuLabel className="text-xs font-semibold text-[var(--text-tertiary)] uppercase tracking-wider px-2 py-1.5">
|
||||
Downloads
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => transcript && downloadSRT(transcript, audioFile?.title || 'transcript', speakerMappings)} className="rounded-[8px] cursor-pointer">
|
||||
<FileImage className="mr-2 h-4 w-4 opacity-70" /> Download SRT
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => { setDownloadFormat('txt'); setDownloadDialogOpen(true); }} className="rounded-[8px] cursor-pointer">
|
||||
<AlignLeft className="mr-2 h-4 w-4 opacity-70" /> Download Text
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => { setDownloadFormat('json'); setDownloadDialogOpen(true); }} className="rounded-[8px] cursor-pointer">
|
||||
<FileJson className="mr-2 h-4 w-4 opacity-70" /> Download JSON
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator className="bg-[var(--border-subtle)] my-1" />
|
||||
|
||||
<DropdownMenuLabel className="text-xs font-semibold text-[var(--text-tertiary)] uppercase tracking-wider px-2 py-1.5">
|
||||
System
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => setExecutionDialogOpen(true)} className="rounded-[8px] cursor-pointer">
|
||||
<Activity className="mr-2 h-4 w-4 opacity-70" /> Execution Info
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setLogsDialogOpen(true)} className="rounded-[8px] cursor-pointer">
|
||||
<FileText className="mr-2 h-4 w-4 opacity-70" /> View Logs
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
@@ -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}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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<string, string>;
|
||||
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<HTMLDivElement>(null);
|
||||
@@ -117,18 +112,16 @@ export function TranscriptSection({
|
||||
|
||||
// Helpers
|
||||
const getDetectedSpeakers = () => {
|
||||
// Safe check for segments array
|
||||
if (!transcript?.segments) return [];
|
||||
const speakers = new Set<string>();
|
||||
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 (
|
||||
<div className="bg-[var(--bg-main)] rounded-[var(--radius-card)] border border-[var(--border-subtle)] p-4 md:p-6 transition-all duration-300">
|
||||
{/* Sticky Toolbar */}
|
||||
<div className="mb-6 sticky top-4 z-10 flex justify-center pointer-events-none">
|
||||
<div className="pointer-events-auto bg-[var(--bg-card)]/80 backdrop-blur-xl border border-[var(--border-subtle)] shadow-lg rounded-[var(--radius-card)] p-1">
|
||||
<TranscriptToolbar
|
||||
transcriptMode={transcriptMode}
|
||||
setTranscriptMode={setTranscriptMode}
|
||||
autoScrollEnabled={autoScrollEnabled}
|
||||
setAutoScrollEnabled={setAutoScrollEnabled}
|
||||
notesOpen={notesOpen}
|
||||
setNotesOpen={setNotesOpen}
|
||||
notes={notes}
|
||||
onOpenExecutionInfo={onOpenExecutionInfo}
|
||||
onOpenLogs={onOpenLogs}
|
||||
hasSpeakers={hasSpeakers()}
|
||||
detectedSpeakersCount={getDetectedSpeakers().length}
|
||||
onOpenSpeakerRename={() => 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`)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-[var(--bg-card)] rounded-[var(--radius-card)] border border-[var(--border-subtle)] shadow-[var(--shadow-card)] p-1 overflow-hidden transition-shadow hover:shadow-[var(--shadow-float)]">
|
||||
<div className="bg-[var(--bg-main)]/50 rounded-[calc(var(--radius-card)-4px)] p-4 md:p-6 min-h-[500px]">
|
||||
{/*
|
||||
TOOLBAR REMOVED -> Moved to Context Menu
|
||||
*/}
|
||||
|
||||
{/* Transcript Content - Systematic Typography */}
|
||||
<div className="relative overflow-hidden font-sans">
|
||||
<div className="w-full text-[var(--text-secondary)] leading-relaxed">
|
||||
<div ref={transcriptRef} className="relative">
|
||||
<TranscriptView
|
||||
transcript={transcript}
|
||||
mode={transcriptMode}
|
||||
currentWordIndex={currentWordIndex}
|
||||
notes={notes}
|
||||
highlightedWordRef={highlightedWordRef}
|
||||
speakerMappings={speakerMappings}
|
||||
autoScrollEnabled={autoScrollEnabled}
|
||||
/>
|
||||
{/* Transcript Content - Systematic Typography */}
|
||||
<div className="relative overflow-hidden font-sans">
|
||||
<div className="w-full text-[var(--text-secondary)] leading-relaxed">
|
||||
<div ref={transcriptRef} className="relative">
|
||||
<TranscriptView
|
||||
transcript={transcript}
|
||||
mode={transcriptMode}
|
||||
currentWordIndex={currentWordIndex}
|
||||
notes={notes}
|
||||
highlightedWordRef={highlightedWordRef}
|
||||
speakerMappings={speakerMappings}
|
||||
autoScrollEnabled={autoScrollEnabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Download Dialog */}
|
||||
<DownloadDialog
|
||||
audioId={audioId}
|
||||
isOpen={downloadDialogOpen}
|
||||
onClose={setDownloadDialogOpen}
|
||||
initialFormat={downloadFormat}
|
||||
/>
|
||||
{/* Download Dialog */}
|
||||
<DownloadDialog
|
||||
audioId={audioId}
|
||||
isOpen={downloadDialogOpen}
|
||||
onClose={setDownloadDialogOpen}
|
||||
initialFormat={downloadFormat}
|
||||
/>
|
||||
|
||||
{/* Speaker Rename Dialog */}
|
||||
<SpeakerRenameDialog
|
||||
open={speakerRenameOpen}
|
||||
onOpenChange={setSpeakerRenameOpen}
|
||||
transcriptionId={audioId}
|
||||
initialSpeakers={getDetectedSpeakers()}
|
||||
onSpeakerMappingsUpdate={() => { }}
|
||||
/>
|
||||
{/* Speaker Rename Dialog */}
|
||||
<SpeakerRenameDialog
|
||||
open={speakerRenameOpen}
|
||||
onOpenChange={setSpeakerRenameOpen}
|
||||
transcriptionId={audioId}
|
||||
initialSpeakers={getDetectedSpeakers()}
|
||||
onSpeakerMappingsUpdate={() => { }}
|
||||
/>
|
||||
|
||||
{/* Portals */}
|
||||
{createPortal(
|
||||
<>
|
||||
{/* Selection Menu Bubble (Glass) */}
|
||||
<div className="z-[9999]">
|
||||
<TranscriptSelectionMenu
|
||||
isOpen={showSelectionMenu}
|
||||
isMobile={isMobile}
|
||||
{/* Portals */}
|
||||
{createPortal(
|
||||
<>
|
||||
{/* Selection Menu Bubble (Glass) */}
|
||||
<div className="z-[9999]">
|
||||
<TranscriptSelectionMenu
|
||||
isOpen={showSelectionMenu}
|
||||
isMobile={isMobile}
|
||||
position={selectionViewportPos}
|
||||
onAddNote={openEditor}
|
||||
onListenFromHere={handleListenFromHere}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Note Editor Dialog */}
|
||||
<NoteEditorDialog
|
||||
isOpen={showEditor}
|
||||
quote={pendingSelection?.quote || ""}
|
||||
position={selectionViewportPos}
|
||||
onAddNote={openEditor}
|
||||
onListenFromHere={handleListenFromHere}
|
||||
onSave={handleSaveNote}
|
||||
onCancel={closeEditor}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Note Editor Dialog */}
|
||||
<NoteEditorDialog
|
||||
isOpen={showEditor}
|
||||
quote={pendingSelection?.quote || ""}
|
||||
position={selectionViewportPos}
|
||||
onSave={handleSaveNote}
|
||||
onCancel={closeEditor}
|
||||
/>
|
||||
{/* Backdrop for menu/editor */}
|
||||
{(showSelectionMenu || showEditor) && (
|
||||
<div
|
||||
style={{ position: 'fixed', inset: 0, zIndex: 9995, background: 'transparent' }}
|
||||
onMouseDown={() => {
|
||||
if (showSelectionMenu && !showEditor) {
|
||||
closeEditor();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Backdrop for menu/editor */}
|
||||
{(showSelectionMenu || showEditor) && (
|
||||
<div
|
||||
style={{ position: 'fixed', inset: 0, zIndex: 9995, background: 'transparent' }}
|
||||
onMouseDown={() => {
|
||||
if (showSelectionMenu && !showEditor) {
|
||||
closeEditor();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Notes Sidebar - Premium Drawer */}
|
||||
{notesOpen && (
|
||||
<div className="fixed inset-y-0 right-0 w-[90vw] max-w-[400px] bg-[var(--bg-card)] border-l border-[var(--border-subtle)] shadow-[var(--shadow-float)] z-[9990] transition-transform duration-300 transform-gpu">
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="px-6 py-5 border-b border-[var(--border-subtle)] flex items-center justify-between">
|
||||
<h3 className="font-bold text-[var(--text-primary)] flex items-center gap-2 text-lg">
|
||||
<StickyNote className="h-5 w-5 text-[var(--brand-solid)]" />
|
||||
Notes
|
||||
<span className="ml-1 text-xs font-semibold rounded-full px-2 py-0.5 bg-[var(--bg-main)] text-[var(--text-secondary)] border border-[var(--border-subtle)]">
|
||||
{notes.length}
|
||||
</span>
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setNotesOpen(false)}
|
||||
className="h-8 w-8 inline-flex items-center justify-center rounded-[var(--radius-btn)] text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-main)] transition-colors"
|
||||
aria-label="Close notes"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||
<NotesSidebar
|
||||
notes={notes}
|
||||
onEdit={(id, content) => updateNote({ id, content })}
|
||||
onDelete={(id) => deleteNote(id)}
|
||||
onJumpTo={(t) => {
|
||||
onSeek(t);
|
||||
if (isMobile) setNotesOpen(false);
|
||||
}}
|
||||
/>
|
||||
{/* Notes Sidebar - Premium Drawer */}
|
||||
{notesOpen && (
|
||||
<div className="fixed inset-y-0 right-0 w-[90vw] max-w-[400px] bg-[var(--bg-card)] border-l border-[var(--border-subtle)] shadow-[var(--shadow-float)] z-[9990] transition-transform duration-300 transform-gpu">
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="px-6 py-5 border-b border-[var(--border-subtle)] flex items-center justify-between">
|
||||
<h3 className="font-bold text-[var(--text-primary)] flex items-center gap-2 text-lg">
|
||||
<StickyNote className="h-5 w-5 text-[var(--brand-solid)]" />
|
||||
Notes
|
||||
<span className="ml-1 text-xs font-semibold rounded-full px-2 py-0.5 bg-[var(--bg-main)] text-[var(--text-secondary)] border border-[var(--border-subtle)]">
|
||||
{notes.length}
|
||||
</span>
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setNotesOpen(false)}
|
||||
className="h-8 w-8 inline-flex items-center justify-center rounded-[var(--radius-btn)] text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-main)] transition-colors"
|
||||
aria-label="Close notes"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||
<NotesSidebar
|
||||
notes={notes}
|
||||
onEdit={(id, content) => updateNote({ id, content })}
|
||||
onDelete={(id) => deleteNote(id)}
|
||||
onJumpTo={(t) => {
|
||||
onSeek(t);
|
||||
if (isMobile) setNotesOpen(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>,
|
||||
document.body
|
||||
)}
|
||||
)}
|
||||
</>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user