Spaces:
Sleeping
Sleeping
Commit ·
72c115f
1
Parent(s): c0320bb
on the chat screen, when a user gives a wrong answer, don't say what the
Browse files
src/ai/dev.ts
CHANGED
|
@@ -2,4 +2,5 @@ import { config } from 'dotenv';
|
|
| 2 |
config();
|
| 3 |
|
| 4 |
import '@/ai/flows/summarize-image.ts';
|
| 5 |
-
import '@/ai/flows/generate-mcq.ts';
|
|
|
|
|
|
| 2 |
config();
|
| 3 |
|
| 4 |
import '@/ai/flows/summarize-image.ts';
|
| 5 |
+
import '@/ai/flows/generate-mcq.ts';
|
| 6 |
+
import '@/ai/flows/explain-incorrect-answer-flow.ts';
|
src/ai/flows/explain-incorrect-answer-flow.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
'use server';
|
| 3 |
+
/**
|
| 4 |
+
* @fileOverview Provides an explanation for why a selected MCQ answer is incorrect.
|
| 5 |
+
*
|
| 6 |
+
* - explainIncorrectAnswer - A function that generates an explanation for an incorrect MCQ answer.
|
| 7 |
+
* - ExplainIncorrectAnswerInput - The input type for the explainIncorrectAnswer function.
|
| 8 |
+
* - ExplainIncorrectAnswerOutput - The return type for the explainIncorrectAnswer function.
|
| 9 |
+
*/
|
| 10 |
+
|
| 11 |
+
import {ai} from '@/ai/genkit';
|
| 12 |
+
import {z} from 'genkit';
|
| 13 |
+
|
| 14 |
+
const ExplainIncorrectAnswerInputSchema = z.object({
|
| 15 |
+
question: z.string().describe('The multiple-choice question that was asked.'),
|
| 16 |
+
options: z.array(z.string()).describe('All the options provided for the question.'),
|
| 17 |
+
selectedAnswer: z.string().describe('The answer incorrectly selected by the user.'),
|
| 18 |
+
correctAnswer: z.string().describe('The actual correct answer to the question.'),
|
| 19 |
+
});
|
| 20 |
+
export type ExplainIncorrectAnswerInput = z.infer<typeof ExplainIncorrectAnswerInputSchema>;
|
| 21 |
+
|
| 22 |
+
const ExplainIncorrectAnswerOutputSchema = z.object({
|
| 23 |
+
explanation: z.string().describe('The explanation of why the selected answer is incorrect.'),
|
| 24 |
+
});
|
| 25 |
+
export type ExplainIncorrectAnswerOutput = z.infer<typeof ExplainIncorrectAnswerOutputSchema>;
|
| 26 |
+
|
| 27 |
+
export async function explainIncorrectAnswer(input: ExplainIncorrectAnswerInput): Promise<ExplainIncorrectAnswerOutput> {
|
| 28 |
+
return explainIncorrectAnswerFlow(input);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
const prompt = ai.definePrompt({
|
| 32 |
+
name: 'explainIncorrectAnswerPrompt',
|
| 33 |
+
input: {schema: ExplainIncorrectAnswerInputSchema},
|
| 34 |
+
output: {schema: ExplainIncorrectAnswerOutputSchema},
|
| 35 |
+
prompt: `You are an expert educator. A student was asked the following multiple-choice question:
|
| 36 |
+
Question: "{{question}}"
|
| 37 |
+
Options:
|
| 38 |
+
{{#each options}}
|
| 39 |
+
- {{this}}
|
| 40 |
+
{{/each}}
|
| 41 |
+
The student incorrectly chose: "{{selectedAnswer}}".
|
| 42 |
+
The correct answer is "{{correctAnswer}}".
|
| 43 |
+
|
| 44 |
+
Explain concisely why "{{selectedAnswer}}" is not the correct choice for the question.
|
| 45 |
+
Focus only on the incorrectness of the chosen option.
|
| 46 |
+
Do NOT reveal what the correct answer is in your explanation.
|
| 47 |
+
Your explanation should be helpful for the student to learn from their mistake and try again.
|
| 48 |
+
For example, if the question is "What is the capital of France?" with options "Paris, London, Berlin, Rome", the correct answer is "Paris", and the student chose "London", you should explain why London is not the capital of France (e.g., "London is the capital of the United Kingdom, not France."), without stating that "The correct answer is Paris."
|
| 49 |
+
`,
|
| 50 |
+
});
|
| 51 |
+
|
| 52 |
+
const explainIncorrectAnswerFlow = ai.defineFlow(
|
| 53 |
+
{
|
| 54 |
+
name: 'explainIncorrectAnswerFlow',
|
| 55 |
+
inputSchema: ExplainIncorrectAnswerInputSchema,
|
| 56 |
+
outputSchema: ExplainIncorrectAnswerOutputSchema,
|
| 57 |
+
},
|
| 58 |
+
async input => {
|
| 59 |
+
const {output} = await prompt(input);
|
| 60 |
+
return output!;
|
| 61 |
+
}
|
| 62 |
+
);
|
src/components/chatbot/chat-message.tsx
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
import type { ChatMessage as ChatMessageType } from '@/types';
|
| 5 |
import { cn } from '@/lib/utils';
|
| 6 |
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
| 7 |
-
import { Bot, User, AlertTriangle, CheckCircle,
|
| 8 |
import { Card, CardContent } from '@/components/ui/card';
|
| 9 |
import { Badge } from '@/components/ui/badge';
|
| 10 |
import { MCQOptions } from './mcq-options';
|
|
@@ -27,13 +27,13 @@ export function ChatMessage({
|
|
| 27 |
const Icon = isUser ? User : Bot;
|
| 28 |
const avatarFallback = isUser ? 'U' : 'AI';
|
| 29 |
|
| 30 |
-
|
|
|
|
| 31 |
message.type === 'mcq' &&
|
| 32 |
message.mcq &&
|
| 33 |
activeMCQ &&
|
| 34 |
-
message.mcq.mcq === activeMCQ.mcq && // Compare question text
|
| 35 |
-
message.mcq.options
|
| 36 |
-
|
| 37 |
|
| 38 |
return (
|
| 39 |
<div
|
|
@@ -65,12 +65,13 @@ export function ChatMessage({
|
|
| 65 |
{String.fromCharCode(65 + index)}. {option}
|
| 66 |
</Badge>
|
| 67 |
))}
|
| 68 |
-
{
|
| 69 |
-
|
|
|
|
| 70 |
<MCQOptions
|
| 71 |
mcq={message.mcq}
|
| 72 |
onOptionSelect={onOptionSelectActiveMCQ}
|
| 73 |
-
disabled={!isAwaitingActiveMCQAnswer}
|
| 74 |
/>
|
| 75 |
</div>
|
| 76 |
)}
|
|
@@ -78,19 +79,21 @@ export function ChatMessage({
|
|
| 78 |
)}
|
| 79 |
{message.type === 'text' && message.text && <p>{message.text}</p>}
|
| 80 |
{message.type === 'feedback' && message.text && (
|
| 81 |
-
<div className="flex items-
|
| 82 |
{message.isCorrect ? (
|
| 83 |
-
<CheckCircle className="mr-2 h-5 w-5 text-green-500" />
|
| 84 |
) : (
|
| 85 |
-
|
|
|
|
|
|
|
| 86 |
)}
|
| 87 |
-
<p>{message.text}</p>
|
| 88 |
</div>
|
| 89 |
)}
|
| 90 |
{message.type === 'error' && message.text && (
|
| 91 |
-
<div className="flex items-
|
| 92 |
-
<AlertTriangle className="mr-2 h-5 w-5" />
|
| 93 |
-
<p>{message.text}</p>
|
| 94 |
</div>
|
| 95 |
)}
|
| 96 |
{message.type === 'user' && message.text && (
|
|
|
|
| 4 |
import type { ChatMessage as ChatMessageType } from '@/types';
|
| 5 |
import { cn } from '@/lib/utils';
|
| 6 |
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
| 7 |
+
import { Bot, User, AlertTriangle, CheckCircle, Info } from 'lucide-react'; // Using Info or AlertTriangle for explanation
|
| 8 |
import { Card, CardContent } from '@/components/ui/card';
|
| 9 |
import { Badge } from '@/components/ui/badge';
|
| 10 |
import { MCQOptions } from './mcq-options';
|
|
|
|
| 27 |
const Icon = isUser ? User : Bot;
|
| 28 |
const avatarFallback = isUser ? 'U' : 'AI';
|
| 29 |
|
| 30 |
+
// Determine if this message is the currently active MCQ being interacted with
|
| 31 |
+
const isCurrentActiveMCQMessage =
|
| 32 |
message.type === 'mcq' &&
|
| 33 |
message.mcq &&
|
| 34 |
activeMCQ &&
|
| 35 |
+
message.mcq.mcq === activeMCQ.mcq && // Compare question text
|
| 36 |
+
JSON.stringify(message.mcq.options) === JSON.stringify(activeMCQ.options); // Compare options
|
|
|
|
| 37 |
|
| 38 |
return (
|
| 39 |
<div
|
|
|
|
| 65 |
{String.fromCharCode(65 + index)}. {option}
|
| 66 |
</Badge>
|
| 67 |
))}
|
| 68 |
+
{/* Render options if this message is the active MCQ and we are awaiting an answer for it */}
|
| 69 |
+
{isCurrentActiveMCQMessage && onOptionSelectActiveMCQ && (
|
| 70 |
+
<div className="mt-3">
|
| 71 |
<MCQOptions
|
| 72 |
mcq={message.mcq}
|
| 73 |
onOptionSelect={onOptionSelectActiveMCQ}
|
| 74 |
+
disabled={!isAwaitingActiveMCQAnswer} // Controls interactability
|
| 75 |
/>
|
| 76 |
</div>
|
| 77 |
)}
|
|
|
|
| 79 |
)}
|
| 80 |
{message.type === 'text' && message.text && <p>{message.text}</p>}
|
| 81 |
{message.type === 'feedback' && message.text && (
|
| 82 |
+
<div className="flex items-start"> {/* items-start for better alignment with multi-line text */}
|
| 83 |
{message.isCorrect ? (
|
| 84 |
+
<CheckCircle className="mr-2 h-5 w-5 shrink-0 text-green-500 mt-0.5" />
|
| 85 |
) : (
|
| 86 |
+
// Using Info icon for explanations, or AlertTriangle for a slightly stronger visual cue for "incorrect"
|
| 87 |
+
<Info className="mr-2 h-5 w-5 shrink-0 text-blue-500 mt-0.5" />
|
| 88 |
+
// <AlertTriangle className="mr-2 h-5 w-5 shrink-0 text-orange-500 mt-0.5" />
|
| 89 |
)}
|
| 90 |
+
<p className="flex-1">{message.text}</p>
|
| 91 |
</div>
|
| 92 |
)}
|
| 93 |
{message.type === 'error' && message.text && (
|
| 94 |
+
<div className="flex items-start text-destructive">
|
| 95 |
+
<AlertTriangle className="mr-2 h-5 w-5 shrink-0 mt-0.5" />
|
| 96 |
+
<p className="flex-1">{message.text}</p>
|
| 97 |
</div>
|
| 98 |
)}
|
| 99 |
{message.type === 'user' && message.text && (
|
src/components/chatbot/chatbot.tsx
CHANGED
|
@@ -7,10 +7,11 @@ import { Button } from '@/components/ui/button';
|
|
| 7 |
import { ScrollArea } from '@/components/ui/scroll-area';
|
| 8 |
import { Skeleton } from '@/components/ui/skeleton';
|
| 9 |
import { generateMCQ, type GenerateMCQOutput } from '@/ai/flows/generate-mcq';
|
|
|
|
| 10 |
import type { ChatMessage as ChatMessageType } from '@/types';
|
| 11 |
import { ChatMessage } from './chat-message';
|
| 12 |
import { useToast } from '@/hooks/use-toast';
|
| 13 |
-
import { Send } from 'lucide-react';
|
| 14 |
|
| 15 |
interface ChatbotProps {
|
| 16 |
imageDataUri: string | null;
|
|
@@ -22,6 +23,7 @@ export function Chatbot({ imageDataUri, journeyTitle }: ChatbotProps) {
|
|
| 22 |
const [currentMCQ, setCurrentMCQ] = useState<GenerateMCQOutput | null>(null);
|
| 23 |
const [isLoading, setIsLoading] = useState(false);
|
| 24 |
const [isAwaitingAnswer, setIsAwaitingAnswer] = useState(false);
|
|
|
|
| 25 |
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
| 26 |
const { toast } = useToast();
|
| 27 |
|
|
@@ -36,6 +38,7 @@ export function Chatbot({ imageDataUri, journeyTitle }: ChatbotProps) {
|
|
| 36 |
}
|
| 37 |
setIsLoading(true);
|
| 38 |
setIsAwaitingAnswer(false);
|
|
|
|
| 39 |
setCurrentMCQ(null);
|
| 40 |
|
| 41 |
try {
|
|
@@ -58,7 +61,7 @@ export function Chatbot({ imageDataUri, journeyTitle }: ChatbotProps) {
|
|
| 58 |
};
|
| 59 |
|
| 60 |
useEffect(() => {
|
| 61 |
-
setMessages([]);
|
| 62 |
addMessage({
|
| 63 |
sender: 'ai',
|
| 64 |
type: 'text',
|
|
@@ -73,12 +76,7 @@ export function Chatbot({ imageDataUri, journeyTitle }: ChatbotProps) {
|
|
| 73 |
}, [imageDataUri, journeyTitle]);
|
| 74 |
|
| 75 |
useEffect(() => {
|
| 76 |
-
// This effect ensures that if imageDataUri loads after the initial "Preparing..." message,
|
| 77 |
-
// we fetch the first MCQ.
|
| 78 |
if (imageDataUri && messages.length > 0 && messages[messages.length-1].text === "Preparing your journey...") {
|
| 79 |
-
// Find the "Preparing..." message and replace it or just fetch MCQ
|
| 80 |
-
// For simplicity, let's assume the last message was "Preparing..." and then fetch.
|
| 81 |
-
// A more robust way might involve checking the exact content of the last AI message.
|
| 82 |
fetchNewMCQ();
|
| 83 |
}
|
| 84 |
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -91,8 +89,10 @@ export function Chatbot({ imageDataUri, journeyTitle }: ChatbotProps) {
|
|
| 91 |
}
|
| 92 |
}, [messages]);
|
| 93 |
|
| 94 |
-
const handleOptionSelect = (option: string, isCorrect: boolean) => {
|
| 95 |
-
|
|
|
|
|
|
|
| 96 |
|
| 97 |
addMessage({
|
| 98 |
sender: 'user',
|
|
@@ -102,16 +102,28 @@ export function Chatbot({ imageDataUri, journeyTitle }: ChatbotProps) {
|
|
| 102 |
|
| 103 |
if (isCorrect) {
|
| 104 |
addMessage({ sender: 'ai', type: 'feedback', text: "That's correct! Well done.", isCorrect: true });
|
|
|
|
| 105 |
} else {
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
}
|
| 113 |
-
|
| 114 |
-
// Do not automatically fetch next question here. User will click "Next Question" button.
|
| 115 |
};
|
| 116 |
|
| 117 |
return (
|
|
@@ -125,12 +137,12 @@ export function Chatbot({ imageDataUri, journeyTitle }: ChatbotProps) {
|
|
| 125 |
<ChatMessage
|
| 126 |
key={msg.id}
|
| 127 |
message={msg}
|
| 128 |
-
activeMCQ={currentMCQ}
|
| 129 |
-
isAwaitingActiveMCQAnswer={isAwaitingAnswer}
|
| 130 |
onOptionSelectActiveMCQ={handleOptionSelect}
|
| 131 |
/>
|
| 132 |
))}
|
| 133 |
-
{isLoading && !currentMCQ && (
|
| 134 |
<div className="flex items-start space-x-3">
|
| 135 |
<Skeleton className="h-8 w-8 rounded-full" />
|
| 136 |
<div className="space-y-2">
|
|
@@ -144,14 +156,24 @@ export function Chatbot({ imageDataUri, journeyTitle }: ChatbotProps) {
|
|
| 144 |
<div className="border-t bg-background/80 p-4">
|
| 145 |
{isLoading && <p className="text-center text-sm text-muted-foreground">AI is thinking...</p>}
|
| 146 |
|
| 147 |
-
{!isLoading &&
|
| 148 |
-
<Button onClick={fetchNewMCQ} className="w-full" variant="
|
| 149 |
-
|
| 150 |
</Button>
|
| 151 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
{!imageDataUri && !isLoading && (
|
| 153 |
<p className="text-center text-sm text-muted-foreground">Loading image, please wait...</p>
|
| 154 |
)}
|
|
|
|
|
|
|
|
|
|
| 155 |
</div>
|
| 156 |
</div>
|
| 157 |
);
|
|
|
|
| 7 |
import { ScrollArea } from '@/components/ui/scroll-area';
|
| 8 |
import { Skeleton } from '@/components/ui/skeleton';
|
| 9 |
import { generateMCQ, type GenerateMCQOutput } from '@/ai/flows/generate-mcq';
|
| 10 |
+
import { explainIncorrectAnswer } from '@/ai/flows/explain-incorrect-answer-flow';
|
| 11 |
import type { ChatMessage as ChatMessageType } from '@/types';
|
| 12 |
import { ChatMessage } from './chat-message';
|
| 13 |
import { useToast } from '@/hooks/use-toast';
|
| 14 |
+
import { Send, RotateCcw } from 'lucide-react';
|
| 15 |
|
| 16 |
interface ChatbotProps {
|
| 17 |
imageDataUri: string | null;
|
|
|
|
| 23 |
const [currentMCQ, setCurrentMCQ] = useState<GenerateMCQOutput | null>(null);
|
| 24 |
const [isLoading, setIsLoading] = useState(false);
|
| 25 |
const [isAwaitingAnswer, setIsAwaitingAnswer] = useState(false);
|
| 26 |
+
const [hasAnsweredCorrectly, setHasAnsweredCorrectly] = useState(false);
|
| 27 |
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
| 28 |
const { toast } = useToast();
|
| 29 |
|
|
|
|
| 38 |
}
|
| 39 |
setIsLoading(true);
|
| 40 |
setIsAwaitingAnswer(false);
|
| 41 |
+
setHasAnsweredCorrectly(false);
|
| 42 |
setCurrentMCQ(null);
|
| 43 |
|
| 44 |
try {
|
|
|
|
| 61 |
};
|
| 62 |
|
| 63 |
useEffect(() => {
|
| 64 |
+
setMessages([]);
|
| 65 |
addMessage({
|
| 66 |
sender: 'ai',
|
| 67 |
type: 'text',
|
|
|
|
| 76 |
}, [imageDataUri, journeyTitle]);
|
| 77 |
|
| 78 |
useEffect(() => {
|
|
|
|
|
|
|
| 79 |
if (imageDataUri && messages.length > 0 && messages[messages.length-1].text === "Preparing your journey...") {
|
|
|
|
|
|
|
|
|
|
| 80 |
fetchNewMCQ();
|
| 81 |
}
|
| 82 |
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
| 89 |
}
|
| 90 |
}, [messages]);
|
| 91 |
|
| 92 |
+
const handleOptionSelect = async (option: string, isCorrect: boolean) => {
|
| 93 |
+
if (!currentMCQ) return;
|
| 94 |
+
|
| 95 |
+
setIsAwaitingAnswer(false); // Disable options while processing
|
| 96 |
|
| 97 |
addMessage({
|
| 98 |
sender: 'user',
|
|
|
|
| 102 |
|
| 103 |
if (isCorrect) {
|
| 104 |
addMessage({ sender: 'ai', type: 'feedback', text: "That's correct! Well done.", isCorrect: true });
|
| 105 |
+
setHasAnsweredCorrectly(true);
|
| 106 |
} else {
|
| 107 |
+
setIsLoading(true);
|
| 108 |
+
try {
|
| 109 |
+
const explanationResult = await explainIncorrectAnswer({
|
| 110 |
+
question: currentMCQ.mcq,
|
| 111 |
+
options: currentMCQ.options,
|
| 112 |
+
selectedAnswer: option,
|
| 113 |
+
correctAnswer: currentMCQ.correctAnswer,
|
| 114 |
+
});
|
| 115 |
+
addMessage({ sender: 'ai', type: 'feedback', text: explanationResult.explanation, isCorrect: false });
|
| 116 |
+
} catch (error) {
|
| 117 |
+
console.error("Error fetching explanation:", error);
|
| 118 |
+
const errorMsg = error instanceof Error ? error.message : "An unknown error occurred";
|
| 119 |
+
addMessage({ sender: 'ai', type: 'error', text: `Sorry, I couldn't explain that. ${errorMsg}` });
|
| 120 |
+
// Fallback to simpler incorrect message if explanation fails
|
| 121 |
+
addMessage({ sender: 'ai', type: 'feedback', text: "That's not quite right. Try again!", isCorrect: false });
|
| 122 |
+
}
|
| 123 |
+
setHasAnsweredCorrectly(false); // Ensure it's false
|
| 124 |
+
setIsAwaitingAnswer(true); // Re-enable options for the same MCQ
|
| 125 |
+
setIsLoading(false);
|
| 126 |
}
|
|
|
|
|
|
|
| 127 |
};
|
| 128 |
|
| 129 |
return (
|
|
|
|
| 137 |
<ChatMessage
|
| 138 |
key={msg.id}
|
| 139 |
message={msg}
|
| 140 |
+
activeMCQ={currentMCQ} // Pass currentMCQ to determine if this message is the active one
|
| 141 |
+
isAwaitingActiveMCQAnswer={msg.type === 'mcq' && isAwaitingAnswer && !hasAnsweredCorrectly} // Options active if it's an MCQ, awaiting answer, and not yet correct
|
| 142 |
onOptionSelectActiveMCQ={handleOptionSelect}
|
| 143 |
/>
|
| 144 |
))}
|
| 145 |
+
{isLoading && !currentMCQ && messages[messages.length -1]?.type !== 'feedback' && ( // Show loading skeleton only if not loading an explanation
|
| 146 |
<div className="flex items-start space-x-3">
|
| 147 |
<Skeleton className="h-8 w-8 rounded-full" />
|
| 148 |
<div className="space-y-2">
|
|
|
|
| 156 |
<div className="border-t bg-background/80 p-4">
|
| 157 |
{isLoading && <p className="text-center text-sm text-muted-foreground">AI is thinking...</p>}
|
| 158 |
|
| 159 |
+
{!isLoading && currentMCQ && hasAnsweredCorrectly && (
|
| 160 |
+
<Button onClick={fetchNewMCQ} className="w-full" variant="default" disabled={isLoading}>
|
| 161 |
+
Next Question <Send className="ml-2 h-4 w-4" />
|
| 162 |
</Button>
|
| 163 |
)}
|
| 164 |
+
|
| 165 |
+
{!isLoading && currentMCQ && !hasAnsweredCorrectly && isAwaitingAnswer && (
|
| 166 |
+
<p className="text-center text-sm text-muted-foreground">
|
| 167 |
+
Please choose an answer from the options above.
|
| 168 |
+
</p>
|
| 169 |
+
)}
|
| 170 |
+
|
| 171 |
{!imageDataUri && !isLoading && (
|
| 172 |
<p className="text-center text-sm text-muted-foreground">Loading image, please wait...</p>
|
| 173 |
)}
|
| 174 |
+
{!currentMCQ && !isLoading && messages.length > 0 && messages[messages.length -1]?.type !== 'error' && (
|
| 175 |
+
<p className="text-center text-sm text-muted-foreground">Loading first question...</p>
|
| 176 |
+
)}
|
| 177 |
</div>
|
| 178 |
</div>
|
| 179 |
);
|
src/components/chatbot/mcq-options.tsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
import type { GenerateMCQOutput } from '@/ai/flows/generate-mcq';
|
| 4 |
import { Button } from '@/components/ui/button';
|
| 5 |
import { cn } from '@/lib/utils';
|
| 6 |
-
import { useState } from 'react';
|
| 7 |
|
| 8 |
interface MCQOptionsProps {
|
| 9 |
mcq: GenerateMCQOutput;
|
|
@@ -14,20 +15,28 @@ interface MCQOptionsProps {
|
|
| 14 |
export function MCQOptions({ mcq, onOptionSelect, disabled }: MCQOptionsProps) {
|
| 15 |
const [selectedOption, setSelectedOption] = useState<string | null>(null);
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
const handleSelect = (option: string) => {
|
| 18 |
-
|
|
|
|
| 19 |
|
| 20 |
-
setSelectedOption(option);
|
| 21 |
const isCorrect = option === mcq.correctAnswer;
|
| 22 |
-
onOptionSelect(option, isCorrect);
|
| 23 |
};
|
| 24 |
|
| 25 |
return (
|
| 26 |
<div className="mt-4 space-y-2">
|
| 27 |
{mcq.options.map((option, index) => {
|
| 28 |
-
const
|
| 29 |
-
|
| 30 |
-
|
| 31 |
|
| 32 |
return (
|
| 33 |
<Button
|
|
@@ -35,14 +44,13 @@ export function MCQOptions({ mcq, onOptionSelect, disabled }: MCQOptionsProps) {
|
|
| 35 |
variant="outline"
|
| 36 |
className={cn(
|
| 37 |
'w-full justify-start text-left h-auto py-2.5 px-4 whitespace-normal',
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
(disabled || selectedOption) && !isSelected && 'opacity-60 cursor-not-allowed'
|
| 42 |
)}
|
| 43 |
onClick={() => handleSelect(option)}
|
| 44 |
-
disabled={disabled || !
|
| 45 |
-
aria-pressed={
|
| 46 |
>
|
| 47 |
<span className="mr-2 font-medium">{String.fromCharCode(65 + index)}.</span>
|
| 48 |
{option}
|
|
|
|
| 1 |
+
|
| 2 |
"use client";
|
| 3 |
|
| 4 |
import type { GenerateMCQOutput } from '@/ai/flows/generate-mcq';
|
| 5 |
import { Button } from '@/components/ui/button';
|
| 6 |
import { cn } from '@/lib/utils';
|
| 7 |
+
import { useState, useEffect } from 'react';
|
| 8 |
|
| 9 |
interface MCQOptionsProps {
|
| 10 |
mcq: GenerateMCQOutput;
|
|
|
|
| 15 |
export function MCQOptions({ mcq, onOptionSelect, disabled }: MCQOptionsProps) {
|
| 16 |
const [selectedOption, setSelectedOption] = useState<string | null>(null);
|
| 17 |
|
| 18 |
+
// Reset selectedOption when the options become enabled again (for retries)
|
| 19 |
+
useEffect(() => {
|
| 20 |
+
if (!disabled) {
|
| 21 |
+
setSelectedOption(null);
|
| 22 |
+
}
|
| 23 |
+
}, [disabled]);
|
| 24 |
+
|
| 25 |
const handleSelect = (option: string) => {
|
| 26 |
+
// Prevent selection if already disabled (isAwaitingAnswer=false) or an option is already visually selected for this attempt
|
| 27 |
+
if (disabled || selectedOption) return;
|
| 28 |
|
| 29 |
+
setSelectedOption(option); // Visually mark selection for this attempt
|
| 30 |
const isCorrect = option === mcq.correctAnswer;
|
| 31 |
+
onOptionSelect(option, isCorrect); // Parent handles disabling/enabling for next state
|
| 32 |
};
|
| 33 |
|
| 34 |
return (
|
| 35 |
<div className="mt-4 space-y-2">
|
| 36 |
{mcq.options.map((option, index) => {
|
| 37 |
+
const isCurrentlySelectedVisual = selectedOption === option;
|
| 38 |
+
// Visual feedback for correct/incorrect is handled by AI's response message, not here.
|
| 39 |
+
// These buttons are just for selection.
|
| 40 |
|
| 41 |
return (
|
| 42 |
<Button
|
|
|
|
| 44 |
variant="outline"
|
| 45 |
className={cn(
|
| 46 |
'w-full justify-start text-left h-auto py-2.5 px-4 whitespace-normal',
|
| 47 |
+
isCurrentlySelectedVisual && 'font-semibold bg-accent/50', // Highlight current visual selection
|
| 48 |
+
disabled && !isCurrentlySelectedVisual && 'opacity-60 cursor-not-allowed', // Dim unselected options if disabled
|
| 49 |
+
disabled && isCurrentlySelectedVisual && 'opacity-80 cursor-not-allowed' // Slightly less dim for the one that was just clicked
|
|
|
|
| 50 |
)}
|
| 51 |
onClick={() => handleSelect(option)}
|
| 52 |
+
disabled={disabled || (selectedOption !== null && selectedOption !== option)} // Disable other buttons once one is selected for this attempt
|
| 53 |
+
aria-pressed={isCurrentlySelectedVisual}
|
| 54 |
>
|
| 55 |
<span className="mr-2 font-medium">{String.fromCharCode(65 + index)}.</span>
|
| 56 |
{option}
|