Spaces:
Sleeping
Sleeping
better layout
Browse files- src/components/AudioManager.tsx +290 -182
- src/components/TranscribeButton.tsx +71 -29
- src/components/modal/Modal.tsx +1 -1
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
|
259 |
-
|
260 |
-
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
301 |
-
|
302 |
-
|
303 |
-
<div className='
|
304 |
-
<
|
305 |
-
|
306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
307 |
{props.transcriber.progressItems.map((data) => (
|
308 |
-
<
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
</div>
|
314 |
))}
|
315 |
</div>
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
|
|
|
|
|
|
|
|
|
|
325 |
</>
|
326 |
);
|
327 |
}
|
@@ -332,29 +361,73 @@ function SettingsTile(props: {
|
|
332 |
transcriber: Transcriber;
|
333 |
}) {
|
334 |
const [showModal, setShowModal] = useState(false);
|
|
|
335 |
|
336 |
-
|
337 |
-
|
338 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
339 |
|
340 |
-
|
341 |
-
|
342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
343 |
|
344 |
-
|
345 |
-
|
346 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
347 |
|
348 |
-
return (
|
349 |
-
<div className={props.className}>
|
350 |
-
<Tile icon={props.icon} onClick={onClick} />
|
351 |
<SettingsModal
|
352 |
show={showModal}
|
353 |
-
onSubmit={
|
354 |
-
onClose={
|
355 |
transcriber={props.transcriber}
|
356 |
/>
|
357 |
-
|
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 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
555 |
<RecordModal
|
556 |
show={showModal}
|
557 |
-
onSubmit={
|
558 |
-
|
559 |
-
|
|
|
|
|
|
|
|
|
|
|
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 |
-
<
|
593 |
-
<
|
594 |
-
|
595 |
-
|
|
|
|
|
|
|
596 |
</div>
|
597 |
}
|
598 |
content={
|
599 |
-
<div className="space-y-
|
600 |
-
|
601 |
-
|
|
|
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">
|
606 |
</div>
|
607 |
-
<ul className="text-sm text-
|
608 |
-
<li>Speak clearly
|
609 |
-
<li>
|
610 |
-
<li
|
611 |
</ul>
|
612 |
</div>
|
613 |
|
614 |
-
|
615 |
-
|
616 |
-
|
617 |
-
|
618 |
-
|
619 |
-
|
620 |
-
|
|
|
621 |
</div>
|
622 |
|
|
|
623 |
{audioBlob && (
|
624 |
-
<div className="flex items-center
|
625 |
-
<
|
626 |
-
<
|
627 |
-
|
628 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
629 |
</div>
|
630 |
)}
|
631 |
</div>
|
632 |
}
|
633 |
onClose={onClose}
|
634 |
submitText={
|
635 |
-
<div className="flex items-center gap-2">
|
636 |
-
|
637 |
-
|
638 |
-
|
639 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
640 |
</div>
|
641 |
}
|
642 |
submitEnabled={audioBlob !== undefined}
|
643 |
-
submitClassName={
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
674 |
-
|
675 |
-
|
676 |
-
|
677 |
-
stroke='currentColor'
|
678 |
>
|
|
|
|
|
|
|
679 |
<path
|
680 |
-
|
681 |
-
|
682 |
-
|
|
|
|
|
|
|
683 |
/>
|
684 |
<path
|
685 |
-
strokeLinecap=
|
686 |
-
strokeLinejoin=
|
687 |
-
|
|
|
|
|
688 |
/>
|
689 |
</svg>
|
690 |
);
|
@@ -693,16 +766,51 @@ function SettingsIcon() {
|
|
693 |
function MicrophoneIcon() {
|
694 |
return (
|
695 |
<svg
|
696 |
-
|
697 |
-
|
698 |
-
|
699 |
-
|
700 |
-
stroke='currentColor'
|
701 |
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
702 |
<path
|
703 |
-
|
704 |
-
|
705 |
-
|
|
|
|
|
|
|
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={`
|
|
|
|
|
|
|
20 |
${isModelLoading || isTranscribing
|
21 |
-
? '
|
22 |
-
:
|
23 |
-
|
|
|
|
|
|
|
|
|
24 |
>
|
25 |
{isModelLoading ? (
|
26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
) : isTranscribing ? (
|
28 |
-
|
|
|
|
|
|
|
|
|
|
|
29 |
) : (
|
30 |
-
|
|
|
|
|
|
|
|
|
|
|
31 |
)}
|
32 |
</button>
|
33 |
);
|
34 |
}
|
35 |
|
36 |
-
export function
|
|
|
|
|
|
|
|
|
37 |
return (
|
38 |
-
<
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|