@@ -14,64 +14,254 @@ interface AudioPlayerProps {
1414 audioData : {
1515 audio_data_url : string ;
1616 } ;
17- metadata : {
17+ metadata ? : {
1818 path ?: string ;
1919 duration ?: number ;
2020 } ;
21+ transcription ?: string ;
22+ compact ?: boolean ;
2123}
2224
23- const AudioPlayer : React . FC < AudioPlayerProps > = ( { audioData, metadata } ) => {
25+ const AudioPlayer : React . FC < AudioPlayerProps > = ( {
26+ audioData,
27+ metadata,
28+ transcription,
29+ compact = false ,
30+ } ) => {
2431 const [ wavesurfer , setWavesurfer ] = React . useState < WaveSurfer | null > ( null ) ;
32+ const [ isPlaying , setIsPlaying ] = React . useState ( false ) ;
33+ const [ isLoading , setIsLoading ] = React . useState ( true ) ;
34+ const [ error , setError ] = React . useState < string | null > ( null ) ;
35+ const [ duration , setDuration ] = React . useState < number > ( 0 ) ;
36+ const [ currentTime , setCurrentTime ] = React . useState < number > ( 0 ) ;
2537 const waveformRef = React . useRef < HTMLDivElement > ( null ) ;
38+ const audioRef = React . useRef < HTMLAudioElement > ( null ) ;
2639 const isDestroyedRef = React . useRef ( false ) ;
2740
28- // Initialize Wavesurfer
41+ // Compact mode handlers
42+ const handlePlayPauseCompact = ( ) => {
43+ if ( audioRef . current ) {
44+ if ( isPlaying ) {
45+ audioRef . current . pause ( ) ;
46+ } else {
47+ audioRef . current . play ( ) ;
48+ }
49+ }
50+ } ;
51+
52+ const handleTimeUpdate = ( ) => {
53+ if ( audioRef . current ) {
54+ setCurrentTime ( audioRef . current . currentTime ) ;
55+ }
56+ } ;
57+
58+ const handleLoadedMetadata = ( ) => {
59+ if ( audioRef . current ) {
60+ setDuration ( audioRef . current . duration ) ;
61+ setIsLoading ( false ) ;
62+ }
63+ } ;
64+
65+ const handlePlay = ( ) => setIsPlaying ( true ) ;
66+ const handlePause = ( ) => setIsPlaying ( false ) ;
67+ const handleError = ( ) => {
68+ setError ( 'Failed to load audio' ) ;
69+ setIsLoading ( false ) ;
70+ } ;
71+
72+ const formatTime = ( time : number ) => {
73+ const minutes = Math . floor ( time / 60 ) ;
74+ const seconds = Math . floor ( time % 60 ) ;
75+ return `${ minutes } :${ seconds . toString ( ) . padStart ( 2 , '0' ) } ` ;
76+ } ;
77+
78+ // Regular mode - WaveSurfer player effects
2979 React . useEffect ( ( ) => {
30- if ( waveformRef . current && ! wavesurfer && ! isDestroyedRef . current ) {
31- const ws = WaveSurfer . create ( {
32- container : waveformRef . current ,
33- waveColor : '#4f46e5' ,
34- // progressColor: '#7c3aed',
35- cursorColor : 'var(--joy-palette-primary-400)' ,
36- height : 60 ,
37- normalize : true ,
38- barWidth : 3 ,
39- barGap : 2 ,
40- barRadius : 3 ,
41- fillParent : true ,
42- pixelRatio : 1 ,
43- mediaControls : true ,
44- } ) ;
45-
46- ws . load ( audioData . audio_data_url ) ;
47-
48- setWavesurfer ( ws ) ;
49-
50- return ( ) => {
51- isDestroyedRef . current = true ;
52- if ( ws && ! ws . isDestroyed ) {
53- try {
54- ws . pause ( ) ;
55- ws . destroy ( ) ;
56- } catch ( error ) {
57- // Ignore errors during cleanup
58- console . warn ( 'Error destroying wavesurfer:' , error ) ;
80+ if (
81+ ! compact &&
82+ waveformRef . current &&
83+ ! wavesurfer &&
84+ ! isDestroyedRef . current &&
85+ audioData ?. audio_data_url
86+ ) {
87+ setIsLoading ( true ) ;
88+ setError ( null ) ;
89+
90+ try {
91+ const ws = WaveSurfer . create ( {
92+ container : waveformRef . current ,
93+ waveColor : '#4f46e5' ,
94+ progressColor : '#7c3aed' ,
95+ cursorColor : 'var(--joy-palette-primary-400)' ,
96+ height : 60 ,
97+ normalize : true ,
98+ barWidth : 3 ,
99+ barGap : 2 ,
100+ barRadius : 3 ,
101+ fillParent : true ,
102+ mediaControls : false ,
103+ } ) ;
104+
105+ ws . on ( 'ready' , ( ) => {
106+ setIsLoading ( false ) ;
107+ } ) ;
108+
109+ ws . on ( 'play' , ( ) => {
110+ setIsPlaying ( true ) ;
111+ } ) ;
112+
113+ ws . on ( 'pause' , ( ) => {
114+ setIsPlaying ( false ) ;
115+ } ) ;
116+
117+ ws . on ( 'error' , ( ) => {
118+ setError ( 'Failed to load audio' ) ;
119+ setIsLoading ( false ) ;
120+ } ) ;
121+
122+ ws . load ( audioData . audio_data_url ) ;
123+ setWavesurfer ( ws ) ;
124+
125+ return ( ) => {
126+ isDestroyedRef . current = true ;
127+ if ( ws ) {
128+ try {
129+ ws . pause ( ) ;
130+ ws . destroy ( ) ;
131+ } catch ( cleanupError ) {
132+ // Ignore errors during cleanup
133+ }
59134 }
60- }
61- setWavesurfer ( null ) ;
62- } ;
135+ setWavesurfer ( null ) ;
136+ setIsPlaying ( false ) ;
137+ setIsLoading ( true ) ;
138+ setError ( null ) ;
139+ } ;
140+ } catch ( initError ) {
141+ setError ( 'Failed to initialize audio player' ) ;
142+ setIsLoading ( false ) ;
143+ }
63144 }
64- } , [ audioData . audio_data_url ] ) ;
65145
66- // Reset destroyed flag when audio URL changes
146+ return undefined ;
147+ } , [ compact , audioData ?. audio_data_url ] ) ;
148+
67149 React . useEffect ( ( ) => {
68150 isDestroyedRef . current = false ;
69- } , [ audioData . audio_data_url ] ) ;
151+ } , [ audioData ?. audio_data_url ] ) ;
152+
153+ // Regular mode handlers
154+ const handlePlayPauseRegular = ( ) => {
155+ if ( wavesurfer ) {
156+ if ( isPlaying ) {
157+ wavesurfer . pause ( ) ;
158+ } else {
159+ wavesurfer . play ( ) ;
160+ }
161+ }
162+ } ;
163+
164+ // Compact mode render
165+ if ( compact ) {
166+ return (
167+ < Box sx = { { minWidth : '200px' , maxWidth : '300px' } } >
168+ < audio
169+ ref = { audioRef }
170+ src = { audioData . audio_data_url }
171+ onTimeUpdate = { handleTimeUpdate }
172+ onLoadedMetadata = { handleLoadedMetadata }
173+ onPlay = { handlePlay }
174+ onPause = { handlePause }
175+ onError = { handleError }
176+ preload = "metadata"
177+ >
178+ < track kind = "captions" />
179+ </ audio >
180+
181+ < Box sx = { { display : 'flex' , alignItems : 'center' , gap : 1 , mb : 1 } } >
182+ < IconButton
183+ size = "sm"
184+ variant = "soft"
185+ onClick = { handlePlayPauseCompact }
186+ disabled = { isLoading || ! ! error }
187+ >
188+ { isPlaying ? < Pause size = { 14 } /> : < Play size = { 14 } /> }
189+ </ IconButton >
190+
191+ < Volume2 size = { 12 } />
192+
193+ { isLoading && (
194+ < Typography level = "body-sm" sx = { { fontSize : '0.7rem' } } >
195+ Loading...
196+ </ Typography >
197+ ) }
198+
199+ { error && (
200+ < Typography
201+ level = "body-sm"
202+ color = "danger"
203+ sx = { { fontSize : '0.7rem' } }
204+ >
205+ Error
206+ </ Typography >
207+ ) }
208+
209+ { ! isLoading && ! error && (
210+ < Typography level = "body-sm" sx = { { fontSize : '0.7rem' } } >
211+ { formatTime ( currentTime ) } / { formatTime ( duration ) }
212+ </ Typography >
213+ ) }
214+ </ Box >
70215
216+ { metadata ?. path && (
217+ < Typography
218+ level = "body-sm"
219+ sx = { { fontSize : '0.65rem' , color : 'text.secondary' , mb : 0.5 } }
220+ >
221+ < strong > File:</ strong > { metadata . path . split ( '/' ) . pop ( ) }
222+ </ Typography >
223+ ) }
224+
225+ { transcription && (
226+ < Typography
227+ level = "body-sm"
228+ sx = { { fontSize : '0.65rem' , color : 'text.secondary' } }
229+ >
230+ < strong > Transcription:</ strong > { transcription }
231+ </ Typography >
232+ ) }
233+ </ Box >
234+ ) ;
235+ }
236+
237+ // Regular mode render
71238 return (
72- < Card >
239+ < Card sx = { { minWidth : '300px' } } >
73240 < CardContent >
74241 < Stack spacing = { 2 } >
242+ { /* Controls */ }
243+ < Box sx = { { display : 'flex' , alignItems : 'center' , gap : 1 } } >
244+ < IconButton
245+ size = "sm"
246+ variant = "soft"
247+ onClick = { handlePlayPauseRegular }
248+ disabled = { ! wavesurfer || isLoading || ! ! error }
249+ >
250+ { isPlaying ? < Pause size = { 16 } /> : < Play size = { 16 } /> }
251+ </ IconButton >
252+ < Volume2 size = { 16 } />
253+ { isLoading && (
254+ < Typography level = "body-sm" color = "neutral" >
255+ Loading...
256+ </ Typography >
257+ ) }
258+ { error && (
259+ < Typography level = "body-sm" color = "danger" >
260+ { error }
261+ </ Typography >
262+ ) }
263+ </ Box >
264+
75265 { /* Waveform */ }
76266 < Box
77267 ref = { waveformRef }
@@ -82,17 +272,23 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ audioData, metadata }) => {
82272 borderColor : 'divider' ,
83273 borderRadius : 'sm' ,
84274 padding : 1 ,
275+ opacity : isLoading ? 0.5 : 1 ,
85276 } }
86277 />
87278
88- { /* File Path Only */ }
89- { metadata ?. path && (
90- < Box sx = { { fontSize : '0.75rem' , color : 'text.secondary' } } >
91- < div >
279+ { /* Metadata */ }
280+ < Stack spacing = { 1 } >
281+ { metadata ?. path && (
282+ < Box sx = { { fontSize : '0.75rem' , color : 'text.secondary' } } >
92283 < strong > File:</ strong > { metadata . path }
93- </ div >
94- </ Box >
95- ) }
284+ </ Box >
285+ ) }
286+ { metadata ?. duration && (
287+ < Box sx = { { fontSize : '0.75rem' , color : 'text.secondary' } } >
288+ < strong > Duration:</ strong > { metadata . duration } s
289+ </ Box >
290+ ) }
291+ </ Stack >
96292 </ Stack >
97293 </ CardContent >
98294 </ Card >
0 commit comments