import { useCallback, useEffect, useRef } from 'react'; import { SSEProgress } from '../types'; import { IconCross, IconCheckmark, IconGear, IconCopy } from './Icons'; interface ProgressPanelProps { progress: SSEProgress | null; logLines: string[]; visible: boolean; } export default function ProgressPanel({ progress, logLines, visible }: ProgressPanelProps) { const logEndRef = useRef(null); // Auto-scroll the log view to the bottom whenever new lines arrive. useEffect(() => { logEndRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }); }, [logLines]); // All hooks MUST run before any early return, so derive the values the // callback needs without depending on `progress` being non-null. const message = progress?.message ?? ''; const handleCopyError = useCallback(() => { const text = logLines.length > 0 ? logLines.join('\n') : message; navigator.clipboard.writeText(text).catch(() => { const el = document.querySelector('.error-detail-textarea') as HTMLTextAreaElement | null; if (el) { el.select(); document.execCommand('copy'); } }); }, [logLines, message]); if (!visible || !progress) return null; const isFailed = progress.stage === '生成失败' || progress.stage === '错误'; const isDone = progress.stage === '完成'; return (
{isFailed ? <> 生成失败 : isDone ? <> 生成完成 : <> 词云生成中…}
{progress.stage}
{(progress.elapsedSeconds != null || progress.clientElapsedSeconds != null) && (
{progress.elapsedSeconds != null && `后端耗时 ${progress.elapsedSeconds.toFixed(2)} 秒`} {progress.elapsedSeconds != null && progress.clientElapsedSeconds != null ? ' · ' : ''} {progress.clientElapsedSeconds != null && `前端显示耗时 ${progress.clientElapsedSeconds.toFixed(2)} 秒`}
)} {!isFailed && (
)} {/* ── 详细日志(实时滚动) ───────────────────────────────── */} {logLines.length > 0 && (
{logLines.map((line, i) => (
{line}
))}
)} {/* ── 失败时的错误详情 + 复制按钮 ───────────────────────── */}
{isFailed ? (
) : null}
); }