Current File : /home/mdkeenpw/public_html/wp-content/plugins/extendify/src/Agent/components/ChatMessages.jsx
import { createElement, useEffect, useRef, useState } from '@wordpress/element';
import { ScrollDownButton } from '@agent/components/ScrollDownButton';
import { ScrollIntoViewOnce } from '@agent/components/ScrollIntoViewOnce';
import { AgentMessage } from '@agent/components/messages/AgentMessage';
import { StatusMessage } from '@agent/components/messages/StatusMessage';
import { UserMessage } from '@agent/components/messages/UserMessage';
import { WorkflowComponent } from '@agent/components/messages/WorkflowComponent';
import { WorkflowMessage } from '@agent/components/messages/WorkflowMessage';
import { useChatStore } from '@agent/state/chat';
import { useGlobalStore } from '@agent/state/global';
import { useWorkflowStore } from '@agent/state/workflows';

export const ChatMessages = () => {
	const { open } = useGlobalStore();
	const { messages } = useChatStore();
	const { getWhenFinishedToolProps, getWorkflow } = useWorkflowStore();
	const workflow = getWorkflow();
	const whenFinishedToolProps = getWhenFinishedToolProps();
	const whenFinishedComponent = workflow?.whenFinished?.component;
	const [canScrollDown, setCanScrollDown] = useState(false);
	const containerRef = useRef(null);
	const isFreshPageLoad = useRef(true);

	// If last message is a user message, move it to the top
	const isUserMessage =
		messages.filter(({ type }) => type !== 'status').at(-1)?.details?.role ===
		'user';

	useEffect(() => {
		if (!containerRef.current || !open) return;
		if (!isFreshPageLoad.current) return;
		isFreshPageLoad.current = false;
		// Scroll to the bottom of the chat container on load
		containerRef.current.scrollTo({ top: containerRef.current.scrollHeight });
		return () => {
			isFreshPageLoad.current = true;
		};
	}, [open]);

	// Handles scrolling to the top of the last user message
	// TODO: if the user sends in a long message, maybe we scroll to the bottom
	// of the message offset by 2-3 lines
	useEffect(() => {
		if (!containerRef.current) return;
		if (!isUserMessage) return;
		const c = containerRef.current;
		const messages = c.querySelectorAll('[data-agent-message-role="user"]');
		const last = messages[messages.length - 1];
		if (!last || messages.length < 2) return;
		const scrollArea = c.querySelector('#extendify-agent-chat-scroll-area');
		const lastRect = last.getBoundingClientRect();
		const innerHeight = Array.from(scrollArea.children).reduce(
			(sum, child) => sum + child.offsetHeight,
			0,
		);
		const minHeight = innerHeight + c.clientHeight - lastRect.height;
		scrollArea.style.minHeight = `${minHeight}px`;
		last.scrollIntoView({ behavior: 'smooth', block: 'start' });
	}, [isUserMessage, messages]);

	// Handles the scroll down button visibility
	useEffect(() => {
		if (!containerRef.current) return;
		const c = containerRef.current;
		const last = c.querySelector(
			'#extendify-agent-chat-scroll-area > :last-child',
		);
		if (!last) return;

		const observer = new IntersectionObserver(
			([entry]) => {
				const containerRect = c.getBoundingClientRect();
				const lastRect = entry.boundingClientRect;
				const isBelowViewport = lastRect.bottom > containerRect.bottom;
				setCanScrollDown(!entry.isIntersecting && isBelowViewport);
			},
			{ root: c },
		);
		observer.observe(last);
		return () => observer.disconnect();
	}, [messages]);

	return (
		<div
			ref={containerRef}
			style={{ overscrollBehavior: 'contain' }}
			className="relative flex-grow overflow-y-auto overflow-x-hidden p-1 pb-0 text-sm text-gray-900 md:p-2">
			<div id="extendify-agent-chat-scroll-area">
				{messages.map((message) => {
					const isLastMessage = messages.at(-1)?.id === message.id;
					const freshLoad = isFreshPageLoad.current;
					if (message.details?.role === 'user') {
						return <UserMessage key={message.id} message={message} />;
					}
					if (message.details?.role === 'assistant') {
						return (
							<AgentMessage
								key={message.id}
								animate={!freshLoad}
								message={message}
							/>
						);
					}
					if (message.type === 'workflow') {
						return <WorkflowMessage key={message.id} message={message} />;
					}
					if (message.type === 'workflow-component') {
						return <WorkflowComponent key={message.id} message={message} />;
					}
					if (
						message.type === 'status' &&
						// Only show the status if it's last, or a workflow-tool-completed message
						(isLastMessage ||
							['workflow-tool-completed', 'workflow-tool-canceled'].includes(
								message.details?.type,
							))
					) {
						const isError = message.details?.type === 'error';
						return (
							<StatusMessage
								animate={!isError}
								key={message.id}
								status={message}
							/>
						);
					}
					return null;
				})}
				{!workflow?.needsRedirect?.() &&
				whenFinishedToolProps?.id &&
				whenFinishedComponent ? (
					<ScrollIntoViewOnce>
						{createElement(whenFinishedComponent, whenFinishedToolProps)}
					</ScrollIntoViewOnce>
				) : null}
				{workflow?.needsRedirect?.() ? <workflow.redirectComponent /> : null}
			</div>
			<ScrollDownButton
				canScrollDown={canScrollDown}
				onClick={() => {
					if (!containerRef.current) return;
					const c = containerRef.current;
					// Scroll the last message into view
					const last = c.querySelector(
						'#extendify-agent-chat-scroll-area > :last-child',
					);
					if (!last) return;
					last.scrollIntoView({ behavior: 'smooth', block: 'start' });
				}}
			/>
		</div>
	);
};