feat(ui): add ConfirmDialog and StreamingTerminal components

This commit is contained in:
Scrum4Me Agent 2026-05-13 18:00:15 +02:00
parent 2baf116841
commit 394e8cdde3
2 changed files with 151 additions and 0 deletions

View file

@ -0,0 +1,62 @@
'use client'
type Props = {
open: boolean
title?: string
commandPreview: string
onConfirm: () => void
onCancel: () => void
loading?: boolean
}
export default function ConfirmDialog({
open,
title = 'Confirm action',
commandPreview,
onConfirm,
onCancel,
loading = false,
}: Props) {
if (!open) return null
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div
className="absolute inset-0 bg-black/50"
onClick={onCancel}
aria-hidden="true"
/>
<div className="relative z-10 w-full max-w-lg rounded-xl border border-border bg-background p-6 shadow-lg">
<h2 className="text-base font-semibold tracking-tight">{title}</h2>
<p className="mt-1 text-sm text-muted-foreground">
The following command will be executed on the server:
</p>
<pre className="mt-3 overflow-x-auto rounded-lg border border-border bg-muted/50 px-4 py-3 font-mono text-xs text-foreground select-all">
{commandPreview}
</pre>
<p className="mt-3 text-xs text-muted-foreground">
This action cannot be undone. Review the command above before confirming.
</p>
<div className="mt-5 flex justify-end gap-3">
<button
onClick={onCancel}
disabled={loading}
className="rounded-lg border border-border px-4 py-2 text-sm hover:bg-muted/50 disabled:opacity-50 transition-colors"
>
Cancel
</button>
<button
onClick={onConfirm}
disabled={loading}
className="rounded-lg bg-destructive/10 border border-destructive/30 px-4 py-2 text-sm font-medium text-destructive hover:bg-destructive/20 disabled:opacity-50 transition-colors"
>
{loading ? 'Running…' : 'Confirm'}
</button>
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,89 @@
'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>
)
}