89 lines
2.5 KiB
TypeScript
89 lines
2.5 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useRef } from 'react'
|
|
|
|
export type TerminalLine = {
|
|
type: 'stdout' | 'stderr'
|
|
text: string
|
|
}
|
|
|
|
export type TerminalStatus = 'idle' | 'running' | 'done' | 'failed' | 'error'
|
|
|
|
type Props = {
|
|
lines: TerminalLine[]
|
|
status: TerminalStatus
|
|
error?: string | null
|
|
className?: string
|
|
}
|
|
|
|
export default function StreamingTerminal({ lines, status, error, className = '' }: Props) {
|
|
const bottomRef = useRef<HTMLDivElement>(null)
|
|
|
|
useEffect(() => {
|
|
bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
}, [lines])
|
|
|
|
const statusBar = () => {
|
|
if (status === 'running') {
|
|
return (
|
|
<div className="flex items-center gap-2 px-3 py-1.5 text-xs text-muted-foreground border-t border-border">
|
|
<span className="inline-block size-2 rounded-full bg-amber-400 animate-pulse" />
|
|
Running…
|
|
</div>
|
|
)
|
|
}
|
|
if (status === 'done') {
|
|
return (
|
|
<div className="flex items-center gap-2 px-3 py-1.5 text-xs text-green-400 border-t border-border">
|
|
<span className="inline-block size-2 rounded-full bg-green-400" />
|
|
Completed successfully
|
|
</div>
|
|
)
|
|
}
|
|
if (status === 'failed') {
|
|
return (
|
|
<div className="flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 border-t border-border">
|
|
<span className="inline-block size-2 rounded-full bg-red-500" />
|
|
Exited with error
|
|
</div>
|
|
)
|
|
}
|
|
if (status === 'error') {
|
|
return (
|
|
<div className="flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 border-t border-border">
|
|
<span className="inline-block size-2 rounded-full bg-red-500" />
|
|
{error ?? 'Unknown error'}
|
|
</div>
|
|
)
|
|
}
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={
|
|
'flex flex-col rounded-lg border border-border bg-zinc-950 font-mono text-xs overflow-hidden ' +
|
|
className
|
|
}
|
|
>
|
|
<div className="flex-1 overflow-y-auto max-h-96 p-3 space-y-0">
|
|
{lines.length === 0 && status === 'running' && (
|
|
<span className="text-zinc-500 animate-pulse">Waiting for output…</span>
|
|
)}
|
|
{lines.map((line, i) => (
|
|
<pre
|
|
key={i}
|
|
className={
|
|
'whitespace-pre-wrap break-all leading-5 ' +
|
|
(line.type === 'stderr' ? 'text-red-400' : 'text-zinc-100')
|
|
}
|
|
>
|
|
{line.text}
|
|
</pre>
|
|
))}
|
|
<div ref={bottomRef} />
|
|
</div>
|
|
{statusBar()}
|
|
</div>
|
|
)
|
|
}
|