add more data + image on recipe
This commit is contained in:
parent
bd5891ff1b
commit
223b593495
@ -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;
|
||||
@ -21,14 +21,21 @@ model User {
|
||||
}
|
||||
|
||||
model Recipe {
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
ingredients String
|
||||
userPrompt String
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
ingredients String
|
||||
userPrompt String
|
||||
generatedRecipe String
|
||||
audioUrl String?
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
audioUrl String?
|
||||
imageUrl String?
|
||||
preparationTime Int?
|
||||
cookingTime Int?
|
||||
servings Int?
|
||||
difficulty String?
|
||||
steps String?
|
||||
tips String?
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
@ -24,20 +24,34 @@ module.exports = fp(async function (fastify, opts) {
|
||||
// Génération de recette avec 01-mini
|
||||
fastify.decorate('generateRecipe', async (ingredients, prompt) => {
|
||||
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: [
|
||||
{
|
||||
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",
|
||||
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
|
||||
|
||||
@ -47,15 +47,31 @@ module.exports = async function (fastify, opts) {
|
||||
const ingredients = transcription;
|
||||
|
||||
// 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
|
||||
const recipe = await fastify.prisma.recipe.create({
|
||||
data: {
|
||||
title: `Recette du ${new Date().toLocaleDateString()}`,
|
||||
ingredients,
|
||||
title: recipeData.titre,
|
||||
ingredients: ingredientsString,
|
||||
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,
|
||||
userId: request.user.id
|
||||
}
|
||||
@ -66,7 +82,13 @@ module.exports = async function (fastify, opts) {
|
||||
id: recipe.id,
|
||||
title: recipe.title,
|
||||
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
|
||||
}
|
||||
};
|
||||
@ -86,7 +108,11 @@ module.exports = async function (fastify, opts) {
|
||||
id: true,
|
||||
title: true,
|
||||
ingredients: true,
|
||||
generatedRecipe: true,
|
||||
preparationTime: true,
|
||||
cookingTime: true,
|
||||
servings: true,
|
||||
difficulty: true,
|
||||
imageUrl: true,
|
||||
createdAt: true
|
||||
}
|
||||
});
|
||||
@ -112,7 +138,13 @@ module.exports = async function (fastify, opts) {
|
||||
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) {
|
||||
fastify.log.error(error);
|
||||
return reply.code(500).send({ error: 'Erreur lors de la récupération de la recette' });
|
||||
|
||||
Binary file not shown.
@ -28,9 +28,9 @@ export function Header() {
|
||||
const navItems = [
|
||||
{ name: "Accueil", path: "/", icon: Home, public: true },
|
||||
{ name: "Recettes", path: "/recipes", icon: BookOpen, public: true },
|
||||
{ name: "Mes recettes", path: "/recipes", icon: BookOpen, public: false },
|
||||
{ name: "Favoris", path: "/favorites", icon: Heart, public: false },
|
||||
{ name: "Profil", path: "/profile", icon: User, public: false },
|
||||
// { name: "Mes recettes", path: "/recipes", icon: BookOpen, public: false },
|
||||
// { name: "Favoris", path: "/favorites", icon: Heart, public: false },
|
||||
// { name: "Profil", path: "/profile", icon: User, public: false },
|
||||
]
|
||||
|
||||
const filteredNavItems = navItems.filter((item) => item.public || isAuthenticated)
|
||||
|
||||
@ -46,22 +46,43 @@ export default function RecipeDetail() {
|
||||
// ✅ GET RECIPE DETAILS
|
||||
const recipeData = await recipeService.getRecipeById(id)
|
||||
|
||||
// Optionnel : conversion ingrédients / instructions si nécessaire
|
||||
let ingredients = recipeData.ingredients
|
||||
if (typeof ingredients === "string") {
|
||||
ingredients = ingredients.split("\n").filter((item) => item.trim() !== "")
|
||||
// Traiter les données JSON de la recette
|
||||
let parsedRecipe = recipeData;
|
||||
|
||||
// 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
|
||||
let instructions: string[] = []
|
||||
if (recipeData.generatedRecipe) {
|
||||
instructions = recipeData.generatedRecipe.split("\n").filter((item) => item.trim() !== "")
|
||||
// Préparer les ingrédients
|
||||
let ingredients = parsedRecipe.ingredients;
|
||||
if (typeof ingredients === "string") {
|
||||
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({
|
||||
...recipeData,
|
||||
...parsedRecipe,
|
||||
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
|
||||
@ -397,6 +418,17 @@ export default function RecipeDetail() {
|
||||
</li>
|
||||
)}
|
||||
</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>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@ -27,7 +27,24 @@ export default function RecipeList() {
|
||||
try {
|
||||
setLoading(true)
|
||||
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) {
|
||||
setError("Impossible de charger les recettes")
|
||||
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">
|
||||
{/* <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">
|
||||
<TabsTrigger value="all">Toutes</TabsTrigger>
|
||||
<TabsTrigger value="easy">Faciles</TabsTrigger>
|
||||
<TabsTrigger value="quick">Rapides</TabsTrigger>
|
||||
<TabsTrigger value="vegetarian">Végé</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs> */}
|
||||
</Tabs>
|
||||
{
|
||||
filteredRecipes.length !== 0 &&
|
||||
<Button
|
||||
@ -153,6 +170,16 @@ interface RecipeCardProps {
|
||||
function RecipeCard({ recipe, index }: RecipeCardProps) {
|
||||
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 (
|
||||
<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">
|
||||
@ -173,19 +200,9 @@ function RecipeCard({ recipe, index }: RecipeCardProps) {
|
||||
</div>
|
||||
|
||||
<div className="absolute top-2 right-2 flex gap-1">
|
||||
{recipe.difficulty === "Facile" && (
|
||||
<Badge variant="secondary" className="bg-green-500/80 text-white hover:bg-green-600/80">
|
||||
Facile
|
||||
</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
|
||||
{recipe.difficulty && (
|
||||
<Badge variant="secondary" className={difficultyClass}>
|
||||
{recipe.difficulty.charAt(0).toUpperCase() + recipe.difficulty.slice(1)}
|
||||
</Badge>
|
||||
)}
|
||||
{(recipe.preparationTime || 0) <= 30 && (
|
||||
@ -198,7 +215,7 @@ function RecipeCard({ recipe, index }: RecipeCardProps) {
|
||||
|
||||
<CardContent className="p-4">
|
||||
<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
|
||||
key={tag}
|
||||
variant="outline"
|
||||
@ -210,12 +227,10 @@ function RecipeCard({ recipe, index }: RecipeCardProps) {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||
{recipe.preparationTime && (
|
||||
<div className="flex items-center">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
<span>{recipe.preparationTime} min</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
<span>{recipe.preparationTime || 0} min</span>
|
||||
</div>
|
||||
|
||||
{recipe.servings && (
|
||||
<div className="flex items-center">
|
||||
|
||||
22
recorder/package-lock.json
generated
22
recorder/package-lock.json
generated
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
Loading…
x
Reference in New Issue
Block a user