|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>A.R.I.A | Advanced Voice Assistant</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
<style>
|
|
:root {
|
|
--primary: #3b82f6;
|
|
--secondary: #1d4ed8;
|
|
--accent: #60a5fa;
|
|
--bg-dark: #0f172a;
|
|
--bg-darker: #020617;
|
|
--text-light: #f1f5f9;
|
|
}
|
|
|
|
body {
|
|
background-color: var(--bg-darker);
|
|
color: var(--text-light);
|
|
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
position: relative;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
.glass-effect {
|
|
background: rgba(15, 23, 42, 0.75);
|
|
backdrop-filter: blur(12px);
|
|
-webkit-backdrop-filter: blur(12px);
|
|
border: 1px solid rgba(148, 163, 184, 0.1);
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
|
|
.header-title {
|
|
font-size: 6rem;
|
|
font-weight: 800;
|
|
background: linear-gradient(to right, #60a5fa, #3b82f6);
|
|
-webkit-background-clip: text;
|
|
background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
position: relative;
|
|
letter-spacing: 0.2em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.header-subtitle {
|
|
font-size: 1.25rem;
|
|
color: #94a3b8;
|
|
letter-spacing: 0.1em;
|
|
margin-top: 1rem;
|
|
font-weight: 500;
|
|
background: linear-gradient(to right, #94a3b8, #64748b);
|
|
-webkit-background-clip: text;
|
|
background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
|
|
.background-grid {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-image:
|
|
linear-gradient(rgba(30, 41, 59, 0.1) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(30, 41, 59, 0.1) 1px, transparent 1px);
|
|
background-size: 30px 30px;
|
|
z-index: -1;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.glow-effect {
|
|
position: absolute;
|
|
width: 150px;
|
|
height: 150px;
|
|
border-radius: 50%;
|
|
background: var(--primary);
|
|
filter: blur(100px);
|
|
opacity: 0.15;
|
|
animation: float 8s infinite ease-in-out;
|
|
}
|
|
|
|
@keyframes float {
|
|
0%, 100% { transform: translate(0, 0); }
|
|
50% { transform: translate(30px, -30px); }
|
|
}
|
|
|
|
.pulse {
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0% {
|
|
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
|
|
}
|
|
70% {
|
|
box-shadow: 0 0 0 15px rgba(59, 130, 246, 0);
|
|
}
|
|
100% {
|
|
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
|
|
}
|
|
}
|
|
|
|
.wave {
|
|
position: relative;
|
|
height: 60px;
|
|
width: 60px;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.wave .dot {
|
|
display: inline-block;
|
|
width: 4px;
|
|
height: 4px;
|
|
border-radius: 50%;
|
|
background: var(--primary);
|
|
animation: wave 1.3s linear infinite;
|
|
}
|
|
|
|
.wave .dot:nth-child(2) {
|
|
animation-delay: -1.1s;
|
|
}
|
|
|
|
.wave .dot:nth-child(3) {
|
|
animation-delay: -0.9s;
|
|
}
|
|
|
|
@keyframes wave {
|
|
0%, 60%, 100% {
|
|
transform: initial;
|
|
}
|
|
30% {
|
|
transform: translateY(-10px);
|
|
}
|
|
}
|
|
|
|
.voice-btn {
|
|
transition: all 0.3s ease;
|
|
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
|
}
|
|
|
|
.voice-btn:hover {
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.voice-btn.active {
|
|
background: var(--accent);
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
.response-text {
|
|
border-left: 3px solid var(--primary);
|
|
animation: textAppear 0.5s ease-out;
|
|
}
|
|
|
|
@keyframes textAppear {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateX(-10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
.gradient-border {
|
|
position: relative;
|
|
border-radius: 0.75rem;
|
|
}
|
|
|
|
.gradient-border::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: -1px;
|
|
left: -1px;
|
|
right: -1px;
|
|
bottom: -1px;
|
|
background: linear-gradient(135deg, var(--primary), var(--accent));
|
|
border-radius: 0.75rem;
|
|
z-index: -1;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
#voiceSelect {
|
|
background-color: rgba(30, 41, 59, 0.9);
|
|
color: var(--text-light);
|
|
border: 1px solid var(--primary);
|
|
}
|
|
|
|
.status-badge {
|
|
background: rgba(30, 41, 59, 0.9);
|
|
border: 1px solid rgba(148, 163, 184, 0.2);
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.mode-btn {
|
|
position: relative;
|
|
overflow: hidden;
|
|
transform: scale(1);
|
|
}
|
|
|
|
.mode-btn::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: currentColor;
|
|
opacity: 0;
|
|
transition: opacity 0.3s;
|
|
z-index: -1;
|
|
}
|
|
|
|
.mode-btn:hover {
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.mode-btn.selected {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
.mode-btn.selected.text-blue-400 {
|
|
box-shadow: 0 0 15px rgba(59, 130, 246, 0.3);
|
|
}
|
|
|
|
.mode-btn.selected.text-purple-400 {
|
|
box-shadow: 0 0 15px rgba(168, 85, 247, 0.3);
|
|
}
|
|
|
|
.mode-btn.selected.text-green-400 {
|
|
box-shadow: 0 0 15px rgba(74, 222, 128, 0.3);
|
|
}
|
|
|
|
.spectrum-button {
|
|
width: 80px;
|
|
height: 80px;
|
|
position: relative;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.spectrum-bars {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 3px;
|
|
height: 40px;
|
|
}
|
|
|
|
.spectrum-bar {
|
|
width: 4px;
|
|
height: 20px;
|
|
background: white;
|
|
border-radius: 2px;
|
|
transition: height 0.2s ease;
|
|
}
|
|
|
|
.spectrum-button.active .spectrum-bars {
|
|
opacity: 1;
|
|
}
|
|
|
|
.spectrum-button.active .spectrum-bar {
|
|
animation: spectrum-dance 0.5s ease infinite;
|
|
}
|
|
|
|
.spectrum-button:hover {
|
|
transform: scale(1.05);
|
|
box-shadow: 0 0 20px rgba(var(--primary), 0.5);
|
|
}
|
|
|
|
.spectrum-button.active {
|
|
transform: scale(0.95);
|
|
background: var(--accent);
|
|
}
|
|
|
|
@keyframes spectrum-dance {
|
|
0%, 100% {
|
|
height: 20px;
|
|
}
|
|
50% {
|
|
height: 35px;
|
|
}
|
|
}
|
|
|
|
.spectrum-bar:nth-child(1) { animation-delay: 0.1s; }
|
|
.spectrum-bar:nth-child(2) { animation-delay: 0.2s; }
|
|
.spectrum-bar:nth-child(3) { animation-delay: 0.3s; }
|
|
.spectrum-bar:nth-child(4) { animation-delay: 0.4s; }
|
|
.spectrum-bar:nth-child(5) { animation-delay: 0.5s; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="background-grid"></div>
|
|
<div class="glow-effect" style="top: 20%; left: 10%;"></div>
|
|
<div class="glow-effect" style="top: 60%; right: 10%; animation-delay: -4s; background: var(--accent);"></div>
|
|
|
|
<div class="min-h-screen flex flex-col items-center justify-center p-4">
|
|
<div class="glass-effect rounded-2xl p-8 w-full max-w-3xl mx-auto relative">
|
|
|
|
<div class="text-center mb-12">
|
|
<h1 class="header-title" data-text="A.R.I.A">A.R.I.A</h1>
|
|
<p class="header-subtitle">Your Futuristic Voice-Controlled Companion</p>
|
|
<p class="text-slate-500 text-sm mt-3">Powered by smolLM2</p>
|
|
</div>
|
|
|
|
|
|
<div class="flex justify-center mb-6">
|
|
<div class="status-badge rounded-full px-4 py-2 flex items-center space-x-2">
|
|
<div id="statusIndicator" class="w-2.5 h-2.5 rounded-full bg-slate-500"></div>
|
|
<span id="statusText" class="text-sm font-medium text-slate-300">Ready</span>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="flex justify-center mb-6">
|
|
<div id="voiceVisualization" class="wave hidden">
|
|
<div class="dot"></div>
|
|
<div class="dot"></div>
|
|
<div class="dot"></div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div id="responseArea" class="glass-effect rounded-xl p-6 mb-6 min-h-[120px] gradient-border">
|
|
<div class="flex items-start space-x-4">
|
|
<div class="flex-shrink-0 h-10 w-10 rounded-full bg-blue-900/50 flex items-center justify-center">
|
|
<i class="fas fa-robot text-blue-400"></i>
|
|
</div>
|
|
<div class="flex-1">
|
|
<p class="font-medium mb-2 text-blue-400">Assistant</p>
|
|
<div id="responseText" class="response-text pl-4 text-slate-300">
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="space-y-4">
|
|
|
|
<div class="flex justify-center space-x-3 mb-4">
|
|
<button id="modeWeb" class="mode-btn px-4 py-2 rounded-lg glass-effect text-sm font-medium border transition-all duration-300 text-blue-400 border-blue-400/30 hover:border-blue-400/60 selected">
|
|
<i class="fas fa-search mr-2"></i>Web Search
|
|
</button>
|
|
<button id="modeReasoning" class="mode-btn px-4 py-2 rounded-lg glass-effect text-sm font-medium border transition-all duration-300 text-purple-400 border-purple-400/30 hover:border-purple-400/60">
|
|
<i class="fas fa-brain mr-2"></i>Reasoning
|
|
</button>
|
|
<button id="modeCreative" class="mode-btn px-4 py-2 rounded-lg glass-effect text-sm font-medium border transition-all duration-300 text-green-400 border-green-400/30 hover:border-green-400/60">
|
|
<i class="fas fa-magic mr-2"></i>Creative
|
|
</button>
|
|
</div>
|
|
|
|
|
|
<div class="flex justify-center">
|
|
<button id="voiceButton" class="spectrum-button shadow-lg">
|
|
<div class="spectrum-bars">
|
|
<div class="spectrum-bar"></div>
|
|
<div class="spectrum-bar"></div>
|
|
<div class="spectrum-bar"></div>
|
|
<div class="spectrum-bar"></div>
|
|
<div class="spectrum-bar"></div>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="mt-6 text-center">
|
|
<p class="text-slate-400 text-xs">Press and hold to speak, release to send</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<form id="queryForm" action="/" method="POST" class="hidden">
|
|
<input type="text" id="queryInput" name="query">
|
|
</form>
|
|
|
|
<script>
|
|
|
|
const voiceButton = document.getElementById('voiceButton');
|
|
const responseArea = document.getElementById('responseArea');
|
|
const responseText = document.getElementById('responseText');
|
|
const statusIndicator = document.getElementById('statusIndicator');
|
|
const statusText = document.getElementById('statusText');
|
|
const voiceVisualization = document.getElementById('voiceVisualization');
|
|
const queryForm = document.getElementById('queryForm');
|
|
const queryInput = document.getElementById('queryInput');
|
|
|
|
|
|
if (!responseText.textContent.trim()) {
|
|
responseArea.classList.add('hidden');
|
|
}
|
|
|
|
|
|
let recognition;
|
|
let isListening = false;
|
|
let finalTranscript = '';
|
|
let speechSynthesis = window.speechSynthesis;
|
|
let recognitionTimeout;
|
|
let recognitionRetries = 0;
|
|
const MAX_RETRIES = 3;
|
|
|
|
const checkSpeechSupport = () => {
|
|
return 'SpeechRecognition' in window || 'webkitSpeechRecognition' in window;
|
|
};
|
|
|
|
|
|
function initSpeechRecognition() {
|
|
if (checkSpeechSupport()) {
|
|
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
recognition = new SpeechRecognition();
|
|
recognition.continuous = false;
|
|
recognition.interimResults = true;
|
|
recognition.lang = 'en-US';
|
|
|
|
recognition.onstart = () => {
|
|
clearTimeout(recognitionTimeout);
|
|
isListening = true;
|
|
voiceButton.classList.add('active');
|
|
statusIndicator.classList.remove('bg-gray-500', 'bg-red-500');
|
|
statusIndicator.classList.add('bg-green-500');
|
|
statusText.textContent = 'Listening...';
|
|
voiceVisualization.classList.remove('hidden');
|
|
finalTranscript = '';
|
|
recognitionRetries = 0;
|
|
};
|
|
|
|
recognition.onresult = (event) => {
|
|
let interimTranscript = '';
|
|
|
|
for (let i = event.resultIndex; i < event.results.length; i++) {
|
|
const transcript = event.results[i][0].transcript;
|
|
if (event.results[i].isFinal) {
|
|
finalTranscript += transcript;
|
|
} else {
|
|
interimTranscript += transcript;
|
|
}
|
|
}
|
|
|
|
|
|
if (interimTranscript || finalTranscript) {
|
|
responseArea.classList.remove('hidden');
|
|
responseText.innerHTML = `<span class="text-gray-400">You said: "${interimTranscript || finalTranscript}"</span>${interimTranscript ? '<span class="typing-cursor"></span>' : ''}`;
|
|
}
|
|
};
|
|
|
|
recognition.onerror = (event) => {
|
|
console.error('Speech recognition error', event.error);
|
|
isListening = false;
|
|
voiceButton.classList.remove('active');
|
|
voiceVisualization.classList.add('hidden');
|
|
statusIndicator.classList.remove('bg-green-500');
|
|
statusIndicator.classList.add('bg-red-500');
|
|
|
|
|
|
if (event.error === 'not-allowed') {
|
|
statusText.textContent = 'Microphone permission denied';
|
|
} else if (event.error === 'network') {
|
|
statusText.textContent = 'Network error. Check your connection.';
|
|
} else {
|
|
statusText.textContent = 'Error: ' + event.error;
|
|
}
|
|
|
|
setTimeout(resetStatus, 3000);
|
|
};
|
|
|
|
recognition.onend = () => {
|
|
clearTimeout(recognitionTimeout);
|
|
isListening = false;
|
|
voiceButton.classList.remove('active');
|
|
voiceVisualization.classList.add('hidden');
|
|
|
|
if (finalTranscript) {
|
|
processVoiceCommand(finalTranscript);
|
|
} else if (recognitionRetries < MAX_RETRIES) {
|
|
|
|
recognitionRetries++;
|
|
statusText.textContent = `No speech detected, retrying (${recognitionRetries}/${MAX_RETRIES})...`;
|
|
setTimeout(() => {
|
|
try {
|
|
recognition.start();
|
|
} catch (err) {
|
|
console.error('Failed to restart recognition:', err);
|
|
resetStatus();
|
|
}
|
|
}, 1000);
|
|
} else {
|
|
statusText.textContent = 'No speech detected. Please try again.';
|
|
setTimeout(resetStatus, 2000);
|
|
}
|
|
};
|
|
|
|
|
|
voiceButton.addEventListener('mousedown', startListening);
|
|
voiceButton.addEventListener('touchstart', startListening);
|
|
voiceButton.addEventListener('mouseup', stopListening);
|
|
voiceButton.addEventListener('touchend', stopListening);
|
|
voiceButton.addEventListener('mouseleave', stopListening);
|
|
|
|
return true;
|
|
} else {
|
|
console.error('Speech recognition not supported in this browser');
|
|
voiceButton.disabled = true;
|
|
voiceButton.innerHTML = '<i class="fas fa-microphone-slash"></i>';
|
|
statusIndicator.classList.remove('bg-gray-500');
|
|
statusIndicator.classList.add('bg-red-500');
|
|
statusText.textContent = 'Voice not supported';
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
let speechInitialized = false;
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
speechInitialized = initSpeechRecognition();
|
|
|
|
|
|
const flaskResponse = {{ response|tojson|safe }} || "";
|
|
if (flaskResponse && flaskResponse.trim().length > 0) {
|
|
responseArea.classList.remove('hidden');
|
|
responseText.textContent = flaskResponse;
|
|
speakResponse(flaskResponse);
|
|
}
|
|
|
|
|
|
if (speechInitialized) {
|
|
try {
|
|
|
|
const testRecognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
|
|
testRecognition.continuous = false;
|
|
testRecognition.interimResults = false;
|
|
testRecognition.maxAlternatives = 1;
|
|
|
|
let testTimeout = setTimeout(() => {
|
|
try { testRecognition.stop(); } catch(e) {}
|
|
}, 1000);
|
|
|
|
testRecognition.onstart = () => {
|
|
clearTimeout(testTimeout);
|
|
setTimeout(() => {
|
|
try { testRecognition.stop(); } catch(e) {}
|
|
}, 100);
|
|
};
|
|
|
|
testRecognition.start();
|
|
} catch(e) {
|
|
console.warn('Speech recognition test failed:', e);
|
|
}
|
|
}
|
|
});
|
|
|
|
function startListening(e) {
|
|
e.preventDefault();
|
|
if (!isListening && recognition) {
|
|
try {
|
|
recognition.start();
|
|
|
|
recognitionTimeout = setTimeout(() => {
|
|
if (!isListening) {
|
|
console.warn("Recognition didn't start properly, retrying...");
|
|
try {
|
|
recognition.stop();
|
|
setTimeout(() => {
|
|
try {
|
|
recognition.start();
|
|
} catch(err) {
|
|
console.error('Failed to restart recognition:', err);
|
|
resetStatus();
|
|
}
|
|
}, 300);
|
|
} catch (err) {
|
|
console.error('Failed to stop non-started recognition:', err);
|
|
resetStatus();
|
|
}
|
|
}
|
|
}, 2000);
|
|
} catch (err) {
|
|
console.error('Recognition error:', err);
|
|
statusIndicator.classList.remove('bg-gray-500');
|
|
statusIndicator.classList.add('bg-red-500');
|
|
statusText.textContent = 'Error starting recognition';
|
|
setTimeout(resetStatus, 3000);
|
|
}
|
|
}
|
|
}
|
|
|
|
function stopListening(e) {
|
|
e.preventDefault();
|
|
if (recognition) {
|
|
try {
|
|
recognition.stop();
|
|
} catch (err) {
|
|
console.error('Error stopping recognition:', err);
|
|
}
|
|
}
|
|
}
|
|
|
|
function resetStatus() {
|
|
statusIndicator.classList.remove('bg-green-500', 'bg-red-500', 'bg-yellow-500', 'bg-blue-500');
|
|
statusIndicator.classList.add('bg-gray-500');
|
|
statusText.textContent = 'Ready';
|
|
}
|
|
|
|
|
|
const modeButtons = document.querySelectorAll('.mode-btn');
|
|
let currentMode = 'web';
|
|
|
|
function setActiveMode(selectedButton) {
|
|
|
|
modeButtons.forEach(btn => btn.classList.remove('selected'));
|
|
|
|
selectedButton.classList.add('selected');
|
|
|
|
currentMode = selectedButton.id.replace('mode', '').toLowerCase();
|
|
}
|
|
|
|
|
|
document.getElementById('modeWeb').classList.add('selected');
|
|
|
|
modeButtons.forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
setActiveMode(button);
|
|
});
|
|
});
|
|
|
|
function processVoiceCommand(command) {
|
|
|
|
responseArea.classList.remove('hidden');
|
|
responseText.innerHTML = `<span class="text-gray-400">You said: "${command}"</span>`;
|
|
|
|
|
|
queryInput.value = command;
|
|
|
|
|
|
let modeInput = queryForm.querySelector('input[name="mode"]');
|
|
if (!modeInput) {
|
|
modeInput = document.createElement('input');
|
|
modeInput.type = 'hidden';
|
|
modeInput.name = 'mode';
|
|
queryForm.appendChild(modeInput);
|
|
}
|
|
modeInput.value = currentMode;
|
|
|
|
|
|
statusIndicator.classList.remove('bg-green-500');
|
|
statusIndicator.classList.add('bg-yellow-500');
|
|
statusText.textContent = 'Processing...';
|
|
|
|
|
|
queryForm.submit();
|
|
}
|
|
|
|
function speakResponse(text) {
|
|
if (speechSynthesis) {
|
|
|
|
speechSynthesis.cancel();
|
|
|
|
const utterance = new SpeechSynthesisUtterance(text);
|
|
|
|
utterance.rate = 1.1;
|
|
utterance.pitch = 1.1;
|
|
|
|
|
|
speechSynthesis.speak(utterance);
|
|
|
|
|
|
statusIndicator.classList.remove('bg-yellow-500');
|
|
statusIndicator.classList.add('bg-blue-500');
|
|
statusText.textContent = 'Speaking';
|
|
|
|
utterance.onend = () => {
|
|
resetStatus();
|
|
};
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |