Spaces:
Sleeping
Sleeping
Commit ·
2e63a82
1
Parent(s): 003df95
that's correct! well done message should always have a tickmark.
Browse files
src/app/journey/[journeyId]/page.tsx
CHANGED
|
@@ -99,16 +99,24 @@ export default function JourneyPage() {
|
|
| 99 |
|
| 100 |
|
| 101 |
const handleOpenSummaryDialog = useCallback(() => {
|
| 102 |
-
if (!imageDataUri) {
|
| 103 |
toast({
|
| 104 |
-
title: "
|
| 105 |
-
description: "The image
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
variant: "destructive",
|
| 107 |
});
|
| 108 |
return;
|
| 109 |
}
|
| 110 |
setIsSummaryDialogOpen(true);
|
| 111 |
-
}, [imageDataUri, toast]);
|
| 112 |
|
| 113 |
|
| 114 |
if (error) {
|
|
@@ -145,18 +153,18 @@ export default function JourneyPage() {
|
|
| 145 |
showBackButton
|
| 146 |
backHref="/select-journey"
|
| 147 |
title={journeyTitle}
|
| 148 |
-
showDetailsButton={!!journey
|
| 149 |
onDetailsClick={handleOpenSummaryDialog}
|
| 150 |
/>
|
| 151 |
<main className="flex-1 overflow-hidden p-2 md:p-4">
|
| 152 |
<div className="grid h-full grid-cols-1 gap-2 md:grid-cols-2 md:gap-4">
|
| 153 |
<div className="relative h-full w-full overflow-hidden rounded-lg shadow-lg bg-muted">
|
| 154 |
-
{isLoadingJourney
|
| 155 |
<div className="flex h-full w-full flex-col items-center justify-center">
|
| 156 |
<Loader2 className="h-16 w-16 animate-spin text-primary mb-4" />
|
| 157 |
<p className="text-muted-foreground">Loading image for {journey?.title || 'journey'}...</p>
|
| 158 |
</div>
|
| 159 |
-
) : (
|
| 160 |
<Image
|
| 161 |
src={journey.imageUrl}
|
| 162 |
alt={journey.title}
|
|
@@ -166,8 +174,7 @@ export default function JourneyPage() {
|
|
| 166 |
data-ai-hint={journey.imageHint}
|
| 167 |
priority
|
| 168 |
/>
|
| 169 |
-
)
|
| 170 |
-
{!isLoadingJourney && !journey?.imageUrl && journey && (
|
| 171 |
<div className="flex h-full w-full flex-col items-center justify-center p-4 text-center">
|
| 172 |
<AlertTriangle className="h-16 w-16 text-destructive mb-4" />
|
| 173 |
<p className="text-destructive-foreground font-semibold">Image Not Available</p>
|
|
@@ -177,16 +184,20 @@ export default function JourneyPage() {
|
|
| 177 |
</div>
|
| 178 |
|
| 179 |
<div className="h-full overflow-y-auto">
|
| 180 |
-
{isLoadingJourney && !imageDataUri && (
|
| 181 |
<div className="flex h-full flex-col items-center justify-center rounded-lg border bg-card p-4 shadow-xl">
|
| 182 |
<Loader2 className="h-12 w-12 animate-spin text-primary mb-4" />
|
| 183 |
<p className="text-muted-foreground">Preparing AI interactions...</p>
|
| 184 |
</div>
|
| 185 |
)}
|
| 186 |
{!isLoadingJourney && imageDataUri && journey && (
|
| 187 |
-
<Chatbot
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
)}
|
| 189 |
-
{!isLoadingJourney && !imageDataUri && journey && (
|
| 190 |
<div className="flex h-full flex-col items-center justify-center rounded-lg border bg-card p-4 shadow-xl text-center">
|
| 191 |
<AlertTriangle className="h-12 w-12 text-destructive mb-4" />
|
| 192 |
<p className="font-semibold text-destructive">AI Features Unavailable</p>
|
|
@@ -200,7 +211,7 @@ export default function JourneyPage() {
|
|
| 200 |
<SummaryReportDialog
|
| 201 |
isOpen={isSummaryDialogOpen}
|
| 202 |
onOpenChange={setIsSummaryDialogOpen}
|
| 203 |
-
imageDataUri={imageDataUri}
|
| 204 |
journeyTitle={journey.title}
|
| 205 |
/>
|
| 206 |
)}
|
|
|
|
| 99 |
|
| 100 |
|
| 101 |
const handleOpenSummaryDialog = useCallback(() => {
|
| 102 |
+
if (!imageDataUri && !journey?.imageUrl) { // Check if journey itself lacks an image
|
| 103 |
toast({
|
| 104 |
+
title: "Summary Not Available",
|
| 105 |
+
description: "The journey image is missing, so a summary cannot be generated.",
|
| 106 |
+
variant: "destructive",
|
| 107 |
+
});
|
| 108 |
+
return;
|
| 109 |
+
}
|
| 110 |
+
if (!imageDataUri && journey?.imageUrl) { // Image exists for journey but failed to load as data URI
|
| 111 |
+
toast({
|
| 112 |
+
title: "Image Not Ready for Summary",
|
| 113 |
+
description: "The image data is still loading or failed to load. Please wait or try refreshing to view the summary.",
|
| 114 |
variant: "destructive",
|
| 115 |
});
|
| 116 |
return;
|
| 117 |
}
|
| 118 |
setIsSummaryDialogOpen(true);
|
| 119 |
+
}, [imageDataUri, journey, toast]);
|
| 120 |
|
| 121 |
|
| 122 |
if (error) {
|
|
|
|
| 153 |
showBackButton
|
| 154 |
backHref="/select-journey"
|
| 155 |
title={journeyTitle}
|
| 156 |
+
showDetailsButton={!!journey} // Show details button if journey exists, even if image data is loading/failed
|
| 157 |
onDetailsClick={handleOpenSummaryDialog}
|
| 158 |
/>
|
| 159 |
<main className="flex-1 overflow-hidden p-2 md:p-4">
|
| 160 |
<div className="grid h-full grid-cols-1 gap-2 md:grid-cols-2 md:gap-4">
|
| 161 |
<div className="relative h-full w-full overflow-hidden rounded-lg shadow-lg bg-muted">
|
| 162 |
+
{isLoadingJourney && journey?.imageUrl ? ( // Show loader only if an image URL exists and is loading
|
| 163 |
<div className="flex h-full w-full flex-col items-center justify-center">
|
| 164 |
<Loader2 className="h-16 w-16 animate-spin text-primary mb-4" />
|
| 165 |
<p className="text-muted-foreground">Loading image for {journey?.title || 'journey'}...</p>
|
| 166 |
</div>
|
| 167 |
+
) : journey?.imageUrl && imageDataUri ? ( // Image URL exists and data URI is loaded
|
| 168 |
<Image
|
| 169 |
src={journey.imageUrl}
|
| 170 |
alt={journey.title}
|
|
|
|
| 174 |
data-ai-hint={journey.imageHint}
|
| 175 |
priority
|
| 176 |
/>
|
| 177 |
+
) : ( // No image URL or imageDataUri failed to load (after isLoadingJourney is false)
|
|
|
|
| 178 |
<div className="flex h-full w-full flex-col items-center justify-center p-4 text-center">
|
| 179 |
<AlertTriangle className="h-16 w-16 text-destructive mb-4" />
|
| 180 |
<p className="text-destructive-foreground font-semibold">Image Not Available</p>
|
|
|
|
| 184 |
</div>
|
| 185 |
|
| 186 |
<div className="h-full overflow-y-auto">
|
| 187 |
+
{isLoadingJourney && !imageDataUri && journey?.imageUrl && ( // Show AI loading if journey has an image but data URI is not ready
|
| 188 |
<div className="flex h-full flex-col items-center justify-center rounded-lg border bg-card p-4 shadow-xl">
|
| 189 |
<Loader2 className="h-12 w-12 animate-spin text-primary mb-4" />
|
| 190 |
<p className="text-muted-foreground">Preparing AI interactions...</p>
|
| 191 |
</div>
|
| 192 |
)}
|
| 193 |
{!isLoadingJourney && imageDataUri && journey && (
|
| 194 |
+
<Chatbot
|
| 195 |
+
imageDataUri={imageDataUri}
|
| 196 |
+
journeyTitle={journey.title}
|
| 197 |
+
onOpenSummaryDialog={handleOpenSummaryDialog}
|
| 198 |
+
/>
|
| 199 |
)}
|
| 200 |
+
{((!isLoadingJourney && !imageDataUri && journey) || (!journey?.imageUrl && !isLoadingJourney)) && ( // Covers both failed data URI load AND no image URL in journey
|
| 201 |
<div className="flex h-full flex-col items-center justify-center rounded-lg border bg-card p-4 shadow-xl text-center">
|
| 202 |
<AlertTriangle className="h-12 w-12 text-destructive mb-4" />
|
| 203 |
<p className="font-semibold text-destructive">AI Features Unavailable</p>
|
|
|
|
| 211 |
<SummaryReportDialog
|
| 212 |
isOpen={isSummaryDialogOpen}
|
| 213 |
onOpenChange={setIsSummaryDialogOpen}
|
| 214 |
+
imageDataUri={imageDataUri} // Pass imageDataUri, dialog handles null
|
| 215 |
journeyTitle={journey.title}
|
| 216 |
/>
|
| 217 |
)}
|
src/components/chatbot/chatbot.tsx
CHANGED
|
@@ -12,20 +12,24 @@ import { explainCorrectAnswer } from '@/ai/flows/explain-correct-answer-flow';
|
|
| 12 |
import type { ChatMessage as ChatMessageType, ActiveMCQ } from '@/types';
|
| 13 |
import { ChatMessage } from './chat-message';
|
| 14 |
import { useToast } from '@/hooks/use-toast';
|
| 15 |
-
import { Send } from 'lucide-react';
|
|
|
|
|
|
|
| 16 |
|
| 17 |
interface ChatbotProps {
|
| 18 |
imageDataUri: string | null;
|
| 19 |
journeyTitle: string;
|
|
|
|
| 20 |
}
|
| 21 |
|
| 22 |
-
export function Chatbot({ imageDataUri, journeyTitle }: ChatbotProps) {
|
| 23 |
const [messages, setMessages] = useState<ChatMessageType[]>([]);
|
| 24 |
const [currentMCQ, setCurrentMCQ] = useState<ActiveMCQ | null>(null);
|
| 25 |
const [isLoading, setIsLoading] = useState(false);
|
| 26 |
const [isAwaitingAnswer, setIsAwaitingAnswer] = useState(false);
|
| 27 |
const [hasAnsweredCorrectly, setHasAnsweredCorrectly] = useState(false);
|
| 28 |
const [incorrectAttempts, setIncorrectAttempts] = useState<string[]>([]);
|
|
|
|
| 29 |
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
| 30 |
const { toast } = useToast();
|
| 31 |
const messageIdCounter = useRef(0);
|
|
@@ -41,20 +45,20 @@ export function Chatbot({ imageDataUri, journeyTitle }: ChatbotProps) {
|
|
| 41 |
useEffect(() => {
|
| 42 |
let ignore = false;
|
| 43 |
|
| 44 |
-
// Comprehensive reset for new journey/image
|
| 45 |
setMessages([]);
|
| 46 |
setCurrentMCQ(null);
|
| 47 |
setIsLoading(false);
|
| 48 |
setIsAwaitingAnswer(false);
|
| 49 |
setHasAnsweredCorrectly(false);
|
| 50 |
setIncorrectAttempts([]);
|
|
|
|
| 51 |
isInitialQuestionRef.current = true;
|
| 52 |
messageIdCounter.current = 0;
|
| 53 |
|
| 54 |
|
| 55 |
const performInitialFetch = async () => {
|
| 56 |
if (!imageDataUri) {
|
| 57 |
-
if (!ignore && messages.length === 0) {
|
| 58 |
addMessage({ sender: 'ai', type: 'text', text: "Preparing your journey..." });
|
| 59 |
}
|
| 60 |
return;
|
|
@@ -69,7 +73,7 @@ export function Chatbot({ imageDataUri, journeyTitle }: ChatbotProps) {
|
|
| 69 |
|
| 70 |
let activeMcqData: ActiveMCQ = {
|
| 71 |
...mcqResult,
|
| 72 |
-
originalQuestionTextForFlow: mcqResult.mcq,
|
| 73 |
};
|
| 74 |
|
| 75 |
if (isInitialQuestionRef.current) {
|
|
@@ -99,21 +103,19 @@ export function Chatbot({ imageDataUri, journeyTitle }: ChatbotProps) {
|
|
| 99 |
};
|
| 100 |
|
| 101 |
if (imageDataUri) {
|
| 102 |
-
// If there's an existing "Preparing..." message, clear it before fetching
|
| 103 |
if (messages.length === 1 && messages[0].text === "Preparing your journey...") {
|
| 104 |
setMessages([]);
|
| 105 |
}
|
| 106 |
performInitialFetch();
|
| 107 |
-
} else if (messages.length === 0) {
|
| 108 |
addMessage({ sender: 'ai', type: 'text', text: "Preparing your journey..." });
|
| 109 |
}
|
| 110 |
|
| 111 |
-
|
| 112 |
return () => {
|
| 113 |
ignore = true;
|
| 114 |
};
|
| 115 |
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 116 |
-
}, [imageDataUri, journeyTitle]);
|
| 117 |
|
| 118 |
|
| 119 |
useEffect(() => {
|
|
@@ -153,12 +155,17 @@ export function Chatbot({ imageDataUri, journeyTitle }: ChatbotProps) {
|
|
| 153 |
addMessage({ sender: 'ai', type: 'feedback', text: combinedMessage, isCorrect: true });
|
| 154 |
setHasAnsweredCorrectly(true);
|
| 155 |
setIncorrectAttempts([]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
} else { // Incorrect answer
|
| 157 |
const updatedIncorrectAttempts = [...incorrectAttempts, option];
|
| 158 |
setIncorrectAttempts(updatedIncorrectAttempts);
|
| 159 |
|
| 160 |
if (updatedIncorrectAttempts.length >= currentMCQ.options.length - 1) {
|
| 161 |
-
// All incorrect options have been tried
|
| 162 |
let combinedText = "";
|
| 163 |
try {
|
| 164 |
const incorrectExplanationResult = await explainIncorrectAnswer({
|
|
@@ -197,6 +204,11 @@ export function Chatbot({ imageDataUri, journeyTitle }: ChatbotProps) {
|
|
| 197 |
});
|
| 198 |
setHasAnsweredCorrectly(true);
|
| 199 |
setIncorrectAttempts([]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
|
| 201 |
} else { // Still other options left to try
|
| 202 |
try {
|
|
@@ -235,6 +247,13 @@ export function Chatbot({ imageDataUri, journeyTitle }: ChatbotProps) {
|
|
| 235 |
};
|
| 236 |
|
| 237 |
const handleNextQuestionClick = async () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
setIsAwaitingAnswer(false);
|
| 239 |
setHasAnsweredCorrectly(false);
|
| 240 |
setCurrentMCQ(null);
|
|
@@ -272,35 +291,21 @@ export function Chatbot({ imageDataUri, journeyTitle }: ChatbotProps) {
|
|
| 272 |
return (
|
| 273 |
<div className="flex h-full flex-col rounded-lg border bg-card shadow-xl">
|
| 274 |
<div className="border-b p-4">
|
| 275 |
-
<h2 className="font-headline text-lg font-semibold text-center">AI Chat Helper</h2>
|
| 276 |
</div>
|
| 277 |
<ScrollArea className="flex-1 p-4" ref={scrollAreaRef}>
|
| 278 |
<div className="space-y-4">
|
| 279 |
{messages.map((msg, index) => {
|
| 280 |
-
|
| 281 |
msg.type === 'mcq' &&
|
| 282 |
-
msg.mcq &&
|
| 283 |
currentMCQ &&
|
| 284 |
-
msg.mcq.mcq === currentMCQ.mcq &&
|
| 285 |
-
JSON.stringify(msg.mcq.options) === JSON.stringify(currentMCQ.options) &&
|
| 286 |
-
msg.mcq.correctAnswer === currentMCQ.correctAnswer;
|
| 287 |
-
|
| 288 |
-
const lastInstanceOfCurrentMCQIndex = messages.findLastIndex(
|
| 289 |
-
(m) =>
|
| 290 |
-
m.type === 'mcq' &&
|
| 291 |
-
m.mcq &&
|
| 292 |
-
currentMCQ &&
|
| 293 |
-
m.mcq.mcq === currentMCQ.mcq &&
|
| 294 |
-
JSON.stringify(m.mcq.options) === JSON.stringify(currentMCQ.options) &&
|
| 295 |
-
m.mcq.correctAnswer === currentMCQ.correctAnswer
|
| 296 |
-
);
|
| 297 |
|
| 298 |
const shouldThisMessageBeInteractive =
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
isAwaitingAnswer &&
|
| 302 |
!hasAnsweredCorrectly;
|
| 303 |
-
|
| 304 |
return (
|
| 305 |
<ChatMessage
|
| 306 |
key={msg.id}
|
|
@@ -330,7 +335,13 @@ export function Chatbot({ imageDataUri, journeyTitle }: ChatbotProps) {
|
|
| 330 |
<div className="border-t bg-background/80 p-4">
|
| 331 |
{isLoading && <p className="text-center text-sm text-muted-foreground">AI is thinking...</p>}
|
| 332 |
|
| 333 |
-
{!isLoading &&
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
<Button onClick={handleNextQuestionClick} className="w-full" variant="default" disabled={isLoading}>
|
| 335 |
Next Question <Send className="ml-2 h-4 w-4" />
|
| 336 |
</Button>
|
|
|
|
| 12 |
import type { ChatMessage as ChatMessageType, ActiveMCQ } from '@/types';
|
| 13 |
import { ChatMessage } from './chat-message';
|
| 14 |
import { useToast } from '@/hooks/use-toast';
|
| 15 |
+
import { Send, FileText } from 'lucide-react';
|
| 16 |
+
|
| 17 |
+
const MAX_QUESTIONS = 5;
|
| 18 |
|
| 19 |
interface ChatbotProps {
|
| 20 |
imageDataUri: string | null;
|
| 21 |
journeyTitle: string;
|
| 22 |
+
onOpenSummaryDialog: () => void;
|
| 23 |
}
|
| 24 |
|
| 25 |
+
export function Chatbot({ imageDataUri, journeyTitle, onOpenSummaryDialog }: ChatbotProps) {
|
| 26 |
const [messages, setMessages] = useState<ChatMessageType[]>([]);
|
| 27 |
const [currentMCQ, setCurrentMCQ] = useState<ActiveMCQ | null>(null);
|
| 28 |
const [isLoading, setIsLoading] = useState(false);
|
| 29 |
const [isAwaitingAnswer, setIsAwaitingAnswer] = useState(false);
|
| 30 |
const [hasAnsweredCorrectly, setHasAnsweredCorrectly] = useState(false);
|
| 31 |
const [incorrectAttempts, setIncorrectAttempts] = useState<string[]>([]);
|
| 32 |
+
const [questionCount, setQuestionCount] = useState(0);
|
| 33 |
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
| 34 |
const { toast } = useToast();
|
| 35 |
const messageIdCounter = useRef(0);
|
|
|
|
| 45 |
useEffect(() => {
|
| 46 |
let ignore = false;
|
| 47 |
|
|
|
|
| 48 |
setMessages([]);
|
| 49 |
setCurrentMCQ(null);
|
| 50 |
setIsLoading(false);
|
| 51 |
setIsAwaitingAnswer(false);
|
| 52 |
setHasAnsweredCorrectly(false);
|
| 53 |
setIncorrectAttempts([]);
|
| 54 |
+
setQuestionCount(0);
|
| 55 |
isInitialQuestionRef.current = true;
|
| 56 |
messageIdCounter.current = 0;
|
| 57 |
|
| 58 |
|
| 59 |
const performInitialFetch = async () => {
|
| 60 |
if (!imageDataUri) {
|
| 61 |
+
if (!ignore && messages.length === 0) {
|
| 62 |
addMessage({ sender: 'ai', type: 'text', text: "Preparing your journey..." });
|
| 63 |
}
|
| 64 |
return;
|
|
|
|
| 73 |
|
| 74 |
let activeMcqData: ActiveMCQ = {
|
| 75 |
...mcqResult,
|
| 76 |
+
originalQuestionTextForFlow: mcqResult.mcq,
|
| 77 |
};
|
| 78 |
|
| 79 |
if (isInitialQuestionRef.current) {
|
|
|
|
| 103 |
};
|
| 104 |
|
| 105 |
if (imageDataUri) {
|
|
|
|
| 106 |
if (messages.length === 1 && messages[0].text === "Preparing your journey...") {
|
| 107 |
setMessages([]);
|
| 108 |
}
|
| 109 |
performInitialFetch();
|
| 110 |
+
} else if (messages.length === 0) {
|
| 111 |
addMessage({ sender: 'ai', type: 'text', text: "Preparing your journey..." });
|
| 112 |
}
|
| 113 |
|
|
|
|
| 114 |
return () => {
|
| 115 |
ignore = true;
|
| 116 |
};
|
| 117 |
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 118 |
+
}, [imageDataUri, journeyTitle]);
|
| 119 |
|
| 120 |
|
| 121 |
useEffect(() => {
|
|
|
|
| 155 |
addMessage({ sender: 'ai', type: 'feedback', text: combinedMessage, isCorrect: true });
|
| 156 |
setHasAnsweredCorrectly(true);
|
| 157 |
setIncorrectAttempts([]);
|
| 158 |
+
const newQuestionCount = questionCount + 1;
|
| 159 |
+
setQuestionCount(newQuestionCount);
|
| 160 |
+
if (newQuestionCount >= MAX_QUESTIONS) {
|
| 161 |
+
addMessage({ sender: 'ai', type: 'text', text: "You've completed all 5 questions! Please proceed to the summary report." });
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
} else { // Incorrect answer
|
| 165 |
const updatedIncorrectAttempts = [...incorrectAttempts, option];
|
| 166 |
setIncorrectAttempts(updatedIncorrectAttempts);
|
| 167 |
|
| 168 |
if (updatedIncorrectAttempts.length >= currentMCQ.options.length - 1) {
|
|
|
|
| 169 |
let combinedText = "";
|
| 170 |
try {
|
| 171 |
const incorrectExplanationResult = await explainIncorrectAnswer({
|
|
|
|
| 204 |
});
|
| 205 |
setHasAnsweredCorrectly(true);
|
| 206 |
setIncorrectAttempts([]);
|
| 207 |
+
const newQuestionCount = questionCount + 1;
|
| 208 |
+
setQuestionCount(newQuestionCount);
|
| 209 |
+
if (newQuestionCount >= MAX_QUESTIONS) {
|
| 210 |
+
addMessage({ sender: 'ai', type: 'text', text: "You've completed all 5 questions! Please proceed to the summary report." });
|
| 211 |
+
}
|
| 212 |
|
| 213 |
} else { // Still other options left to try
|
| 214 |
try {
|
|
|
|
| 247 |
};
|
| 248 |
|
| 249 |
const handleNextQuestionClick = async () => {
|
| 250 |
+
if (questionCount >= MAX_QUESTIONS) {
|
| 251 |
+
addMessage({ sender: 'ai', type: 'text', text: "You've completed all the questions! Please proceed to the summary." });
|
| 252 |
+
setIsLoading(false);
|
| 253 |
+
setHasAnsweredCorrectly(true); // Keep UI consistent for summary button
|
| 254 |
+
return;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
setIsAwaitingAnswer(false);
|
| 258 |
setHasAnsweredCorrectly(false);
|
| 259 |
setCurrentMCQ(null);
|
|
|
|
| 291 |
return (
|
| 292 |
<div className="flex h-full flex-col rounded-lg border bg-card shadow-xl">
|
| 293 |
<div className="border-b p-4">
|
| 294 |
+
<h2 className="font-headline text-lg font-semibold text-center">AI Chat Helper ({questionCount}/{MAX_QUESTIONS} Questions)</h2>
|
| 295 |
</div>
|
| 296 |
<ScrollArea className="flex-1 p-4" ref={scrollAreaRef}>
|
| 297 |
<div className="space-y-4">
|
| 298 |
{messages.map((msg, index) => {
|
| 299 |
+
const isLastMessageTheActiveMCQ =
|
| 300 |
msg.type === 'mcq' &&
|
|
|
|
| 301 |
currentMCQ &&
|
| 302 |
+
msg.id === messages.findLast(m => m.type === 'mcq' && m.mcq?.mcq === currentMCQ.mcq && JSON.stringify(m.mcq?.options) === JSON.stringify(currentMCQ.options))?.id;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
|
| 304 |
const shouldThisMessageBeInteractive =
|
| 305 |
+
isLastMessageTheActiveMCQ &&
|
| 306 |
+
isAwaitingAnswer &&
|
|
|
|
| 307 |
!hasAnsweredCorrectly;
|
| 308 |
+
|
| 309 |
return (
|
| 310 |
<ChatMessage
|
| 311 |
key={msg.id}
|
|
|
|
| 335 |
<div className="border-t bg-background/80 p-4">
|
| 336 |
{isLoading && <p className="text-center text-sm text-muted-foreground">AI is thinking...</p>}
|
| 337 |
|
| 338 |
+
{!isLoading && questionCount >= MAX_QUESTIONS && hasAnsweredCorrectly && (
|
| 339 |
+
<Button onClick={onOpenSummaryDialog} className="w-full" variant="default">
|
| 340 |
+
Go to Report Summary <FileText className="ml-2 h-4 w-4" />
|
| 341 |
+
</Button>
|
| 342 |
+
)}
|
| 343 |
+
|
| 344 |
+
{!isLoading && questionCount < MAX_QUESTIONS && currentMCQ && hasAnsweredCorrectly && (
|
| 345 |
<Button onClick={handleNextQuestionClick} className="w-full" variant="default" disabled={isLoading}>
|
| 346 |
Next Question <Send className="ml-2 h-4 w-4" />
|
| 347 |
</Button>
|