Spaces:
Sleeping
Sleeping
Commit
·
c24ed27
1
Parent(s):
43020c4
I still see a toast message only. not the summary screen. Also, the subt
Browse files
src/app/journey/[journeyId]/page.tsx
CHANGED
|
@@ -50,50 +50,66 @@ export default function JourneyPage() {
|
|
| 50 |
setJourney(currentJourney);
|
| 51 |
setError(null);
|
| 52 |
} else {
|
| 53 |
-
setError("Journey not found. Please select a valid journey.");
|
| 54 |
setJourney(null);
|
| 55 |
-
toast
|
| 56 |
-
title: "Error",
|
| 57 |
-
description: "Journey not found.",
|
| 58 |
-
variant: "destructive",
|
| 59 |
-
});
|
| 60 |
}
|
| 61 |
} else {
|
| 62 |
-
setError("No journey ID provided.");
|
| 63 |
-
toast
|
| 64 |
-
title: "Error",
|
| 65 |
-
description: "No journey ID in URL.",
|
| 66 |
-
variant: "destructive",
|
| 67 |
-
});
|
| 68 |
}
|
| 69 |
-
}, [journeyId
|
| 70 |
|
| 71 |
useEffect(() => {
|
|
|
|
| 72 |
if (journey?.imageUrl) {
|
| 73 |
-
setIsLoadingJourney(true);
|
| 74 |
imageToDataUri(journey.imageUrl)
|
| 75 |
.then(uri => {
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
| 84 |
}
|
| 85 |
})
|
| 86 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
} else if (journey && !journey.imageUrl) {
|
| 88 |
-
setError("
|
| 89 |
toast({
|
| 90 |
-
title: "Missing Image",
|
| 91 |
-
description: "
|
| 92 |
variant: "destructive",
|
| 93 |
});
|
| 94 |
setIsLoadingJourney(false);
|
|
|
|
|
|
|
| 95 |
}
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
|
| 99 |
const handleNavigateToSummary = useCallback(() => {
|
|
@@ -108,32 +124,47 @@ export default function JourneyPage() {
|
|
| 108 |
router.push(`/journey/${journeyId}/summary`);
|
| 109 |
}, [journeyId, router, toast]);
|
| 110 |
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
return (
|
| 114 |
<div className="flex min-h-screen flex-col">
|
| 115 |
<AppHeader showBackButton backHref="/select-journey" />
|
| 116 |
<main className="container mx-auto flex flex-1 items-center justify-center p-4">
|
| 117 |
<Alert variant="destructive" className="max-w-md">
|
| 118 |
<AlertTriangle className="h-4 w-4" />
|
| 119 |
-
<AlertTitle>Error</AlertTitle>
|
| 120 |
-
<AlertDescription>
|
| 121 |
</Alert>
|
| 122 |
</main>
|
| 123 |
</div>
|
| 124 |
);
|
| 125 |
}
|
| 126 |
|
| 127 |
-
|
| 128 |
-
|
|
|
|
| 129 |
<div className="flex min-h-screen flex-col">
|
| 130 |
-
<AppHeader showBackButton backHref="/select-journey" />
|
| 131 |
<main className="container mx-auto flex flex-1 items-center justify-center p-4">
|
| 132 |
<Loader2 className="h-12 w-12 animate-spin text-primary" />
|
| 133 |
</main>
|
| 134 |
</div>
|
| 135 |
);
|
| 136 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
const journeyTitle = journey?.title || 'Loading Journey...';
|
| 139 |
|
|
@@ -168,18 +199,20 @@ export default function JourneyPage() {
|
|
| 168 |
<div className="flex h-full w-full flex-col items-center justify-center p-4 text-center">
|
| 169 |
<AlertTriangle className="h-16 w-16 text-destructive mb-4" />
|
| 170 |
<p className="text-destructive-foreground font-semibold">Image Not Available</p>
|
| 171 |
-
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
|
| 172 |
</div>
|
| 173 |
)}
|
| 174 |
</div>
|
| 175 |
|
| 176 |
<div className="h-full overflow-y-auto">
|
| 177 |
-
{isLoadingJourney && !imageDataUri && journey?.imageUrl
|
| 178 |
<div className="flex h-full flex-col items-center justify-center rounded-lg border bg-card p-4 shadow-xl">
|
| 179 |
<Loader2 className="h-12 w-12 animate-spin text-primary mb-4" />
|
| 180 |
<p className="text-muted-foreground">Preparing AI interactions...</p>
|
| 181 |
</div>
|
| 182 |
-
)}
|
| 183 |
{!isLoadingJourney && imageDataUri && journey && (
|
| 184 |
<Chatbot
|
| 185 |
imageDataUri={imageDataUri}
|
|
@@ -187,11 +220,16 @@ export default function JourneyPage() {
|
|
| 187 |
journeyId={journeyId}
|
| 188 |
/>
|
| 189 |
)}
|
| 190 |
-
{
|
|
|
|
| 191 |
<div className="flex h-full flex-col items-center justify-center rounded-lg border bg-card p-4 shadow-xl text-center">
|
| 192 |
<AlertTriangle className="h-12 w-12 text-destructive mb-4" />
|
| 193 |
<p className="font-semibold text-destructive">AI Features Unavailable</p>
|
| 194 |
-
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
</div>
|
| 196 |
)}
|
| 197 |
</div>
|
|
|
|
| 50 |
setJourney(currentJourney);
|
| 51 |
setError(null);
|
| 52 |
} else {
|
| 53 |
+
setError("Journey not found. Please select a valid journey from the selection page.");
|
| 54 |
setJourney(null);
|
| 55 |
+
// No toast here, error will be displayed on page
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
}
|
| 57 |
} else {
|
| 58 |
+
setError("No journey ID provided in the URL. Please navigate from the selection page.");
|
| 59 |
+
// No toast here, error will be displayed on page
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
}
|
| 61 |
+
}, [journeyId]);
|
| 62 |
|
| 63 |
useEffect(() => {
|
| 64 |
+
let isMounted = true;
|
| 65 |
if (journey?.imageUrl) {
|
| 66 |
+
setIsLoadingJourney(true);
|
| 67 |
imageToDataUri(journey.imageUrl)
|
| 68 |
.then(uri => {
|
| 69 |
+
if (isMounted) {
|
| 70 |
+
setImageDataUri(uri);
|
| 71 |
+
if (!uri) {
|
| 72 |
+
setError(prev => prev || "Failed to load journey image for AI interaction. Some features may be unavailable.");
|
| 73 |
+
toast({
|
| 74 |
+
title: "Image Load Error",
|
| 75 |
+
description: "Could not load image data for AI features. Chat and summary might be limited.",
|
| 76 |
+
variant: "destructive",
|
| 77 |
+
});
|
| 78 |
+
}
|
| 79 |
}
|
| 80 |
})
|
| 81 |
+
.catch(() => {
|
| 82 |
+
if (isMounted) {
|
| 83 |
+
setError(prev => prev || "Error processing journey image. Some features may be unavailable.");
|
| 84 |
+
toast({
|
| 85 |
+
title: "Image Processing Error",
|
| 86 |
+
description: "Could not process image data. Chat and summary might be limited.",
|
| 87 |
+
variant: "destructive",
|
| 88 |
+
});
|
| 89 |
+
}
|
| 90 |
+
})
|
| 91 |
+
.finally(() => {
|
| 92 |
+
if (isMounted) {
|
| 93 |
+
setIsLoadingJourney(false);
|
| 94 |
+
}
|
| 95 |
+
});
|
| 96 |
} else if (journey && !journey.imageUrl) {
|
| 97 |
+
setError(prev => prev || "The selected journey is missing its primary image. AI features cannot be initialized.");
|
| 98 |
toast({
|
| 99 |
+
title: "Missing Journey Image",
|
| 100 |
+
description: "This journey has no image, so AI chat and summary are unavailable.",
|
| 101 |
variant: "destructive",
|
| 102 |
});
|
| 103 |
setIsLoadingJourney(false);
|
| 104 |
+
} else if (!journey && !journeyId) { // Handles case where journeyId itself is missing from start
|
| 105 |
+
setIsLoadingJourney(false); // Stop loading if no journeyId to begin with
|
| 106 |
}
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
return () => {
|
| 110 |
+
isMounted = false;
|
| 111 |
+
};
|
| 112 |
+
}, [journey, toast, journeyId]);
|
| 113 |
|
| 114 |
|
| 115 |
const handleNavigateToSummary = useCallback(() => {
|
|
|
|
| 124 |
router.push(`/journey/${journeyId}/summary`);
|
| 125 |
}, [journeyId, router, toast]);
|
| 126 |
|
| 127 |
+
if (!journeyId) { // Early exit if journeyId is not in URL
|
| 128 |
+
return (
|
|
|
|
| 129 |
<div className="flex min-h-screen flex-col">
|
| 130 |
<AppHeader showBackButton backHref="/select-journey" />
|
| 131 |
<main className="container mx-auto flex flex-1 items-center justify-center p-4">
|
| 132 |
<Alert variant="destructive" className="max-w-md">
|
| 133 |
<AlertTriangle className="h-4 w-4" />
|
| 134 |
+
<AlertTitle>Error: Missing Journey</AlertTitle>
|
| 135 |
+
<AlertDescription>No journey ID was provided. Please return to the selection page and choose a journey.</AlertDescription>
|
| 136 |
</Alert>
|
| 137 |
</main>
|
| 138 |
</div>
|
| 139 |
);
|
| 140 |
}
|
| 141 |
|
| 142 |
+
|
| 143 |
+
if (isLoadingJourney && !journey && !error) { // Initial loading state for journey itself
|
| 144 |
+
return (
|
| 145 |
<div className="flex min-h-screen flex-col">
|
| 146 |
+
<AppHeader showBackButton backHref="/select-journey" title="Loading Journey..." />
|
| 147 |
<main className="container mx-auto flex flex-1 items-center justify-center p-4">
|
| 148 |
<Loader2 className="h-12 w-12 animate-spin text-primary" />
|
| 149 |
</main>
|
| 150 |
</div>
|
| 151 |
);
|
| 152 |
}
|
| 153 |
+
|
| 154 |
+
if (error && !journey?.imageUrl) { // Display error if journey loading failed or image is missing preventing essential functions
|
| 155 |
+
return (
|
| 156 |
+
<div className="flex min-h-screen flex-col">
|
| 157 |
+
<AppHeader showBackButton backHref="/select-journey" title="Journey Error"/>
|
| 158 |
+
<main className="container mx-auto flex flex-1 items-center justify-center p-4">
|
| 159 |
+
<Alert variant="destructive" className="max-w-lg">
|
| 160 |
+
<AlertTriangle className="h-4 w-4" />
|
| 161 |
+
<AlertTitle>Error Loading Journey</AlertTitle>
|
| 162 |
+
<AlertDescription>{error}</AlertDescription>
|
| 163 |
+
</Alert>
|
| 164 |
+
</main>
|
| 165 |
+
</div>
|
| 166 |
+
);
|
| 167 |
+
}
|
| 168 |
|
| 169 |
const journeyTitle = journey?.title || 'Loading Journey...';
|
| 170 |
|
|
|
|
| 199 |
<div className="flex h-full w-full flex-col items-center justify-center p-4 text-center">
|
| 200 |
<AlertTriangle className="h-16 w-16 text-destructive mb-4" />
|
| 201 |
<p className="text-destructive-foreground font-semibold">Image Not Available</p>
|
| 202 |
+
<p className="text-sm text-muted-foreground">
|
| 203 |
+
{error && error.includes("Failed to load journey image") ? error : "The image for this journey could not be displayed."}
|
| 204 |
+
</p>
|
| 205 |
</div>
|
| 206 |
)}
|
| 207 |
</div>
|
| 208 |
|
| 209 |
<div className="h-full overflow-y-auto">
|
| 210 |
+
{(isLoadingJourney && !imageDataUri && journey?.imageUrl) ? (
|
| 211 |
<div className="flex h-full flex-col items-center justify-center rounded-lg border bg-card p-4 shadow-xl">
|
| 212 |
<Loader2 className="h-12 w-12 animate-spin text-primary mb-4" />
|
| 213 |
<p className="text-muted-foreground">Preparing AI interactions...</p>
|
| 214 |
</div>
|
| 215 |
+
) : null}
|
| 216 |
{!isLoadingJourney && imageDataUri && journey && (
|
| 217 |
<Chatbot
|
| 218 |
imageDataUri={imageDataUri}
|
|
|
|
| 220 |
journeyId={journeyId}
|
| 221 |
/>
|
| 222 |
)}
|
| 223 |
+
{/* Case: Journey loaded, but image data URI failed or no image URL, and not actively loading image anymore */}
|
| 224 |
+
{ !isLoadingJourney && !imageDataUri && journey && (
|
| 225 |
<div className="flex h-full flex-col items-center justify-center rounded-lg border bg-card p-4 shadow-xl text-center">
|
| 226 |
<AlertTriangle className="h-12 w-12 text-destructive mb-4" />
|
| 227 |
<p className="font-semibold text-destructive">AI Features Unavailable</p>
|
| 228 |
+
<p className="text-sm text-muted-foreground">
|
| 229 |
+
{error && (error.includes("Failed to load") || error.includes("missing"))
|
| 230 |
+
? error
|
| 231 |
+
: "Could not load image data required for AI chat. Please check the journey image."}
|
| 232 |
+
</p>
|
| 233 |
</div>
|
| 234 |
)}
|
| 235 |
</div>
|
src/app/journey/[journeyId]/summary/page.tsx
CHANGED
|
@@ -39,120 +39,152 @@ export default function SummaryPage() {
|
|
| 39 |
const [journey, setJourney] = useState<Journey | null>(null);
|
| 40 |
const [imageDataUri, setImageDataUri] = useState<string | null>(null);
|
| 41 |
const [summary, setSummary] = useState<string | null>(null);
|
| 42 |
-
|
| 43 |
-
const [
|
| 44 |
-
const [
|
|
|
|
|
|
|
| 45 |
const { toast } = useToast();
|
| 46 |
|
| 47 |
useEffect(() => {
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
title: "Error",
|
| 58 |
-
description: "Journey not found.",
|
| 59 |
-
variant: "destructive",
|
| 60 |
-
});
|
| 61 |
-
router.replace('/select-journey'); // Redirect if journey is invalid
|
| 62 |
}
|
| 63 |
-
|
| 64 |
-
setError("No journey ID provided.");
|
| 65 |
-
toast({
|
| 66 |
-
title: "Error",
|
| 67 |
-
description: "No journey ID in URL.",
|
| 68 |
-
variant: "destructive",
|
| 69 |
-
});
|
| 70 |
-
router.replace('/select-journey'); // Redirect if no ID
|
| 71 |
}
|
| 72 |
-
}, [journeyId, toast, router]);
|
| 73 |
|
| 74 |
-
|
| 75 |
-
if (
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
}
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
| 96 |
});
|
| 97 |
-
|
| 98 |
-
}
|
| 99 |
-
}, [
|
| 100 |
|
| 101 |
useEffect(() => {
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
const fetchSummary = async () => {
|
|
|
|
| 104 |
setIsLoadingSummary(true);
|
| 105 |
try {
|
| 106 |
-
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
} catch (fetchError) {
|
| 109 |
console.error('Error generating summary:', fetchError);
|
| 110 |
-
const errorMessage = fetchError instanceof Error ? fetchError.message : "An unknown error occurred.";
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
|
|
|
|
|
|
| 117 |
} finally {
|
| 118 |
-
setIsLoadingSummary(false);
|
| 119 |
}
|
| 120 |
};
|
| 121 |
fetchSummary();
|
| 122 |
}
|
| 123 |
-
|
|
|
|
| 124 |
|
| 125 |
|
| 126 |
-
|
| 127 |
-
|
|
|
|
| 128 |
<div className="flex min-h-screen flex-col">
|
| 129 |
-
<AppHeader showBackButton backHref={journeyId ? `/journey/${journeyId}` : "/select-journey"} title="
|
| 130 |
<main className="container mx-auto flex flex-1 items-center justify-center p-4">
|
| 131 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
</main>
|
| 133 |
</div>
|
| 134 |
);
|
| 135 |
}
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
|
|
|
| 139 |
<div className="flex min-h-screen flex-col">
|
| 140 |
-
<AppHeader showBackButton backHref="/select-journey" title="
|
| 141 |
<main className="container mx-auto flex flex-1 items-center justify-center p-4">
|
| 142 |
-
<
|
| 143 |
-
<AlertTriangle className="h-4 w-4" />
|
| 144 |
-
<AlertTitle>Error</AlertTitle>
|
| 145 |
-
<AlertDescription>{error}</AlertDescription>
|
| 146 |
-
</Alert>
|
| 147 |
</main>
|
| 148 |
</div>
|
| 149 |
);
|
| 150 |
}
|
| 151 |
-
|
| 152 |
const pageTitle = journey ? `Summary: ${journey.title}` : 'Summary Report';
|
| 153 |
const backNavigation = journeyId ? `/journey/${journeyId}` : '/select-journey';
|
| 154 |
|
| 155 |
-
|
| 156 |
return (
|
| 157 |
<div className="flex h-screen flex-col">
|
| 158 |
<AppHeader
|
|
@@ -164,14 +196,9 @@ export default function SummaryPage() {
|
|
| 164 |
<div className="grid h-full grid-cols-1 gap-2 md:grid-cols-2 md:gap-4">
|
| 165 |
{/* Left Column: Image */}
|
| 166 |
<div className="relative h-full w-full overflow-hidden rounded-lg shadow-lg bg-muted">
|
| 167 |
-
{
|
| 168 |
-
<div className="flex h-full w-full flex-col items-center justify-center">
|
| 169 |
-
<Loader2 className="h-16 w-16 animate-spin text-primary mb-4" />
|
| 170 |
-
<p className="text-muted-foreground">Loading image for {journey?.title || 'journey'}...</p>
|
| 171 |
-
</div>
|
| 172 |
-
) : journey?.imageUrl && imageDataUri ? (
|
| 173 |
<Image
|
| 174 |
-
src={journey.imageUrl}
|
| 175 |
alt={journey.title}
|
| 176 |
layout="fill"
|
| 177 |
objectFit="cover"
|
|
@@ -183,7 +210,9 @@ export default function SummaryPage() {
|
|
| 183 |
<div className="flex h-full w-full flex-col items-center justify-center p-4 text-center">
|
| 184 |
<AlertTriangle className="h-16 w-16 text-destructive mb-4" />
|
| 185 |
<p className="text-destructive-foreground font-semibold">Image Not Available</p>
|
| 186 |
-
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
|
| 187 |
</div>
|
| 188 |
)}
|
| 189 |
</div>
|
|
@@ -202,35 +231,39 @@ export default function SummaryPage() {
|
|
| 202 |
<Skeleton className="h-5 w-full rounded-md" />
|
| 203 |
</div>
|
| 204 |
)}
|
| 205 |
-
{!isLoadingSummary && summary && (
|
| 206 |
<div className="prose prose-sm dark:prose-invert max-w-none whitespace-pre-wrap font-body">
|
| 207 |
{summary}
|
| 208 |
</div>
|
| 209 |
)}
|
| 210 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
<div className="flex flex-col items-center justify-center py-10 text-muted-foreground">
|
| 212 |
<Loader2 className="h-8 w-8 animate-spin mb-2 text-primary" />
|
| 213 |
-
<p>
|
| 214 |
</div>
|
| 215 |
)}
|
| 216 |
-
{!imageDataUri && !
|
| 217 |
<div className="py-10 text-center text-muted-foreground">
|
| 218 |
<AlertTriangle className="h-8 w-8 text-destructive mx-auto mb-2" />
|
| 219 |
<p className="font-semibold text-destructive">Summary Unavailable</p>
|
| 220 |
-
<p>Image data is not available, so a summary cannot be generated.</p>
|
| 221 |
-
</div>
|
| 222 |
-
)}
|
| 223 |
-
{error && !summary && !isLoadingSummary && ( // An error occurred that prevented summary generation
|
| 224 |
-
<div className="py-10 text-center">
|
| 225 |
-
<Alert variant="destructive">
|
| 226 |
-
<AlertTriangle className="h-4 w-4" />
|
| 227 |
-
<AlertTitle>Summary Error</AlertTitle>
|
| 228 |
-
<AlertDescription>
|
| 229 |
-
{error.includes("Journey image is missing") || error.includes("Failed to load journey image")
|
| 230 |
-
? error
|
| 231 |
-
: "Could not generate summary at this time."}
|
| 232 |
-
</AlertDescription>
|
| 233 |
-
</Alert>
|
| 234 |
</div>
|
| 235 |
)}
|
| 236 |
</div>
|
|
|
|
| 39 |
const [journey, setJourney] = useState<Journey | null>(null);
|
| 40 |
const [imageDataUri, setImageDataUri] = useState<string | null>(null);
|
| 41 |
const [summary, setSummary] = useState<string | null>(null);
|
| 42 |
+
|
| 43 |
+
const [isLoadingJourneyData, setIsLoadingJourneyData] = useState(true); // For journey object and image data URI
|
| 44 |
+
const [isLoadingSummary, setIsLoadingSummary] = useState(false); // Specifically for AI summary generation
|
| 45 |
+
|
| 46 |
+
const [pageError, setPageError] = useState<string | null>(null); // For critical errors displayed on page
|
| 47 |
const { toast } = useToast();
|
| 48 |
|
| 49 |
useEffect(() => {
|
| 50 |
+
let isMounted = true;
|
| 51 |
+
setIsLoadingJourneyData(true);
|
| 52 |
+
setPageError(null); // Reset page error on new journeyId
|
| 53 |
+
|
| 54 |
+
if (!journeyId) {
|
| 55 |
+
if (isMounted) {
|
| 56 |
+
setPageError("No journey ID provided. Please select a journey.");
|
| 57 |
+
setIsLoadingJourneyData(false);
|
| 58 |
+
// router.replace('/select-journey'); // Consider redirecting
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
}
|
| 60 |
+
return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
}
|
|
|
|
| 62 |
|
| 63 |
+
const currentJourney = getJourneyById(journeyId);
|
| 64 |
+
if (!currentJourney) {
|
| 65 |
+
if (isMounted) {
|
| 66 |
+
setPageError(`Journey with ID "${journeyId}" not found. Please select a valid journey.`);
|
| 67 |
+
setIsLoadingJourneyData(false);
|
| 68 |
+
// router.replace('/select-journey'); // Consider redirecting
|
| 69 |
+
}
|
| 70 |
+
return;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
if (isMounted) setJourney(currentJourney);
|
| 74 |
+
|
| 75 |
+
if (!currentJourney.imageUrl) {
|
| 76 |
+
if (isMounted) {
|
| 77 |
+
setPageError("The selected journey is missing an image. A summary cannot be generated without an image.");
|
| 78 |
+
setImageDataUri(null); // Ensure imageDataUri is null
|
| 79 |
+
setIsLoadingJourneyData(false);
|
| 80 |
+
}
|
| 81 |
+
return;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
imageToDataUri(currentJourney.imageUrl)
|
| 85 |
+
.then(uri => {
|
| 86 |
+
if (isMounted) {
|
| 87 |
+
if (uri) {
|
| 88 |
+
setImageDataUri(uri);
|
| 89 |
+
} else {
|
| 90 |
+
setPageError("Failed to load and process the journey image. The summary cannot be generated.");
|
| 91 |
+
setImageDataUri(null);
|
| 92 |
}
|
| 93 |
+
}
|
| 94 |
+
})
|
| 95 |
+
.catch(() => {
|
| 96 |
+
if (isMounted) {
|
| 97 |
+
setPageError("An error occurred while converting the image. The summary cannot be generated.");
|
| 98 |
+
setImageDataUri(null);
|
| 99 |
+
}
|
| 100 |
+
})
|
| 101 |
+
.finally(() => {
|
| 102 |
+
if (isMounted) setIsLoadingJourneyData(false);
|
| 103 |
});
|
| 104 |
+
|
| 105 |
+
return () => { isMounted = false; };
|
| 106 |
+
}, [journeyId, router]);
|
| 107 |
|
| 108 |
useEffect(() => {
|
| 109 |
+
let isMounted = true;
|
| 110 |
+
// Fetch summary only if:
|
| 111 |
+
// 1. Journey data and imageDataUri are loaded (isLoadingJourneyData is false)
|
| 112 |
+
// 2. imageDataUri is actually available
|
| 113 |
+
// 3. No critical pageError exists
|
| 114 |
+
// 4. Summary hasn't been fetched yet (summary is null)
|
| 115 |
+
// 5. Not currently loading a summary
|
| 116 |
+
if (!isLoadingJourneyData && imageDataUri && !pageError && !summary && !isLoadingSummary) {
|
| 117 |
const fetchSummary = async () => {
|
| 118 |
+
if (!isMounted) return;
|
| 119 |
setIsLoadingSummary(true);
|
| 120 |
try {
|
| 121 |
+
// imageDataUri is confirmed non-null by the if condition
|
| 122 |
+
const result = await summarizeImage({ imageDataUri: imageDataUri! });
|
| 123 |
+
if (isMounted) {
|
| 124 |
+
if (result && result.summary) {
|
| 125 |
+
setSummary(result.summary);
|
| 126 |
+
} else {
|
| 127 |
+
// This case handles if AI returns unexpected structure
|
| 128 |
+
setSummary("Failed to generate summary: The AI response was not in the expected format.");
|
| 129 |
+
toast({
|
| 130 |
+
title: "Summary Generation Issue",
|
| 131 |
+
description: "The AI did not provide a summary in the expected format. Please try again later.",
|
| 132 |
+
variant: "destructive",
|
| 133 |
+
});
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
} catch (fetchError) {
|
| 137 |
console.error('Error generating summary:', fetchError);
|
| 138 |
+
const errorMessage = fetchError instanceof Error ? fetchError.message : "An unknown error occurred during summary generation.";
|
| 139 |
+
if (isMounted) {
|
| 140 |
+
setSummary(`Failed to generate summary: ${errorMessage}`); // Show error in summary area
|
| 141 |
+
toast({
|
| 142 |
+
title: "Summary Generation Error",
|
| 143 |
+
description: `Could not generate summary. ${errorMessage}`,
|
| 144 |
+
variant: "destructive",
|
| 145 |
+
});
|
| 146 |
+
}
|
| 147 |
} finally {
|
| 148 |
+
if (isMounted) setIsLoadingSummary(false);
|
| 149 |
}
|
| 150 |
};
|
| 151 |
fetchSummary();
|
| 152 |
}
|
| 153 |
+
return () => { isMounted = false; };
|
| 154 |
+
}, [isLoadingJourneyData, imageDataUri, pageError, summary, isLoadingSummary, toast]);
|
| 155 |
|
| 156 |
|
| 157 |
+
// Display for critical page errors (journey not found, no ID, image missing/failed to load)
|
| 158 |
+
if (!isLoadingJourneyData && pageError) {
|
| 159 |
+
return (
|
| 160 |
<div className="flex min-h-screen flex-col">
|
| 161 |
+
<AppHeader showBackButton backHref={journeyId ? `/journey/${journeyId}` : "/select-journey"} title="Summary Error" />
|
| 162 |
<main className="container mx-auto flex flex-1 items-center justify-center p-4">
|
| 163 |
+
<Alert variant="destructive" className="max-w-md">
|
| 164 |
+
<AlertTriangle className="h-4 w-4" />
|
| 165 |
+
<AlertTitle>Unable to Load Summary</AlertTitle>
|
| 166 |
+
<AlertDescription>{pageError}</AlertDescription>
|
| 167 |
+
</Alert>
|
| 168 |
</main>
|
| 169 |
</div>
|
| 170 |
);
|
| 171 |
}
|
| 172 |
+
|
| 173 |
+
// Display for initial loading of journey data and image
|
| 174 |
+
if (isLoadingJourneyData) {
|
| 175 |
+
return (
|
| 176 |
<div className="flex min-h-screen flex-col">
|
| 177 |
+
<AppHeader showBackButton backHref={journeyId ? `/journey/${journeyId}` : "/select-journey"} title="Loading Summary Data..." />
|
| 178 |
<main className="container mx-auto flex flex-1 items-center justify-center p-4">
|
| 179 |
+
<Loader2 className="h-12 w-12 animate-spin text-primary" />
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
</main>
|
| 181 |
</div>
|
| 182 |
);
|
| 183 |
}
|
| 184 |
+
|
| 185 |
const pageTitle = journey ? `Summary: ${journey.title}` : 'Summary Report';
|
| 186 |
const backNavigation = journeyId ? `/journey/${journeyId}` : '/select-journey';
|
| 187 |
|
|
|
|
| 188 |
return (
|
| 189 |
<div className="flex h-screen flex-col">
|
| 190 |
<AppHeader
|
|
|
|
| 196 |
<div className="grid h-full grid-cols-1 gap-2 md:grid-cols-2 md:gap-4">
|
| 197 |
{/* Left Column: Image */}
|
| 198 |
<div className="relative h-full w-full overflow-hidden rounded-lg shadow-lg bg-muted">
|
| 199 |
+
{journey?.imageUrl && imageDataUri ? (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
<Image
|
| 201 |
+
src={journey.imageUrl} /* Use original URL for display if data URI conversion was just for AI */
|
| 202 |
alt={journey.title}
|
| 203 |
layout="fill"
|
| 204 |
objectFit="cover"
|
|
|
|
| 210 |
<div className="flex h-full w-full flex-col items-center justify-center p-4 text-center">
|
| 211 |
<AlertTriangle className="h-16 w-16 text-destructive mb-4" />
|
| 212 |
<p className="text-destructive-foreground font-semibold">Image Not Available</p>
|
| 213 |
+
<p className="text-sm text-muted-foreground">
|
| 214 |
+
{pageError || "The image for this journey could not be displayed."}
|
| 215 |
+
</p>
|
| 216 |
</div>
|
| 217 |
)}
|
| 218 |
</div>
|
|
|
|
| 231 |
<Skeleton className="h-5 w-full rounded-md" />
|
| 232 |
</div>
|
| 233 |
)}
|
| 234 |
+
{!isLoadingSummary && summary && !summary.startsWith("Failed to generate summary:") && (
|
| 235 |
<div className="prose prose-sm dark:prose-invert max-w-none whitespace-pre-wrap font-body">
|
| 236 |
{summary}
|
| 237 |
</div>
|
| 238 |
)}
|
| 239 |
+
{/* Displaying error message from summary state if summary generation failed */}
|
| 240 |
+
{!isLoadingSummary && summary && summary.startsWith("Failed to generate summary:") && (
|
| 241 |
+
<Alert variant="destructive">
|
| 242 |
+
<AlertTriangle className="h-4 w-4" />
|
| 243 |
+
<AlertTitle>Summary Generation Failed</AlertTitle>
|
| 244 |
+
<AlertDescription>{summary}</AlertDescription>
|
| 245 |
+
</Alert>
|
| 246 |
+
)}
|
| 247 |
+
{/* Message if summary cannot be generated due to earlier pageError (e.g., no image) */}
|
| 248 |
+
{!isLoadingSummary && !summary && pageError && (
|
| 249 |
+
<Alert variant="destructive">
|
| 250 |
+
<AlertTriangle className="h-4 w-4" />
|
| 251 |
+
<AlertTitle>Summary Unavailable</AlertTitle>
|
| 252 |
+
<AlertDescription>{pageError}</AlertDescription>
|
| 253 |
+
</Alert>
|
| 254 |
+
)}
|
| 255 |
+
{/* Fallback for when summary is null, not loading, and no specific page error for summary generation */}
|
| 256 |
+
{!isLoadingSummary && !summary && !pageError && imageDataUri && (
|
| 257 |
<div className="flex flex-col items-center justify-center py-10 text-muted-foreground">
|
| 258 |
<Loader2 className="h-8 w-8 animate-spin mb-2 text-primary" />
|
| 259 |
+
<p>Preparing summary insights...</p> {/* Should transition to loading or result */}
|
| 260 |
</div>
|
| 261 |
)}
|
| 262 |
+
{!imageDataUri && !isLoadingJourneyData && !pageError && ( // Case where image is confirmed unavailable, not loading, and no other critical error
|
| 263 |
<div className="py-10 text-center text-muted-foreground">
|
| 264 |
<AlertTriangle className="h-8 w-8 text-destructive mx-auto mb-2" />
|
| 265 |
<p className="font-semibold text-destructive">Summary Unavailable</p>
|
| 266 |
+
<p>Image data is not available, so a summary cannot be generated for this journey.</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
</div>
|
| 268 |
)}
|
| 269 |
</div>
|
src/components/chatbot/chatbot.tsx
CHANGED
|
@@ -13,21 +13,21 @@ import { explainCorrectAnswer } from '@/ai/flows/explain-correct-answer-flow';
|
|
| 13 |
import type { ChatMessage as ChatMessageType, ActiveMCQ } from '@/types';
|
| 14 |
import { ChatMessage } from './chat-message';
|
| 15 |
import { useToast } from '@/hooks/use-toast';
|
| 16 |
-
import { Send, FileText } from 'lucide-react';
|
| 17 |
|
| 18 |
const MAX_QUESTIONS = 5;
|
| 19 |
|
| 20 |
interface ChatbotProps {
|
| 21 |
imageDataUri: string | null;
|
| 22 |
journeyTitle: string;
|
| 23 |
-
journeyId: string;
|
| 24 |
}
|
| 25 |
|
| 26 |
export function Chatbot({ imageDataUri, journeyTitle, journeyId }: ChatbotProps) {
|
| 27 |
const router = useRouter();
|
| 28 |
const [messages, setMessages] = useState<ChatMessageType[]>([]);
|
| 29 |
const [currentMCQ, setCurrentMCQ] = useState<ActiveMCQ | null>(null);
|
| 30 |
-
const [isLoading, setIsLoading] = useState(false);
|
| 31 |
const [isAwaitingAnswer, setIsAwaitingAnswer] = useState(false);
|
| 32 |
const [hasAnsweredCorrectly, setHasAnsweredCorrectly] = useState(false);
|
| 33 |
const [incorrectAttempts, setIncorrectAttempts] = useState<string[]>([]);
|
|
@@ -58,10 +58,12 @@ export function Chatbot({ imageDataUri, journeyTitle, journeyId }: ChatbotProps)
|
|
| 58 |
isInitialQuestionRef.current = true;
|
| 59 |
messageIdCounter.current = 0;
|
| 60 |
|
| 61 |
-
|
| 62 |
const performInitialFetch = async () => {
|
| 63 |
if (!imageDataUri) {
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
| 65 |
addMessage({ sender: 'ai', type: 'text', text: "Preparing your journey..." });
|
| 66 |
}
|
| 67 |
return;
|
|
@@ -70,13 +72,18 @@ export function Chatbot({ imageDataUri, journeyTitle, journeyId }: ChatbotProps)
|
|
| 70 |
if (!ignore) setIsLoading(true);
|
| 71 |
|
| 72 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
const mcqResult = await generateMCQ({ imageDataUri });
|
| 74 |
|
| 75 |
if (ignore) return;
|
| 76 |
|
| 77 |
let activeMcqData: ActiveMCQ = {
|
| 78 |
...mcqResult,
|
| 79 |
-
originalQuestionTextForFlow: mcqResult.mcq,
|
| 80 |
};
|
| 81 |
|
| 82 |
if (isInitialQuestionRef.current) {
|
|
@@ -105,18 +112,19 @@ export function Chatbot({ imageDataUri, journeyTitle, journeyId }: ChatbotProps)
|
|
| 105 |
}
|
| 106 |
};
|
| 107 |
|
|
|
|
|
|
|
| 108 |
if (imageDataUri) {
|
| 109 |
-
// Clear "Preparing..." message if it exists, then fetch
|
| 110 |
-
if (messages.length === 1 && messages[0].text === "Preparing your journey...") {
|
| 111 |
-
setMessages([]); // Clear it before fetching
|
| 112 |
-
}
|
| 113 |
performInitialFetch();
|
| 114 |
-
} else if (messages.length === 0) {
|
|
|
|
|
|
|
| 115 |
addMessage({ sender: 'ai', type: 'text', text: "Preparing your journey..." });
|
| 116 |
}
|
| 117 |
|
|
|
|
| 118 |
return () => {
|
| 119 |
-
ignore = true;
|
| 120 |
};
|
| 121 |
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 122 |
}, [imageDataUri, journeyTitle]); // addMessage is memoized
|
|
@@ -131,11 +139,11 @@ export function Chatbot({ imageDataUri, journeyTitle, journeyId }: ChatbotProps)
|
|
| 131 |
const handleOptionSelect = async (option: string, isCorrect: boolean) => {
|
| 132 |
if (!currentMCQ) return;
|
| 133 |
|
| 134 |
-
setIsAwaitingAnswer(false);
|
| 135 |
|
| 136 |
addMessage({
|
| 137 |
sender: 'user',
|
| 138 |
-
type: 'user',
|
| 139 |
text: option,
|
| 140 |
});
|
| 141 |
|
|
@@ -156,12 +164,13 @@ export function Chatbot({ imageDataUri, journeyTitle, journeyId }: ChatbotProps)
|
|
| 156 |
const errorMsg = error instanceof Error ? error.message : "An unknown error occurred";
|
| 157 |
combinedMessage += ` (Sorry, I couldn't provide an explanation for that: ${errorMsg})`;
|
| 158 |
}
|
|
|
|
| 159 |
addMessage({ sender: 'ai', type: 'feedback', text: combinedMessage, isCorrect: true });
|
| 160 |
setHasAnsweredCorrectly(true);
|
| 161 |
setIncorrectAttempts([]);
|
| 162 |
const newQuestionCount = questionCount + 1;
|
| 163 |
setQuestionCount(newQuestionCount);
|
| 164 |
-
|
| 165 |
addMessage({ sender: 'ai', type: 'text', text: "You've completed all 5 questions! Please proceed to the summary report." });
|
| 166 |
}
|
| 167 |
|
|
@@ -169,9 +178,11 @@ export function Chatbot({ imageDataUri, journeyTitle, journeyId }: ChatbotProps)
|
|
| 169 |
const updatedIncorrectAttempts = [...incorrectAttempts, option];
|
| 170 |
setIncorrectAttempts(updatedIncorrectAttempts);
|
| 171 |
|
| 172 |
-
if
|
|
|
|
| 173 |
let combinedText = "";
|
| 174 |
try {
|
|
|
|
| 175 |
const incorrectExplanationResult = await explainIncorrectAnswer({
|
| 176 |
question: questionForAI,
|
| 177 |
options: currentMCQ.options,
|
|
@@ -181,13 +192,14 @@ export function Chatbot({ imageDataUri, journeyTitle, journeyId }: ChatbotProps)
|
|
| 181 |
combinedText += `That's not quite right. ${incorrectExplanationResult.explanation} `;
|
| 182 |
} catch (error) {
|
| 183 |
console.error("Error fetching explanation for the last incorrect answer:", error);
|
| 184 |
-
|
| 185 |
-
|
| 186 |
}
|
| 187 |
|
| 188 |
combinedText += `The correct answer is "${currentMCQ.correctAnswer}". `;
|
| 189 |
|
| 190 |
try {
|
|
|
|
| 191 |
const correctExplanationResult = await explainCorrectAnswer({
|
| 192 |
question: questionForAI,
|
| 193 |
options: currentMCQ.options,
|
|
@@ -202,7 +214,7 @@ export function Chatbot({ imageDataUri, journeyTitle, journeyId }: ChatbotProps)
|
|
| 202 |
|
| 203 |
addMessage({
|
| 204 |
sender: 'ai',
|
| 205 |
-
type: 'feedback',
|
| 206 |
text: combinedText.trim(),
|
| 207 |
isCorrect: true, // Mark as 'correct' flow-wise to enable next question/summary
|
| 208 |
});
|
|
@@ -236,7 +248,6 @@ export function Chatbot({ imageDataUri, journeyTitle, journeyId }: ChatbotProps)
|
|
| 236 |
} catch (error) {
|
| 237 |
console.error("Error fetching explanation for incorrect answer:", error);
|
| 238 |
const errorMsg = error instanceof Error ? error.message : "An unknown error occurred";
|
| 239 |
-
// Fallback if explanation fails - still re-prompt
|
| 240 |
const fallbackRepromptText = `That's not quite right. (Sorry, I couldn't explain that: ${errorMsg}) Please try another option from the question above.`;
|
| 241 |
const fallbackMCQData: ActiveMCQ = {
|
| 242 |
...currentMCQ,
|
|
@@ -253,21 +264,20 @@ export function Chatbot({ imageDataUri, journeyTitle, journeyId }: ChatbotProps)
|
|
| 253 |
|
| 254 |
const handleNextQuestionClick = async () => {
|
| 255 |
if (questionCount >= MAX_QUESTIONS) {
|
| 256 |
-
// This case should ideally be handled by the summary button appearing
|
| 257 |
-
// but as a fallback, prevent fetching more questions.
|
| 258 |
addMessage({ sender: 'ai', type: 'text', text: "You've completed all the questions! Please proceed to the summary." });
|
| 259 |
setIsLoading(false);
|
| 260 |
-
setHasAnsweredCorrectly(true);
|
| 261 |
return;
|
| 262 |
}
|
| 263 |
|
| 264 |
-
setIsAwaitingAnswer(false);
|
| 265 |
setHasAnsweredCorrectly(false);
|
| 266 |
-
setCurrentMCQ(null);
|
| 267 |
-
setIncorrectAttempts([]);
|
| 268 |
|
| 269 |
if (!imageDataUri) {
|
| 270 |
addMessage({ sender: 'ai', type: 'error', text: "Image data is not available to generate new questions." });
|
|
|
|
| 271 |
return;
|
| 272 |
}
|
| 273 |
setIsLoading(true);
|
|
@@ -275,7 +285,7 @@ export function Chatbot({ imageDataUri, journeyTitle, journeyId }: ChatbotProps)
|
|
| 275 |
const mcqResult = await generateMCQ({ imageDataUri });
|
| 276 |
const nextMCQData: ActiveMCQ = {
|
| 277 |
...mcqResult,
|
| 278 |
-
originalQuestionTextForFlow: mcqResult.mcq,
|
| 279 |
};
|
| 280 |
setCurrentMCQ(nextMCQData);
|
| 281 |
addMessage({ sender: 'ai', type: 'mcq', mcq: nextMCQData });
|
|
@@ -289,6 +299,7 @@ export function Chatbot({ imageDataUri, journeyTitle, journeyId }: ChatbotProps)
|
|
| 289 |
description: `Could not generate a new question. ${errorMessage}`,
|
| 290 |
variant: "destructive",
|
| 291 |
});
|
|
|
|
| 292 |
} finally {
|
| 293 |
setIsLoading(false);
|
| 294 |
}
|
|
@@ -307,15 +318,11 @@ export function Chatbot({ imageDataUri, journeyTitle, journeyId }: ChatbotProps)
|
|
| 307 |
<ScrollArea className="flex-1 p-4" ref={scrollAreaRef}>
|
| 308 |
<div className="space-y-4">
|
| 309 |
{messages.map((msg) => {
|
| 310 |
-
// Determine if this message is the currently active MCQ
|
| 311 |
const isLastMessageTheActiveMCQ =
|
| 312 |
msg.type === 'mcq' &&
|
| 313 |
currentMCQ &&
|
| 314 |
-
// Compare based on the unique ID of the message object
|
| 315 |
msg.id === messages.findLast(m => m.type === 'mcq' && m.mcq?.mcq === currentMCQ.mcq && JSON.stringify(m.mcq?.options) === JSON.stringify(currentMCQ.options))?.id;
|
| 316 |
|
| 317 |
-
// Options should be interactive only for the last instance of the currentMCQ
|
| 318 |
-
// and if we are awaiting an answer and it hasn't been correctly answered yet.
|
| 319 |
const shouldThisMessageBeInteractive =
|
| 320 |
isLastMessageTheActiveMCQ &&
|
| 321 |
isAwaitingAnswer &&
|
|
@@ -323,21 +330,20 @@ export function Chatbot({ imageDataUri, journeyTitle, journeyId }: ChatbotProps)
|
|
| 323 |
|
| 324 |
return (
|
| 325 |
<ChatMessage
|
| 326 |
-
key={msg.id}
|
| 327 |
message={msg}
|
| 328 |
activeMCQ={currentMCQ}
|
| 329 |
isAwaitingActiveMCQAnswer={shouldThisMessageBeInteractive}
|
| 330 |
onOptionSelectActiveMCQ={handleOptionSelect}
|
| 331 |
incorrectAttemptsForMCQ={
|
| 332 |
-
shouldThisMessageBeInteractive
|
| 333 |
? incorrectAttempts
|
| 334 |
-
: []
|
| 335 |
}
|
| 336 |
/>
|
| 337 |
);
|
| 338 |
})}
|
| 339 |
-
{
|
| 340 |
-
{isLoading && !currentMCQ && messages[messages.length -1]?.type !== 'feedback' && ( // Avoid skeleton if last msg was feedback for resolved q
|
| 341 |
<div className="flex items-start space-x-3">
|
| 342 |
<Skeleton className="h-8 w-8 rounded-full" />
|
| 343 |
<div className="space-y-2">
|
|
@@ -349,7 +355,12 @@ export function Chatbot({ imageDataUri, journeyTitle, journeyId }: ChatbotProps)
|
|
| 349 |
</div>
|
| 350 |
</ScrollArea>
|
| 351 |
<div className="border-t bg-background/80 p-4">
|
| 352 |
-
{isLoading &&
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
|
| 354 |
{!isLoading && questionCount >= MAX_QUESTIONS && hasAnsweredCorrectly && (
|
| 355 |
<Button onClick={handleGoToSummary} className="w-full" variant="default">
|
|
@@ -362,25 +373,21 @@ export function Chatbot({ imageDataUri, journeyTitle, journeyId }: ChatbotProps)
|
|
| 362 |
Next Question <Send className="ml-2 h-4 w-4" />
|
| 363 |
</Button>
|
| 364 |
)}
|
| 365 |
-
|
| 366 |
-
{/* Message when awaiting answer for a current MCQ */}
|
| 367 |
{!isLoading && currentMCQ && !hasAnsweredCorrectly && isAwaitingAnswer && (
|
| 368 |
<p className="text-center text-sm text-muted-foreground">
|
| 369 |
Please choose an answer from the options above.
|
| 370 |
</p>
|
| 371 |
)}
|
| 372 |
|
| 373 |
-
{
|
| 374 |
-
{!imageDataUri && !isLoading && messages.length > 0 && messages[messages.length -1]?.text === "Preparing your journey..." &&(
|
| 375 |
<p className="text-center text-sm text-muted-foreground">Loading image, please wait...</p>
|
| 376 |
)}
|
| 377 |
-
{/* Message for initial question load (image ready, no messages yet or error) */}
|
| 378 |
{!currentMCQ && !isLoading && imageDataUri && messages.length === 0 && (
|
| 379 |
<p className="text-center text-sm text-muted-foreground">Loading first question...</p>
|
| 380 |
)}
|
| 381 |
-
{/* Message if initial question load failed */}
|
| 382 |
{!currentMCQ && !isLoading && imageDataUri && messages.length > 0 && messages[messages.length -1]?.type === 'error' && (
|
| 383 |
-
<p className="text-center text-sm text-destructive">Could not load question. Try refreshing.</p>
|
| 384 |
)}
|
| 385 |
</div>
|
| 386 |
</div>
|
|
|
|
| 13 |
import type { ChatMessage as ChatMessageType, ActiveMCQ } from '@/types';
|
| 14 |
import { ChatMessage } from './chat-message';
|
| 15 |
import { useToast } from '@/hooks/use-toast';
|
| 16 |
+
import { Send, FileText, Loader2 } from 'lucide-react';
|
| 17 |
|
| 18 |
const MAX_QUESTIONS = 5;
|
| 19 |
|
| 20 |
interface ChatbotProps {
|
| 21 |
imageDataUri: string | null;
|
| 22 |
journeyTitle: string;
|
| 23 |
+
journeyId: string;
|
| 24 |
}
|
| 25 |
|
| 26 |
export function Chatbot({ imageDataUri, journeyTitle, journeyId }: ChatbotProps) {
|
| 27 |
const router = useRouter();
|
| 28 |
const [messages, setMessages] = useState<ChatMessageType[]>([]);
|
| 29 |
const [currentMCQ, setCurrentMCQ] = useState<ActiveMCQ | null>(null);
|
| 30 |
+
const [isLoading, setIsLoading] = useState(false); // General loading for AI responses
|
| 31 |
const [isAwaitingAnswer, setIsAwaitingAnswer] = useState(false);
|
| 32 |
const [hasAnsweredCorrectly, setHasAnsweredCorrectly] = useState(false);
|
| 33 |
const [incorrectAttempts, setIncorrectAttempts] = useState<string[]>([]);
|
|
|
|
| 58 |
isInitialQuestionRef.current = true;
|
| 59 |
messageIdCounter.current = 0;
|
| 60 |
|
|
|
|
| 61 |
const performInitialFetch = async () => {
|
| 62 |
if (!imageDataUri) {
|
| 63 |
+
// This case should ideally be handled by parent not rendering Chatbot,
|
| 64 |
+
// or showing a "waiting for image" state if Chatbot is rendered before URI is ready.
|
| 65 |
+
if (!ignore && messages.length === 0) {
|
| 66 |
+
// Add a temporary "preparing" message if no URI yet, but this will be cleared.
|
| 67 |
addMessage({ sender: 'ai', type: 'text', text: "Preparing your journey..." });
|
| 68 |
}
|
| 69 |
return;
|
|
|
|
| 72 |
if (!ignore) setIsLoading(true);
|
| 73 |
|
| 74 |
try {
|
| 75 |
+
// Clear any "Preparing..." message before fetching the actual question
|
| 76 |
+
if (messages.length === 1 && messages[0].text === "Preparing your journey...") {
|
| 77 |
+
setMessages([]);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
const mcqResult = await generateMCQ({ imageDataUri });
|
| 81 |
|
| 82 |
if (ignore) return;
|
| 83 |
|
| 84 |
let activeMcqData: ActiveMCQ = {
|
| 85 |
...mcqResult,
|
| 86 |
+
originalQuestionTextForFlow: mcqResult.mcq,
|
| 87 |
};
|
| 88 |
|
| 89 |
if (isInitialQuestionRef.current) {
|
|
|
|
| 112 |
}
|
| 113 |
};
|
| 114 |
|
| 115 |
+
// Trigger initial fetch if imageDataUri is present.
|
| 116 |
+
// If imageDataUri is not present initially, this effect will re-run when it becomes available.
|
| 117 |
if (imageDataUri) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
performInitialFetch();
|
| 119 |
+
} else if (messages.length === 0) {
|
| 120 |
+
// If there's no image URI and no messages, show "Preparing..."
|
| 121 |
+
// This typically means the parent component is still loading the image.
|
| 122 |
addMessage({ sender: 'ai', type: 'text', text: "Preparing your journey..." });
|
| 123 |
}
|
| 124 |
|
| 125 |
+
|
| 126 |
return () => {
|
| 127 |
+
ignore = true; // Cleanup to prevent state updates on unmounted component
|
| 128 |
};
|
| 129 |
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 130 |
}, [imageDataUri, journeyTitle]); // addMessage is memoized
|
|
|
|
| 139 |
const handleOptionSelect = async (option: string, isCorrect: boolean) => {
|
| 140 |
if (!currentMCQ) return;
|
| 141 |
|
| 142 |
+
setIsAwaitingAnswer(false); // User has made a selection
|
| 143 |
|
| 144 |
addMessage({
|
| 145 |
sender: 'user',
|
| 146 |
+
type: 'user', // This type indicates it's the user's selected option text
|
| 147 |
text: option,
|
| 148 |
});
|
| 149 |
|
|
|
|
| 164 |
const errorMsg = error instanceof Error ? error.message : "An unknown error occurred";
|
| 165 |
combinedMessage += ` (Sorry, I couldn't provide an explanation for that: ${errorMsg})`;
|
| 166 |
}
|
| 167 |
+
|
| 168 |
addMessage({ sender: 'ai', type: 'feedback', text: combinedMessage, isCorrect: true });
|
| 169 |
setHasAnsweredCorrectly(true);
|
| 170 |
setIncorrectAttempts([]);
|
| 171 |
const newQuestionCount = questionCount + 1;
|
| 172 |
setQuestionCount(newQuestionCount);
|
| 173 |
+
if (newQuestionCount >= MAX_QUESTIONS) {
|
| 174 |
addMessage({ sender: 'ai', type: 'text', text: "You've completed all 5 questions! Please proceed to the summary report." });
|
| 175 |
}
|
| 176 |
|
|
|
|
| 178 |
const updatedIncorrectAttempts = [...incorrectAttempts, option];
|
| 179 |
setIncorrectAttempts(updatedIncorrectAttempts);
|
| 180 |
|
| 181 |
+
// Check if all incorrect options have been exhausted
|
| 182 |
+
if (updatedIncorrectAttempts.length >= currentMCQ.options.length - 1) {
|
| 183 |
let combinedText = "";
|
| 184 |
try {
|
| 185 |
+
// Explain why the *last chosen* option was incorrect
|
| 186 |
const incorrectExplanationResult = await explainIncorrectAnswer({
|
| 187 |
question: questionForAI,
|
| 188 |
options: currentMCQ.options,
|
|
|
|
| 192 |
combinedText += `That's not quite right. ${incorrectExplanationResult.explanation} `;
|
| 193 |
} catch (error) {
|
| 194 |
console.error("Error fetching explanation for the last incorrect answer:", error);
|
| 195 |
+
const errorMsg = error instanceof Error ? error.message : "An unknown error occurred";
|
| 196 |
+
combinedText += `That's not quite right. (Failed to get explanation for your choice: ${errorMsg}) `;
|
| 197 |
}
|
| 198 |
|
| 199 |
combinedText += `The correct answer is "${currentMCQ.correctAnswer}". `;
|
| 200 |
|
| 201 |
try {
|
| 202 |
+
// Explain why the actual correct answer is correct
|
| 203 |
const correctExplanationResult = await explainCorrectAnswer({
|
| 204 |
question: questionForAI,
|
| 205 |
options: currentMCQ.options,
|
|
|
|
| 214 |
|
| 215 |
addMessage({
|
| 216 |
sender: 'ai',
|
| 217 |
+
type: 'feedback', // This will be handled by ChatMessage to not show options
|
| 218 |
text: combinedText.trim(),
|
| 219 |
isCorrect: true, // Mark as 'correct' flow-wise to enable next question/summary
|
| 220 |
});
|
|
|
|
| 248 |
} catch (error) {
|
| 249 |
console.error("Error fetching explanation for incorrect answer:", error);
|
| 250 |
const errorMsg = error instanceof Error ? error.message : "An unknown error occurred";
|
|
|
|
| 251 |
const fallbackRepromptText = `That's not quite right. (Sorry, I couldn't explain that: ${errorMsg}) Please try another option from the question above.`;
|
| 252 |
const fallbackMCQData: ActiveMCQ = {
|
| 253 |
...currentMCQ,
|
|
|
|
| 264 |
|
| 265 |
const handleNextQuestionClick = async () => {
|
| 266 |
if (questionCount >= MAX_QUESTIONS) {
|
|
|
|
|
|
|
| 267 |
addMessage({ sender: 'ai', type: 'text', text: "You've completed all the questions! Please proceed to the summary." });
|
| 268 |
setIsLoading(false);
|
| 269 |
+
setHasAnsweredCorrectly(true);
|
| 270 |
return;
|
| 271 |
}
|
| 272 |
|
| 273 |
+
setIsAwaitingAnswer(false);
|
| 274 |
setHasAnsweredCorrectly(false);
|
| 275 |
+
setCurrentMCQ(null);
|
| 276 |
+
setIncorrectAttempts([]);
|
| 277 |
|
| 278 |
if (!imageDataUri) {
|
| 279 |
addMessage({ sender: 'ai', type: 'error', text: "Image data is not available to generate new questions." });
|
| 280 |
+
toast({ title: "Error", description: "Cannot fetch new question: Image data missing.", variant: "destructive" });
|
| 281 |
return;
|
| 282 |
}
|
| 283 |
setIsLoading(true);
|
|
|
|
| 285 |
const mcqResult = await generateMCQ({ imageDataUri });
|
| 286 |
const nextMCQData: ActiveMCQ = {
|
| 287 |
...mcqResult,
|
| 288 |
+
originalQuestionTextForFlow: mcqResult.mcq,
|
| 289 |
};
|
| 290 |
setCurrentMCQ(nextMCQData);
|
| 291 |
addMessage({ sender: 'ai', type: 'mcq', mcq: nextMCQData });
|
|
|
|
| 299 |
description: `Could not generate a new question. ${errorMessage}`,
|
| 300 |
variant: "destructive",
|
| 301 |
});
|
| 302 |
+
// Potentially allow user to try again or go to summary if multiple errors occur
|
| 303 |
} finally {
|
| 304 |
setIsLoading(false);
|
| 305 |
}
|
|
|
|
| 318 |
<ScrollArea className="flex-1 p-4" ref={scrollAreaRef}>
|
| 319 |
<div className="space-y-4">
|
| 320 |
{messages.map((msg) => {
|
|
|
|
| 321 |
const isLastMessageTheActiveMCQ =
|
| 322 |
msg.type === 'mcq' &&
|
| 323 |
currentMCQ &&
|
|
|
|
| 324 |
msg.id === messages.findLast(m => m.type === 'mcq' && m.mcq?.mcq === currentMCQ.mcq && JSON.stringify(m.mcq?.options) === JSON.stringify(currentMCQ.options))?.id;
|
| 325 |
|
|
|
|
|
|
|
| 326 |
const shouldThisMessageBeInteractive =
|
| 327 |
isLastMessageTheActiveMCQ &&
|
| 328 |
isAwaitingAnswer &&
|
|
|
|
| 330 |
|
| 331 |
return (
|
| 332 |
<ChatMessage
|
| 333 |
+
key={msg.id}
|
| 334 |
message={msg}
|
| 335 |
activeMCQ={currentMCQ}
|
| 336 |
isAwaitingActiveMCQAnswer={shouldThisMessageBeInteractive}
|
| 337 |
onOptionSelectActiveMCQ={handleOptionSelect}
|
| 338 |
incorrectAttemptsForMCQ={
|
| 339 |
+
shouldThisMessageBeInteractive
|
| 340 |
? incorrectAttempts
|
| 341 |
+
: []
|
| 342 |
}
|
| 343 |
/>
|
| 344 |
);
|
| 345 |
})}
|
| 346 |
+
{isLoading && !currentMCQ && messages[messages.length -1]?.type !== 'feedback' && (
|
|
|
|
| 347 |
<div className="flex items-start space-x-3">
|
| 348 |
<Skeleton className="h-8 w-8 rounded-full" />
|
| 349 |
<div className="space-y-2">
|
|
|
|
| 355 |
</div>
|
| 356 |
</ScrollArea>
|
| 357 |
<div className="border-t bg-background/80 p-4">
|
| 358 |
+
{isLoading && (
|
| 359 |
+
<div className="flex items-center justify-center text-sm text-muted-foreground">
|
| 360 |
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
| 361 |
+
AI is thinking...
|
| 362 |
+
</div>
|
| 363 |
+
)}
|
| 364 |
|
| 365 |
{!isLoading && questionCount >= MAX_QUESTIONS && hasAnsweredCorrectly && (
|
| 366 |
<Button onClick={handleGoToSummary} className="w-full" variant="default">
|
|
|
|
| 373 |
Next Question <Send className="ml-2 h-4 w-4" />
|
| 374 |
</Button>
|
| 375 |
)}
|
| 376 |
+
|
|
|
|
| 377 |
{!isLoading && currentMCQ && !hasAnsweredCorrectly && isAwaitingAnswer && (
|
| 378 |
<p className="text-center text-sm text-muted-foreground">
|
| 379 |
Please choose an answer from the options above.
|
| 380 |
</p>
|
| 381 |
)}
|
| 382 |
|
| 383 |
+
{!imageDataUri && !isLoading && messages.length > 0 && messages[messages.length -1]?.text === "Preparing your journey..." &&(
|
|
|
|
| 384 |
<p className="text-center text-sm text-muted-foreground">Loading image, please wait...</p>
|
| 385 |
)}
|
|
|
|
| 386 |
{!currentMCQ && !isLoading && imageDataUri && messages.length === 0 && (
|
| 387 |
<p className="text-center text-sm text-muted-foreground">Loading first question...</p>
|
| 388 |
)}
|
|
|
|
| 389 |
{!currentMCQ && !isLoading && imageDataUri && messages.length > 0 && messages[messages.length -1]?.type === 'error' && (
|
| 390 |
+
<p className="text-center text-sm text-destructive">Could not load question. Try refreshing the page or selecting another journey.</p>
|
| 391 |
)}
|
| 392 |
</div>
|
| 393 |
</div>
|
src/components/journey/summary-report-dialog.tsx
CHANGED
|
@@ -1,2 +1,4 @@
|
|
| 1 |
-
|
| 2 |
-
//
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
// This file is no longer needed as the summary report is a dedicated page.
|
| 3 |
+
// This file can be deleted from the project.
|
| 4 |
+
// The build system should prune unreferenced files.
|