Ana / templates /index.html
OrbitMC's picture
Update templates/index.html
a4d107b verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>J.A.R.V.I.S. AI</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #0a0a1a; color: #e0e0e0;
height: 100vh; display: flex; flex-direction: column; overflow: hidden;
}
.header {
background: linear-gradient(135deg, #0d1b2a, #1b2838);
border-bottom: 1px solid #00d4ff33;
padding: 12px 20px; display: flex; align-items: center;
justify-content: space-between; flex-shrink: 0;
}
.header-left { display: flex; align-items: center; gap: 12px; }
.arc-reactor {
width: 38px; height: 38px; border-radius: 50%;
background: radial-gradient(circle, #00d4ff 0%, #0088aa 40%, #004466 70%, transparent 100%);
box-shadow: 0 0 20px #00d4ff88, 0 0 40px #00d4ff44, inset 0 0 10px #00d4ff66;
animation: pulse 2s ease-in-out infinite; position: relative;
}
.arc-reactor::after {
content: ''; position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%); width: 12px; height: 12px;
border-radius: 50%; background: #00d4ff; box-shadow: 0 0 8px #00d4ff;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 20px #00d4ff88, 0 0 40px #00d4ff44; }
50% { box-shadow: 0 0 30px #00d4ffaa, 0 0 60px #00d4ff66; }
}
.header-title h1 { font-size: 1.2rem; color: #00d4ff; letter-spacing: 3px; text-transform: uppercase; }
.header-title p { font-size: 0.65rem; color: #5a8a9a; letter-spacing: 1px; }
.header-controls { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.ctrl-btn {
background: #0d1b2a; border: 1px solid #00d4ff44; color: #00d4ff;
padding: 5px 12px; border-radius: 6px; cursor: pointer;
font-size: 0.7rem; transition: all 0.3s; letter-spacing: 0.5px;
}
.ctrl-btn:hover { background: #00d4ff22; border-color: #00d4ff88; }
.ctrl-btn.active { background: #00d4ff22; border-color: #00d4ff; box-shadow: 0 0 8px #00d4ff44; }
.status-dot {
width: 8px; height: 8px; border-radius: 50%;
background: #00ff88; box-shadow: 0 0 6px #00ff88;
}
.status-dot.error { background: #ff4444; box-shadow: 0 0 6px #ff4444; }
/* Config panel */
.config-bar {
background: #0d1117; border-bottom: 1px solid #00d4ff15;
padding: 8px 20px; display: none; flex-wrap: wrap; gap: 12px;
align-items: center; flex-shrink: 0;
}
.config-bar.open { display: flex; }
.config-group { display: flex; align-items: center; gap: 6px; }
.config-group label { font-size: 0.65rem; color: #5a8a9a; text-transform: uppercase; letter-spacing: 1px; }
.config-group select {
background: #0f1923; border: 1px solid #00d4ff33; color: #00d4ff;
padding: 4px 8px; border-radius: 4px; font-size: 0.7rem; cursor: pointer;
}
.config-group select:focus { border-color: #00d4ff; outline: none; }
.config-tag {
font-size: 0.6rem; padding: 3px 8px; border-radius: 10px;
background: #00d4ff15; border: 1px solid #00d4ff33; color: #00d4ffaa;
}
/* Chat */
.chat-container {
flex: 1; overflow-y: auto; padding: 16px 20px;
display: flex; flex-direction: column; gap: 14px; scroll-behavior: smooth;
}
.chat-container::-webkit-scrollbar { width: 3px; }
.chat-container::-webkit-scrollbar-thumb { background: #00d4ff33; border-radius: 2px; }
.message {
max-width: 80%; padding: 12px 16px; border-radius: 14px;
font-size: 0.9rem; line-height: 1.6; animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.message.user {
align-self: flex-end; background: linear-gradient(135deg, #1a3a5c, #0d2847);
border: 1px solid #00d4ff33; color: #c8e6ff; border-bottom-right-radius: 4px;
}
.message.assistant {
align-self: flex-start; background: linear-gradient(135deg, #141e30, #0f1923);
border: 1px solid #00d4ff22; color: #e0e0e0; border-bottom-left-radius: 4px;
}
.message .label { font-size: 0.58rem; color: #00d4ff88; letter-spacing: 2px; margin-bottom: 5px; text-transform: uppercase; }
.message .text-content { white-space: pre-wrap; word-wrap: break-word; }
.message .audio-controls { margin-top: 8px; display: flex; align-items: center; gap: 8px; }
.audio-btn {
display: inline-flex; align-items: center; gap: 4px;
background: #00d4ff15; border: 1px solid #00d4ff33; color: #00d4ff;
padding: 3px 10px; border-radius: 10px; cursor: pointer;
font-size: 0.65rem; transition: all 0.2s;
}
.audio-btn:hover { background: #00d4ff25; border-color: #00d4ff66; }
.audio-btn:disabled { opacity: 0.3; cursor: wait; }
.audio-status { font-size: 0.58rem; color: #5a8a9a; }
.typing-indicator { align-self: flex-start; display: flex; gap: 5px; padding: 14px 18px; }
.typing-indicator span {
width: 7px; height: 7px; border-radius: 50%; background: #00d4ff; animation: typing 1.4s infinite;
}
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing {
0%, 60%, 100% { opacity: 0.2; transform: scale(0.8); }
30% { opacity: 1; transform: scale(1.1); }
}
.welcome {
display: flex; flex-direction: column; align-items: center;
justify-content: center; flex: 1; gap: 10px; opacity: 0.5;
}
.welcome .big-reactor {
width: 70px; height: 70px; border-radius: 50%;
background: radial-gradient(circle, #00d4ff 0%, #0088aa 35%, #004466 65%, transparent 100%);
box-shadow: 0 0 40px #00d4ff66; animation: pulse 2s ease-in-out infinite;
}
.welcome h2 { color: #00d4ff; font-size: 1rem; letter-spacing: 4px; }
.welcome p { color: #5a8a9a; font-size: 0.75rem; }
.welcome .model-info { font-size: 0.65rem; color: #3a5a6a; margin-top: 4px; }
.input-container {
padding: 14px 20px; background: linear-gradient(0deg, #0d1b2a, #0a0a1a);
border-top: 1px solid #00d4ff22; flex-shrink: 0;
}
.input-wrapper { display: flex; gap: 8px; max-width: 900px; margin: 0 auto; }
#messageInput {
flex: 1; background: #0f1923; border: 1px solid #00d4ff33; border-radius: 12px;
padding: 11px 16px; color: #e0e0e0; font-size: 0.9rem; outline: none;
transition: border-color 0.3s; font-family: inherit;
}
#messageInput:focus { border-color: #00d4ff88; box-shadow: 0 0 12px #00d4ff22; }
#messageInput::placeholder { color: #3a5a6a; }
#sendBtn {
background: linear-gradient(135deg, #00d4ff, #0088cc); border: none; border-radius: 12px;
padding: 11px 22px; color: #0a0a1a; font-weight: 700; cursor: pointer;
font-size: 0.8rem; letter-spacing: 1px; transition: all 0.3s; text-transform: uppercase;
}
#sendBtn:hover { box-shadow: 0 0 18px #00d4ff66; transform: translateY(-1px); }
#sendBtn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
.input-footer {
display: flex; justify-content: space-between; margin-top: 5px;
max-width: 900px; margin-left: auto; margin-right: auto;
}
.input-footer span { font-size: 0.6rem; color: #3a5a6a; }
@media (max-width: 640px) {
.header { padding: 10px 12px; }
.header-title h1 { font-size: 1rem; }
.message { max-width: 92%; font-size: 0.82rem; }
.chat-container { padding: 10px; }
.input-container { padding: 10px; }
.config-bar { padding: 6px 12px; }
}
</style>
</head>
<body>
<div class="header">
<div class="header-left">
<div class="arc-reactor"></div>
<div class="header-title">
<h1>J.A.R.V.I.S.</h1>
<p>Just A Rather Very Intelligent System</p>
</div>
</div>
<div class="header-controls">
<div class="status-dot" id="statusDot"></div>
<button class="ctrl-btn" id="configToggle" onclick="toggleConfig()">⚙ CONFIG</button>
<button class="ctrl-btn active" id="ttsToggle" onclick="toggleTTS()">🔊 VOICE</button>
<button class="ctrl-btn" onclick="clearChat()">🗑 CLEAR</button>
</div>
</div>
<!-- Config Panel -->
<div class="config-bar" id="configPanel">
<div class="config-group">
<label>LLM:</label>
<span class="config-tag" id="llmTag">loading...</span>
</div>
<div class="config-group">
<label>TTS:</label>
<span class="config-tag" id="ttsTag">loading...</span>
</div>
<div class="config-group">
<label>Voice:</label>
<select id="voiceSelect">
<option value="Kiki">Kiki</option>
<option value="Bella">Bella</option>
<option value="Jasper">Jasper</option>
<option value="Luna">Luna</option>
<option value="Bruno">Bruno</option>
<option value="Rosie">Rosie</option>
<option value="Hugo">Hugo</option>
<option value="Leo">Leo</option>
</select>
</div>
<div class="config-group">
<label>Options (set via env vars):</label>
<span class="config-tag">LLM_MODE: gemma-3-270m-it | minilm-semantic</span>
<span class="config-tag">TTS_MODE: nano-fp32 | nano-int8 | micro | mini</span>
</div>
</div>
<div class="chat-container" id="chatContainer">
<div class="welcome" id="welcome">
<div class="big-reactor"></div>
<h2>SYSTEMS ONLINE</h2>
<p>Type a message below to begin interaction</p>
<div class="model-info" id="welcomeInfo">Initializing...</div>
</div>
</div>
<div class="input-container">
<div class="input-wrapper">
<input type="text" id="messageInput" placeholder="Talk to J.A.R.V.I.S..." autocomplete="off" />
<button id="sendBtn" onclick="sendMessage()">SEND</button>
</div>
<div class="input-footer">
<span id="memoryCount">Memory: 0 turns</span>
<span id="modelInfo">Loading...</span>
</div>
</div>
<script>
let sessionId = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(36) + Math.random().toString(36).slice(2);
let ttsEnabled = true;
let isProcessing = false;
let msgCounter = 0;
let currentVoice = 'Kiki';
const chatEl = document.getElementById('chatContainer');
const inputEl = document.getElementById('messageInput');
const sendBtn = document.getElementById('sendBtn');
const welcome = document.getElementById('welcome');
inputEl.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
});
document.getElementById('voiceSelect').addEventListener('change', function() {
currentVoice = this.value;
});
function toggleTTS() {
ttsEnabled = !ttsEnabled;
const btn = document.getElementById('ttsToggle');
btn.classList.toggle('active', ttsEnabled);
btn.textContent = ttsEnabled ? '🔊 VOICE' : '🔇 MUTE';
}
function toggleConfig() {
const panel = document.getElementById('configPanel');
const btn = document.getElementById('configToggle');
panel.classList.toggle('open');
btn.classList.toggle('active');
}
async function sendMessage() {
const text = inputEl.value.trim();
if (!text || isProcessing) return;
if (welcome) welcome.style.display = 'none';
addUserMessage(text);
inputEl.value = '';
isProcessing = true;
sendBtn.disabled = true;
const typingEl = showTyping();
const msgId = ++msgCounter;
try {
// Phase 1: Text response
const res = await fetch('/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: text, session_id: sessionId })
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
typingEl.remove();
const msgEl = addAssistantMessage(data.response, msgId);
document.getElementById('memoryCount').textContent = `Memory: ${data.memory_length} turns`;
// Phase 2: TTS async
if (ttsEnabled && data.tts_available) {
fetchAndPlayAudio(data.response, msgId, msgEl);
}
} catch (err) {
typingEl.remove();
addAssistantMessage('System malfunction. Please try again.', msgId);
console.error(err);
}
isProcessing = false;
sendBtn.disabled = false;
inputEl.focus();
}
async function fetchAndPlayAudio(text, msgId, msgEl) {
const statusEl = msgEl.querySelector('.audio-status');
const playBtn = msgEl.querySelector('.audio-btn');
if (statusEl) statusEl.textContent = '⏳ Generating voice...';
if (playBtn) playBtn.disabled = true;
try {
const res = await fetch('/tts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: text, voice: currentVoice })
});
const data = await res.json();
if (data.audio) {
if (playBtn) { playBtn.dataset.audio = data.audio; playBtn.disabled = false; playBtn.textContent = '▶ Play'; }
if (statusEl) statusEl.textContent = '✅ Ready';
playAudioBase64(data.audio);
} else {
if (statusEl) statusEl.textContent = '⚠️ Voice unavailable';
if (playBtn) playBtn.style.display = 'none';
}
} catch (e) {
if (statusEl) statusEl.textContent = '⚠️ Voice error';
if (playBtn) playBtn.style.display = 'none';
}
}
function addUserMessage(text) {
const div = document.createElement('div');
div.className = 'message user';
div.innerHTML = `<div class="text-content">${esc(text)}</div>`;
chatEl.appendChild(div);
scroll();
}
function addAssistantMessage(text, msgId) {
const div = document.createElement('div');
div.className = 'message assistant';
div.id = `msg-${msgId}`;
div.innerHTML = `
<div class="label">⟐ JARVIS</div>
<div class="text-content">${esc(text)}</div>
${ttsEnabled ? `
<div class="audio-controls">
<button class="audio-btn" disabled onclick="replayAudio(this)">⏳</button>
<span class="audio-status">Requesting voice...</span>
</div>` : ''}`;
chatEl.appendChild(div);
scroll();
return div;
}
function showTyping() {
const div = document.createElement('div');
div.className = 'typing-indicator';
div.innerHTML = '<span></span><span></span><span></span>';
chatEl.appendChild(div);
scroll();
return div;
}
function playAudioBase64(b64) {
try {
const bin = atob(b64);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
const blob = new Blob([bytes], { type: 'audio/wav' });
const url = URL.createObjectURL(blob);
const audio = new Audio(url);
audio.play().catch(e => console.log('Autoplay blocked:', e));
audio.onended = () => URL.revokeObjectURL(url);
} catch (e) { console.error(e); }
}
function replayAudio(btn) { if (btn.dataset.audio) playAudioBase64(btn.dataset.audio); }
async function clearChat() {
await fetch('/clear', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId })
});
chatEl.innerHTML = `
<div class="welcome" id="welcome">
<div class="big-reactor"></div>
<h2>SYSTEMS ONLINE</h2>
<p>Type a message below to begin</p>
</div>`;
document.getElementById('memoryCount').textContent = 'Memory: 0 turns';
sessionId = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(36) + Math.random().toString(36).slice(2);
}
function esc(t) { const d = document.createElement('div'); d.textContent = t; return d.innerHTML; }
function scroll() { chatEl.scrollTop = chatEl.scrollHeight; }
// Health check & populate config
fetch('/health').then(r => r.json()).then(d => {
document.getElementById('llmTag').textContent = d.llm_mode;
document.getElementById('ttsTag').textContent = d.tts_mode + (d.tts_model === 'DISABLED' ? ' (OFF)' : '');
document.getElementById('modelInfo').textContent = `${d.llm_mode} · ${d.tts_mode} · ${d.tts_voice} · CPU`;
const wi = document.getElementById('welcomeInfo');
if (wi) wi.textContent = `LLM: ${d.llm_mode} | TTS: ${d.tts_mode} | Voice: ${d.tts_voice}`;
if (d.tts_model === 'DISABLED') document.getElementById('statusDot').classList.add('error');
// Set voice dropdown
if (d.tts_voice) { document.getElementById('voiceSelect').value = d.tts_voice; currentVoice = d.tts_voice; }
}).catch(() => {});
inputEl.focus();
</script>
</body>
</html>