reidmen commited on
Commit
4c8239b
·
1 Parent(s): ad98a04

better layout

Browse files
src/components/AudioManager.tsx CHANGED
@@ -255,73 +255,102 @@ export function AudioManager(props: { transcriber: Transcriber }) {
255
 
256
  return (
257
  <>
258
- <div className='flex flex-col justify-center items-center rounded-lg bg-white shadow-xl shadow-black/5 ring-1 ring-slate-700/10'>
259
- <div className='flex flex-row space-x-2 py-2 px-2'>
260
- {/* <VerticalBar />
261
- <FileTile
262
- icon={<FolderIcon />}
263
- text={"From file"}
264
- onFileUpdate={(decoded, blobUrl, mimeType) => {
265
- props.transcriber.onInputChange();
266
- setHasRecorded(false);
267
- setAudioData({
268
- buffer: decoded,
269
- url: blobUrl,
270
- source: AudioSource.FILE,
271
- mimeType: mimeType,
272
- });
273
- }}
274
- /> */}
275
- {navigator.mediaDevices && (
276
- <>
277
- <VerticalBar />
278
- <RecordTile
279
- icon={<MicrophoneIcon />}
280
- text={"Record"}
281
- setAudioData={setAudioFromRecording}
282
- />
283
- <TranscribeButton
284
- onClick={() => {
285
- audioData && props.transcriber.start(audioData.buffer);
 
 
 
 
 
 
 
 
 
 
 
 
286
  }}
287
- isModelLoading={props.transcriber.isModelLoading}
288
- isTranscribing={props.transcriber.isBusy}
289
- disabled={!audioData || !hasRecorded}
290
- className={`${(!audioData || !hasRecorded) ? 'bg-red-500 hover:bg-red-600' : ''}`}
291
  />
292
- </>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  )}
294
  </div>
295
- <AudioDataBar
296
- progress={progress !== undefined && audioData ? 1 : (progress ?? 0)}
297
- />
298
- </div>
299
 
300
- {audioData && (
301
- <>
302
- {props.transcriber.progressItems.length > 0 && (
303
- <div className='relative z-10 p-4 w-full text-center'>
304
- <label>
305
- Loading model files... (only run once)
306
- </label>
 
 
 
 
 
 
307
  {props.transcriber.progressItems.map((data) => (
308
- <div key={data.file}>
309
- <Progress
310
- text={data.file}
311
- percentage={data.progress}
312
- />
313
- </div>
314
  ))}
315
  </div>
316
- )}
317
- </>
318
- )}
319
-
320
- <SettingsTile
321
- className='absolute bottom-4 right-4'
322
- transcriber={props.transcriber}
323
- icon={<SettingsIcon />}
324
- />
 
 
 
 
 
325
  </>
326
  );
327
  }
@@ -332,29 +361,73 @@ function SettingsTile(props: {
332
  transcriber: Transcriber;
333
  }) {
334
  const [showModal, setShowModal] = useState(false);
 
335
 
336
- const onClick = () => {
337
- setShowModal(true);
338
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
 
340
- const onClose = () => {
341
- setShowModal(false);
342
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
343
 
344
- const onSubmit = () => {
345
- onClose();
346
- };
 
 
 
 
 
 
347
 
348
- return (
349
- <div className={props.className}>
350
- <Tile icon={props.icon} onClick={onClick} />
351
  <SettingsModal
352
  show={showModal}
353
- onSubmit={onSubmit}
354
- onClose={onClose}
355
  transcriber={props.transcriber}
356
  />
357
- </div>
358
  );
359
  }
360
 
@@ -508,24 +581,6 @@ function SettingsModal(props: {
508
  );
509
  }
510
 
511
- function VerticalBar() {
512
- return <div className='w-[1px] bg-slate-200'></div>;
513
- }
514
-
515
- function AudioDataBar(props: { progress: number }) {
516
- return <ProgressBar progress={`${Math.round(props.progress * 100)}%`} />;
517
- }
518
-
519
- function ProgressBar(props: { progress: string }) {
520
- return (
521
- <div className='w-full rounded-full h-1 bg-gray-200 dark:bg-gray-700'>
522
- <div
523
- className='bg-blue-600 h-1 rounded-full transition-all duration-100'
524
- style={{ width: props.progress }}
525
- ></div>
526
- </div>
527
- );
528
- }
529
 
530
  function RecordTile(props: {
531
  icon: JSX.Element;
@@ -534,29 +589,32 @@ function RecordTile(props: {
534
  }) {
535
  const [showModal, setShowModal] = useState(false);
536
 
537
- const onClick = () => {
538
- setShowModal(true);
539
- };
540
-
541
- const onClose = () => {
542
- setShowModal(false);
543
- };
544
-
545
- const onSubmit = (data: Blob | undefined) => {
546
- if (data) {
547
- props.setAudioData(data);
548
- onClose();
549
- }
550
- };
551
-
552
  return (
553
  <>
554
- <Tile icon={props.icon} text={props.text} onClick={onClick} />
 
 
 
 
 
 
 
 
 
 
 
 
 
555
  <RecordModal
556
  show={showModal}
557
- onSubmit={onSubmit}
558
- onProgress={(_data) => { }}
559
- onClose={onClose}
 
 
 
 
 
560
  />
561
  </>
562
  );
@@ -588,103 +646,118 @@ function RecordModal(props: {
588
  <Modal
589
  show={props.show}
590
  title={
591
- <div className="flex items-center gap-3">
592
- <svg className="w-6 h-6 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
593
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" />
594
- </svg>
595
- <span className="text-xl font-semibold">Record Your Voice</span>
 
 
 
596
  </div>
597
  }
598
  content={
599
- <div className="space-y-4">
600
- <div className="bg-blue-50 rounded-lg p-4 mb-4">
601
- <div className="flex items-center gap-2 text-blue-700 mb-2">
 
602
  <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
603
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
604
  </svg>
605
- <span className="font-medium">Recording Tips:</span>
606
  </div>
607
- <ul className="text-sm text-blue-600 ml-7 list-disc space-y-1">
608
- <li>Speak clearly and at a normal pace</li>
609
- <li>Keep background noise to a minimum</li>
610
- <li>Stay close to your microphone</li>
611
  </ul>
612
  </div>
613
 
614
- <div className="bg-white rounded-lg border border-gray-200 p-4">
615
- <AudioRecorder
616
- onRecordingProgress={(blob) => {
617
- props.onProgress(blob);
618
- }}
619
- onRecordingComplete={onRecordingComplete}
620
- />
 
621
  </div>
622
 
 
623
  {audioBlob && (
624
- <div className="flex items-center gap-2 text-green-600 bg-green-50 p-3 rounded-lg">
625
- <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
626
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
627
- </svg>
628
- <span className="text-sm font-medium">Recording ready to load</span>
 
 
 
 
 
 
 
 
 
 
 
629
  </div>
630
  )}
631
  </div>
632
  }
633
  onClose={onClose}
634
  submitText={
635
- <div className="flex items-center gap-2">
636
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
637
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0l-4 4m4-4v12" />
638
- </svg>
639
- {audioBlob ? "Load Recording" : "Record First"}
 
 
 
 
 
 
640
  </div>
641
  }
642
  submitEnabled={audioBlob !== undefined}
643
- submitClassName={`${audioBlob ? 'bg-blue-600 hover:bg-blue-700' : 'bg-gray-400'} text-white px-4 py-2 rounded-lg transition-colors duration-200`}
 
 
 
 
 
 
644
  onSubmit={onSubmit}
645
  />
646
  );
647
  }
648
 
649
- function Tile(props: {
650
- icon: JSX.Element;
651
- text?: string;
652
- onClick?: () => void;
653
- }) {
654
- return (
655
- <button
656
- onClick={props.onClick}
657
- className='flex items-center justify-center rounded-lg p-2 bg-blue text-slate-500 hover:text-indigo-600 hover:bg-indigo-50 transition-all duration-200'
658
- >
659
- <div className='w-4 h-4'>{props.icon}</div>
660
- {props.text && (
661
- <div className='ml-2 break-text text-center text-md w-30'>
662
- {props.text}
663
- </div>
664
- )}
665
- </button>
666
- );
667
- }
668
-
669
-
670
  function SettingsIcon() {
671
  return (
672
  <svg
673
- xmlns='http://www.w3.org/2000/svg'
674
- fill='none'
675
- viewBox='0 0 24 24'
676
- strokeWidth='1.25'
677
- stroke='currentColor'
678
  >
 
 
 
679
  <path
680
- strokeLinecap='round'
681
- strokeLinejoin='round'
682
- d='M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z'
 
 
 
683
  />
684
  <path
685
- strokeLinecap='round'
686
- strokeLinejoin='round'
687
- d='M15 12a3 3 0 11-6 0 3 3 0 016 0z'
 
 
688
  />
689
  </svg>
690
  );
@@ -693,16 +766,51 @@ function SettingsIcon() {
693
  function MicrophoneIcon() {
694
  return (
695
  <svg
696
- xmlns='http://www.w3.org/2000/svg'
697
- fill='none'
698
- viewBox='0 0 24 24'
699
- strokeWidth={1.5}
700
- stroke='currentColor'
701
  >
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702
  <path
703
- strokeLinecap='round'
704
- strokeLinejoin='round'
705
- d='M12 18.75a6 6 0 006-6v-1.5m-6 7.5a6 6 0 01-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 01-3-3V4.5a3 3 0 116 0v8.25a3 3 0 01-3 3z'
 
 
 
706
  />
707
  </svg>
708
  );
 
255
 
256
  return (
257
  <>
258
+ <div className='flex flex-col gap-4 w-full max-w-2xl mx-auto'>
259
+ {/* Main Control Panel */}
260
+ <div className='relative overflow-hidden rounded-xl bg-white shadow-lg border border-gray-100 transition-all duration-300 hover:shadow-xl'>
261
+ {/* Recording Controls */}
262
+ <div className='flex items-center justify-center p-4 gap-4'>
263
+ {navigator.mediaDevices && (
264
+ <>
265
+ <RecordTile
266
+ icon={<MicrophoneIcon />}
267
+ text="Record"
268
+ setAudioData={setAudioFromRecording}
269
+ />
270
+ <TranscribeButton
271
+ onClick={() => audioData && props.transcriber.start(audioData.buffer)}
272
+ isModelLoading={props.transcriber.isModelLoading}
273
+ isTranscribing={props.transcriber.isBusy}
274
+ disabled={!audioData || !hasRecorded}
275
+ className={`
276
+ flex-1 py-3 px-6 rounded-lg font-medium transition-all duration-300
277
+ ${(!audioData || !hasRecorded)
278
+ ? 'bg-gray-100 text-gray-400 cursor-not-allowed'
279
+ : 'bg-indigo-600 hover:bg-indigo-700 text-white shadow-md hover:shadow-lg active:scale-95'
280
+ }
281
+ `}
282
+ />
283
+ </>
284
+ )}
285
+ </div>
286
+
287
+ {/* Progress Bar */}
288
+ <div className='px-4 pb-4'>
289
+ <div className='relative h-2 bg-gray-100 rounded-full overflow-hidden'>
290
+ <div
291
+ className={`
292
+ absolute h-full left-0 top-0 rounded-full
293
+ ${audioData ? 'bg-green-500' : 'bg-indigo-500'}
294
+ transition-all duration-300 ease-out
295
+ `}
296
+ style={{
297
+ width: `${Math.round((progress !== undefined && audioData ? 1 : (progress ?? 0)) * 100)}%`,
298
  }}
 
 
 
 
299
  />
300
+ </div>
301
+ </div>
302
+
303
+ {/* Status Indicator */}
304
+ {(audioData || progress !== undefined) && (
305
+ <div className='absolute top-2 right-2'>
306
+ <span className={`
307
+ inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
308
+ ${audioData
309
+ ? 'bg-green-100 text-green-800'
310
+ : 'bg-indigo-100 text-indigo-800'
311
+ }
312
+ `}>
313
+ {audioData ? 'Ready' : 'Processing...'}
314
+ </span>
315
+ </div>
316
  )}
317
  </div>
 
 
 
 
318
 
319
+ {/* Model Loading Progress */}
320
+ {audioData && props.transcriber.progressItems.length > 0 && (
321
+ <div className='bg-white rounded-xl p-6 shadow-lg border border-gray-100'>
322
+ <div className='flex items-center gap-2 mb-4 text-indigo-600'>
323
+ <svg className="w-5 h-5 animate-spin" viewBox="0 0 24 24">
324
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
325
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
326
+ </svg>
327
+ <span className='font-medium'>Loading model files...</span>
328
+ <span className='text-sm text-gray-500'>(one-time process)</span>
329
+ </div>
330
+
331
+ <div className='space-y-3'>
332
  {props.transcriber.progressItems.map((data) => (
333
+ <Progress
334
+ key={data.file}
335
+ text={data.file}
336
+ percentage={data.progress}
337
+ />
 
338
  ))}
339
  </div>
340
+ </div>
341
+ )}
342
+
343
+ {/* Settings Button */}
344
+ <div className='fixed bottom-6 right-6'>
345
+ <SettingsTile
346
+ icon={<SettingsIcon />}
347
+ transcriber={props.transcriber}
348
+ className="group p-3 bg-white rounded-full shadow-lg hover:shadow-xl
349
+ border border-gray-100 transition-all duration-300
350
+ hover:bg-indigo-50 active:scale-95"
351
+ />
352
+ </div>
353
+ </div>
354
  </>
355
  );
356
  }
 
361
  transcriber: Transcriber;
362
  }) {
363
  const [showModal, setShowModal] = useState(false);
364
+ const [isHovered, setIsHovered] = useState(false);
365
 
366
+ return (
367
+ <>
368
+ <button
369
+ onClick={() => setShowModal(true)}
370
+ onMouseEnter={() => setIsHovered(true)}
371
+ onMouseLeave={() => setIsHovered(false)}
372
+ className={`
373
+ relative group flex items-center justify-center
374
+ p-3 rounded-full
375
+ bg-white shadow-lg hover:shadow-xl
376
+ border border-gray-100
377
+ transition-all duration-300
378
+ hover:bg-indigo-50 active:scale-95
379
+ ${props.className || ''}
380
+ `}
381
+ data-settings-tile
382
+ >
383
+ {/* Ripple effect on hover */}
384
+ <div className={`
385
+ absolute inset-0 rounded-full
386
+ bg-indigo-100 opacity-0 scale-90
387
+ transition-all duration-300 ease-out
388
+ ${isHovered ? 'opacity-50 scale-105' : ''}
389
+ `} />
390
+
391
+ {/* Icon container */}
392
+ <div className="relative flex items-center gap-2">
393
+ <div className="w-6 h-6 text-slate-600 group-hover:text-indigo-600 transition-colors duration-200">
394
+ {props.icon}
395
+ </div>
396
 
397
+ {/* Tooltip */}
398
+ <div className={`
399
+ absolute right-full mr-3 whitespace-nowrap
400
+ px-2 py-1 rounded-lg text-xs font-medium
401
+ bg-gray-800 text-white
402
+ opacity-0 -translate-x-2
403
+ transition-all duration-200
404
+ ${isHovered ? 'opacity-100 translate-x-0' : ''}
405
+ `}>
406
+ Configure Settings
407
+ {/* Tooltip arrow */}
408
+ <div className="absolute top-1/2 right-0 -mt-1
409
+ border-4 border-transparent
410
+ border-l-gray-800" />
411
+ </div>
412
+ </div>
413
 
414
+ {/* Status indicator */}
415
+ {props.transcriber.multilingual && (
416
+ <div className="absolute -top-1 -right-1
417
+ w-3 h-3 rounded-full
418
+ bg-indigo-500 border-2 border-white
419
+ transition-transform duration-200
420
+ group-hover:scale-125" />
421
+ )}
422
+ </button>
423
 
 
 
 
424
  <SettingsModal
425
  show={showModal}
426
+ onSubmit={() => setShowModal(false)}
427
+ onClose={() => setShowModal(false)}
428
  transcriber={props.transcriber}
429
  />
430
+ </>
431
  );
432
  }
433
 
 
581
  );
582
  }
583
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
584
 
585
  function RecordTile(props: {
586
  icon: JSX.Element;
 
589
  }) {
590
  const [showModal, setShowModal] = useState(false);
591
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
592
  return (
593
  <>
594
+ <button
595
+ onClick={() => setShowModal(true)}
596
+ className="group flex items-center gap-2 py-3 px-4 bg-white rounded-lg
597
+ text-slate-600 hover:text-indigo-600 hover:bg-indigo-50
598
+ shadow-sm hover:shadow border border-gray-100
599
+ transition-all duration-200 active:scale-95"
600
+ >
601
+ <div className="w-5 h-5">
602
+ {props.icon}
603
+ </div>
604
+ <span className="font-medium text-sm">
605
+ {props.text}
606
+ </span>
607
+ </button>
608
  <RecordModal
609
  show={showModal}
610
+ onSubmit={(data) => {
611
+ if (data) {
612
+ props.setAudioData(data);
613
+ setShowModal(false);
614
+ }
615
+ }}
616
+ onProgress={() => { }}
617
+ onClose={() => setShowModal(false)}
618
  />
619
  </>
620
  );
 
646
  <Modal
647
  show={props.show}
648
  title={
649
+ <div className="flex items-center gap-3 mb-2">
650
+ <div className="relative">
651
+ <svg className="w-6 h-6 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
652
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" />
653
+ </svg>
654
+ <div className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full animate-pulse"></div>
655
+ </div>
656
+ <span className="text-xl font-semibold">Voice Recorder</span>
657
  </div>
658
  }
659
  content={
660
+ <div className="space-y-6">
661
+ {/* Tips Card */}
662
+ <div className="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl p-4">
663
+ <div className="flex items-center gap-2 text-indigo-700 mb-3">
664
  <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
665
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
666
  </svg>
667
+ <span className="font-medium">Quick Tips</span>
668
  </div>
669
+ <ul className="text-sm text-indigo-600 ml-7 list-disc space-y-1.5">
670
+ <li className="transition-all duration-200 hover:translate-x-1">Speak clearly at a normal pace</li>
671
+ <li className="transition-all duration-200 hover:translate-x-1">Minimize background noise</li>
672
+ <li className="transition-all duration-200 hover:translate-x-1">Keep microphone close</li>
673
  </ul>
674
  </div>
675
 
676
+ {/* Recorder Card */}
677
+ <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
678
+ <div className="flex flex-col items-center">
679
+ <AudioRecorder
680
+ onRecordingProgress={(blob) => props.onProgress(blob)}
681
+ onRecordingComplete={onRecordingComplete}
682
+ />
683
+ </div>
684
  </div>
685
 
686
+ {/* Status Indicator */}
687
  {audioBlob && (
688
+ <div className="flex items-center justify-between bg-gradient-to-r from-green-50 to-emerald-50 p-4 rounded-xl border border-green-100">
689
+ <div className="flex items-center gap-2">
690
+ <div className="relative">
691
+ <svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
692
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
693
+ </svg>
694
+ <div className="absolute -top-1 -right-1 w-2 h-2 bg-green-400 rounded-full"></div>
695
+ </div>
696
+ <span className="text-sm font-medium text-green-700">Recording ready!</span>
697
+ </div>
698
+ <button
699
+ onClick={onClose}
700
+ className="text-xs text-green-600 hover:text-green-700 underline underline-offset-2"
701
+ >
702
+ Record again
703
+ </button>
704
  </div>
705
  )}
706
  </div>
707
  }
708
  onClose={onClose}
709
  submitText={
710
+ <div className="flex items-center justify-center w-full gap-2 group">
711
+ {audioBlob ? (
712
+ <>
713
+ <svg className="w-4 h-4 transition-transform duration-200 group-hover:scale-110" fill="none" stroke="currentColor" viewBox="0 0 24 24">
714
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0l-4 4m4-4v12" />
715
+ </svg>
716
+ <span className="font-medium">Load Recording</span>
717
+ </>
718
+ ) : (
719
+ <span className="font-medium">Start Recording</span>
720
+ )}
721
  </div>
722
  }
723
  submitEnabled={audioBlob !== undefined}
724
+ submitClassName={`
725
+ w-full px-6 py-3 rounded-xl transition-all duration-300
726
+ ${audioBlob
727
+ ? 'bg-gradient-to-r from-indigo-500 to-blue-500 hover:from-indigo-600 hover:to-blue-600 text-white shadow-md hover:shadow-lg active:scale-98'
728
+ : 'bg-gray-100 text-gray-400 cursor-not-allowed'
729
+ }
730
+ `}
731
  onSubmit={onSubmit}
732
  />
733
  );
734
  }
735
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
736
  function SettingsIcon() {
737
  return (
738
  <svg
739
+ className="w-full h-full transition-colors duration-200"
740
+ viewBox="0 0 24 24"
741
+ fill="none"
742
+ xmlns="http://www.w3.org/2000/svg"
 
743
  >
744
+ <g className="opacity-0 hover:opacity-100 transition-opacity duration-300">
745
+ <circle cx="12" cy="12" r="10" className="animate-pulse" fill="currentColor" fillOpacity="0.1" />
746
+ </g>
747
  <path
748
+ className="transition-transform duration-300 origin-center hover:rotate-90"
749
+ strokeLinecap="round"
750
+ strokeLinejoin="round"
751
+ strokeWidth="1.5"
752
+ stroke="currentColor"
753
+ d="M12 15a3 3 0 100-6 3 3 0 000 6z"
754
  />
755
  <path
756
+ strokeLinecap="round"
757
+ strokeLinejoin="round"
758
+ strokeWidth="1.5"
759
+ stroke="currentColor"
760
+ d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z"
761
  />
762
  </svg>
763
  );
 
766
  function MicrophoneIcon() {
767
  return (
768
  <svg
769
+ className="w-full h-full transition-colors duration-200"
770
+ viewBox="0 0 24 24"
771
+ fill="none"
772
+ xmlns="http://www.w3.org/2000/svg"
 
773
  >
774
+ {/* Pulse effect circle */}
775
+ <g className="opacity-0 group-hover:opacity-100 transition-opacity duration-300">
776
+ <circle
777
+ cx="12"
778
+ cy="12"
779
+ r="10"
780
+ className="animate-pulse"
781
+ fill="currentColor"
782
+ fillOpacity="0.1"
783
+ />
784
+ </g>
785
+
786
+ {/* Microphone body */}
787
+ <path
788
+ className="transition-all duration-300 origin-center group-hover:scale-105"
789
+ strokeLinecap="round"
790
+ strokeLinejoin="round"
791
+ strokeWidth="1.5"
792
+ stroke="currentColor"
793
+ d="M12 15.75a4 4 0 004-4V6a4 4 0 00-8 0v5.75a4 4 0 004 4z"
794
+ />
795
+
796
+ {/* Sound waves */}
797
+ <path
798
+ className="transition-opacity duration-300 group-hover:opacity-100 opacity-70"
799
+ strokeLinecap="round"
800
+ strokeLinejoin="round"
801
+ strokeWidth="1.5"
802
+ stroke="currentColor"
803
+ d="M8 9.75v2a4 4 0 008 0v-2"
804
+ />
805
+
806
+ {/* Stand */}
807
  <path
808
+ className="transition-all duration-300 origin-bottom group-hover:scale-y-110"
809
+ strokeLinecap="round"
810
+ strokeLinejoin="round"
811
+ strokeWidth="1.5"
812
+ stroke="currentColor"
813
+ d="M12 15.75V19m-4 2h8"
814
  />
815
  </svg>
816
  );
src/components/TranscribeButton.tsx CHANGED
@@ -16,44 +16,86 @@ export function TranscribeButton(props: Props): JSX.Element {
16
  }
17
  }}
18
  disabled={isTranscribing || disabled}
19
- className={`text-white font-medium rounded-lg text-sm px-5 py-2.5 text-center mr-2 inline-flex items-center
 
 
 
20
  ${isModelLoading || isTranscribing
21
- ? 'animate-pulse-green bg-green-600'
22
- : className || 'bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800'
23
- }`}
 
 
 
 
24
  >
25
  {isModelLoading ? (
26
- <Spinner text={"Loading model..."} />
 
 
 
 
 
 
27
  ) : isTranscribing ? (
28
- <Spinner text={"Transcribing..."} />
 
 
 
 
 
29
  ) : (
30
- "Transcribe Audio"
 
 
 
 
 
31
  )}
32
  </button>
33
  );
34
  }
35
 
36
- export function Spinner(props: { text: string }): JSX.Element {
 
 
 
 
37
  return (
38
- <div role='status'>
39
- <svg
40
- aria-hidden='true'
41
- role='status'
42
- className='inline w-4 h-4 mr-3 text-white animate-spin'
43
- viewBox='0 0 100 101'
44
- fill='none'
45
- xmlns='http://www.w3.org/2000/svg'
46
- >
47
- <path
48
- d='M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z'
49
- fill='#E5E7EB'
50
- />
51
- <path
52
- d='M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z'
53
- fill='currentColor'
54
- />
55
- </svg>
56
- {props.text}
57
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  );
59
- }
 
16
  }
17
  }}
18
  disabled={isTranscribing || disabled}
19
+ className={`
20
+ relative group flex items-center justify-center gap-2
21
+ min-w-[180px] px-6 py-3 rounded-xl font-semibold text-sm
22
+ transform transition-all duration-200
23
  ${isModelLoading || isTranscribing
24
+ ? 'bg-green-600 hover:bg-green-700 animate-pulse-slow'
25
+ : disabled
26
+ ? 'bg-gray-300 cursor-not-allowed'
27
+ : className || 'bg-blue-600 hover:bg-blue-700 hover:scale-105 active:scale-100'
28
+ }
29
+ shadow-lg hover:shadow-xl text-white
30
+ `}
31
  >
32
  {isModelLoading ? (
33
+ <>
34
+ <svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
35
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
36
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
37
+ </svg>
38
+ <span>Loading Model...</span>
39
+ </>
40
  ) : isTranscribing ? (
41
+ <>
42
+ <svg className="w-5 h-5 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
43
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" />
44
+ </svg>
45
+ <span>Transcribing...</span>
46
+ </>
47
  ) : (
48
+ <>
49
+ <svg className="w-5 h-5 transition-transform group-hover:scale-110" fill="none" stroke="currentColor" viewBox="0 0 24 24">
50
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" />
51
+ </svg>
52
+ <span>Transcribe Audio</span>
53
+ </>
54
  )}
55
  </button>
56
  );
57
  }
58
 
59
+ export function RecordButton(props: {
60
+ isRecording: boolean;
61
+ onClick: () => void;
62
+ disabled?: boolean;
63
+ }): JSX.Element {
64
  return (
65
+ <button
66
+ onClick={props.onClick}
67
+ disabled={props.disabled}
68
+ className={`
69
+ relative group flex items-center justify-center gap-2
70
+ min-w-[160px] px-6 py-3 rounded-xl font-semibold text-sm
71
+ transform transition-all duration-200
72
+ ${props.isRecording
73
+ ? 'bg-red-600 hover:bg-red-700 animate-pulse-slow'
74
+ : props.disabled
75
+ ? 'bg-gray-300 cursor-not-allowed'
76
+ : 'bg-blue-600 hover:bg-blue-700 hover:scale-105 active:scale-100'
77
+ }
78
+ shadow-lg hover:shadow-xl text-white
79
+ `}
80
+ >
81
+ {props.isRecording ? (
82
+ <>
83
+ <span className="absolute left-0 top-0 h-full w-full overflow-hidden rounded-xl">
84
+ <span className="absolute -left-1 top-1/2 h-8 w-8 -translate-y-1/2 translate-x-0 animate-record-pulse rounded-full bg-red-400/30"></span>
85
+ </span>
86
+ <svg className="w-5 h-5 animate-pulse" fill="currentColor" viewBox="0 0 24 24">
87
+ <rect x="6" y="6" width="12" height="12" rx="1" />
88
+ </svg>
89
+ <span>Stop Recording</span>
90
+ </>
91
+ ) : (
92
+ <>
93
+ <svg className="w-5 h-5 transition-transform group-hover:scale-110" fill="none" stroke="currentColor" viewBox="0 0 24 24">
94
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 18.75a6 6 0 006-6v-1.5m-6 7.5a6 6 0 01-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 01-3-3V4.5a3 3 0 116 0v8.25a3 3 0 01-3 3z" />
95
+ </svg>
96
+ <span>Start Recording</span>
97
+ </>
98
+ )}
99
+ </button>
100
  );
101
+ }
src/components/modal/Modal.tsx CHANGED
@@ -80,7 +80,7 @@ export default function Modal({
80
  )}
81
  <button
82
  type='button'
83
- className='inline-flex justify-center rounded-md border border-transparent bg-indigo-100 px-4 py-2 text-sm font-medium text-indigo-900 hover:bg-indigo-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 transition-all duration-300'
84
  onClick={onClose}
85
  >
86
  Close
 
80
  )}
81
  <button
82
  type='button'
83
+ className='inline-flex justify-center items-center rounded-md border border-transparent bg-indigo-100 px-4 py-2 text-sm font-medium text-indigo-900 hover:bg-indigo-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 transition-all duration-300 mx-auto'
84
  onClick={onClose}
85
  >
86
  Close