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 ? (

) : (
)}
)}
{!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 && (
)}
);
},
);