// ============================================================ // ChatPanel — 消息展示面板 // 职责:渲染对话消息列表、流式光标、元数据、自动滚动、文本输入 // ============================================================ import { useEffect, useRef, useState } from "react"; import { useI18n } from "../../lib/i18n"; import type { ChatMessage } from "../../types"; import type { ConnectionStatus } from "../../lib/websocket"; interface ChatPanelProps { messages: ChatMessage[]; currentReply?: string; connectionStatus: ConnectionStatus; onSendText?: (text: string) => void; } export function ChatPanel({ messages, currentReply, connectionStatus, onSendText }: ChatPanelProps) { const bottomRef = useRef(null); const containerRef = useRef(null); const isAutoScroll = useRef(true); const [inputText, setInputText] = useState(""); const { t } = useI18n(); const isConnected = connectionStatus === "connected"; // 用户上滚时暂停自动滚动,滚到底部时恢复 useEffect(() => { const container = containerRef.current; if (!container) return; const handleScroll = () => { const { scrollTop, scrollHeight, clientHeight } = container; isAutoScroll.current = scrollHeight - scrollTop - clientHeight < 60; }; container.addEventListener("scroll", handleScroll); return () => container.removeEventListener("scroll", handleScroll); }, []); // 新消息或流式更新时自动滚动 useEffect(() => { if (isAutoScroll.current) { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); } }, [messages, currentReply]); // 提交文本消息 const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!inputText.trim() || !onSendText) return; onSendText(inputText); setInputText(""); }; // 未连接时的空状态 if (!isConnected) { return (
💬

{t("chat.empty.prompt")}

{t("chat.empty.hint")}
); } return (
{/* 空状态提示 */} {messages.length === 0 && !currentReply && (
💬

{t("chat.welcome.prompt")}

{t("chat.welcome.hint")}
)} {messages.map((msg, index) => (
{msg.role === "user" ? t("chat.userLabel") : "AI"}
{msg.content}
{msg.role === "assistant" && msg.tokensUsed !== undefined && (
{msg.tokensUsed} tokens {msg.latencyMs !== undefined && ` · ${(msg.latencyMs / 1000).toFixed(1)}s`} {msg.model && ` · ${msg.model}`}
)}
))} {/* 流式回复(尚未完成) */} {currentReply && (
AI
{currentReply}
)}
{/* 文本输入框 - 连接后始终显示 */} {onSendText && (
setInputText(e.target.value)} />
)}
); }