@@ -8,12 +8,10 @@ import type {
88import {
99 ArrowDown ,
1010 ArrowUp ,
11- AudioLines ,
1211 Bot ,
1312 CalendarClock ,
1413 CheckCircle ,
1514 ChevronDownIcon ,
16- CirclePause ,
1715 CirclePlay ,
1816 Copy ,
1917 Loader2 ,
@@ -148,19 +146,62 @@ function groupToolCalls(content: ContentBlock[]): ExtendedContentBlock[] {
148146 return result ;
149147}
150148
149+ const AUDIO_WAVE_LINES : Array < { x : number ; y1 : number ; y2 : number } > = [
150+ { x : 2 , y1 : 10 , y2 : 13 } ,
151+ { x : 6 , y1 : 6 , y2 : 17 } ,
152+ { x : 10 , y1 : 3 , y2 : 21 } ,
153+ { x : 14 , y1 : 8 , y2 : 15 } ,
154+ { x : 18 , y1 : 5 , y2 : 18 } ,
155+ { x : 22 , y1 : 10 , y2 : 13 } ,
156+ ] ;
157+
158+ function AudioWave ( { isPlaying = true , className } : { isPlaying ?: boolean ; className ?: string } ) {
159+ return (
160+ < >
161+ { isPlaying && (
162+ < style > { `
163+ @keyframes audioWave {
164+ 0%, 100% { transform: scaleY(1); }
165+ 50% { transform: scaleY(0.3); }
166+ }
167+ ` } </ style >
168+ ) }
169+ < svg
170+ xmlns = "http://www.w3.org/2000/svg"
171+ width = "24"
172+ height = "24"
173+ viewBox = "0 0 24 24"
174+ fill = "none"
175+ stroke = "currentColor"
176+ strokeWidth = { 2 }
177+ strokeLinecap = "round"
178+ strokeLinejoin = "round"
179+ className = { className }
180+ >
181+ { AUDIO_WAVE_LINES . map ( ( { x, y1, y2 } , i ) => (
182+ < line
183+ key = { x }
184+ x1 = { x }
185+ x2 = { x }
186+ y1 = { y1 }
187+ y2 = { y2 }
188+ style = { {
189+ transformOrigin : `${ x } px 12px` ,
190+ animation : isPlaying
191+ ? `audioWave 0.8s ease-in-out ${ i * 0.12 } s infinite`
192+ : 'none' ,
193+ } }
194+ />
195+ ) ) }
196+ </ svg >
197+ </ >
198+ ) ;
199+ }
200+
151201/**
152202 * Inline audio control rendered *inside* the time/usage Badge so the play
153203 * icon visually merges into the same chip rather than floating as its own
154204 * pill.
155- *
156- * While the block is still being streamed by the assistant we show a pulsing
157- * {@link AudioLines} icon — actual playback during that window is driven by
158- * {@link useAudioBlock}'s live player. Once the stream finishes the manager
159- * publishes an Object URL and we render a clickable Circle Play/Pause icon
160- * for replay.
161- *
162- * For historical messages loaded from the server the block isn't tracked by
163- * the manager — we fall back to a data URL built from the accumulated base64.
164205 */
165206function AudioInlineControl ( { block } : { block : DataBlock } ) {
166207 const { t } = useTranslation ( ) ;
@@ -207,13 +248,7 @@ function AudioInlineControl({ block }: { block: DataBlock }) {
207248 } , [ interruptCount ] ) ;
208249
209250 if ( isStreaming ) {
210- return (
211- < AudioLines
212- data-icon = "inline-start"
213- className = "ml-1 animate-pulse"
214- aria-label = { t ( 'messageBubble.audioGenerating' ) }
215- />
216- ) ;
251+ return < AudioWave isPlaying className = "ml-1" /> ;
217252 }
218253
219254 if ( ! src ) return null ;
@@ -234,7 +269,6 @@ function AudioInlineControl({ block }: { block: DataBlock }) {
234269 }
235270 } ;
236271
237- const Icon = isPlaying ? CirclePause : CirclePlay ;
238272 return (
239273 < >
240274 < button
@@ -245,7 +279,11 @@ function AudioInlineControl({ block }: { block: DataBlock }) {
245279 }
246280 className = "ml-1 inline-flex cursor-pointer items-center transition-opacity hover:opacity-70"
247281 >
248- < Icon className = "size-3" />
282+ { isPlaying ? (
283+ < AudioWave isPlaying className = "size-3" />
284+ ) : (
285+ < CirclePlay className = "size-3" />
286+ ) }
249287 </ button >
250288 < audio
251289 ref = { audioRef }
0 commit comments