aakashbansal commited on
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: "Image Not Ready",
105
- description: "The image data is still loading or failed to load. Please wait or try refreshing.",
 
 
 
 
 
 
 
 
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 && !!imageDataUri}
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 || !journey?.imageUrl ? (
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 imageDataUri={imageDataUri} journeyTitle={journey.title} />
 
 
 
 
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) { // Only add if no messages and not ignoring
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, // Store original question text
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) { // Only add "Preparing..." if no messages exist
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]); // addMessage and toast are stable, so not needed in deps
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
- const isThisMessageTheCurrentMCQ =
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
- isThisMessageTheCurrentMCQ &&
300
- index === lastInstanceOfCurrentMCQIndex &&
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 && currentMCQ && hasAnsweredCorrectly && (
 
 
 
 
 
 
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>