73 lines
2.7 KiB
TypeScript
73 lines
2.7 KiB
TypeScript
|
|
import { useEffect, useRef } from 'react'
|
||
|
|
import { Terminal } from 'lucide-react'
|
||
|
|
import type { TerminalLine } from '../../types'
|
||
|
|
import { resolveInfraNode } from '../../lib/infraCatalog'
|
||
|
|
import { cn } from '../../lib/utils'
|
||
|
|
|
||
|
|
type Props = {
|
||
|
|
subjectId: string | null
|
||
|
|
subjectLabel: string
|
||
|
|
lines: TerminalLine[]
|
||
|
|
busy: boolean
|
||
|
|
expanded: boolean
|
||
|
|
onToggle: () => void
|
||
|
|
}
|
||
|
|
|
||
|
|
const LEVEL: Record<string, string> = {
|
||
|
|
info: 'text-foreground-muted',
|
||
|
|
ok: 'text-success',
|
||
|
|
warn: 'text-warning',
|
||
|
|
err: 'text-danger',
|
||
|
|
cmd: 'text-docker',
|
||
|
|
llm: 'text-violet-400',
|
||
|
|
}
|
||
|
|
|
||
|
|
export function TerminalDock({ subjectId, subjectLabel, lines, busy, expanded, onToggle }: Props) {
|
||
|
|
const bottomRef = useRef<HTMLDivElement>(null)
|
||
|
|
const infra = resolveInfraNode(subjectId)
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (expanded) bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||
|
|
}, [lines, busy, expanded])
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className={cn('flex shrink-0 flex-col border-t border-border bg-black/80', expanded ? 'h-[200px]' : 'h-9')}>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={onToggle}
|
||
|
|
className="flex shrink-0 items-center justify-between px-3 py-2 text-left hover:bg-white/5"
|
||
|
|
>
|
||
|
|
<span className="flex items-center gap-2 text-[10px] font-medium text-emerald-300">
|
||
|
|
<Terminal className="h-3.5 w-3.5" />
|
||
|
|
Terminal — {subjectLabel}
|
||
|
|
{busy && <span className="animate-pulse text-docker">● live</span>}
|
||
|
|
</span>
|
||
|
|
<span className="font-mono text-[8px] text-foreground-faint">{lines.length} lines · {expanded ? '▼' : '▲'}</span>
|
||
|
|
</button>
|
||
|
|
|
||
|
|
{expanded && (
|
||
|
|
<div className="scrollbar-thin min-h-0 flex-1 overflow-y-auto px-3 pb-2 font-mono text-[9px] leading-relaxed">
|
||
|
|
{infra && (
|
||
|
|
<p className="mb-1 text-foreground-faint">
|
||
|
|
<span className="text-docker">$</span> {infra.ssh} <span className="text-foreground-faint/70">(gekopieerd bij Shell-knop)</span>
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
{lines.length === 0 && (
|
||
|
|
<p className="py-4 text-center text-foreground-faint">Selecteer een node of agent · klik Shell of Probe om output te zien</p>
|
||
|
|
)}
|
||
|
|
{lines.map((line) => (
|
||
|
|
<div key={line.id} className={LEVEL[line.level] || 'text-foreground-muted'}>
|
||
|
|
<span className="text-foreground-faint/60">
|
||
|
|
{line.ts ? new Date(line.ts).toLocaleTimeString('en-US', { hour12: false }) : ''}
|
||
|
|
</span>{' '}
|
||
|
|
<span className="text-docker/80">[{line.phase}]</span> {line.text}
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
{busy && <span className="text-docker animate-pulse">█</span>}
|
||
|
|
<div ref={bottomRef} />
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|