import { useStore } from '@nanostores/react'; import { useLocation } from '@remix-run/react'; import classNames from 'classnames'; import type { ForwardedRef } from 'react'; import { Fragment, forwardRef, memo, useEffect, useMemo, useRef } from 'react'; import { toast } from 'sonner'; import WithTooltip from '~/.client/components/ui/Tooltip'; import { useAuth } from '~/.client/hooks/useAuth'; import { useChatOperate } from '~/.client/hooks/useChatOperate'; import { useSnapScroll } from '~/.client/hooks/useSnapScroll'; import { aiState, type ParsedUIMessage } from '~/.client/stores/ai-state'; import { AssistantMessage } from './AssistantMessage'; import styles from './Messages.module.scss'; import { UserMessage } from './UserMessage'; interface MessagesProps { id?: string; className?: string; isStreaming?: boolean; } const MessageItem = memo( forwardRef< HTMLDivElement, { message: ParsedUIMessage; index: number; isFirst: boolean; isLast: boolean; isStreaming: boolean; userInfo: any; onRewind: (messageId: string) => void; onFork: (messageId: string) => void; } >(({ message, index, isFirst, isLast, isStreaming, userInfo, onRewind, onFork }, ref) => { const { role, id: messageId } = message; const isUserMessage = role === 'user'; const isHidden = message.metadata?.isHidden; if (isHidden) { return ; } return (
{isUserMessage && (
{userInfo?.picture ? ( {userInfo?.user ) : (
)}
)}
{isUserMessage ? : }
{!isUserMessage && (
{messageId && (
)}
); }), ); export const Messages = forwardRef( (props: MessagesProps, ref: ForwardedRef | undefined) => { const { id } = props; const location = useLocation(); const { userInfo } = useAuth(); const { forkMessage } = useChatOperate(); const { chatId, parseMessages, isStreaming } = useStore(aiState); const containerRef = useRef(null); // 使用useSnapScroll钩子获取自动滚动功能 const [messageRef, scrollRef] = useSnapScroll(); // 组合refs: 外部传入的ref、内部的containerRef和scrollRef useEffect(() => { if (containerRef.current) { scrollRef(containerRef.current); } // 连接外部ref和内部ref if (typeof ref === 'function') { ref(containerRef.current); } else if (ref) { ref.current = containerRef.current; } }, [ref, scrollRef]); const handleRewind = (messageId: string) => { const searchParams = new URLSearchParams(location.search); searchParams.set('rewindTo', messageId); window.location.search = searchParams.toString(); }; const handleFork = async (messageId: string) => { try { if (!chatId) { return; } const id = await forkMessage(chatId, messageId); window.location.href = `/chat/${id}`; } catch (error) { toast.error('分叉聊天失败: ' + (error as Error).message); } }; const messageItems = useMemo(() => { return parseMessages.map((message, index) => { const isFirst = index === 0; const isLast = index === parseMessages.length - 1; const refToApply = isLast ? messageRef : undefined; return ( ); }); }, [isStreaming, parseMessages, userInfo, messageRef]); return (
{parseMessages.length > 0 ? messageItems : null} {isStreaming && (
)}
); }, );