| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import { useEffect, useRef } from 'react'; |
| |
|
| | export default function TranscriptionDisplay({ fixedText, activeText, timestamp, isRecording, autoScroll = true, onAutoScrollToggle }) { |
| | const containerRef = useRef(null); |
| |
|
| | |
| | useEffect(() => { |
| | if (autoScroll && containerRef.current) { |
| | containerRef.current.scrollTop = containerRef.current.scrollHeight; |
| | } |
| | }, [fixedText, activeText, autoScroll]); |
| |
|
| | const formatTimestamp = (seconds) => { |
| | const mins = Math.floor(seconds / 60); |
| | const secs = (seconds % 60).toFixed(1); |
| | return `${mins}:${secs.padStart(4, '0')}`; |
| | }; |
| |
|
| | return ( |
| | <div className="w-full max-w-4xl mx-auto"> |
| | <div className="bg-gray-900 rounded-lg border border-gray-700 p-6 shadow-xl"> |
| | {/* Header */} |
| | <div className="flex items-center justify-between mb-4 pb-4 border-b border-gray-700"> |
| | <h2 className="text-xl font-semibold text-gray-100"> |
| | Live Transcription |
| | </h2> |
| | <div className="flex items-center gap-4"> |
| | {isRecording && ( |
| | <div className="flex items-center gap-2"> |
| | <div className="w-3 h-3 bg-red-500 rounded-full animate-pulse"></div> |
| | <span className="text-sm text-gray-300">Recording</span> |
| | </div> |
| | )} |
| | {timestamp > 0 && ( |
| | <span className="text-sm text-gray-400 font-mono"> |
| | {formatTimestamp(timestamp)} |
| | </span> |
| | )} |
| | </div> |
| | </div> |
| | |
| | {/* Transcription Text */} |
| | <div |
| | ref={containerRef} |
| | className="min-h-[200px] max-h-[400px] overflow-y-auto font-sans text-lg leading-relaxed" |
| | > |
| | {!fixedText && !activeText && !isRecording && ( |
| | <p className="text-gray-500 italic"> |
| | Click "Start Recording" to begin transcription... |
| | </p> |
| | )} |
| | |
| | {!fixedText && !activeText && isRecording && ( |
| | <p className="text-gray-500 italic animate-pulse"> |
| | Listening... |
| | </p> |
| | )} |
| | |
| | {/* Fixed text (yellow) - sentences that won't change */} |
| | {fixedText && ( |
| | <span className="text-yellow-400 font-medium"> |
| | {fixedText} |
| | </span> |
| | )} |
| | |
| | {/* Space between fixed and active */} |
| | {fixedText && activeText && ' '} |
| | |
| | {/* Active text (cyan dim) - current partial transcription */} |
| | {activeText && ( |
| | <span className="text-cyan-400 opacity-80"> |
| | {activeText} |
| | </span> |
| | )} |
| | </div> |
| | |
| | {/* Legend + Auto-scroll Toggle */} |
| | <div className="mt-4 pt-4 border-t border-gray-700 flex items-center justify-between"> |
| | <div className="flex gap-6 text-sm"> |
| | <div className="flex items-center gap-2"> |
| | <div className="w-4 h-4 bg-yellow-400 rounded"></div> |
| | <span className="text-gray-300">Fixed sentences</span> |
| | </div> |
| | <div className="flex items-center gap-2"> |
| | <div className="w-4 h-4 bg-cyan-400 opacity-80 rounded"></div> |
| | <span className="text-gray-300">Active transcription</span> |
| | </div> |
| | </div> |
| | |
| | {/* Auto-scroll Toggle */} |
| | {onAutoScrollToggle && ( |
| | <button |
| | onClick={onAutoScrollToggle} |
| | className={`px-3 py-1 rounded text-xs font-medium transition-all duration-200 ${ |
| | autoScroll |
| | ? 'bg-cyan-900/20 border border-cyan-700/50 text-cyan-400 hover:bg-cyan-900/30' |
| | : 'bg-gray-800 border border-gray-600 text-gray-400 hover:bg-gray-700' |
| | }`} |
| | title={autoScroll ? 'Disable auto-scroll to read from top' : 'Enable auto-scroll to follow live transcription'} |
| | > |
| | {autoScroll ? '🔒 Auto-scroll' : '🔓 Scroll locked'} |
| | </button> |
| | )} |
| | </div> |
| | </div> |
| | |
| | {/* Technical Details */} |
| | <div className="mt-4 text-xs text-gray-500 text-center"> |
| | <p> |
| | Smart progressive streaming: Growing window (0-15s) → Sentence-aware sliding (>15s) |
| | </p> |
| | </div> |
| | </div> |
| | ); |
| | } |
| |
|