import { useEffect, useState, useCallback, useRef } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useAppStore } from '@/store'; import { useChat } from '@/hooks/useChat'; import { ChatMessage } from './ChatMessage'; import { ChatInput } from './ChatInput'; import { PipelineProgress } from './PipelineProgress'; import { Button } from '@/components/ui/button'; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select'; import { RefreshCw, Loader2, StopCircle, X, MessageCircle, ArrowDown } from 'lucide-react'; import { api } from '@/lib/api'; import type { Account, Message } from '@/types'; export function ChatView() { const { conversationId } = useParams<{ conversationId?: string }>(); const navigate = useNavigate(); const { setConversations, selectedAccountId, conversations } = useAppStore(); const { messages, connected, thinking, toolStatus, pipeline, send, stop, createConversation, removeMessage } = useChat(conversationId || null); const [manifestPath, setManifestPath] = useState(null); const [accounts, setAccounts] = useState([]); const [quote, setQuote] = useState(null); const [pendingMessage, setPendingMessage] = useState<{ content: string; images?: Array<{ type: 'image'; data: string; mimeType: string }> } | null>(null); const [showScrollBtn, setShowScrollBtn] = useState(false); const creatingRef = useRef(false); const scrollRef = useRef(null); const userScrolledUp = useRef(false); useEffect(() => { api.listAccounts().then(setAccounts).catch(() => {}); }, []); useEffect(() => { api.listConversations().then(setConversations).catch(() => {}); }, [messages]); useEffect(() => { const toolMsgs = messages.filter((m) => m.role === 'tool'); if (toolMsgs.length > 0) { try { const lastTool = JSON.parse(toolMsgs[toolMsgs.length - 1].content); if (lastTool.manifest) setManifestPath(lastTool.manifest); } catch {} } }, [messages]); // Auto-scroll: follow new content unless user has scrolled up useEffect(() => { const el = scrollRef.current; if (!el) return; if (userScrolledUp.current) return; el.scrollTop = el.scrollHeight; }, [messages, thinking, pipeline, toolStatus]); // Detect user manual scroll useEffect(() => { const el = scrollRef.current; if (!el) return; const onScroll = () => { const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80; userScrolledUp.current = !atBottom; setShowScrollBtn(!atBottom); }; el.addEventListener('scroll', onScroll, { passive: true }); return () => el.removeEventListener('scroll', onScroll); }, []); // Reset scroll state on conversation switch useEffect(() => { userScrolledUp.current = false; setShowScrollBtn(false); }, [conversationId]); // After navigating to a new conversation, send the pending message once history is loaded useEffect(() => { if (conversationId && connected && pendingMessage) { const timer = setTimeout(() => { send(pendingMessage.content, pendingMessage.images); setPendingMessage(null); }, 300); return () => clearTimeout(timer); } }, [conversationId, connected, pendingMessage]); const handleResume = async () => { if (!manifestPath) return; await fetch('/api/pipeline/resume', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ manifest: manifestPath }), }); }; const findPrecedingUser = useCallback((msgId: string): Message | null => { const idx = messages.findIndex((m) => m.id === msgId); if (idx <= 0) return null; for (let i = idx - 1; i >= 0; i--) { if (messages[i].role === 'user') return messages[i]; } return null; }, [messages]); const handleRegenerate = useCallback((msgId: string) => { const prev = findPrecedingUser(msgId); if (prev) send(prev.content); }, [findPrecedingUser, send]); const handleContinue = useCallback(() => { send('请继续'); }, [send]); const handleQuote = useCallback((content: string) => { setQuote(content.slice(0, 200)); }, []); const handleDeleteMsg = useCallback((msgId: string) => { removeMessage(msgId); }, [removeMessage]); // Delayed conversation creation const handleSendNew = useCallback(async (content: string, images?: Array<{ type: 'image'; data: string; mimeType: string }>) => { if (creatingRef.current) return; creatingRef.current = true; setPendingMessage({ content, images }); try { const newId = await createConversation(content.slice(0, 30), selectedAccountId || undefined); if (newId) { const list = await api.listConversations(); setConversations(list); navigate(`/chat/${newId}`); } } catch { // fallback } creatingRef.current = false; }, [createConversation, selectedAccountId]); const handleSend = useCallback((content: string, images?: Array<{ type: 'image'; data: string; mimeType: string }>) => { if (quote) { content = `> ${quote}\n\n${content}`; setQuote(null); } if (conversationId) { send(content, images); } else { handleSendNew(content, images); } }, [send, quote, conversationId]); const handleStop = useCallback(() => { stop(); }, [stop]); // Empty state - no conversation selected if (!conversationId) { const recentConvs = conversations.slice(0, 5); return (

开始新对话

输入消息开始创作,对话将自动创建

{accounts.length > 0 && (
当前账号:
)} {recentConvs.length > 0 && (

最近对话

{recentConvs.map((conv) => ( ))}
)}
); } // Active conversation return (
{/* Header bar */}
{connected ? '在线' : '连接中'}
{manifestPath && ( )} {thinking && ( )}
{/* Chat area */}
{/* Message list */}
{messages.map((msg, i) => ( ))} {pipeline && ( )} {thinking && !pipeline && !messages.some(m => m.role === 'tool' && m.toolStatus === 'running') && (
{toolStatus || '思考中...'}
)}
{/* Scroll to bottom FAB */} {showScrollBtn && ( )} {/* Quote bar */} {quote && (
引用: {quote}
)} {/* Input */}
); }