Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 79 additions & 9 deletions components/Chatbot/Chatbot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import ChatbotHeader from '../Interface-Chatbot/ChatbotHeader';
import ChatbotHeaderTab from '../Interface-Chatbot/ChatbotHeaderTab';
import ChatbotTextField from '../Interface-Chatbot/ChatbotTextField';
import MessageList from '../Interface-Chatbot/Messages/MessageList';
import PlanningQuestionsCard from '../Interface-Chatbot/Messages/PlanningQuestionsCard';
import StarterQuestions from '../Interface-Chatbot/Messages/StarterQuestions';

// Utils
Expand All @@ -24,6 +25,7 @@ import { setToggleDrawer } from '@/store/chat/chatSlice';
import { useAppDispatch } from '@/store/useTypedHooks';
import { useCustomSelector } from '@/utils/deepCheckSelector';
import { useChatEffects } from './hooks/useChatEffects';
import { useSendMessage } from './hooks/useChatActions';
import { useColor } from './hooks/useColor';
import { useHelloEffects } from './hooks/useHelloEffects';
import { useReduxEffects } from './hooks/useReduxEffects';
Expand Down Expand Up @@ -63,16 +65,84 @@ const EmptyChatView = React.memo(({ defaultMessage }: { defaultMessage?: string
</div>
));

const ActiveChatView = React.memo(() => (
<div className="flex flex-col h-full overflow-auto" style={{ height: '100vh' }} data-testid="chatbot-active-view">
<div className="flex-1 overflow-y-auto max-w-5xl mx-auto w-full scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent" data-testid="chatbot-messages-container">
<MessageList />
</div>
<div className="max-w-5xl mx-auto p-3 pb-3 w-full" data-testid="chatbot-input-section">
<ChatbotTextField />
const ActiveChatView = React.memo(() => {
const { activePlanningQuestions, executionWaitingTaskId, executionWaitingQuestions, isLastMessageStreaming } = useCustomSelector((state) => {
const subThreadId = state.Chat?.subThreadId;
const messageIds = subThreadId ? state.Chat?.messageIds?.[subThreadId] || [] : [];
const lastMessageId = messageIds[0] || null;
const lastMessage = lastMessageId ? state.Chat?.msgIdAndDataMap?.[subThreadId]?.[lastMessageId] : null;
const planning = lastMessage?.planning;
const planData = planning?.plan;
const execution = planning?.execution;
const isPlanningCompleted = execution?.state === "completed";
const isNewPlanFormat = Boolean(planData && typeof planData === "object" && ("message_to_user" in planData || "questions" in planData));
const planQuestions = isNewPlanFormat && !isPlanningCompleted ? (planData.questions || []) : [];

let waitingTaskId = "";
let waitingQuestions: Array<{ id: string; question: string; options?: string[] }> = [];
if (planQuestions.length === 0 && execution?.tasks && typeof execution.tasks === "object") {
const entry = Object.entries(execution.tasks).find(
([, t]: [string, any]) => t?.status === "waiting_for_user" && Array.isArray(t?.questions) && t.questions.length > 0,
);
if (entry) {
waitingTaskId = entry[0];
waitingQuestions = (entry[1] as any).questions;
}
}

return {
activePlanningQuestions: planQuestions as Array<{ id: string; question: string; options?: string[] }>,
executionWaitingTaskId: waitingTaskId,
executionWaitingQuestions: waitingQuestions,
isLastMessageStreaming: Boolean(lastMessage?.isStreaming),
};
});

const sendMessage = useSendMessage({});

const showExecutionQuestions = activePlanningQuestions.length === 0 && executionWaitingQuestions.length > 0;

return (
<div className="flex flex-col h-full overflow-auto" style={{ height: '100vh' }} data-testid="chatbot-active-view">
<div className="flex-1 overflow-y-auto max-w-5xl mx-auto w-full scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent" data-testid="chatbot-messages-container">
<MessageList />
</div>
<div className="max-w-5xl mx-auto w-full" data-testid="chatbot-input-section">
{activePlanningQuestions.length > 0 && (
<PlanningQuestionsCard
questions={activePlanningQuestions}
isStreaming={isLastMessageStreaming}
floatingMode
onSubmit={(answersText) =>
sendMessage({ message: answersText, mode: "plan", skipUserEcho: true, silent: true })
}
/>
)}
{showExecutionQuestions && (
<PlanningQuestionsCard
questions={executionWaitingQuestions}
isStreaming={isLastMessageStreaming}
floatingMode
onSubmit={(answersText) =>
sendMessage({
message: answersText,
action: "respond",
mode: "plan",
skipUserEcho: true,
silent: true,
task_id: executionWaitingTaskId,
})
}
/>
)}
<div className="px-3 pb-3">
<ChatbotTextField />
</div>
</div>
</div>
</div>
));
);
});



function Chatbot({ chatSessionId, tabSessionId }: ChatbotProps) {
Expand Down
4 changes: 4 additions & 0 deletions components/Chatbot/hooks/useChatActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,12 +356,16 @@ export const useSendMessage = ({
if (parsed.event === "task_waiting_for_user") {
isExecutionStreamActive = true;
isExecutionWaitingForUser = true;
const incomingQuestions = Array.isArray(parsed.questions) ? parsed.questions : [];
globalDispatch(updatePlanningExecutionState({
executionState: "paused",
taskUpdate: {
id: parsed.task_id,
title: parsed.title,
status: "waiting_for_user",
...(incomingQuestions.length > 0 ? { questions: incomingQuestions } : {}),
...(parsed.result ? { result: parsed.result } : {}),
...(parsed.error ? { error: parsed.error } : {}),
},
}));
return true;
Expand Down
52 changes: 35 additions & 17 deletions components/Interface-Chatbot/Messages/AssistantMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { AlertCircle, Check, Copy, Loader2, Maximize2, ThumbsDown, ThumbsUp } fr
import React, { useContext, useMemo, useCallback } from "react";
import ReactMarkdown from "react-markdown";
import ImageWithFallback from "./ImageWithFallback";
import Image from "next/image";
import { AiIcon } from "@/assests/assestsIndex";
import "./Message.css";
import RenderNode from "../../richUI/RenderNode";
import { componentRegistry } from "../../richUI/componentRegistry";
Expand Down Expand Up @@ -163,8 +165,8 @@ const AssistantMessageCard = React.memo(
/>
</div>
</div> */}
<div className="flex flex-col max-w-[90%] animate-slide-left w-full ">
<div className="p-2.5">
<div className={`flex flex-col animate-slide-left w-full ${message?.planning ? 'max-w-full' : 'max-w-[90%]'}`}>
<div className="p-2.5 w-full">

{message?.wait ? (
<div className="w-full">
Expand Down Expand Up @@ -222,6 +224,7 @@ const AssistantMessageCard = React.memo(
toolsData={toolsData}
isStreaming={message?.isStreaming}
hasContent={!!message?.content}
planning={planning}
hideToolCall={hideToolCall}
/>
) : (
Expand All @@ -232,10 +235,10 @@ const AssistantMessageCard = React.memo(
{item.message_to_user && (
<p className="not-prose text-sm leading-relaxed mb-1">{item.message_to_user}</p>
)}
{Array.isArray(item.questions) && item.questions.length > 0 && (
<PlanningQuestionsCard
questions={item.questions}
isHistorical
{item.questions && item.questions.length > 0 && (
<PlanningQuestionsCard
questions={item.questions}
isHistorical={true}
answers={item.answers}
/>
)}
Expand All @@ -245,22 +248,37 @@ const AssistantMessageCard = React.memo(
<p className="not-prose text-sm leading-relaxed mb-1">{messageToUser}</p>
)}
{isNewPlanFormat && planQuestions.length > 0 && !isPlanningCompleted && (
<PlanningQuestionsCard
questions={planQuestions}
isStreaming={message?.isStreaming}
onSubmit={(answersText) => sendMessage({ message: answersText, mode: "plan", skipUserEcho: true, silent: true })}
/>
/* Active planning questions are now shown above the text field as a floating panel */
null
)}
{message?.isPlanningLoading && (
<div className="flex items-center gap-2 mt-3 mb-2 px-1">
<Loader2 className="w-3.5 h-3.5 animate-spin text-base-content/50 dark:text-base-content/60 shrink-0" />
<span className="text-xs text-base-content/50 dark:text-base-content/60">Planning...</span>
<div className="flex items-center gap-2 mt-3 mb-2 px-1 not-prose">
<Image
src={AiIcon}
width={22}
height={22}
alt="AI"
className="rounded-full bg-[#3EA9FC] p-1 shrink-0"
/>
<span className="text-[13px] font-semibold text-base-content/60 dark:text-base-content/70">
Planning
</span>
<Loader2 className="w-3.5 h-3.5 animate-spin text-base-content/40 dark:text-base-content/50 shrink-0 ml-1" />
</div>
)}
{message?.isSynthesizerLoading && (
<div className="flex items-center gap-2 mt-3 mb-2 px-1">
<Loader2 className="w-3.5 h-3.5 animate-spin text-base-content/50 dark:text-base-content/60 shrink-0" />
<span className="text-xs text-base-content/50 dark:text-base-content/60">Preparing output...</span>
<div className="flex items-center gap-2 mt-3 mb-2 px-1 not-prose">
<Image
src={AiIcon}
width={22}
height={22}
alt="AI"
className="rounded-full bg-[#3EA9FC] p-1 shrink-0"
/>
<span className="text-[13px] font-semibold text-base-content/60 dark:text-base-content/70">
Preparing output
</span>
<Loader2 className="w-3.5 h-3.5 animate-spin text-base-content/40 dark:text-base-content/50 shrink-0 ml-1" />
</div>
)}
{planning && <PlanningTasksCard plan={planning} isStreaming={message?.isStreaming} onAction={handlePlanningAction} />}
Expand Down
Loading