import React, { useRef, useEffect, useState } from 'react'; import { motion, useMotionValue, useTransform } from 'framer-motion'; import { MessageCard } from './MessageCard'; import { NoticeCard } from './NoticeCard'; import { TypeCardSkeleton } from './TypeCardSkeleton'; import { TaskCard } from './TaskCard'; import { VanillaChatStore } from '@/store/chatStore'; interface QueryGroup { queryId: string; userMessage: any; taskMessage?: any; otherMessages: any[]; } interface UserQueryGroupProps { chatId: string; chatStore: VanillaChatStore; queryGroup: QueryGroup; isActive: boolean; onQueryActive: (queryId: string | null) => void; index: number; } export const UserQueryGroup: React.FC = ({ chatId, chatStore, queryGroup, isActive, onQueryActive, index }) => { const groupRef = useRef(null); const taskBoxRef = useRef(null); const [isTaskBoxSticky, setIsTaskBoxSticky] = useState(false); const chatState = chatStore.getState(); const activeTaskId = chatState.activeTaskId; // Show task if this query group has a task message OR if it's the most recent user query during splitting // During splitting phase (no to_sub_tasks yet), show task for the most recent query only // Exclude human-reply scenarios (when user is replying to an activeAsk) const isHumanReply = queryGroup.userMessage && activeTaskId && chatState.tasks[activeTaskId] && (chatState.tasks[activeTaskId].activeAsk || // Check if this user message follows an 'ask' message in the message sequence (() => { const messages = chatState.tasks[activeTaskId].messages; const userMessageIndex = messages.findIndex((m: any) => m.id === queryGroup.userMessage.id); if (userMessageIndex > 0) { // Check the previous message - if it's an agent message with step 'ask', this is a human-reply const prevMessage = messages[userMessageIndex - 1]; return prevMessage?.role === 'agent' && prevMessage?.step === 'ask'; } return false; })()); const isLastUserQuery = !queryGroup.taskMessage && !isHumanReply && activeTaskId && chatState.tasks[activeTaskId] && queryGroup.userMessage && queryGroup.userMessage.id === chatState.tasks[activeTaskId].messages.filter((m: any) => m.role === 'user').pop()?.id && // Only show during active phases (not finished) chatState.tasks[activeTaskId].status !== 'finished'; const task = (queryGroup.taskMessage || isLastUserQuery) && activeTaskId ? chatState.tasks[activeTaskId] : null; // Set up intersection observer for this query group useEffect(() => { if (!groupRef.current) return; const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { onQueryActive(queryGroup.queryId); } }); }, { rootMargin: '-20% 0px -60% 0px', threshold: 0.1 } ); observer.observe(groupRef.current); return () => { observer.disconnect(); }; }, [queryGroup.queryId, onQueryActive]); // Set up intersection observer for sticky detection useEffect(() => { if (!taskBoxRef.current || !task) return; // Create a sentinel element to detect when the sticky element becomes stuck const sentinel = document.createElement('div'); sentinel.style.position = 'absolute'; sentinel.style.top = '0px'; sentinel.style.left = '0px'; sentinel.style.width = '1px'; sentinel.style.height = '1px'; sentinel.style.pointerEvents = 'none'; sentinel.style.zIndex = '-1'; // Insert sentinel before the sticky element taskBoxRef.current.parentNode?.insertBefore(sentinel, taskBoxRef.current); const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { // When sentinel is not visible, the sticky element is stuck const isSticky = !entry.isIntersecting; setIsTaskBoxSticky(isSticky); }); }, { rootMargin: '0px 0px 0px 0px', threshold: 0 } ); observer.observe(sentinel); return () => { observer.disconnect(); sentinel.remove(); }; }, [task]); // Check if we're in skeleton phase const isSkeletonPhase = task && ( (!task.messages.find((m: any) => m.step === "to_sub_tasks") && !task.hasWaitComfirm && task.messages.length > 0) || task.isTakeControl ); return ( {/* User Query (render only if exists) */} {queryGroup.userMessage && ( {}} attaches={queryGroup.userMessage.attaches} /> )} {/* Sticky Task Box - Show for each query group that has a task */} {task && (
{ chatState.setIsTaskEdit(activeTaskId as string, true); chatState.addTaskInfo(); }} onUpdateTask={(taskIndex, content) => { chatState.setIsTaskEdit(activeTaskId as string, true); chatState.updateTaskInfo(taskIndex, content); }} onDeleteTask={(taskIndex) => { chatState.setIsTaskEdit(activeTaskId as string, true); chatState.deleteTaskInfo(taskIndex); }} clickable={true} />
)} {/* Other Messages */} {queryGroup.otherMessages.map((message) => { if (message.content.length > 0) { if (message.step === "end") { return ( {}} /> {/* File List */} {message.fileList && (
{message.fileList.map((file: any) => ( { chatState.setSelectedFile(activeTaskId as string, file); chatState.setActiveWorkSpace(activeTaskId as string, "documentWorkSpace"); }} className="flex items-center gap-2 bg-message-fill-default rounded-sm px-2 py-1 w-[140px] cursor-pointer hover:bg-message-fill-hover transition-colors" >
{file.name.split(".")[0]}
{file.type}
))}
)}
); } else if (message.content === "skip") { return ( {}} /> ); } else { return ( {}} attaches={message.attaches} /> ); } } else if (message.step === "end" && message.content === "") { return ( {message.fileList && (
{message.fileList.map((file: any) => ( { chatState.setSelectedFile(activeTaskId as string, file); chatState.setActiveWorkSpace(activeTaskId as string, "documentWorkSpace"); }} className="flex items-center gap-2 bg-message-fill-default rounded-sm px-2 py-1 w-[140px] cursor-pointer hover:bg-message-fill-hover transition-colors" >
{file.name.split(".")[0]}
{file.type}
))}
)}
); } // Notice Card if ( message.step === "notice_card" && !task?.isTakeControl && task?.cotList && task.cotList.length > 0 ) { return ; } return null; })} {/* Skeleton for loading state */} {isSkeletonPhase && ( )}
); };