add more data + image on recipe

This commit is contained in:
Arthur Barre 2025-03-13 22:17:39 +01:00
parent bd5891ff1b
commit 223b593495
11 changed files with 164 additions and 395 deletions

View File

@ -0,0 +1,8 @@
-- AlterTable
ALTER TABLE "Recipe" ADD COLUMN "cookingTime" INTEGER;
ALTER TABLE "Recipe" ADD COLUMN "difficulty" TEXT;
ALTER TABLE "Recipe" ADD COLUMN "imageUrl" TEXT;
ALTER TABLE "Recipe" ADD COLUMN "preparationTime" INTEGER;
ALTER TABLE "Recipe" ADD COLUMN "servings" INTEGER;
ALTER TABLE "Recipe" ADD COLUMN "steps" TEXT;
ALTER TABLE "Recipe" ADD COLUMN "tips" TEXT;

View File

@ -27,6 +27,13 @@ model Recipe {
userPrompt String userPrompt String
generatedRecipe String generatedRecipe String
audioUrl String? audioUrl String?
imageUrl String?
preparationTime Int?
cookingTime Int?
servings Int?
difficulty String?
steps String?
tips String?
userId String userId String
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())

View File

@ -24,20 +24,34 @@ module.exports = fp(async function (fastify, opts) {
// Génération de recette avec 01-mini // Génération de recette avec 01-mini
fastify.decorate('generateRecipe', async (ingredients, prompt) => { fastify.decorate('generateRecipe', async (ingredients, prompt) => {
const completion = await openai.chat.completions.create({ const completion = await openai.chat.completions.create({
model: "gpt-3.5-turbo", // Remplacer par 01-mini quand disponible model: "gpt-4o-mini", // Remplacer par 01-mini quand disponible
messages: [ messages: [
{ {
role: "system", role: "system",
content: "Tu es un chef cuisinier expert qui crée des recettes délicieuses et faciles à réaliser." content: "Tu es un chef cuisinier expert qui crée des recettes délicieuses et faciles à réaliser. Tu dois toujours répondre avec un objet JSON valide contenant les champs suivants: titre, ingredients, etapes, temps_preparation (en minutes), temps_cuisson (en minutes), portions, difficulte (facile, moyen, difficile), et conseils."
}, },
{ {
role: "user", role: "user",
content: `Voici les ingrédients disponibles: ${ingredients}. ${prompt || 'Propose une recette avec ces ingrédients.'}` content: `Voici les ingrédients disponibles: ${ingredients}. ${prompt || 'Propose une recette avec ces ingrédients.'} Réponds uniquement avec un objet JSON.`
} }
], ],
response_format: { type: "json_object" },
}); });
return completion.choices[0].message.content; const recipeData = JSON.parse(completion.choices[0].message.content);
// Génération de l'image du plat
const imageResponse = await openai.images.generate({
model: "dall-e-3",
prompt: `Une photo culinaire professionnelle et appétissante du plat "${recipeData.titre}". Le plat est présenté sur une belle assiette, avec un éclairage professionnel, style photographie gastronomique.`,
n: 1,
size: "1024x1024",
});
// Ajouter l'URL de l'image à l'objet recette
recipeData.image_url = imageResponse.data[0].url;
return recipeData;
}); });
// Gestion du téléchargement de fichiers audio // Gestion du téléchargement de fichiers audio

View File

@ -47,15 +47,31 @@ module.exports = async function (fastify, opts) {
const ingredients = transcription; const ingredients = transcription;
// Générer la recette // Générer la recette
const generatedRecipe = await fastify.generateRecipe(ingredients, "Crée une recette délicieuse et détaillée"); const recipeData = await fastify.generateRecipe(ingredients, "Crée une recette délicieuse et détaillée");
// Convertir les tableaux en chaînes de caractères pour la base de données
const ingredientsString = Array.isArray(recipeData.ingredients)
? recipeData.ingredients.join('\n')
: recipeData.ingredients;
const stepsString = Array.isArray(recipeData.etapes)
? recipeData.etapes.join('\n')
: recipeData.etapes;
// Créer la recette en base de données // Créer la recette en base de données
const recipe = await fastify.prisma.recipe.create({ const recipe = await fastify.prisma.recipe.create({
data: { data: {
title: `Recette du ${new Date().toLocaleDateString()}`, title: recipeData.titre,
ingredients, ingredients: ingredientsString,
userPrompt: transcription, userPrompt: transcription,
generatedRecipe, generatedRecipe: JSON.stringify(recipeData),
imageUrl: recipeData.image_url,
preparationTime: recipeData.temps_preparation,
cookingTime: recipeData.temps_cuisson,
servings: recipeData.portions,
difficulty: recipeData.difficulte,
steps: stepsString,
tips: recipeData.conseils,
audioUrl: audioPath, audioUrl: audioPath,
userId: request.user.id userId: request.user.id
} }
@ -66,7 +82,13 @@ module.exports = async function (fastify, opts) {
id: recipe.id, id: recipe.id,
title: recipe.title, title: recipe.title,
ingredients: recipe.ingredients, ingredients: recipe.ingredients,
generatedRecipe: recipe.generatedRecipe, preparationTime: recipe.preparationTime,
cookingTime: recipe.cookingTime,
servings: recipe.servings,
difficulty: recipe.difficulty,
steps: recipe.steps,
tips: recipe.tips,
imageUrl: recipe.imageUrl,
createdAt: recipe.createdAt createdAt: recipe.createdAt
} }
}; };
@ -86,7 +108,11 @@ module.exports = async function (fastify, opts) {
id: true, id: true,
title: true, title: true,
ingredients: true, ingredients: true,
generatedRecipe: true, preparationTime: true,
cookingTime: true,
servings: true,
difficulty: true,
imageUrl: true,
createdAt: true createdAt: true
} }
}); });
@ -112,7 +138,13 @@ module.exports = async function (fastify, opts) {
return reply.code(404).send({ error: 'Recette non trouvée' }); return reply.code(404).send({ error: 'Recette non trouvée' });
} }
return { recipe }; // Vous pouvez choisir de parser le JSON ici ou le laisser tel quel
const recipeData = {
...recipe,
generatedRecipe: JSON.parse(recipe.generatedRecipe)
};
return { recipe: recipeData };
} catch (error) { } catch (error) {
fastify.log.error(error); fastify.log.error(error);
return reply.code(500).send({ error: 'Erreur lors de la récupération de la recette' }); return reply.code(500).send({ error: 'Erreur lors de la récupération de la recette' });

View File

@ -28,9 +28,9 @@ export function Header() {
const navItems = [ const navItems = [
{ name: "Accueil", path: "/", icon: Home, public: true }, { name: "Accueil", path: "/", icon: Home, public: true },
{ name: "Recettes", path: "/recipes", icon: BookOpen, public: true }, { name: "Recettes", path: "/recipes", icon: BookOpen, public: true },
{ name: "Mes recettes", path: "/recipes", icon: BookOpen, public: false }, // { name: "Mes recettes", path: "/recipes", icon: BookOpen, public: false },
{ name: "Favoris", path: "/favorites", icon: Heart, public: false }, // { name: "Favoris", path: "/favorites", icon: Heart, public: false },
{ name: "Profil", path: "/profile", icon: User, public: false }, // { name: "Profil", path: "/profile", icon: User, public: false },
] ]
const filteredNavItems = navItems.filter((item) => item.public || isAuthenticated) const filteredNavItems = navItems.filter((item) => item.public || isAuthenticated)

View File

@ -46,22 +46,43 @@ export default function RecipeDetail() {
// ✅ GET RECIPE DETAILS // ✅ GET RECIPE DETAILS
const recipeData = await recipeService.getRecipeById(id) const recipeData = await recipeService.getRecipeById(id)
// Optionnel : conversion ingrédients / instructions si nécessaire // Traiter les données JSON de la recette
let ingredients = recipeData.ingredients let parsedRecipe = recipeData;
if (typeof ingredients === "string") {
ingredients = ingredients.split("\n").filter((item) => item.trim() !== "") // Si generatedRecipe est une chaîne JSON, la parser
if (typeof recipeData.generatedRecipe === 'string') {
try {
parsedRecipe.generatedRecipe = JSON.parse(recipeData.generatedRecipe);
} catch (e) {
console.log("La recette n'est pas au format JSON:", e);
}
} }
// Correction: déclarer la variable instructions // Préparer les ingrédients
let instructions: string[] = [] let ingredients = parsedRecipe.ingredients;
if (recipeData.generatedRecipe) { if (typeof ingredients === "string") {
instructions = recipeData.generatedRecipe.split("\n").filter((item) => item.trim() !== "") ingredients = ingredients.split("\n").filter((item) => item.trim() !== "");
}
// Préparer les étapes
let steps = parsedRecipe.steps || [];
if (typeof steps === "string") {
steps = steps.split("\n").filter((item) => item.trim() !== "");
} else if (parsedRecipe.generatedRecipe && parsedRecipe.generatedRecipe.etapes) {
steps = parsedRecipe.generatedRecipe.etapes;
} }
setRecipe({ setRecipe({
...recipeData, ...parsedRecipe,
ingredients, ingredients,
instructions, // Ajouter instructions au recipe instructions: steps,
// Utiliser les données structurées si disponibles
title: parsedRecipe.title || (parsedRecipe.generatedRecipe?.titre || parsedRecipe.title),
preparationTime: parsedRecipe.preparationTime || parsedRecipe.generatedRecipe?.temps_preparation,
cookingTime: parsedRecipe.cookingTime || parsedRecipe.generatedRecipe?.temps_cuisson,
servings: parsedRecipe.servings || parsedRecipe.generatedRecipe?.portions,
difficulty: parsedRecipe.difficulty || parsedRecipe.generatedRecipe?.difficulte,
tips: parsedRecipe.tips || parsedRecipe.generatedRecipe?.conseils,
}) })
// ✅ GET FAVORITE RECIPES & CHECK IF FAVORITE // ✅ GET FAVORITE RECIPES & CHECK IF FAVORITE
@ -397,6 +418,17 @@ export default function RecipeDetail() {
</li> </li>
)} )}
</ol> </ol>
{/* Conseils du chef */}
{recipe.tips && (
<div className="mt-8 p-4 bg-amber-50 dark:bg-amber-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 flex items-center">
<ChefHat className="h-5 w-5 mr-2 text-amber-600 dark:text-amber-400" />
Conseils du chef
</h3>
<p className="text-amber-800 dark:text-amber-300">{recipe.tips}</p>
</div>
)}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@ -27,7 +27,24 @@ export default function RecipeList() {
try { try {
setLoading(true) setLoading(true)
const data = await recipeService.getRecipes() const data = await recipeService.getRecipes()
setRecipes(data)
// Traiter les données pour s'assurer que les propriétés sont correctement formatées
const processedData = data.map(recipe => {
// Convertir les difficultés pour correspondre aux filtres
let normalizedDifficulty = recipe.difficulty;
if (recipe.difficulty === "facile") normalizedDifficulty = "Facile";
if (recipe.difficulty === "moyen") normalizedDifficulty = "Moyen";
if (recipe.difficulty === "difficile") normalizedDifficulty = "Difficile";
return {
...recipe,
difficulty: normalizedDifficulty,
// Ajouter des tags par défaut si nécessaire
tags: recipe.tags || []
};
});
setRecipes(processedData);
} catch (err) { } catch (err) {
setError("Impossible de charger les recettes") setError("Impossible de charger les recettes")
console.error(err) console.error(err)
@ -88,14 +105,14 @@ export default function RecipeList() {
)} )}
<div className="flex flex-col mx-4 md:mx-4 sm:flex-row sm:items-center sm:justify-between mb-6 mt-6"> <div className="flex flex-col mx-4 md:mx-4 sm:flex-row sm:items-center sm:justify-between mb-6 mt-6">
{/* <Tabs defaultValue="all" className="w-full sm:w-auto" onValueChange={setActiveFilter}> <Tabs defaultValue="all" className="w-full sm:w-auto" onValueChange={setActiveFilter}>
<TabsList className="grid grid-cols-4 w-full sm:w-auto"> <TabsList className="grid grid-cols-4 w-full sm:w-auto">
<TabsTrigger value="all">Toutes</TabsTrigger> <TabsTrigger value="all">Toutes</TabsTrigger>
<TabsTrigger value="easy">Faciles</TabsTrigger> <TabsTrigger value="easy">Faciles</TabsTrigger>
<TabsTrigger value="quick">Rapides</TabsTrigger> <TabsTrigger value="quick">Rapides</TabsTrigger>
<TabsTrigger value="vegetarian">Végé</TabsTrigger> <TabsTrigger value="vegetarian">Végé</TabsTrigger>
</TabsList> </TabsList>
</Tabs> */} </Tabs>
{ {
filteredRecipes.length !== 0 && filteredRecipes.length !== 0 &&
<Button <Button
@ -153,6 +170,16 @@ interface RecipeCardProps {
function RecipeCard({ recipe, index }: RecipeCardProps) { function RecipeCard({ recipe, index }: RecipeCardProps) {
const navigate = useNavigate() const navigate = useNavigate()
// Normaliser la difficulté pour l'affichage des badges
const difficultyClass = {
"Facile": "bg-green-500/80 text-white hover:bg-green-600/80",
"facile": "bg-green-500/80 text-white hover:bg-green-600/80",
"Moyen": "bg-yellow-500/80 text-white hover:bg-yellow-600/80",
"moyen": "bg-yellow-500/80 text-white hover:bg-yellow-600/80",
"Difficile": "bg-red-500/80 text-white hover:bg-red-600/80",
"difficile": "bg-red-500/80 text-white hover:bg-red-600/80"
}[recipe.difficulty || ""] || "bg-gray-500/80 text-white hover:bg-gray-600/80";
return ( return (
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: index * 0.05 }}> <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: index * 0.05 }}>
<Card className="overflow-hidden h-full border-none shadow-md hover:shadow-lg transition-all duration-300"> <Card className="overflow-hidden h-full border-none shadow-md hover:shadow-lg transition-all duration-300">
@ -173,19 +200,9 @@ function RecipeCard({ recipe, index }: RecipeCardProps) {
</div> </div>
<div className="absolute top-2 right-2 flex gap-1"> <div className="absolute top-2 right-2 flex gap-1">
{recipe.difficulty === "Facile" && ( {recipe.difficulty && (
<Badge variant="secondary" className="bg-green-500/80 text-white hover:bg-green-600/80"> <Badge variant="secondary" className={difficultyClass}>
Facile {recipe.difficulty.charAt(0).toUpperCase() + recipe.difficulty.slice(1)}
</Badge>
)}
{recipe.difficulty === "Moyen" && (
<Badge variant="secondary" className="bg-yellow-500/80 text-white hover:bg-yellow-600/80">
Moyen
</Badge>
)}
{recipe.difficulty === "Difficile" && (
<Badge variant="secondary" className="bg-red-500/80 text-white hover:bg-red-600/80">
Difficile
</Badge> </Badge>
)} )}
{(recipe.preparationTime || 0) <= 30 && ( {(recipe.preparationTime || 0) <= 30 && (
@ -198,7 +215,7 @@ function RecipeCard({ recipe, index }: RecipeCardProps) {
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex flex-wrap gap-1 mb-3"> <div className="flex flex-wrap gap-1 mb-3">
{recipe.tags?.slice(0, 3).map((tag) => ( {recipe.tags && Array.isArray(recipe.tags) && recipe.tags.slice(0, 3).map((tag) => (
<Badge <Badge
key={tag} key={tag}
variant="outline" variant="outline"
@ -210,12 +227,10 @@ function RecipeCard({ recipe, index }: RecipeCardProps) {
</div> </div>
<div className="flex items-center justify-between text-sm text-muted-foreground"> <div className="flex items-center justify-between text-sm text-muted-foreground">
{recipe.preparationTime && (
<div className="flex items-center"> <div className="flex items-center">
<Clock className="h-3 w-3 mr-1" /> <Clock className="h-3 w-3 mr-1" />
<span>{recipe.preparationTime} min</span> <span>{recipe.preparationTime || 0} min</span>
</div> </div>
)}
{recipe.servings && ( {recipe.servings && (
<div className="flex items-center"> <div className="flex items-center">

View File

@ -1,22 +0,0 @@
{
"name": "recorder",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "recorder",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"vmsg": "^0.4.0"
}
},
"node_modules/vmsg": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/vmsg/-/vmsg-0.4.0.tgz",
"integrity": "sha512-46BBqRSfqdFGUpO2j+Hpz8T9YE5uWG0/PWal1PT+R1o8NEthtjG/XWl4HzbB8hIHpg/UtmKvsxL2OKQBrIYcHQ==",
"license": "CC0-1.0"
}
}
}

View File

@ -1,15 +0,0 @@
{
"name": "recorder",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"vmsg": "^0.4.0"
}
}

View File

@ -1,302 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Enregistreur Audio</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f9f9f9;
color: #333;
}
h1 {
text-align: center;
color: #e67e22;
}
.container {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 20px;
margin-top: 20px;
}
.recorder-controls {
display: flex;
justify-content: center;
margin: 20px 0;
}
button {
background-color: #e67e22;
color: white;
border: none;
border-radius: 4px;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
display: flex;
align-items: center;
gap: 8px;
}
button:hover {
background-color: #d35400;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.recordings {
margin-top: 20px;
}
.recording-item {
background-color: #f5f5f5;
border-radius: 4px;
padding: 15px;
margin-bottom: 10px;
}
.recording-info {
margin-bottom: 10px;
font-size: 14px;
color: #666;
}
audio {
width: 100%;
}
.status {
text-align: center;
margin: 10px 0;
font-style: italic;
color: #666;
}
.pulse {
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
.mic-icon {
width: 20px;
height: 20px;
}
</style>
<!-- Charger le script vmsg avant notre script principal -->
<script src="https://unpkg.com/vmsg@0.3.0/vmsg.js"></script>
</head>
<body>
<h1>Enregistreur Audio</h1>
<div class="container">
<p>Enregistrez votre voix et écoutez le résultat. Les enregistrements sont stockés localement dans votre navigateur.
</p>
<div id="status" class="status">Prêt à enregistrer</div>
<div class="recorder-controls">
<button id="recordButton">
<svg class="mic-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
<line x1="12" y1="19" x2="12" y2="23"></line>
<line x1="8" y1="23" x2="16" y2="23"></line>
</svg>
Commencer l'enregistrement
</button>
</div>
<div id="recordings" class="recordings">
<h3>Vos enregistrements</h3>
<div id="recordingsList"></div>
</div>
</div>
<script>
// Attendre que vmsg soit complètement chargé
window.onload = function () {
// Vérifier si vmsg est disponible
if (typeof vmsg === 'undefined') {
console.error("La bibliothèque vmsg n'a pas été chargée correctement");
document.getElementById('status').textContent = "Erreur: Impossible de charger l'enregistreur audio";
document.getElementById('recordButton').disabled = true;
return;
}
// Initialiser le recorder
const recorder = new vmsg.Recorder({
wasmURL: "https://unpkg.com/vmsg@0.3.0/vmsg.wasm"
});
const recordButton = document.getElementById('recordButton');
const statusElement = document.getElementById('status');
const recordingsList = document.getElementById('recordingsList');
let isLoading = false;
let isRecording = false;
let recordings = [];
// Fonction pour mettre à jour l'interface utilisateur
function updateUI() {
if (isLoading) {
recordButton.disabled = true;
statusElement.textContent = isRecording
? "Arrêt de l'enregistrement..."
: "Initialisation de l'enregistrement...";
} else if (isRecording) {
recordButton.innerHTML = `
<svg class="mic-icon pulse" viewBox="0 0 24 24" fill="none" stroke="red" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
<line x1="12" y1="19" x2="12" y2="23"></line>
<line x1="8" y1="23" x2="16" y2="23"></line>
</svg>
Arrêter l'enregistrement
`;
recordButton.style.backgroundColor = '#e74c3c';
statusElement.textContent = "Enregistrement en cours...";
} else {
recordButton.disabled = false;
recordButton.innerHTML = `
<svg class="mic-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
<line x1="12" y1="19" x2="12" y2="23"></line>
<line x1="8" y1="23" x2="16" y2="23"></line>
</svg>
Commencer l'enregistrement
`;
recordButton.style.backgroundColor = '#e67e22';
statusElement.textContent = "Prêt à enregistrer";
}
}
// Fonction pour gérer l'enregistrement
async function toggleRecording() {
isLoading = true;
updateUI();
if (isRecording) {
try {
const blob = await recorder.stopRecording();
const url = URL.createObjectURL(blob);
const timestamp = new Date().toLocaleString();
const size = (blob.size / 1024).toFixed(2);
recordings.push({ url, timestamp, size });
renderRecordings();
isRecording = false;
} catch (e) {
console.error("Erreur lors de l'arrêt de l'enregistrement:", e);
alert("Une erreur est survenue lors de l'arrêt de l'enregistrement.");
}
} else {
try {
await recorder.initAudio();
await recorder.initWorker();
recorder.startRecording();
isRecording = true;
} catch (e) {
console.error("Erreur lors du démarrage de l'enregistrement:", e);
alert("Impossible d'accéder au microphone. Vérifiez les permissions.");
}
}
isLoading = false;
updateUI();
}
// Fonction pour afficher les enregistrements
function renderRecordings() {
recordingsList.innerHTML = '';
if (recordings.length === 0) {
recordingsList.innerHTML = '<p>Aucun enregistrement pour le moment.</p>';
return;
}
recordings.forEach((recording, index) => {
const recordingItem = document.createElement('div');
recordingItem.className = 'recording-item';
recordingItem.innerHTML = `
<div class="recording-info">
<strong>Enregistrement #${index + 1}</strong> - ${recording.timestamp} (${recording.size} KB)
</div>
<audio controls src="${recording.url}"></audio>
<div style="margin-top: 10px;">
<button class="download-btn" data-index="${index}" style="background-color: #3498db;">
Télécharger
</button>
<button class="delete-btn" data-index="${index}" style="background-color: #e74c3c; margin-left: 10px;">
Supprimer
</button>
</div>
`;
recordingsList.appendChild(recordingItem);
});
// Ajouter les écouteurs d'événements pour les boutons
document.querySelectorAll('.download-btn').forEach(btn => {
btn.addEventListener('click', function () {
const index = parseInt(this.dataset.index);
const recording = recordings[index];
const a = document.createElement('a');
a.href = recording.url;
a.download = `enregistrement-${index + 1}.mp3`;
a.click();
});
});
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', function () {
const index = parseInt(this.dataset.index);
URL.revokeObjectURL(recordings[index].url);
recordings.splice(index, 1);
renderRecordings();
});
});
}
// Initialiser l'interface
renderRecordings();
updateUI();
// Ajouter l'écouteur d'événement pour le bouton d'enregistrement
recordButton.addEventListener('click', toggleRecording);
};
</script>
</body>
</html>