TrueVox / templates /index.html
tannuiscoding's picture
application file added
09000c8
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Speech Emotion Detector</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&family=Open+Sans:wght@400;500;600&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<script src="https://unpkg.com/@lucide/web@latest"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#ed1b76',
dark: '#d01868',
light: '#ff3d8e'
},
accent: {
DEFAULT: '#249f9c',
dark: '#037a76',
light: '#3cbfbc'
}
},
fontFamily: {
poppins: ['Poppins', 'sans-serif'],
opensans: ['Open Sans', 'sans-serif']
}
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.text-gradient {
@apply bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent;
}
.progress-ring-circle {
transition: stroke-dashoffset 0.35s;
transform: rotate(-90deg);
transform-origin: 50% 50%;
}
}
</style>
</head>
<body class="font-opensans bg-gray-50 text-gray-800 dark:bg-gray-900 dark:text-gray-100 transition-colors duration-200">
<!-- Toast Notifications -->
<div id="toast-container" class="fixed top-4 right-4 z-50 flex flex-col gap-2"></div>
<!-- Dark Mode Toggle -->
<div class="absolute top-4 right-4 z-40">
<button id="theme-toggle" class="p-2 rounded-full bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">
<i data-lucide="sun" class="hidden dark:block w-5 h-5 text-yellow-400"></i>
<i data-lucide="moon" class="block dark:hidden w-5 h-5 text-gray-700"></i>
</button>
</div>
<!-- Hero Section -->
<header class="relative bg-gradient-to-b from-white to-gray-100 dark:from-gray-800 dark:to-gray-900 pt-16 pb-12 px-4 sm:px-6 lg:px-8 text-center">
<div class="max-w-4xl mx-auto">
<h1 class="font-poppins font-bold text-4xl md:text-5xl lg:text-6xl mb-4 text-gradient">
Speech Emotion Detector
</h1>
<p class="text-lg md:text-xl text-gray-600 dark:text-gray-300 mb-8">
Detect emotion and transcribe speech with a single upload
</p>
<div class="flex justify-center">
<button id="about-btn" class="flex items-center gap-2 px-4 py-2 rounded-full bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">
<i data-lucide="info" class="w-4 h-4"></i>
<span>How it works</span>
</button>
</div>
</div>
</header>
<main class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Upload Section -->
<section class="mb-12">
<div id="upload-container" class="border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg p-8 text-center transition-all hover:border-primary dark:hover:border-primary cursor-pointer">
<div class="flex flex-col items-center justify-center gap-4">
<div class="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center">
<i data-lucide="upload-cloud" class="w-8 h-8 text-primary"></i>
</div>
<div>
<h2 class="font-poppins font-semibold text-xl mb-2">Upload Audio File</h2>
<p class="text-gray-500 dark:text-gray-400 mb-4">Drag & drop your audio file here or click to browse</p>
<p class="text-sm text-gray-400 dark:text-gray-500">Supported formats: .wav, .mp3, .ogg</p>
</div>
<input type="file" id="file-input" class="hidden" accept=".wav,.mp3,.ogg">
</div>
</div>
<div id="file-info" class="hidden mt-4 p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<i data-lucide="file-audio" class="w-6 h-6 text-primary"></i>
<div>
<p id="file-name" class="font-medium"></p>
<p id="file-size" class="text-sm text-gray-500 dark:text-gray-400"></p>
</div>
</div>
<button id="remove-file" class="p-1 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<i data-lucide="x" class="w-5 h-5 text-gray-500 dark:text-gray-400"></i>
</button>
</div>
</div>
<div class="flex flex-wrap gap-4 mt-6">
<button id="analyze-btn" class="flex-1 bg-primary hover:bg-primary-dark text-white font-medium py-3 px-6 rounded-lg flex items-center justify-center gap-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
<i data-lucide="activity" class="w-5 h-5"></i>
<span>Analyze Audio</span>
</button>
<button id="record-btn" class="flex-1 bg-accent hover:bg-accent-dark text-white font-medium py-3 px-6 rounded-lg flex items-center justify-center gap-2 transition-colors">
<i data-lucide="mic" class="w-5 h-5"></i>
<span>Record Audio</span>
</button>
</div>
</section>
<!-- Loading Animation -->
<div id="loading" class="hidden">
<div class="flex flex-col items-center justify-center py-12">
<div class="relative w-20 h-20">
<div class="absolute top-0 left-0 w-full h-full border-4 border-gray-200 dark:border-gray-700 rounded-full"></div>
<div class="absolute top-0 left-0 w-full h-full border-4 border-t-primary border-r-transparent border-b-transparent border-l-transparent rounded-full animate-spin"></div>
</div>
<p class="mt-4 text-lg font-medium">Analyzing audio...</p>
<p class="text-gray-500 dark:text-gray-400">This may take a few moments</p>
</div>
</div>
<!-- Results Section -->
<section id="results" class="hidden">
<h2 class="font-poppins font-semibold text-2xl mb-6">Analysis Results</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<!-- Emotion Card -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-md overflow-hidden">
<div class="p-6">
<h3 class="font-poppins font-medium text-lg mb-4">Detected Emotion</h3>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div id="emotion-emoji" class="text-4xl"></div>
<div>
<p id="emotion-label" class="font-poppins font-semibold text-2xl"></p>
<p class="text-gray-500 dark:text-gray-400">Primary emotion</p>
</div>
</div>
<div class="relative w-20 h-20">
<svg class="w-full h-full" viewBox="0 0 36 36">
<circle cx="18" cy="18" r="16" fill="none" stroke-width="2" class="stroke-gray-200 dark:stroke-gray-700"></circle>
<circle id="confidence-circle" cx="18" cy="18" r="16" fill="none" stroke-width="2" class="stroke-primary progress-ring-circle" stroke-dasharray="100" stroke-dashoffset="0"></circle>
<text id="confidence-text" x="18" y="18" text-anchor="middle" dominant-baseline="middle" class="font-medium text-sm"></text>
</svg>
</div>
</div>
</div>
</div>
<!-- All Emotions Card -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-md overflow-hidden">
<div class="p-6">
<h3 class="font-poppins font-medium text-lg mb-4">All Emotions</h3>
<div class="space-y-3">
<div class="emotion-bar" data-emotion="angry">
<div class="flex justify-between mb-1">
<span>Angry 😠</span>
<span class="emotion-percentage">0%</span>
</div>
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5">
<div class="bg-red-500 h-2.5 rounded-full" style="width: 0%"></div>
</div>
</div>
<div class="emotion-bar" data-emotion="disgusted">
<div class="flex justify-between mb-1">
<span>Disgusted 🤢</span>
<span class="emotion-percentage">0%</span>
</div>
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5">
<div class="bg-green-500 h-2.5 rounded-full" style="width: 0%"></div>
</div>
</div>
<div class="emotion-bar" data-emotion="fearful">
<div class="flex justify-between mb-1">
<span>Fearful 😨</span>
<span class="emotion-percentage">0%</span>
</div>
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5">
<div class="bg-purple-500 h-2.5 rounded-full" style="width: 0%"></div>
</div>
</div>
<div class="emotion-bar" data-emotion="happy">
<div class="flex justify-between mb-1">
<span>Happy 😊</span>
<span class="emotion-percentage">0%</span>
</div>
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5">
<div class="bg-yellow-500 h-2.5 rounded-full" style="width: 0%"></div>
</div>
</div>
<div class="emotion-bar" data-emotion="neutral">
<div class="flex justify-between mb-1">
<span>Neutral 😐</span>
<span class="emotion-percentage">0%</span>
</div>
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5">
<div class="bg-blue-400 h-2.5 rounded-full" style="width: 0%"></div>
</div>
</div>
<div class="emotion-bar" data-emotion="sad">
<div class="flex justify-between mb-1">
<span>Sad 😢</span>
<span class="emotion-percentage">0%</span>
</div>
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5">
<div class="bg-blue-600 h-2.5 rounded-full" style="width: 0%"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Transcription Card -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-md overflow-hidden">
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="font-poppins font-medium text-lg">Transcription</h3>
<button id="copy-transcription" class="p-2 rounded-lg bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors">
<i data-lucide="copy" class="w-5 h-5"></i>
</button>
</div>
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 min-h-[100px]">
<p id="transcription-text" class="whitespace-pre-wrap"></p>
</div>
</div>
</div>
</section>
<!-- Error Alert -->
<div id="error-alert" class="hidden bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded-lg mt-6">
<div class="flex items-start">
<div class="flex-shrink-0">
<i data-lucide="alert-circle" class="w-5 h-5"></i>
</div>
<div class="ml-3">
<p id="error-message" class="text-sm font-medium"></p>
</div>
<button id="close-error" class="ml-auto -mx-1.5 -my-1.5 bg-red-100 text-red-500 rounded-lg p-1.5 hover:bg-red-200 inline-flex h-8 w-8 items-center justify-center">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
</div>
</main>
<!-- About Modal -->
<div id="about-modal" class="fixed inset-0 z-50 hidden">
<div class="absolute inset-0 bg-black bg-opacity-50"></div>
<div class="relative top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-y-auto">
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="font-poppins font-semibold text-2xl">How It Works</h2>
<button id="close-modal" class="p-1 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<i data-lucide="x" class="w-6 h-6"></i>
</button>
</div>
<div class="space-y-4">
<p>The Speech Emotion Detector uses advanced machine learning algorithms to analyze audio and detect emotions in speech.</p>
<h3 class="font-poppins font-medium text-lg">How to use:</h3>
<ol class="list-decimal list-inside space-y-2">
<li>Upload an audio file (.wav, .mp3, or .ogg format)</li>
<li>Click "Analyze Audio" to process the file</li>
<li>View the detected emotion, confidence score, and transcription</li>
</ol>
<h3 class="font-poppins font-medium text-lg">Technology:</h3>
<p>This application uses:</p>
<ul class="list-disc list-inside space-y-1">
<li>Speech recognition for transcription</li>
<li>Audio feature extraction (MFCC, chroma, mel spectrogram)</li>
<li>Machine learning models trained on emotional speech datasets</li>
</ul>
<p>The system can detect six basic emotions: Angry, Disgusted, Fearful, Happy, Neutral, and Sad.</p>
</div>
</div>
</div>
</div>
<!-- Record Modal -->
<div id="record-modal" class="fixed inset-0 z-50 hidden">
<div class="absolute inset-0 bg-black bg-opacity-50"></div>
<div class="relative top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white dark:bg-gray-800 rounded-xl shadow-xl max-w-md w-full">
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="font-poppins font-semibold text-2xl">Record Audio</h2>
<button id="close-record-modal" class="p-1 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<i data-lucide="x" class="w-6 h-6"></i>
</button>
</div>
<div class="flex flex-col items-center justify-center py-8">
<div id="record-button" class="w-24 h-24 rounded-full bg-red-500 flex items-center justify-center cursor-pointer hover:bg-red-600 transition-colors mb-4">
<i data-lucide="mic" class="w-12 h-12 text-white"></i>
</div>
<p id="record-status" class="text-lg font-medium">Click to start recording</p>
<p id="record-timer" class="text-gray-500 dark:text-gray-400 mt-2">00:00</p>
</div>
<div class="flex justify-center gap-4">
<button id="cancel-recording" class="bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 font-medium py-2 px-4 rounded-lg transition-colors">
Cancel
</button>
<button id="save-recording" class="bg-primary hover:bg-primary-dark text-white font-medium py-2 px-4 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed" disabled>
Use Recording
</button>
</div>
</div>
</div>
</div>
<script>
// Initialize Lucide icons
lucide.createIcons();
// DOM Elements
const themeToggle = document.getElementById('theme-toggle');
const uploadContainer = document.getElementById('upload-container');
const fileInput = document.getElementById('file-input');
const fileInfo = document.getElementById('file-info');
const fileName = document.getElementById('file-name');
const fileSize = document.getElementById('file-size');
const removeFile = document.getElementById('remove-file');
const analyzeBtn = document.getElementById('analyze-btn');
const recordBtn = document.getElementById('record-btn');
const loading = document.getElementById('loading');
const results = document.getElementById('results');
const emotionEmoji = document.getElementById('emotion-emoji');
const emotionLabel = document.getElementById('emotion-label');
const confidenceCircle = document.getElementById('confidence-circle');
const confidenceText = document.getElementById('confidence-text');
const transcriptionText = document.getElementById('transcription-text');
const copyTranscription = document.getElementById('copy-transcription');
const errorAlert = document.getElementById('error-alert');
const errorMessage = document.getElementById('error-message');
const closeError = document.getElementById('close-error');
const aboutBtn = document.getElementById('about-btn');
const aboutModal = document.getElementById('about-modal');
const closeModal = document.getElementById('close-modal');
const recordModal = document.getElementById('record-modal');
const closeRecordModal = document.getElementById('close-record-modal');
const recordButton = document.getElementById('record-button');
const recordStatus = document.getElementById('record-status');
const recordTimer = document.getElementById('record-timer');
const cancelRecording = document.getElementById('cancel-recording');
const saveRecording = document.getElementById('save-recording');
const toastContainer = document.getElementById('toast-container');
// Check for dark mode preference
if (localStorage.getItem('theme') === 'dark' ||
(!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
// Theme toggle
themeToggle.addEventListener('click', () => {
if (document.documentElement.classList.contains('dark')) {
document.documentElement.classList.remove('dark');
localStorage.setItem('theme', 'light');
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('theme', 'dark');
}
});
// File upload handling
uploadContainer.addEventListener('click', () => {
fileInput.click();
});
uploadContainer.addEventListener('dragover', (e) => {
e.preventDefault();
uploadContainer.classList.add('border-primary');
});
uploadContainer.addEventListener('dragleave', () => {
uploadContainer.classList.remove('border-primary');
});
uploadContainer.addEventListener('drop', (e) => {
e.preventDefault();
uploadContainer.classList.remove('border-primary');
if (e.dataTransfer.files.length) {
handleFile(e.dataTransfer.files[0]);
}
});
fileInput.addEventListener('change', () => {
if (fileInput.files.length) {
handleFile(fileInput.files[0]);
}
});
function handleFile(file) {
// Check if file type is supported
const fileType = file.name.split('.').pop().toLowerCase();
if (!['wav', 'mp3', 'ogg'].includes(fileType)) {
showError('Unsupported file format. Please upload a .wav, .mp3, or .ogg file.');
return;
}
// Display file info
fileName.textContent = file.name;
fileSize.textContent = formatFileSize(file.size);
fileInfo.classList.remove('hidden');
analyzeBtn.disabled = false;
}
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' bytes';
else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
else return (bytes / 1048576).toFixed(1) + ' MB';
}
removeFile.addEventListener('click', () => {
fileInput.value = '';
fileInfo.classList.add('hidden');
analyzeBtn.disabled = true;
});
// Analyze button
analyzeBtn.addEventListener('click', () => {
if (!fileInput.files.length) return;
// Hide upload section and show loading
loading.classList.remove('hidden');
results.classList.add('hidden');
errorAlert.classList.add('hidden');
// Simulate API call with timeout
setTimeout(() => {
loading.classList.add('hidden');
// Simulate random results
const emotions = ['angry', 'disgusted', 'fearful', 'happy', 'neutral', 'sad'];
const randomEmotion = emotions[Math.floor(Math.random() * emotions.length)];
const confidence = Math.random() * 0.5 + 0.5; // Random between 50-100%
// Generate random probabilities for all emotions
const probabilities = {};
let total = 0;
emotions.forEach(emotion => {
if (emotion === randomEmotion) {
probabilities[emotion] = confidence;
} else {
probabilities[emotion] = Math.random() * (1 - confidence);
total += probabilities[emotion];
}
});
// Normalize other probabilities
if (total > 0) {
const scale = (1 - confidence) / total;
emotions.forEach(emotion => {
if (emotion !== randomEmotion) {
probabilities[emotion] *= scale;
}
});
}
// Update UI with results
displayResults(randomEmotion, confidence, probabilities);
// Show toast notification
showToast('Audio analysis complete!', 'success');
}, 2000);
});
function displayResults(emotion, confidence, probabilities) {
// Set emoji and label
const emojis = {
'angry': '😠',
'disgusted': '🤢',
'fearful': '😨',
'happy': '😊',
'neutral': '😐',
'sad': '😢'
};
emotionEmoji.textContent = emojis[emotion];
emotionLabel.textContent = emotion.charAt(0).toUpperCase() + emotion.slice(1);
// Set confidence circle
const circumference = 2 * Math.PI * 16;
const offset = circumference - (confidence * circumference);
confidenceCircle.style.strokeDasharray = `${circumference} ${circumference}`;
confidenceCircle.style.strokeDashoffset = offset;
confidenceText.textContent = `${Math.round(confidence * 100)}%`;
// Update emotion bars
Object.keys(probabilities).forEach(emotion => {
const percentage = Math.round(probabilities[emotion] * 100);
const bar = document.querySelector(`.emotion-bar[data-emotion="${emotion}"] .emotion-percentage`);
const progress = document.querySelector(`.emotion-bar[data-emotion="${emotion}"] div div`);
bar.textContent = `${percentage}%`;
progress.style.width = `${percentage}%`;
});
// Set transcription (simulated)
const transcriptions = [
"Hello, this is a sample transcription of what I'm saying in this audio file.",
"I'm really excited about this new project we're working on together.",
"I don't think that's a good idea. We should reconsider our approach.",
"The weather today is absolutely beautiful, perfect for a walk in the park.",
"I'm feeling a bit under the weather today, might need to take it easy."
];
transcriptionText.textContent = transcriptions[Math.floor(Math.random() * transcriptions.length)];
// Show results section
results.classList.remove('hidden');
}
// Copy transcription
copyTranscription.addEventListener('click', () => {
navigator.clipboard.writeText(transcriptionText.textContent)
.then(() => {
showToast('Transcription copied to clipboard!', 'success');
})
.catch(err => {
showToast('Failed to copy text', 'error');
});
});
// Error handling
function showError(message) {
errorMessage.textContent = message;
errorAlert.classList.remove('hidden');
}
closeError.addEventListener('click', () => {
errorAlert.classList.add('hidden');
});
// About modal
aboutBtn.addEventListener('click', () => {
aboutModal.classList.remove('hidden');
});
closeModal.addEventListener('click', () => {
aboutModal.classList.add('hidden');
});
// Close modal when clicking outside
aboutModal.addEventListener('click', (e) => {
if (e.target === aboutModal) {
aboutModal.classList.add('hidden');
}
});
// Record modal
recordBtn.addEventListener('click', () => {
recordModal.classList.remove('hidden');
});
closeRecordModal.addEventListener('click', () => {
recordModal.classList.add('hidden');
});
// Close record modal when clicking outside
recordModal.addEventListener('click', (e) => {
if (e.target === recordModal) {
recordModal.classList.add('hidden');
}
});
// Record functionality (simulated)
let isRecording = false;
let recordingTimer;
let recordingSeconds = 0;
recordButton.addEventListener('click', () => {
if (!isRecording) {
startRecording();
} else {
stopRecording();
}
});
function startRecording() {
isRecording = true;
recordingSeconds = 0;
recordButton.classList.remove('bg-red-500', 'hover:bg-red-600');
recordButton.classList.add('bg-gray-500', 'hover:bg-gray-600', 'animate-pulse');
recordStatus.textContent = 'Recording...';
saveRecording.disabled = true;
// Start timer
recordingTimer = setInterval(() => {
recordingSeconds++;
const minutes = Math.floor(recordingSeconds / 60).toString().padStart(2, '0');
const seconds = (recordingSeconds % 60).toString().padStart(2, '0');
recordTimer.textContent = `${minutes}:${seconds}`;
// Enable save button after 1 second
if (recordingSeconds >= 1) {
saveRecording.disabled = false;
}
}, 1000);
}
function stopRecording() {
isRecording = false;
clearInterval(recordingTimer);
recordButton.classList.remove('bg-gray-500', 'hover:bg-gray-600', 'animate-pulse');
recordButton.classList.add('bg-red-500', 'hover:bg-red-600');
recordStatus.textContent = 'Recording stopped';
}
cancelRecording.addEventListener('click', () => {
if (isRecording) {
stopRecording();
}
recordModal.classList.add('hidden');
recordStatus.textContent = 'Click to start recording';
recordTimer.textContent = '00:00';
saveRecording.disabled = true;
});
saveRecording.addEventListener('click', () => {
if (isRecording) {
stopRecording();
}
recordModal.classList.add('hidden');
// Simulate file creation
fileInfo.classList.remove('hidden');
fileName.textContent = 'recording_' + new Date().toISOString().slice(0, 19).replace(/[-:T]/g, '') + '.wav';
fileSize.textContent = '256 KB';
analyzeBtn.disabled = false;
showToast('Recording saved successfully!', 'success');
});
// Toast notifications
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `flex items-center p-4 mb-3 rounded-lg shadow-md transition-all transform translate-x-full animate-toast-in ${
type === 'success' ? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100' :
type === 'error' ? 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100' :
'bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-100'
}`;
const icon = document.createElement('div');
icon.className = 'flex-shrink-0 mr-3';
icon.innerHTML = `<i data-lucide="${
type === 'success' ? 'check-circle' :
type === 'error' ? 'alert-circle' : 'info'
}" class="w-5 h-5"></i>`;
const content = document.createElement('div');
content.textContent = message;
const closeBtn = document.createElement('button');
closeBtn.className = 'ml-auto -mx-1.5 -my-1.5 rounded-lg p-1.5 inline-flex h-8 w-8 items-center justify-center hover:bg-opacity-25 hover:bg-gray-500';
closeBtn.innerHTML = '<i data-lucide="x" class="w-4 h-4"></i>';
toast.appendChild(icon);
toast.appendChild(content);
toast.appendChild(closeBtn);
toastContainer.appendChild(toast);
lucide.createIcons();
closeBtn.addEventListener('click', () => {
toast.classList.replace('animate-toast-in', 'animate-toast-out');
setTimeout(() => {
toast.remove();
}, 300);
});
setTimeout(() => {
toast.classList.replace('animate-toast-in', 'animate-toast-out');
setTimeout(() => {
toast.remove();
}, 300);
}, 5000);
}
// Add animation for toast
const style = document.createElement('style');
style.textContent = `
@keyframes toastIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes toastOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
.animate-toast-in {
animation: toastIn 0.3s ease forwards;
}
.animate-toast-out {
animation: toastOut 0.3s ease forwards;
}
`;
document.head.appendChild(style);
</script>
</body>
</html>