@@ -11,6 +11,7 @@ import { FragmentComponent, type Fragment } from "./fragment";
11
11
import { useAssembly } from "./assembly-context" ;
12
12
import { AudioProcessor } from "@/lib/audio-processor" ;
13
13
import { RecordYouButton } from "./record-you-button" ;
14
+ import { transcribeAudio } from "@/lib/assembly-ai" ;
14
15
15
16
const AssemblyView : ( ) => JSX . Element = ( ) => {
16
17
const [ inputText , setInputText ] = useState ( "" ) ;
@@ -39,6 +40,9 @@ const AssemblyView: () => JSX.Element = () => {
39
40
sendAudio,
40
41
isIdleError,
41
42
} = useAssembly ( ) ;
43
+ const [ isUserRecording , setIsUserRecording ] = useState ( false ) ;
44
+ const userRecorderRef = useRef < MediaRecorder | null > ( null ) ;
45
+ const userChunksRef = useRef < Blob [ ] > ( [ ] ) ;
42
46
43
47
const scrollToBottom = ( ) => {
44
48
if ( ! scrollContainerRef . current ) return ;
@@ -264,6 +268,51 @@ const AssemblyView: () => JSX.Element = () => {
264
268
// eslint-disable-next-line react-hooks/exhaustive-deps
265
269
} , [ ] ) ;
266
270
271
+ useEffect ( ( ) => {
272
+ if ( ! microphone ?. stream ) return ;
273
+
274
+ if ( isUserRecording ) {
275
+ // pause live transcription
276
+ setIsPaused ( true ) ;
277
+
278
+ userRecorderRef . current = new MediaRecorder ( microphone . stream ) ;
279
+ userChunksRef . current = [ ] ;
280
+ userRecorderRef . current . ondataavailable = ( e ) => {
281
+ if ( e . data . size ) userChunksRef . current . push ( e . data ) ;
282
+ } ;
283
+ userRecorderRef . current . onstop = async ( ) => {
284
+ const finalBlob = new Blob ( userChunksRef . current , {
285
+ type : "audio/webm" ,
286
+ } ) ;
287
+ try {
288
+ // transcribe the user's new recording
289
+ const result = await transcribeAudio ( finalBlob ) ;
290
+ setFragments ( ( prev ) => [
291
+ ...prev ,
292
+ {
293
+ id : crypto . randomUUID ( ) ,
294
+ text : result . text ,
295
+ type : "speech" ,
296
+ createdAt : Date . now ( ) ,
297
+ } ,
298
+ ] ) ;
299
+ } catch ( err ) {
300
+ console . debug ( "[AssemblyView] user-recording transcribe error" ) ;
301
+ }
302
+ } ;
303
+
304
+ userRecorderRef . current . start ( ) ;
305
+ } else {
306
+ // resume live transcription
307
+ setIsPaused ( false ) ;
308
+
309
+ if ( userRecorderRef . current ?. state === "recording" ) {
310
+ userRecorderRef . current . stop ( ) ;
311
+ }
312
+ userRecorderRef . current = null ;
313
+ }
314
+ } , [ isUserRecording , microphone ?. stream ] ) ;
315
+
267
316
const handleSubmit = ( ) => {
268
317
if ( ! inputText . trim ( ) ) return ;
269
318
setFragments ( ( prev ) => [
@@ -326,7 +375,10 @@ const AssemblyView: () => JSX.Element = () => {
326
375
< div className = "flex-none px-4 pt-2 pb-4 border-t" >
327
376
< div className = "max-w-2xl mx-auto" >
328
377
< div className = "pb-2 flex gap-4 justify-between items-center" >
329
- < RecordYouButton language = { describeLanguage ( outputLanguage ) } />
378
+ < RecordYouButton
379
+ language = { describeLanguage ( outputLanguage ) }
380
+ onToggle = { setIsUserRecording }
381
+ />
330
382
< CloningView
331
383
speechState = { managerRef . current }
332
384
audioBlob = { audioBlob }
0 commit comments