anycoder-e98292e7 / index.html
Multimedix's picture
Upload folder using huggingface_hub
9f1da77 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VoiceDo - Speech Recognition Todo List</title>
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- FontAwesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary: #6366f1;
--primary-dark: #4f46e5;
--secondary: #ec4899;
--bg-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--glass-bg: rgba(255, 255, 255, 0.95);
--glass-border: rgba(255, 255, 255, 0.5);
--text-main: #1f2937;
--text-muted: #6b7280;
--danger: #ef4444;
--success: #10b981;
--shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
--radius: 16px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Outfit', sans-serif;
}
body {
background: var(--bg-gradient);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
overflow-x: hidden;
}
/* Background decoration */
.blobs {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: -1;
overflow: hidden;
}
.blob {
position: absolute;
border-radius: 50%;
filter: blur(60px);
opacity: 0.6;
animation: float 10s infinite ease-in-out;
}
.blob:nth-child(1) {
top: -10%;
left: -10%;
width: 400px;
height: 400px;
background: #4facfe;
}
.blob:nth-child(2) {
bottom: -10%;
right: -10%;
width: 350px;
height: 350px;
background: #f093fb;
animation-delay: -5s;
}
@keyframes float {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(30px, 50px); }
}
/* Main App Container */
.app-container {
width: 100%;
max-width: 500px;
background: var(--glass-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-radius: var(--radius);
box-shadow: var(--shadow);
border: 1px solid var(--glass-border);
overflow: hidden;
display: flex;
flex-direction: column;
max-height: 90vh;
}
/* Header */
header {
padding: 24px 24px 10px;
text-align: center;
}
header h1 {
font-size: 1.8rem;
font-weight: 700;
background: linear-gradient(to right, var(--primary), var(--secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 5px;
}
.anycoder-link {
display: inline-block;
font-size: 0.8rem;
color: var(--text-muted);
text-decoration: none;
font-weight: 500;
transition: color 0.3s;
padding: 4px 10px;
background: rgba(0,0,0,0.05);
border-radius: 20px;
}
.anycoder-link:hover {
color: var(--primary);
background: rgba(99, 102, 241, 0.1);
}
/* Input Section */
.input-section {
padding: 20px 24px;
position: relative;
}
.input-wrapper {
display: flex;
gap: 10px;
position: relative;
}
#todo-input {
flex: 1;
padding: 14px 16px;
border-radius: 12px;
border: 2px solid transparent;
background: #f3f4f6;
font-size: 1rem;
transition: all 0.3s ease;
outline: none;
}
#todo-input:focus {
background: #fff;
border-color: var(--primary);
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1);
}
.btn {
border: none;
cursor: pointer;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
font-size: 1.1rem;
}
#mic-btn {
width: 50px;
background: #f3f4f6;
color: var(--text-muted);
position: relative;
overflow: hidden;
}
#mic-btn:hover {
background: #e5e7eb;
color: var(--text-main);
}
#mic-btn.listening {
background: var(--danger);
color: white;
animation: pulse-red 1.5s infinite;
}
#add-btn {
width: 50px;
background: var(--primary);
color: white;
}
#add-btn:hover {
background: var(--primary-dark);
transform: translateY(-1px);
}
#add-btn:active {
transform: translateY(1px);
}
/* Listening Overlay/Indicator */
.listening-status {
font-size: 0.85rem;
color: var(--danger);
margin-top: 8px;
height: 20px;
opacity: 0;
transition: opacity 0.3s;
display: flex;
align-items: center;
gap: 6px;
}
.listening-status.active {
opacity: 1;
}
.wave-bars {
display: flex;
gap: 2px;
height: 12px;
align-items: center;
}
.bar {
width: 3px;
background: var(--danger);
border-radius: 2px;
animation: wave 0.5s infinite ease-in-out;
}
.bar:nth-child(1) { height: 4px; animation-delay: 0.0s; }
.bar:nth-child(2) { height: 8px; animation-delay: 0.1s; }
.bar:nth-child(3) { height: 12px; animation-delay: 0.2s; }
.bar:nth-child(4) { height: 8px; animation-delay: 0.3s; }
.bar:nth-child(5) { height: 4px; animation-delay: 0.4s; }
@keyframes wave {
0%, 100% { transform: scaleY(1); }
50% { transform: scaleY(1.8); }
}
@keyframes pulse-red {
0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); }
100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
}
/* Filters */
.filters {
display: flex;
padding: 0 24px 15px;
gap: 15px;
border-bottom: 1px solid rgba(0,0,0,0.05);
}
.filter-btn {
background: none;
border: none;
color: var(--text-muted);
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
padding-bottom: 5px;
position: relative;
}
.filter-btn.active {
color: var(--primary);
}
.filter-btn.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
width: 100%;
height: 2px;
background: var(--primary);
border-radius: 2px;
}
/* Task List */
.todo-list-container {
flex: 1;
overflow-y: auto;
padding: 20px 24px;
}
/* Custom Scrollbar */
.todo-list-container::-webkit-scrollbar {
width: 6px;
}
.todo-list-container::-webkit-scrollbar-track {
background: transparent;
}
.todo-list-container::-webkit-scrollbar-thumb {
background: #d1d5db;
border-radius: 10px;
}
.todo-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 12px;
}
.todo-item {
background: white;
border-radius: 12px;
padding: 12px 16px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2px 5px rgba(0,0,0,0.03);
border: 1px solid rgba(0,0,0,0.05);
transition: all 0.3s;
animation: slideIn 0.3s ease-out forwards;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.todo-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
.todo-content {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
overflow: hidden;
}
.custom-checkbox {
appearance: none;
-webkit-appearance: none;
width: 22px;
height: 22px;
border: 2px solid #d1d5db;
border-radius: 6px;
cursor: pointer;
position: relative;
transition: all 0.2s;
flex-shrink: 0;
}
.custom-checkbox:checked {
background: var(--success);
border-color: var(--success);
}
.custom-checkbox:checked::after {
content: '\f00c';
font-family: 'Font Awesome 6 Free';
font-weight: 900;
color: white;
font-size: 12px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.todo-text {
font-size: 1rem;
color: var(--text-main);
transition: all 0.2s;
word-break: break-word;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
color: var(--text-muted);
}
.todo-item.completed {
background: #f9fafb;
opacity: 0.8;
}
.delete-btn {
background: none;
border: none;
color: #d1d5db;
cursor: pointer;
padding: 8px;
font-size: 1rem;
transition: color 0.2s;
margin-left: 8px;
}
.delete-btn:hover {
color: var(--danger);
}
/* Empty State */
.empty-state {
text-align: center;
padding: 40px 20px;
color: var(--text-muted);
display: none;
}
.empty-state i {
font-size: 3rem;
margin-bottom: 15px;
opacity: 0.3;
}
.empty-state p {
font-size: 0.95rem;
}
/* Notification Toast */
.toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: #333;
color: white;
padding: 12px 24px;
border-radius: 50px;
font-size: 0.9rem;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
z-index: 100;
display: flex;
align-items: center;
gap: 10px;
}
.toast.show {
transform: translateX(-50%) translateY(0);
}
/* Responsive */
@media (max-width: 480px) {
.app-container {
height: 100vh;
max-height: 100vh;
border-radius: 0;
}
header h1 {
font-size: 1.5rem;
}
}
</style>
</head>
<body>
<!-- Background Blobs -->
<div class="blobs">
<div class="blob"></div>
<div class="blob"></div>
</div>
<main class="app-container">
<header>
<h1>VoiceDo</h1>
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">Built with anycoder</a>
</header>
<section class="input-section">
<div class="input-wrapper">
<input type="text" id="todo-input" placeholder="Type or say something...">
<button class="btn" id="mic-btn" title="Speak to add">
<i class="fa-solid fa-microphone"></i>
</button>
<button class="btn" id="add-btn" title="Add Task">
<i class="fa-solid fa-plus"></i>
</button>
</div>
<div class="listening-status" id="listening-status">
<div class="wave-bars">
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
</div>
<span>Listening...</span>
</div>
</section>
<section class="filters">
<button class="filter-btn active" data-filter="all">All</button>
<button class="filter-btn" data-filter="active">Active</button>
<button class="filter-btn" data-filter="completed">Completed</button>
</section>
<div class="todo-list-container">
<ul class="todo-list" id="todo-list">
<!-- Tasks will be added here -->
</ul>
<div class="empty-state" id="empty-state">
<i class="fa-solid fa-clipboard-list"></i>
<p>No tasks found. Start by adding one!</p>
</div>
</div>
</main>
<div class="toast" id="toast">
<i class="fa-solid fa-circle-info"></i>
<span id="toast-msg">Notification</span>
</div>
<script>
// --- DOM Elements ---
const todoInput = document.getElementById('todo-input');
const addBtn = document.getElementById('add-btn');
const micBtn = document.getElementById('mic-btn');
const todoList = document.getElementById('todo-list');
const filterBtns = document.querySelectorAll('.filter-btn');
const emptyState = document.getElementById('empty-state');
const listeningStatus = document.getElementById('listening-status');
const toast = document.getElementById('toast');
const toastMsg = document.getElementById('toast-msg');
// --- State ---
let todos = JSON.parse(localStorage.getItem('voicedo_todos')) || [];
let currentFilter = 'all';
let isListening = false;
// --- Speech Recognition Setup ---
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
let recognition;
if (SpeechRecognition) {
recognition = new SpeechRecognition();
recognition.continuous = false;
recognition.lang = 'en-US';
recognition.interimResults = false;
recognition.onstart = () => {
isListening = true;
micBtn.classList.add('listening');
micBtn.innerHTML = '<i class="fa-solid fa-stop"></i>';
listeningStatus.classList.add('active');
todoInput.placeholder = "Listening...";
};
recognition.onend = () => {
isListening = false;
micBtn.classList.remove('listening');
micBtn.innerHTML = '<i class="fa-solid fa-microphone"></i>';
listeningStatus.classList.remove('active');
todoInput.placeholder = "Type or say something...";
};
recognition.onresult = (event) => {
const transcript = event.results[0][0].transcript;
todoInput.value = transcript;
// Optional: Auto-add if confident? Let's keep it manual for review.
todoInput.focus();
showToast(`Heard: "${transcript}"`);
};
recognition.onerror = (event) => {
console.error(event.error);
showToast('Microphone error or permission denied.');
isListening = false;
micBtn.classList.remove('listening');
listeningStatus.classList.remove('active');
};
} else {
micBtn.style.display = 'none'; // Hide mic if not supported
console.log("Speech Recognition not supported in this browser.");
}
// --- Functions ---
function saveTodos() {
localStorage.setItem('voicedo_todos', JSON.stringify(todos));
}
function renderTodos() {
todoList.innerHTML = '';
const filteredTodos = todos.filter(todo => {
if (currentFilter === 'active') return !todo.completed;
if (currentFilter === 'completed') return todo.completed;
return true;
});
if (filteredTodos.length === 0) {
emptyState.style.display = 'block';
} else {
emptyState.style.display = 'none';
filteredTodos.forEach(todo => {
const li = document.createElement('li');
li.className = `todo-item ${todo.completed ? 'completed' : ''}`;
li.innerHTML = `
<div class="todo-content">
<input type="checkbox" class="custom-checkbox"
${todo.completed ? 'checked' : ''}
onclick="toggleTodo(${todo.id})">
<span class="todo-text">${escapeHtml(todo.text)}</span>
</div>
<button class="delete-btn" onclick="deleteTodo(${todo.id})">
<i class="fa-solid fa-trash"></i>
</button>
`;
todoList.appendChild(li);
});
}
}
function addTodo() {
const text = todoInput.value.trim();
if (text === '') {
showToast('Please enter a task!');
return;
}
const newTodo = {
id: Date.now(),
text: text,
completed: false,
createdAt: new Date()
};
todos.unshift(newTodo); // Add to top
saveTodos();
renderTodos();
todoInput.value = '';
showToast('Task added successfully');
}
// Expose to global scope for onclick attributes
window.deleteTodo = function(id) {
todos = todos.filter(t => t.id !== id);
saveTodos();
renderTodos();
showToast('Task deleted');
};
window.toggleTodo = function(id) {
const todo = todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
saveTodos();
renderTodos();
}
};
function toggleSpeech() {
if (!recognition) {
showToast('Speech recognition not supported.');
return;
}
if (isListening) {
recognition.stop();
} else {
recognition.start();
}
}
function showToast(message) {
toastMsg.innerText = message;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}
// --- Event Listeners ---
addBtn.addEventListener('click', addTodo);
todoInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') addTodo();
});
micBtn.addEventListener('click', toggleSpeech);
filterBtns.forEach(btn => {
btn.addEventListener('click', () => {
// Update UI
filterBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// Update Logic
currentFilter = btn.dataset.filter;
renderTodos();
});
});
// --- Initialization ---
renderTodos();
</script>
</body>
</html>