| import { useRef, useEffect, useState, type KeyboardEvent } from 'react'; |
|
|
| interface TerminalLine { |
| type: 'prompt' | 'stdout' | 'stderr' | 'info' | 'dim'; |
| text: string; |
| } |
|
|
| interface TerminalPanelProps { |
| lines: TerminalLine[]; |
| onCommand: (cmd: string) => void; |
| isRunning: boolean; |
| onSendInput: (text: string) => void; |
| } |
|
|
| export type { TerminalLine }; |
|
|
| export default function TerminalPanel({ lines, onCommand, isRunning, onSendInput }: TerminalPanelProps) { |
| const [commandInput, setCommandInput] = useState(''); |
| const [activeTab, setActiveTab] = useState<'terminal' | 'problems' | 'output'>('terminal'); |
| const scrollRef = useRef<HTMLDivElement>(null); |
| const inputRef = useRef<HTMLInputElement>(null); |
|
|
| useEffect(() => { |
| if (scrollRef.current) { |
| scrollRef.current.scrollTop = scrollRef.current.scrollHeight; |
| } |
| }, [lines, commandInput, isRunning]); |
|
|
| useEffect(() => { |
| if (activeTab === 'terminal' && inputRef.current) { |
| inputRef.current.focus(); |
| } |
| }, [isRunning, activeTab, lines]); |
|
|
| const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => { |
| if (e.key === 'Enter') { |
| if (isRunning) { |
| onSendInput(commandInput); |
| } else if (commandInput.trim()) { |
| onCommand(commandInput.trim()); |
| } |
| setCommandInput(''); |
| } |
| }; |
|
|
| return ( |
| <div className="flex flex-col h-full" style={{ background: '#000000' }}> |
| {/* Tab bar */} |
| <div |
| className="flex items-center justify-between px-3" |
| style={{ borderBottom: '1px solid #222233', background: '#000000', minHeight: 36 }} |
| > |
| <div className="flex items-center gap-1"> |
| {(['terminal', 'problems', 'output'] as const).map((tab) => ( |
| <button |
| key={tab} |
| onClick={() => setActiveTab(tab)} |
| className="px-3 py-1.5 text-xs font-bold uppercase tracking-wide transition-colors rounded" |
| style={{ |
| color: activeTab === tab ? '#e2e2f0' : '#55556a', |
| background: activeTab === tab ? '#111111' : 'transparent', |
| border: 'none', |
| cursor: 'pointer', |
| }} |
| > |
| {tab} |
| </button> |
| ))} |
| </div> |
| <button |
| onClick={() => onCommand('clear')} |
| className="text-xs font-semibold transition-colors" |
| style={{ color: '#55556a', background: 'none', border: 'none', cursor: 'pointer' }} |
| onMouseEnter={(e) => (e.currentTarget.style.color = '#e2e2f0')} |
| onMouseLeave={(e) => (e.currentTarget.style.color = '#55556a')} |
| > |
| Clear |
| </button> |
| </div> |
| |
| {/* Terminal body — unified output + inline input */} |
| <div |
| ref={scrollRef} |
| onClick={() => inputRef.current?.focus()} |
| className={`flex-1 overflow-auto min-h-0 p-3 ryp-compiler-scroll ryp-terminal ${activeTab !== 'terminal' ? 'hidden' : ''}`} |
| > |
| {lines.map((line, i) => ( |
| <div key={i} className="flex gap-0 whitespace-pre-wrap" style={{ minHeight: '1.7em' }}> |
| {line.type === 'prompt' && <span className="ryp-terminal__prompt">{line.text}</span>} |
| {line.type === 'stdout' && <span className="ryp-terminal__stdout">{line.text}</span>} |
| {line.type === 'stderr' && <span className="ryp-terminal__stderr">{line.text}</span>} |
| {line.type === 'info' && <span className="ryp-terminal__info">{line.text}</span>} |
| {line.type === 'dim' && <span className="ryp-terminal__dim">{line.text}</span>} |
| </div> |
| ))} |
| |
| {/* Inline input line — last line of the terminal, scrolls with output */} |
| <div className="flex items-center whitespace-pre-wrap" style={{ minHeight: '1.7em' }}> |
| {!isRunning && <span className="ryp-terminal__prompt mr-1">~/ryp $</span>} |
| <input |
| ref={inputRef} |
| value={commandInput} |
| onChange={(e) => setCommandInput(e.target.value)} |
| onKeyDown={handleKeyDown} |
| className="flex-1 bg-transparent border-none outline-none p-0 m-0 font-mono text-sm" |
| style={{ |
| color: '#e2e2f0', |
| caretColor: '#e2e2f0', |
| fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, 'Courier New', monospace", |
| }} |
| spellCheck={false} |
| autoComplete="off" |
| autoFocus |
| /> |
| </div> |
| </div> |
| |
| {/* Problems Tab Content */} |
| <div className={`flex-1 flex items-center justify-center text-[#55556a] text-xs ${activeTab !== 'problems' ? 'hidden' : ''}`}> |
| No problems detected. |
| </div> |
| |
| {/* Output Tab Content */} |
| <div className={`flex-1 flex items-center justify-center text-[#55556a] text-xs ${activeTab !== 'output' ? 'hidden' : ''}`}> |
| Execution output will appear here. |
| </div> |
| </div> |
| ); |
| } |
|
|