import { useEffect, useRef, useState, useCallback } from 'react' import { motion, AnimatePresence } from 'framer-motion' import { X, Sparkles, Loader2, AlertTriangle, Copy, Check, RefreshCw, Database, Settings2, Send, Wand2, Minimize2, History, } from 'lucide-react' import { cn } from '@/lib/cn' import { MarkdownRenderer } from './MarkdownRenderer' import { type ActiveTask, type HistoryReport, minimizeDialog, closeDialog, startAnalysis, } from '@/lib/aiReportStore' interface Props { /** 当前展示的任务;活跃任务或历史报告 */ task: ActiveTask | HistoryReport | null mode: 'active' | 'history' | null minimized: boolean } type Phase = 'loading' | 'streaming' | 'done' | 'error' // 统一字段读取:活跃任务有 phase/createdAt,历史报告没有(按 done 处理) function getPhase(task: ActiveTask | HistoryReport | null): Phase { if (!task) return 'loading' if ('phase' in task) return task.phase return 'done' // 历史报告视为已完成 } function getContent(task: ActiveTask | HistoryReport | null): string { return task?.content ?? '' } function getMeta(task: ActiveTask | HistoryReport | null) { if (!task) return null if ('meta' in task) return task.meta // 历史报告 return { summary: task.summary, periods: task.periods } } export function AiAnalysisDialog({ task, mode, minimized }: Props) { const scrollRef = useRef(null) const focusInputRef = useRef(null) const [focus, setFocus] = useState('') const [copied, setCopied] = useState(false) const phase = getPhase(task) const content = getContent(task) const meta = getMeta(task) const isHistory = mode === 'history' const isWorking = phase === 'loading' || phase === 'streaming' const open = !!task && !minimized // 流式时自动滚动到底部 useEffect(() => { if (open && phase === 'streaming' && scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight } }, [content, phase, open]) // 切换任务时回填 focus useEffect(() => { setFocus(task && 'focus' in task ? task.focus : '') }, [task]) const handleStartNew = useCallback(async () => { if (!task) return const name = 'name' in task ? task.name : '' await startAnalysis(task.symbol, name, focus.trim()) }, [task, focus]) const handleCopy = async () => { if (!content) return try { await navigator.clipboard.writeText(content) setCopied(true) setTimeout(() => setCopied(false), 2000) } catch { /* ignore */ } } if (!open) return null const error = task && 'error' in task ? task.error : '' return ( { if (e.target === e.currentTarget && !isWorking) closeDialog() }} > {/* ===== 头部 ===== */}
{isHistory ? : }
{isHistory ? '历史分析报告' : 'AI 财务分析'} {task && {task.name}} {task && {task.symbol}}
{meta?.summary ? ( {meta.summary} ) : isWorking ? 正在准备数据… : null} {phase === 'streaming' && ( 生成中 )} {isHistory && task && 'created_at' in task && ( {fmtRelative(task.created_at)} )}
{/* 右侧操作按钮 */}
{/* 复制:仅在内容就绪且非生成中显示 */} {content && !isWorking && ( )} {/* 生成中:仅最小化(后台继续生成),无关闭按钮 */} {!isHistory && isWorking && ( )} {/* 完成态/历史报告:显示关闭按钮 */} {(!isWorking || isHistory) && ( )}
{/* ===== 内容区 ===== */}
{/* 加载态 */} {phase === 'loading' && !content && (
AI 正在分析财务数据…
读取利润表 / 资负表 / 现金流 / 核心指标,生成专业报告
)} {/* 错误态 */} {phase === 'error' && (
分析失败
{error}
{error.includes('AI') && ( )}
)} {/* 报告内容 */} {(content || phase === 'streaming') && (
{phase === 'streaming' && ( )}
)}
{/* ===== 底部:自定义关注点输入 ===== */}
关注重点
setFocus(e.target.value)} onKeyDown={e => { if (e.key === 'Enter' && (phase === 'done' || phase === 'error' || isHistory)) handleStartNew() }} disabled={isWorking} placeholder={isHistory ? '修改关注重点,回车重新生成' : (phase === 'done' ? '如:重点看债务风险…回车重新分析' : '可留空,留空则全面分析')} className={cn( 'flex-1 h-8 px-3 rounded-lg bg-base ring-1 ring-border/30 text-xs text-foreground placeholder:text-muted/40', 'focus:outline-none focus:ring-2 focus:ring-purple-400/30 transition-shadow disabled:opacity-50', )} /> {isHistory ? ( ) : ( )}

{isHistory ? '历史报告为静态记录;修改关注重点后将作为新任务重新生成。报告仅供参考,不构成投资建议。' : '报告由项目已配置的 AI 模型基于本地财务数据生成;可在输入框追加关注点后重新生成。报告仅供参考,不构成投资建议。'}

) } // ===== 小工具 ===== function fmtRelative(iso: string): string { try { const t = new Date(iso).getTime() const diff = Date.now() - t if (diff < 60_000) return '刚刚' if (diff < 3600_000) return `${Math.floor(diff / 60_000)} 分钟前` if (diff < 86400_000) return `${Math.floor(diff / 3600_000)} 小时前` if (diff < 7 * 86400_000) return `${Math.floor(diff / 86400_000)} 天前` return new Date(iso).toLocaleDateString('zh-CN') } catch { return '' } }