import React, { useState, useEffect, useRef } from 'react'; import { ScanFace, Upload, X, AlertCircle, Wand2, Download, Grid3x3, CheckCircle2, History, ArrowRight, Trash2, Recycle, Image as ImageIcon, ChevronDown, ChevronUp, Zap, MessageSquarePlus, Camera, Loader2, Sparkles, Palette, Eye, Scissors, UserSquare2, Clock } from 'lucide-react'; // --- SOUND ASSETS --- const SOUNDS = { CLICK: 'https://visual-gimmicks.lima-city.de/support/miguelangelo-assets/prompt-engine/working.mp3', LOADING: 'https://visual-gimmicks.lima-city.de/support/miguelangelo-assets/prompt-engine/typing.mp3', GENERATING: 'https://visual-gimmicks.lima-city.de/support/miguelangelo-assets/prompt-engine/please-wait.mp3', ROBOT: 'https://visual-gimmicks.lima-city.de/support/miguelangelo-assets/prompt-engine/robot.mp3' }; // --- DATA: The 49 Expressions --- const EMOTIONS = [ // "Echte Freude" moved to the end { id: 2, label: "Höfliches Lächeln", desc: "Nur Mundwinkel leicht nach oben, Augenpartie bleibt unbeteiligt und starr." }, { id: 3, label: "Wut", desc: "Brauen tief und zusammengezogen (Zornesfalte), Augen starr, Lippen schmal gepresst." }, { id: 4, label: "Angst", desc: "Brauen hochgezogen und zusammen, Weißes im Auge sichtbar, Mund leicht geöffnet." }, { id: 5, label: "Ekel", desc: "Nase gerümpft, Oberlippe hochgezogen, Augenbrauen leicht gesenkt, verengte Augen." }, { id: 6, label: "Überraschung", desc: "Brauen hoch und gewölbt, Stirnfalten, Augen weit auf, Kiefer locker nach unten." }, { id: 7, label: "Trauer", desc: "Innere Ecken der Brauen hochgezogen, hängende Lider, Mundwinkel nach unten." }, { id: 8, label: "Verachtung", desc: "Einseitig hochgezogener Mundwinkel, leicht zurückgelegter Kopf, Blick nach unten." }, { id: 9, label: "Skepsis", desc: "Eine Augenbraue höher als die andere, zusammengekniffene Augen, Lippen gepresst." }, { id: 10, label: "Konzentration", desc: "Brauen leicht gesenkt und zusammen, fokussierter Blick, fest geschlossener Mund." }, { id: 11, label: "Verwirrung", desc: "Brauen zusammengezogen, Stirn runzelig, Nase gekräuselt, Kopf leicht geneigt." }, { id: 12, label: "Stolz", desc: "Leichtes Lächeln, aufrechte Kopfhaltung, leicht geweitete Brust, direkter Blick." }, { id: 13, label: "Scham", desc: "Blick nach unten/zur Seite, Kopf gesenkt, angespannte Lippen, evtl. Erröten." }, { id: 14, label: "Schuld", desc: "Wie Trauer, aber mit ausweichendem Blick und zusammengekniffenen Lippen." }, { id: 15, label: "Langeweile", desc: "Schläfrige Augenlider, entspannter Mund, Kopf auf Hand gestützt." }, { id: 16, label: "Interesse", desc: "Augen weit offen und fokussiert, Brauen leicht gehoben, Kopf nach vorne." }, { id: 17, label: "Erschöpfung", desc: "Hängende Augenlider, schlaffe Gesichtszüge, leicht geöffneter kraftloser Mund." }, { id: 18, label: "Schmerz", desc: "Augen fest zusammengekniffen, Brauen tief, Nase gerümpft, Mund verzerrt." }, { id: 19, label: "Triumph", desc: "Breit grinsender Mund, Augen zusammengekniffen, Kopf nach hinten geworfen." }, { id: 20, label: "Arroganz", desc: "Lider halb geschlossen, Nase hochmütig gehoben, Mundwinkel neutral oder spöttisch." }, { id: 21, label: "Verlegenheit", desc: "Verlegenes Grinsen, gesenkter Blick, beißt auf Unterlippe." }, { id: 22, label: "Enttäuschung", desc: "Brauen wie bei Trauer, aber leerer Blick, absackende Schultern/Haltung." }, { id: 23, label: "Besorgnis", desc: "Brauen zusammen, Stirnfalten mittig, unruhiger Blick, Lippen gepresst." }, { id: 24, label: "Amüsement", desc: "Mund weit offen (Lachen), Augen zu Schlitzen, Kopf leicht nach hinten." }, { id: 25, label: "Misstrauen", desc: "Augen verengt, seitlicher Blick ohne Kopf zu drehen, Lippen fest." }, { id: 26, label: "Ehrfurcht", desc: "Mund leicht geöffnet, Augen weit/starr, Brauen hoch, Regungslosigkeit." }, { id: 27, label: "Zufriedenheit", desc: "Weiche Gesichtszüge, entspannte Brauen, leichtes geschlossenes Lächeln." }, { id: 28, label: "Eifersucht", desc: "Mischung aus Wut (Brauen tief) und Trauer (Mund), starres Beobachten." }, { id: 29, label: "Frustration", desc: "Zusammengebissene Zähne, tiefe Brauen, tiefes Ausatmen sichtbar." }, // "Hass" removed { id: 31, label: "Einsamkeit", desc: "Blick ins Leere, traurige Mundpartie, Gesicht schlaff und ausdruckslos." }, { id: 32, label: "Verlangen", desc: "Geweitete Pupillen, leicht geöffneter Mund, intensiver Blickkontakt." }, { id: 33, label: "Entsetzen", desc: "Wie Angst, aber Augen extrem weit aufgerissen, Mund schreiend offen." }, { id: 34, label: "Gelassenheit", desc: "Keine Stirnfalten, Augen weich, Mund entspannt ohne Spannung." }, { id: 35, label: "Nervosität", desc: "Schnelles Blinzeln, beißende Lippen, unruhige Augen." }, { id: 36, label: "Gleichgültigkeit", desc: "Halboffene Augen, keine Regung im Mund, maskenhaft." }, { id: 37, label: "Zynismus", desc: "Einseitiges kurzes Hochziehen der Oberlippe, kalter Blick." }, { id: 38, label: "Mitleid", desc: "Hochgezogene innere Augenbrauen, weicher Blick, Kopf zur Seite." }, { id: 39, label: "Begeisterung", desc: "Funkelnde Augen, hohe Energie in Wangen, breites Lächeln mit Zähnen." }, { id: 40, label: "Eingeschüchtert", desc: "Blick nach oben, gesenktes Kinn, zusammengezogene Schultern." }, { id: 41, label: "Hoffnung", desc: "Blick nach oben, leichtes Lächeln, leuchtende Augen." }, { id: 42, label: "Ungeduld", desc: "Zusammengepresste Lippen, Kiefermahlen, wandernder Blick." }, { id: 43, label: "Reue", desc: "Blick zu Boden, zusammengezogene Brauen, Mund klein und fest." }, { id: 44, label: "Erleichterung", desc: "Augen kurz zu, Muskeln erschlaffen, Ausatmen." }, { id: 45, label: "Bitternis", desc: "Mundwinkel extrem nach unten, Augen hart und unversöhnlich." }, { id: 46, label: "Spott", desc: "Augenbrauen hoch, schiefes Grinsen, leichtes Kopfschütteln." }, { id: 47, label: "Faszination", desc: "Fixierte Augen, kein Blinzeln, Mund leicht offen, Brauen gehoben." }, { id: 48, label: "Paranoia", desc: "Extrem weite, wandernde Augen, angespannte Nackenmuskeln." }, { id: 49, label: "Melancholie", desc: "Verträumter trauriger Blick, keine Spannung, Kopf leicht gesenkt." }, { id: 50, label: "Entschlossenheit", desc: "Brauen tief/fest, starrer Blick, Lippen schmal, Kinn nach vorne." }, // "Echte Freude" moved here { id: 1, label: "Echte Freude", desc: "Duchenne-Lächeln: Mundwinkel nach oben, Wangen angehoben, Krähenfüße an den Augen." } ]; export default function App() { const [inputImage, setInputImage] = useState(null); const [inputFileName, setInputFileName] = useState("image"); const [outputImage, setOutputImage] = useState(null); const [loading, setLoading] = useState(false); const [loadingType, setLoadingType] = useState(null); const [selectedEmotionIds, setSelectedEmotionIds] = useState([]); const [customPrompt, setCustomPrompt] = useState(""); const [history, setHistory] = useState([]); const [error, setError] = useState(null); const [showHistory, setShowHistory] = useState(false); const [currentOutputBoost, setCurrentOutputBoost] = useState(false); const [sessionCounter, setSessionCounter] = useState(0); const [currentResultData, setCurrentResultData] = useState(null); // New State for Mood Match & Modal const [moodMatchImage, setMoodMatchImage] = useState(null); const [isMoodMatchSelected, setIsMoodMatchSelected] = useState(false); const [analyzedMoodDescription, setAnalyzedMoodDescription] = useState(null); const [isAnalyzingMood, setIsAnalyzingMood] = useState(false); const [isVibeModalOpen, setIsVibeModalOpen] = useState(false); const [vibeMatchInstruction, setVibeMatchInstruction] = useState("Gesichtsausdruck übernehmen"); // Timer State const [timeElapsed, setTimeElapsed] = useState(0); const TOTAL_TIME = 60 * 60; const typingAudioRef = useRef(null); // Initialize Audio Ref (empty initially) useEffect(() => { typingAudioRef.current = new Audio(); typingAudioRef.current.loop = true; return () => { if (typingAudioRef.current) { typingAudioRef.current.pause(); typingAudioRef.current = null; } }; }, []); // Handle Background Sounds (Typing vs Generating) useEffect(() => { const audio = typingAudioRef.current; if (!audio) return; // Logic: Generating (Image) > Analyzing (Text) > Stop if (loading) { if (!audio.src.includes('please-wait.mp3')) { audio.src = SOUNDS.GENERATING; audio.currentTime = 0; audio.play().catch(e => console.error("Gen sound failed", e)); } else if (audio.paused) { audio.play().catch(() => {}); } } else if (isAnalyzingMood) { if (!audio.src.includes('typing.mp3')) { audio.src = SOUNDS.LOADING; audio.currentTime = 0; audio.play().catch(e => console.error("Typing sound failed", e)); } else if (audio.paused) { audio.play().catch(() => {}); } } else { audio.pause(); audio.currentTime = 0; } }, [loading, isAnalyzingMood]); // Timer Logic useEffect(() => { const timerInterval = setInterval(() => { setTimeElapsed(prev => { if (prev >= TOTAL_TIME) { const audio = new Audio(SOUNDS.ROBOT); audio.play().catch(e => console.error("Robot sound failed", e)); return 0; } return prev + 1; }); }, 1000); return () => clearInterval(timerInterval); }, []); const playClickSound = () => { try { const audio = new Audio(SOUNDS.CLICK); audio.volume = 0.5; audio.play().catch(e => {}); } catch (e) {} }; // History Logic useEffect(() => { try { const saved = localStorage.getItem('vibe_history_v1'); if (saved) setHistory(JSON.parse(saved)); } catch (e) {} }, []); useEffect(() => { try { const historyToSave = history.slice(0, 10); localStorage.setItem('vibe_history_v1', JSON.stringify(historyToSave)); } catch (e) {} }, [history]); // Handle Uploads const handleFileUpload = (e) => { const file = e.target.files[0]; if (file) { playClickSound(); const name = file.name.split('.').slice(0, -1).join('.') || "image"; setInputFileName(name); const reader = new FileReader(); reader.onload = (event) => setInputImage(event.target.result); reader.readAsDataURL(file); } }; const handleDrop = (e) => { e.preventDefault(); e.stopPropagation(); const file = e.dataTransfer.files[0]; if (file && file.type.startsWith('image/')) { playClickSound(); const name = file.name.split('.').slice(0, -1).join('.') || "dropped_image"; setInputFileName(name); const reader = new FileReader(); reader.onload = (event) => setInputImage(event.target.result); reader.readAsDataURL(file); } }; // --- MOOD MATCH MODAL LOGIC --- const openVibeModal = () => { playClickSound(); setIsVibeModalOpen(true); }; const closeVibeModal = () => { playClickSound(); setIsVibeModalOpen(false); }; const handleModalDrop = (e) => { e.preventDefault(); e.stopPropagation(); const file = e.dataTransfer.files[0]; if (file && file.type.startsWith('image/')) { playClickSound(); const reader = new FileReader(); reader.onload = (event) => setMoodMatchImage(event.target.result); reader.readAsDataURL(file); } }; const handleModalUpload = (e) => { const file = e.target.files[0]; if (file) { playClickSound(); const reader = new FileReader(); reader.onload = (event) => setMoodMatchImage(event.target.result); reader.readAsDataURL(file); } }; useEffect(() => { const handlePaste = (e) => { const items = e.clipboardData.items; for (let i = 0; i < items.length; i++) { if (items[i].type.indexOf("image") !== -1) { const blob = items[i].getAsFile(); const reader = new FileReader(); reader.onload = (event) => { playClickSound(); if (isVibeModalOpen) { setMoodMatchImage(event.target.result); } else { setInputImage(event.target.result); setInputFileName("pasted_image"); } }; reader.readAsDataURL(blob); } } }; window.addEventListener("paste", handlePaste); return () => window.removeEventListener("paste", handlePaste); }, [isVibeModalOpen]); // DIRECT ACTION HANDLER FROM MODAL const handleModalAction = async () => { if (!moodMatchImage) return; if (!inputImage) { alert("Bitte zuerst ein Hauptbild im Hauptfenster hochladen!"); return; } playClickSound(); setIsVibeModalOpen(false); try { // PHASE 1: TEXT ANALYSIS const analysis = await analyzeVibeTarget(moodMatchImage, vibeMatchInstruction, false); // PHASE 2: GENERATION await generateVibe(false, analysis); } catch (e) { console.error("Combined Action Failed", e); setError("Vibe-Analyse oder Generierung fehlgeschlagen."); setLoading(false); } }; const analyzeVibeTarget = async (base64Image, instruction, returnOnly = false) => { if (!returnOnly) { setIsAnalyzingMood(true); setLoading(false); } setAnalyzedMoodDescription(null); setError(null); const apiKey = ""; const prompt = `Analysiere dieses Bild. Der User möchte folgendes Feature auf ein anderes Bild übertragen: "${instruction}". Beschreibe dieses Feature visuell präzise und detailliert, damit eine KI es reproduzieren kann. Konzentriere dich NUR auf dieses Feature.`; try { const base64Data = base64Image.split(',')[1]; const mimeType = base64Image.substring(base64Image.indexOf(':') + 1, base64Image.indexOf(';')); const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image-preview:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [ { text: prompt }, { inlineData: { mimeType: mimeType, data: base64Data } } ] }] }) }); if (!response.ok) throw new Error("Analyse fehlgeschlagen"); const data = await response.json(); const analysisText = data.candidates?.[0]?.content?.parts?.[0]?.text; if (analysisText) { if (!returnOnly) { setAnalyzedMoodDescription(analysisText); setIsMoodMatchSelected(true); setSelectedEmotionIds([]); } return analysisText; } else { throw new Error("Keine Beschreibung generiert."); } } catch (err) { console.error(err); if (!returnOnly) { setError("Vibe-Analyse fehlgeschlagen."); setIsMoodMatchSelected(false); } throw err; } finally { if (!returnOnly) setIsAnalyzingMood(false); } }; const toggleEmotion = (id) => { playClickSound(); setIsMoodMatchSelected(false); setSelectedEmotionIds(prev => { if (prev.includes(id)) { return prev.filter(eId => eId !== id); } else { return [...prev, id]; } }); }; const createFilename = (baseName, moodStr, count) => { const date = new Date().toISOString().split('T')[0]; const safeBase = baseName.replace(/[^a-z0-9]/gi, '_').toLowerCase(); const safeMood = moodStr.replace(/[^a-z0-9]/gi, '-').toLowerCase().substring(0, 50); return `${safeBase}-${safeMood}-${date}-${count}.png`; }; const generateVibe = async (boost = false, analysisOverride = null) => { if (!inputImage && !analysisOverride) { if (!inputImage || (!isMoodMatchSelected && selectedEmotionIds.length === 0 && !customPrompt.trim())) return; } setLoading(true); setLoadingType(boost ? 'boost' : 'normal'); setError(null); setOutputImage(null); setCurrentResultData(null); const apiKey = ""; // Construct Prompt let mainInstruction = "Bearbeite das Input-Bild."; let targetDescription = ""; let labelDisplay = ""; let filenameMood = ""; const effectiveAnalysis = analysisOverride || (isMoodMatchSelected ? analyzedMoodDescription : null); if (effectiveAnalysis) { mainInstruction = `Übertrage folgendes Feature (${vibeMatchInstruction}) auf die Person im Bild.`; targetDescription = `Visuelle Referenz-Beschreibung des Features: ${effectiveAnalysis}. `; labelDisplay = `Vibe: ${vibeMatchInstruction}`; filenameMood = `vibe-${vibeMatchInstruction}`; } else if (selectedEmotionIds.length > 0) { mainInstruction = "Ändere den Gesichtsausdruck der Person in diesem Bild."; const selectedEmotions = EMOTIONS.filter(e => selectedEmotionIds.includes(e.id)); const emotionLabels = selectedEmotions.map(e => e.label).join(" + "); const emotionDescs = selectedEmotions.map(e => e.desc).join(" "); targetDescription = `Ziel-Emotionen: ${emotionLabels}. Mimik-Details: ${emotionDescs}. `; labelDisplay = emotionLabels; filenameMood = emotionLabels; } if (customPrompt.trim()) { targetDescription += ` ZUSÄTZLICHE USER-ANWEISUNG (Priorität): ${customPrompt}. `; labelDisplay += labelDisplay ? " + Custom" : "Custom Prompt"; filenameMood += "-custom"; } let boostInstruction = ""; if (boost) { boostInstruction = "ANWEISUNG: Übertreibe den Effekt maximal! Mache den Ausdruck oder das Feature fast karikaturhaft stark (150% Intensität)."; targetDescription += " (EXTREME INTENSITÄT)"; labelDisplay += " ⚡"; filenameMood += "-boost"; } const prompt = `${mainInstruction} ${targetDescription} ${boostInstruction} WICHTIG: Erhalte den exakten künstlerischen Stil des Input-Bildes (z.B. Ölgemälde, Skizze, Foto, Beleuchtung, Textur, Pinselstriche) und den Hintergrund zu 100% bei. Ändere NUR das beschriebene Feature. Die Identität der Person (außer bei Face Swap) muss gleich bleiben. Das Bild darf nicht beschnitten werden.`; try { const base64Data = inputImage.split(',')[1]; const mimeType = inputImage.substring(inputImage.indexOf(':') + 1, inputImage.indexOf(';')); const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image-preview:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [ { text: prompt }, { inlineData: { mimeType: mimeType, data: base64Data } } ] }], generationConfig: { responseModalities: ["IMAGE"] } }) }); if (!response.ok) throw new Error("API Fehler"); const data = await response.json(); const generatedBase64 = data.candidates?.[0]?.content?.parts?.find(p => p.inlineData)?.inlineData?.data; if (generatedBase64) { const finalImage = `data:image/png;base64,${generatedBase64}`; const newCount = sessionCounter + 1; setSessionCounter(newCount); setOutputImage(finalImage); setCurrentOutputBoost(boost); const downloadFilename = createFilename(inputFileName, filenameMood, newCount); const newEntry = { id: Date.now(), input: inputImage, output: finalImage, emotion: { label: labelDisplay }, timestamp: new Date().toLocaleTimeString(), boost: boost, filename: downloadFilename, inputName: inputFileName }; setCurrentResultData(newEntry); setHistory(prev => [newEntry, ...prev]); setShowHistory(true); } else { throw new Error("Kein Bild generiert."); } } catch (err) { console.error(err); setError("Fehler bei der Generierung."); } finally { setLoading(false); setLoadingType(null); } }; const useAsInput = (imgSrc) => { playClickSound(); setInputImage(imgSrc); setOutputImage(null); setCurrentResultData(null); setInputFileName("remixed_image"); }; const deleteHistoryItem = (id) => { playClickSound(); setHistory(prev => prev.filter(item => item.id !== id)); }; const downloadImage = (dataUrl, filename) => { playClickSound(); const link = document.createElement('a'); link.href = dataUrl; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); }; const toggleHistory = () => { playClickSound(); setShowHistory(!showHistory); }; const clearInput = () => { playClickSound(); setInputImage(null); setOutputImage(null); setCurrentResultData(null); } // Helper functions re-added to scope const getActiveLabels = () => { if (isMoodMatchSelected) return `Match: ${vibeMatchInstruction} ${analyzedMoodDescription ? '(Ready)' : '(Analysiere...)'}`; let textParts = []; if (selectedEmotionIds.length > 0) { const labels = EMOTIONS.filter(e => selectedEmotionIds.includes(e.id)).map(e => e.label); if (labels.length <= 2) textParts.push(labels.join(" + ")); else textParts.push(`${labels[0]} + ${labels[1]}...`); } if (customPrompt.trim()) textParts.push(`"${customPrompt.substring(0, 15)}..."`); if (textParts.length === 0) return 'Warte auf Input...'; return textParts.join(" + "); }; const getActiveLabelsFull = () => { if (isMoodMatchSelected) return `Vibe Match (${vibeMatchInstruction}): ` + (analyzedMoodDescription || "Analysiere..."); let fullText = ""; if (selectedEmotionIds.length > 0) fullText += EMOTIONS.filter(e => selectedEmotionIds.includes(e.id)).map(e => e.label).join(" + "); if (customPrompt.trim()) fullText += (fullText ? " + " : "") + `Custom: ${customPrompt}`; return fullText; }; // Calculate Timer const progressPercentage = Math.min((timeElapsed / TOTAL_TIME) * 100, 100); const getTimerColor = () => { if (progressPercentage < 25) return 'from-emerald-500 to-green-500'; if (progressPercentage < 50) return 'from-yellow-400 to-yellow-600'; if (progressPercentage < 75) return 'from-orange-500 to-orange-600'; return 'from-red-500 to-red-700'; }; return (
{/* HEADER */}

AI George's Face-Tune

{Math.floor(timeElapsed / 60)}m
{/* IMAGES */}
e.preventDefault()} onDrop={handleDrop}> {!inputImage ? ( ) : (
Input
Original
)}
{outputImage ? (
Output
{currentOutputBoost ? '⚡ MAX Vibe' : 'Remixed'}
) : (

Dein Vibe erscheint hier

)}
{error && (
{error}
)} {/* CONTROLS */}
{outputImage && currentResultData ? ( <>
Current Mix {currentResultData.emotion.label}
) : (
Wähle Vibes, tippe Text & Action!
)}
{/* MATRIX & MOOD MATCH */}
{/* 50TH SLOT: MOOD MATCH BUTTON (Opens Modal) - MOVED TO START */} {/* OTHER EMOTIONS */} {EMOTIONS.map((emotion) => { const isSelected = selectedEmotionIds.includes(emotion.id); return ( ); })}
{/* CUSTOM INPUT */}
setCustomPrompt(e.target.value)} placeholder="Optional: Eigene Wünsche, Details oder Edits hier eingeben..." className="w-full glass-input rounded-lg py-3 pl-12 pr-4 text-base placeholder:text-slate-500 focus:placeholder:text-slate-400 transition-all" />
{/* VIBE MATCH MODAL */} {isVibeModalOpen && (
e.stopPropagation()}>

VIBE MATCH STUDIO

e.preventDefault()} onDrop={handleModalDrop}> {!moodMatchImage ? ( ) : ( <> )}
setVibeMatchInstruction(e.target.value)} className="w-full bg-black/40 border border-emerald-500/30 rounded-lg p-4 text-white placeholder-slate-500 focus:border-emerald-400 focus:outline-none transition-colors" placeholder="z.B. Frisur, Gesichtsausdruck, Brille..." />
{[ { label: "Gesichtsausdruck", icon: ScanFace }, { label: "Face Swap", icon: UserSquare2 }, { label: "Frisur", icon: Scissors }, { label: "Augen", icon: Eye }, { label: "Makeup", icon: Palette }, { label: "Stil / Vibe", icon: Sparkles }, { label: "Brille", icon: ImageIcon } ].map((opt) => ( ))}
)} {/* HISTORY */}
{showHistory && (
{history.length === 0 ? (
History is empty
) : (
{history.map((item) => (
IN
OUT {item.boost && }
{item.emotion.label}
))}
)}
)}
); }