import { useState, useEffect, useRef } from "react";
import {
Container,
Paper,
Button,
Box,
Typography,
LinearProgress,
} from "@mui/material";
import RestartAltIcon from "@mui/icons-material/RestartAlt";
import axios from "axios";
import { ComicLayout } from "./layouts/ComicLayout";
import { getNextPanelDimensions } from "./layouts/utils";
// Get API URL from environment or default to localhost in development
const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8000";
// Generate a unique client ID
const CLIENT_ID = `client_${Math.random().toString(36).substring(2)}`;
// Create axios instance with default config
const api = axios.create({
headers: {
"x-client-id": CLIENT_ID,
},
});
// Function to convert text with ** to bold elements
const formatTextWithBold = (text) => {
if (!text) return "";
const parts = text.split(/(\*\*.*?\*\*)/g);
return parts.map((part, index) => {
if (part.startsWith("**") && part.endsWith("**")) {
// Remove the ** and wrap in bold
return {part.slice(2, -2)};
}
return part;
});
};
function App() {
const [storySegments, setStorySegments] = useState([]);
const [currentChoices, setCurrentChoices] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [isDebugMode, setIsDebugMode] = useState(false);
const isInitializedRef = useRef(false);
const currentImageRequestRef = useRef(null);
const pendingImageRequests = useRef(new Set()); // Track pending image requests
const generateImageForStory = async (storyText, segmentIndex) => {
try {
// Cancel previous request if it exists
if (currentImageRequestRef.current) {
currentImageRequestRef.current.abort();
}
// Add this segment to pending requests
pendingImageRequests.current.add(segmentIndex);
console.log("Generating image for story:", storyText);
const dimensions = getNextPanelDimensions(storySegments);
console.log("[DEBUG] Story segments:", storySegments);
console.log("[DEBUG] Dimensions object:", dimensions);
console.log(
"[DEBUG] Width:",
dimensions?.width,
"Height:",
dimensions?.height
);
if (!dimensions || !dimensions.width || !dimensions.height) {
console.error("[ERROR] Invalid dimensions:", dimensions);
pendingImageRequests.current.delete(segmentIndex);
return null;
}
// Create new AbortController for this request
const abortController = new AbortController();
currentImageRequestRef.current = abortController;
const response = await api.post(
`${API_URL}/api/${isDebugMode ? "test/" : ""}generate-image`,
{
prompt: `Comic book style scene: ${storyText}`,
width: dimensions.width,
height: dimensions.height,
},
{
signal: abortController.signal,
}
);
// Remove from pending requests
pendingImageRequests.current.delete(segmentIndex);
if (response.data.success) {
return response.data.image_base64;
}
return null;
} catch (error) {
if (axios.isCancel(error)) {
console.log("Image request cancelled for segment", segmentIndex);
// On met quand même à jour le segment pour arrêter le spinner
setStorySegments((prev) => {
const updatedSegments = [...prev];
if (updatedSegments[segmentIndex]) {
updatedSegments[segmentIndex] = {
...updatedSegments[segmentIndex],
image_base64: null,
imageRequestCancelled: true, // Flag pour indiquer que la requête a été annulée
};
}
return updatedSegments;
});
} else {
console.error("Error generating image:", error);
}
pendingImageRequests.current.delete(segmentIndex);
return null;
}
};
const handleStoryAction = async (action, choiceId = null) => {
setIsLoading(true);
try {
// 1. D'abord, obtenir l'histoire
const response = await api.post(
`${API_URL}/api/${isDebugMode ? "test/" : ""}chat`,
{
message: action,
choice_id: choiceId,
}
);
// 2. Créer le nouveau segment sans image
const newSegment = {
text: formatTextWithBold(response.data.story_text),
isChoice: false,
isDeath: response.data.is_death,
isVictory: response.data.is_victory,
radiationLevel: response.data.radiation_level,
is_first_step: response.data.is_first_step,
is_last_step: response.data.is_last_step,
image_base64: null,
};
let segmentIndex;
// 3. Mettre à jour l'état avec le nouveau segment
if (action === "restart") {
setStorySegments([newSegment]);
segmentIndex = 0;
} else {
setStorySegments((prev) => {
segmentIndex = prev.length;
return [...prev, newSegment];
});
}
// 4. Mettre à jour les choix immédiatement
setCurrentChoices(response.data.choices);
// 5. Désactiver le loading car l'histoire est affichée
setIsLoading(false);
// 6. Tenter de générer l'image en arrière-plan
try {
const image_base64 = await generateImageForStory(
response.data.story_text,
segmentIndex
);
if (image_base64) {
setStorySegments((prev) => {
const updatedSegments = [...prev];
if (updatedSegments[segmentIndex]) {
updatedSegments[segmentIndex] = {
...updatedSegments[segmentIndex],
image_base64: image_base64,
};
}
return updatedSegments;
});
}
} catch (imageError) {
console.error("Error generating image:", imageError);
}
} catch (error) {
console.error("Error:", error);
// En cas d'erreur, créer un segment d'erreur qui permet de continuer
const errorSegment = {
text: "Le conteur d'histoires est temporairement indisponible. Veuillez réessayer dans quelques instants...",
isChoice: false,
isDeath: false,
isVictory: false,
radiationLevel:
storySegments.length > 0
? storySegments[storySegments.length - 1].radiationLevel
: 0,
image_base64: null,
};
// Ajouter le segment d'erreur et permettre de réessayer
if (action === "restart") {
setStorySegments([errorSegment]);
} else {
setStorySegments((prev) => [...prev, errorSegment]);
}
// Donner l'option de réessayer
setCurrentChoices([{ id: 1, text: "Réessayer" }]);
setIsLoading(false);
}
};
// Start the story when the component mounts
useEffect(() => {
if (!isInitializedRef.current) {
handleStoryAction("restart");
isInitializedRef.current = true;
}
}, []); // Empty dependency array since we're using a ref
const handleChoice = async (choiceId) => {
// Si c'est l'option "Réessayer", on relance la dernière action
if (currentChoices.length === 1 && currentChoices[0].text === "Réessayer") {
// Supprimer le segment d'erreur
setStorySegments((prev) => prev.slice(0, -1));
// Réessayer la dernière action
await handleStoryAction(
"choice",
storySegments[storySegments.length - 2]?.choiceId || null
);
return;
}
// Comportement normal pour les autres choix
const choice = currentChoices.find((c) => c.id === choiceId);
setStorySegments((prev) => [
...prev,
{
text: choice.text,
isChoice: true,
choiceId: choiceId, // Stocker l'ID du choix pour pouvoir réessayer
},
]);
// Continue the story with this choice
await handleStoryAction("choice", choiceId);
};
// Filter out choice segments
const nonChoiceSegments = storySegments.filter(
(segment) => !segment.isChoice
);
return (
{/*
0 &&
storySegments[storySegments.length - 1].radiationLevel >= 7
? "error.light"
: "inherit",
},
}}
>
Radiation:{" "}
{storySegments.length > 0
? `${
storySegments[storySegments.length - 1].radiationLevel
}/10`
: "0/10"}
}
onClick={() => handleStoryAction("restart")}
disabled={isLoading}
>
Restart
*/}
{/* {isLoading && } */}
{currentChoices.length > 0 ? (
{currentChoices.map((choice) => (
))}
) : storySegments.length > 0 &&
storySegments[storySegments.length - 1].is_last_step ? (
) : null}
);
}
export default App;