111 lines
4.5 KiB
TypeScript
111 lines
4.5 KiB
TypeScript
import { Loader2 } from 'lucide-react'
|
||
import ScriptPreview from './ScriptPreview'
|
||
|
||
export interface ShellExecutePreviewProps {
|
||
/** shell_execute 的输出文本(已做 ANSI 处理) */
|
||
output: string
|
||
/** 顶部和终端中展示的工具名称,默认 shell_execute */
|
||
toolLabel?: string
|
||
/** 是否处于加载中(统一在终端里展示 loading) */
|
||
loading?: boolean
|
||
}
|
||
|
||
export function ShellExecutePreview({
|
||
output,
|
||
toolLabel = 'shell_execute',
|
||
loading = false,
|
||
}: ShellExecutePreviewProps) {
|
||
const safeOutput = output || 'No output'
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="flex items-center justify-center py-10 h-full">
|
||
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div
|
||
className={`flex w-full bg-background text-foreground ${
|
||
!loading ? 'animate-in fade-in-0 slide-in-from-bottom-2 duration-300' : ''
|
||
}`}
|
||
style={{ height: 'calc(100% + 20px)' }}
|
||
>
|
||
{/* 右侧主体区域 */}
|
||
<div className="flex-1 flex flex-col h-full">
|
||
{/* 内容区域:一张终端卡片 */}
|
||
<main className="flex-1 overflow-y-auto custom-scrollbar px-3 md:px-6 py-4 md:py-6 flex items-center justify-center">
|
||
<div className="max-w-4xl mx-auto space-y-4 md:space-y-6 min-w-[60%]">
|
||
{/* 终端卡片,主进场:从下浮现 + 略微缩放 */}
|
||
<div
|
||
className={`min-h-[200px] overflow-hidden rounded-xl border ${
|
||
!loading
|
||
? 'animate-in fade-in-0 zoom-in-95 slide-in-from-bottom-4 duration-500'
|
||
: ''
|
||
}`}
|
||
style={{
|
||
backgroundColor: 'var(--terminal-background)',
|
||
borderColor: 'var(--terminal-border)',
|
||
...( !loading ? { animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)' } : {}),
|
||
}}
|
||
>
|
||
<div
|
||
className="flex items-center justify-between border-b px-3 py-2 md:px-4"
|
||
style={{
|
||
backgroundColor: 'var(--terminal-surface)',
|
||
borderColor: 'var(--terminal-border)',
|
||
}}
|
||
>
|
||
<div
|
||
className={`flex gap-1.5 ${
|
||
!loading ? 'animate-in fade-in zoom-in-75 duration-700 fill-mode-both' : ''
|
||
}`}
|
||
>
|
||
<div className="h-2.5 w-2.5 rounded-full transition-colors" style={{ backgroundColor: 'color-mix(in srgb, var(--destructive) 72%, transparent)' }} />
|
||
<div className="h-2.5 w-2.5 rounded-full transition-colors" style={{ backgroundColor: 'color-mix(in srgb, var(--warning) 72%, transparent)' }} />
|
||
<div className="h-2.5 w-2.5 rounded-full transition-colors" style={{ backgroundColor: 'color-mix(in srgb, var(--success) 72%, transparent)' }} />
|
||
</div>
|
||
<span
|
||
className={`text-[10px] font-mono ${
|
||
!loading ? 'animate-in fade-in slide-in-from-top-1 duration-700 fill-mode-both' : ''
|
||
}`}
|
||
style={{ color: 'var(--terminal-text-muted)' }}
|
||
>
|
||
bash — {toolLabel}
|
||
</span>
|
||
</div>
|
||
<div className="relative space-y-2 py-3 font-mono text-[11px] md:py-4 md:text-xs" style={{ color: 'var(--terminal-text)' }}>
|
||
<div
|
||
className={`flex gap-2 mb-1 px-3 md:px-4 ${
|
||
!loading ? 'animate-in fade-in-0 slide-in-from-left-2 duration-500' : ''
|
||
}`}
|
||
>
|
||
<span style={{ color: 'var(--terminal-prompt)' }}>$</span>
|
||
<span className="truncate" style={{ color: 'var(--terminal-text)' }}>{toolLabel}</span>
|
||
</div>
|
||
<div
|
||
className={`mt-2 max-h-[420px] overflow-auto whitespace-pre-wrap border-t px-3 pt-2 custom-scrollbar md:px-4 ${
|
||
!loading ? 'animate-in fade-in-0 slide-in-from-bottom-2 duration-500' : ''
|
||
}`}
|
||
style={{
|
||
borderColor: 'var(--terminal-border)',
|
||
color: 'var(--terminal-text)',
|
||
}}
|
||
>
|
||
<ScriptPreview
|
||
code={safeOutput}
|
||
language="bash"
|
||
title={toolLabel}
|
||
theme="github-dark-default"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|