"use client"; import type { ChangeEvent } from 'react'; import { useState, useEffect, useRef, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { Button } from '@/components/ui/button'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Skeleton } from '@/components/ui/skeleton'; import { generateMCQ, type GenerateMCQOutput } from '@/ai/flows/generate-mcq'; import { explainIncorrectAnswer } from '@/ai/flows/explain-incorrect-answer-flow'; import { explainCorrectAnswer } from '@/ai/flows/explain-correct-answer-flow'; import type { ChatMessage as ChatMessageType, ActiveMCQ } from '@/types'; import { ChatMessage } from './chat-message'; import { useToast } from '@/hooks/use-toast'; import { Send, FileText, Loader2 } from 'lucide-react'; const MAX_QUESTIONS = 5; interface ChatbotProps { imageDataUri: string | null; journeyTitle: string; journeyId: string; } export function Chatbot({ imageDataUri, journeyTitle, journeyId }: ChatbotProps) { const router = useRouter(); const [messages, setMessages] = useState([]); const [currentMCQ, setCurrentMCQ] = useState(null); const [isLoading, setIsLoading] = useState(false); // General loading for AI responses const [isAwaitingAnswer, setIsAwaitingAnswer] = useState(false); const [hasAnsweredCorrectly, setHasAnsweredCorrectly] = useState(false); const [incorrectAttempts, setIncorrectAttempts] = useState([]); const [questionCount, setQuestionCount] = useState(0); const scrollAreaRef = useRef(null); const { toast } = useToast(); const messageIdCounter = useRef(0); const isInitialQuestionRef = useRef(true); const addMessage = useCallback((message: Omit) => { messageIdCounter.current += 1; const newId = `${Date.now()}-${messageIdCounter.current}`; setMessages(prev => [...prev, { ...message, id: newId, timestamp: new Date() }]); }, []); useEffect(() => { let ignore = false; // Comprehensive reset for new journey/image setMessages([]); setCurrentMCQ(null); setIsLoading(false); setIsAwaitingAnswer(false); setHasAnsweredCorrectly(false); setIncorrectAttempts([]); setQuestionCount(0); isInitialQuestionRef.current = true; messageIdCounter.current = 0; const performInitialFetch = async () => { if (!imageDataUri) { // This case should ideally be handled by parent not rendering Chatbot, // or showing a "waiting for image" state if Chatbot is rendered before URI is ready. if (!ignore && messages.length === 0) { // Add a temporary "preparing" message if no URI yet, but this will be cleared. addMessage({ sender: 'ai', type: 'text', text: "Preparing your journey..." }); } return; } if (!ignore) setIsLoading(true); try { // Clear any "Preparing..." message before fetching the actual question if (messages.length === 1 && messages[0].text === "Preparing your journey...") { setMessages([]); } const mcqResult = await generateMCQ({ imageDataUri }); if (ignore) return; let activeMcqData: ActiveMCQ = { ...mcqResult, originalQuestionTextForFlow: mcqResult.mcq, }; if (isInitialQuestionRef.current) { activeMcqData.mcq = `Okay, let's start with ${journeyTitle}! ${mcqResult.mcq}`; isInitialQuestionRef.current = false; } if (ignore) return; setCurrentMCQ(activeMcqData); addMessage({ sender: 'ai', type: 'mcq', mcq: activeMcqData }); setIsAwaitingAnswer(true); } catch (error) { if (ignore) return; console.error("Error generating initial MCQ:", error); const errorMessage = error instanceof Error ? error.message : "An unknown error occurred."; addMessage({ sender: 'ai', type: 'error', text: `Failed to generate initial question: ${errorMessage}` }); toast({ title: "Error", description: `Could not generate the first question. ${errorMessage}`, variant: "destructive", }); } finally { if (!ignore) setIsLoading(false); } }; // Trigger initial fetch if imageDataUri is present. // If imageDataUri is not present initially, this effect will re-run when it becomes available. if (imageDataUri) { performInitialFetch(); } else if (messages.length === 0) { // If there's no image URI and no messages, show "Preparing..." // This typically means the parent component is still loading the image. addMessage({ sender: 'ai', type: 'text', text: "Preparing your journey..." }); } return () => { ignore = true; // Cleanup to prevent state updates on unmounted component }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [imageDataUri, journeyTitle]); // addMessage is memoized useEffect(() => { if (scrollAreaRef.current) { scrollAreaRef.current.scrollTo({ top: scrollAreaRef.current.scrollHeight, behavior: 'smooth' }); } }, [messages]); const handleOptionSelect = async (option: string, isCorrect: boolean) => { if (!currentMCQ) return; setIsAwaitingAnswer(false); // User has made a selection addMessage({ sender: 'user', type: 'user', // This type indicates it's the user's selected option text text: option, }); setIsLoading(true); const questionForAI = currentMCQ.originalQuestionTextForFlow || currentMCQ.mcq; if (isCorrect) { let combinedMessage = "That's correct! Well done."; try { const explanationResult = await explainCorrectAnswer({ question: questionForAI, options: currentMCQ.options, correctAnswer: currentMCQ.correctAnswer, }); combinedMessage += ` ${explanationResult.explanation}`; } catch (error) { console.error("Error fetching correct answer explanation:", error); const errorMsg = error instanceof Error ? error.message : "An unknown error occurred"; combinedMessage += ` (Sorry, I couldn't provide an explanation for that: ${errorMsg})`; } addMessage({ sender: 'ai', type: 'feedback', text: combinedMessage, isCorrect: true }); setHasAnsweredCorrectly(true); setIncorrectAttempts([]); const newQuestionCount = questionCount + 1; setQuestionCount(newQuestionCount); if (newQuestionCount >= MAX_QUESTIONS) { addMessage({ sender: 'ai', type: 'text', text: "You've completed all 5 questions! Please proceed to the summary report." }); } } else { // Incorrect answer const updatedIncorrectAttempts = [...incorrectAttempts, option]; setIncorrectAttempts(updatedIncorrectAttempts); // Check if all incorrect options have been exhausted if (updatedIncorrectAttempts.length >= currentMCQ.options.length - 1) { let combinedText = ""; try { // Explain why the *last chosen* option was incorrect const incorrectExplanationResult = await explainIncorrectAnswer({ question: questionForAI, options: currentMCQ.options, selectedAnswer: option, correctAnswer: currentMCQ.correctAnswer, }); combinedText += `That's not quite right. ${incorrectExplanationResult.explanation} `; } catch (error) { console.error("Error fetching explanation for the last incorrect answer:", error); const errorMsg = error instanceof Error ? error.message : "An unknown error occurred"; combinedText += `That's not quite right. (Failed to get explanation for your choice: ${errorMsg}) `; } combinedText += `The correct answer is "${currentMCQ.correctAnswer}". `; try { // Explain why the actual correct answer is correct const correctExplanationResult = await explainCorrectAnswer({ question: questionForAI, options: currentMCQ.options, correctAnswer: currentMCQ.correctAnswer, }); combinedText += correctExplanationResult.explanation; } catch (error) { console.error("Error fetching correct answer explanation after exhausting options:", error); const errorMsg = error instanceof Error ? error.message : "An unknown error occurred"; combinedText += `(Failed to get explanation for the correct answer: ${errorMsg})`; } addMessage({ sender: 'ai', type: 'feedback', // This will be handled by ChatMessage to not show options text: combinedText.trim(), isCorrect: true, // Mark as 'correct' flow-wise to enable next question/summary }); setHasAnsweredCorrectly(true); // Allows "Next Question" or "Summary" setIncorrectAttempts([]); const newQuestionCount = questionCount + 1; setQuestionCount(newQuestionCount); if (newQuestionCount >= MAX_QUESTIONS) { addMessage({ sender: 'ai', type: 'text', text: "You've completed all 5 questions! Please proceed to the summary report." }); } } else { // Still other options left to try try { const explanationResult = await explainIncorrectAnswer({ question: questionForAI, options: currentMCQ.options, selectedAnswer: option, correctAnswer: currentMCQ.correctAnswer, }); const repromptText = `That's not quite right. ${explanationResult.explanation} Would you like to try again?`; const repromptMCQData: ActiveMCQ = { ...currentMCQ, // carries over originalQuestionTextForFlow, options, correctAnswer mcq: repromptText, // This is the new display text for the question part of the message }; addMessage({ sender: 'ai', type: 'mcq', mcq: repromptMCQData }); setCurrentMCQ(repromptMCQData); // Update currentMCQ to this new re-prompt setIsAwaitingAnswer(true); } catch (error) { console.error("Error fetching explanation for incorrect answer:", error); const errorMsg = error instanceof Error ? error.message : "An unknown error occurred"; const fallbackRepromptText = `That's not quite right. (Sorry, I couldn't explain that: ${errorMsg}) Please try another option from the question above.`; const fallbackMCQData: ActiveMCQ = { ...currentMCQ, mcq: fallbackRepromptText, }; addMessage({ sender: 'ai', type: 'mcq', mcq: fallbackMCQData }); setCurrentMCQ(fallbackMCQData); setIsAwaitingAnswer(true); } } } setIsLoading(false); }; const handleNextQuestionClick = async () => { if (questionCount >= MAX_QUESTIONS) { addMessage({ sender: 'ai', type: 'text', text: "You've completed all the questions! Please proceed to the summary." }); setIsLoading(false); setHasAnsweredCorrectly(true); return; } setIsAwaitingAnswer(false); setHasAnsweredCorrectly(false); setCurrentMCQ(null); setIncorrectAttempts([]); if (!imageDataUri) { addMessage({ sender: 'ai', type: 'error', text: "Image data is not available to generate new questions." }); toast({ title: "Error", description: "Cannot fetch new question: Image data missing.", variant: "destructive" }); return; } setIsLoading(true); try { const mcqResult = await generateMCQ({ imageDataUri }); const nextMCQData: ActiveMCQ = { ...mcqResult, originalQuestionTextForFlow: mcqResult.mcq, }; setCurrentMCQ(nextMCQData); addMessage({ sender: 'ai', type: 'mcq', mcq: nextMCQData }); setIsAwaitingAnswer(true); } catch (error) { console.error("Error generating next MCQ:", error); const errorMessage = error instanceof Error ? error.message : "An unknown error occurred."; addMessage({ sender: 'ai', type: 'error', text: `Failed to generate new question: ${errorMessage}` }); toast({ title: "Error", description: `Could not generate a new question. ${errorMessage}`, variant: "destructive", }); // Potentially allow user to try again or go to summary if multiple errors occur } finally { setIsLoading(false); } }; const handleGoToSummary = () => { router.push(`/journey/${journeyId}/summary`); }; return (

AI Chat Helper

{messages.map((msg) => { const isLastMessageTheActiveMCQ = msg.type === 'mcq' && currentMCQ && msg.id === messages.findLast(m => m.type === 'mcq' && m.mcq?.mcq === currentMCQ.mcq && JSON.stringify(m.mcq?.options) === JSON.stringify(currentMCQ.options))?.id; const shouldThisMessageBeInteractive = isLastMessageTheActiveMCQ && isAwaitingAnswer && !hasAnsweredCorrectly; return ( ); })} {isLoading && !currentMCQ && messages[messages.length -1]?.type !== 'feedback' && (
)}
{isLoading && (
AI is thinking...
)} {!isLoading && questionCount >= MAX_QUESTIONS && hasAnsweredCorrectly && ( )} {!isLoading && questionCount < MAX_QUESTIONS && currentMCQ && hasAnsweredCorrectly && ( )} {!isLoading && currentMCQ && !hasAnsweredCorrectly && isAwaitingAnswer && (

Please choose an answer from the options above.

)} {!imageDataUri && !isLoading && messages.length > 0 && messages[messages.length -1]?.text === "Preparing your journey..." &&(

Loading image, please wait...

)} {!currentMCQ && !isLoading && imageDataUri && messages.length === 0 && (

Loading first question...

)} {!currentMCQ && !isLoading && imageDataUri && messages.length > 0 && messages[messages.length -1]?.type === 'error' && (

Could not load question. Try refreshing the page or selecting another journey.

)}
); }