| """Chat UI HTML for Singularity LLM — served at / endpoint.""" |
|
|
| CHAT_UI_HTML = """<!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Singularity LLM</title> |
| <style> |
| * { margin: 0; padding: 0; box-sizing: border-box; } |
| body { background: #0a0a0f; color: #e0e0e0; font-family: 'Segoe UI', system-ui, sans-serif; height: 100vh; display: flex; flex-direction: column; } |
| #header { background: #12121a; padding: 12px 20px; border-bottom: 1px solid #222; display: flex; justify-content: space-between; align-items: center; } |
| #header h1 { font-size: 18px; color: #7c7cff; } |
| #header .stats-btn { background: #1a1a2e; color: #888; border: 1px solid #333; padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 13px; } |
| #header .stats-btn:hover { background: #222238; color: #aaa; } |
| #chat { flex: 1; overflow-y: auto; padding: 20px; max-width: 900px; margin: 0 auto; width: 100%; } |
| .msg { margin-bottom: 16px; max-width: 80%; } |
| .msg.user { margin-left: auto; } |
| .msg .bubble { padding: 12px 16px; border-radius: 12px; line-height: 1.5; white-space: pre-wrap; word-wrap: break-word; } |
| .msg.user .bubble { background: #1a3a5c; color: #cde; } |
| .msg.assistant .bubble { background: #1c1c2e; color: #d0d0e0; } |
| .msg .meta { font-size: 11px; color: #555; margin-top: 4px; } |
| #input-area { background: #12121a; padding: 16px 20px; border-top: 1px solid #222; } |
| #input-form { display: flex; gap: 10px; max-width: 900px; margin: 0 auto; } |
| #msg-input { flex: 1; background: #1a1a2e; color: #e0e0e0; border: 1px solid #333; padding: 12px 16px; border-radius: 8px; font-size: 14px; outline: none; } |
| #msg-input:focus { border-color: #444; } |
| #send-btn { background: #4a4aff; color: white; border: none; padding: 12px 24px; border-radius: 8px; cursor: pointer; font-size: 14px; } |
| #send-btn:hover { background: #5a5aff; } |
| #send-btn:disabled { background: #333; cursor: not-allowed; } |
| #voice-btn { background: #1a1a2e; color: #888; border: 1px solid #333; padding: 12px 16px; border-radius: 8px; cursor: pointer; font-size: 14px; } |
| #voice-btn:hover { background: #222238; } |
| #voice-btn.active { background: #ff4a4a; color: white; border-color: #ff4a4a; } |
| #stats-panel { display: none; position: fixed; top: 0; right: 0; width: 400px; height: 100vh; background: #12121a; border-left: 1px solid #222; padding: 20px; overflow-y: auto; z-index: 100; } |
| #stats-panel h2 { font-size: 16px; color: #7c7cff; margin-bottom: 16px; } |
| #stats-panel pre { font-size: 12px; color: #aaa; white-space: pre-wrap; line-height: 1.4; } |
| #stats-panel .close { position: absolute; top: 16px; right: 16px; cursor: pointer; color: #666; font-size: 20px; } |
| .typing { color: #666; font-style: italic; } |
| </style> |
| </head> |
| <body> |
| <div id="header"> |
| <h1>⚡ Singularity LLM</h1> |
| <button class="stats-btn" onclick="toggleStats()">Stats</button> |
| </div> |
| <div id="chat"></div> |
| <div id="input-area"> |
| <form id="input-form"> |
| <button type="button" id="voice-btn" onclick="toggleVoice()">🎤</button> |
| <input type="text" id="msg-input" placeholder="Type a message..." autocomplete="off" autofocus> |
| <button type="submit" id="send-btn">Send</button> |
| </form> |
| </div> |
| <div id="stats-panel"> |
| <span class="close" onclick="toggleStats()">×</span> |
| <h2>Model Statistics</h2> |
| <pre id="stats-content">Loading...</pre> |
| </div> |
| <script> |
| const chat = document.getElementById('chat'); |
| const form = document.getElementById('input-form'); |
| const input = document.getElementById('msg-input'); |
| const sendBtn = document.getElementById('send-btn'); |
| const voiceBtn = document.getElementById('voice-btn'); |
| |
| function addMsg(role, text, meta) { |
| const div = document.createElement('div'); |
| div.className = 'msg ' + role; |
| div.innerHTML = '<div class="bubble"></div>' + (meta ? '<div class="meta">' + meta + '</div>' : ''); |
| div.querySelector('.bubble').textContent = text; |
| chat.appendChild(div); |
| chat.scrollTop = chat.scrollHeight; |
| return div; |
| } |
| |
| async function sendMessage(text) { |
| if (!text.trim()) return; |
| addMsg('user', text); |
| input.value = ''; |
| sendBtn.disabled = true; |
| |
| const typing = addMsg('assistant', '...'); |
| typing.querySelector('.bubble').className = 'bubble typing'; |
| typing.querySelector('.bubble').textContent = 'Thinking...'; |
| |
| try { |
| const resp = await fetch('/v1/chat', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ message: text, channel: 'web' }), |
| }); |
| const data = await resp.json(); |
| typing.querySelector('.bubble').className = 'bubble'; |
| typing.querySelector('.bubble').textContent = data.response || '(no response)'; |
| if (data.elapsed_s) { |
| typing.querySelector('.meta').textContent = data.elapsed_s + 's'; |
| } |
| } catch (e) { |
| typing.querySelector('.bubble').textContent = 'Error: ' + e.message; |
| } |
| sendBtn.disabled = false; |
| input.focus(); |
| } |
| |
| form.addEventListener('submit', (e) => { |
| e.preventDefault(); |
| sendMessage(input.value); |
| }); |
| |
| // Voice support via Web Speech API |
| let recognition = null; |
| let isListening = false; |
| |
| function toggleVoice() { |
| if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) { |
| alert('Voice input not supported in this browser'); |
| return; |
| } |
| if (isListening) { |
| recognition.stop(); |
| return; |
| } |
| const SR = window.SpeechRecognition || window.webkitSpeechRecognition; |
| recognition = new SR(); |
| recognition.continuous = false; |
| recognition.interimResults = false; |
| recognition.lang = 'en-US'; |
| |
| recognition.onstart = () => { |
| isListening = true; |
| voiceBtn.classList.add('active'); |
| }; |
| recognition.onresult = (e) => { |
| const text = e.results[0][0].transcript; |
| sendMessage(text); |
| }; |
| recognition.onerror = (e) => { |
| console.error('Voice error:', e.error); |
| }; |
| recognition.onend = () => { |
| isListening = false; |
| voiceBtn.classList.remove('active'); |
| }; |
| recognition.start(); |
| } |
| |
| // Stats panel |
| function toggleStats() { |
| const panel = document.getElementById('stats-panel'); |
| if (panel.style.display === 'block') { |
| panel.style.display = 'none'; |
| } else { |
| panel.style.display = 'block'; |
| loadStats(); |
| } |
| } |
| |
| async function loadStats() { |
| try { |
| const resp = await fetch('/v1/stats'); |
| const data = await resp.json(); |
| document.getElementById('stats-content').textContent = JSON.stringify(data, null, 2); |
| } catch (e) { |
| document.getElementById('stats-content').textContent = 'Error: ' + e.message; |
| } |
| } |
| </script> |
| </body> |
| </html>""" |
|
|
| JARVIS_UI_HTML = """<!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Jarvis — Singularity Voice Assistant</title> |
| <style> |
| * { margin: 0; padding: 0; box-sizing: border-box; } |
| body { background: #0a0a0f; color: #e0e0e0; font-family: 'Segoe UI', system-ui, sans-serif; height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; } |
| h1 { color: #7c7cff; font-size: 24px; margin-bottom: 8px; } |
| #status { color: #666; font-size: 14px; margin-bottom: 30px; } |
| #waveform { width: 200px; height: 60px; margin-bottom: 30px; display: flex; align-items: center; justify-content: center; gap: 3px; } |
| .bar { width: 4px; background: #4a4aff; border-radius: 2px; transition: height 0.1s; height: 4px; } |
| #talk-btn { width: 120px; height: 120px; border-radius: 50%; border: 3px solid #333; background: #1a1a2e; color: #666; font-size: 40px; cursor: pointer; transition: all 0.2s; } |
| #talk-btn:hover { border-color: #4a4aff; color: #aaa; } |
| #talk-btn.listening { border-color: #ff4a4a; background: #2a1a1a; color: #ff4a4a; animation: pulse 1s infinite; } |
| #talk-btn.speaking { border-color: #4aff4a; background: #1a2a1a; color: #4aff4a; } |
| @keyframes pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.05); } } |
| #conversation { width: 90%; max-width: 600px; margin-top: 30px; max-height: 200px; overflow-y: auto; } |
| .msg { margin-bottom: 8px; font-size: 14px; } |
| .msg.user { color: #6a9; text-align: right; } |
| .msg.jarvis { color: #7c7cff; } |
| </style> |
| </head> |
| <body> |
| <h1>⚡ Jarvis</h1> |
| <div id="status">Click to talk</div> |
| <div id="waveform"></div> |
| <button id="talk-btn" onclick="toggleTalk()">🎤</button> |
| <div id="conversation"></div> |
| <script> |
| const statusEl = document.getElementById('status'); |
| const talkBtn = document.getElementById('talk-btn'); |
| const convEl = document.getElementById('conversation'); |
| const waveEl = document.getElementById('waveform'); |
| |
| // Create waveform bars |
| for (let i = 0; i < 40; i++) { |
| const bar = document.createElement('div'); |
| bar.className = 'bar'; |
| waveEl.appendChild(bar); |
| } |
| const bars = document.querySelectorAll('.bar'); |
| |
| let recognition = null; |
| let synth = window.speechSynthesis; |
| let isListening = false; |
| let isSpeaking = false; |
| |
| function addMsg(role, text) { |
| const div = document.createElement('div'); |
| div.className = 'msg ' + role; |
| div.textContent = (role === 'user' ? 'You: ' : 'Jarvis: ') + text; |
| convEl.appendChild(div); |
| convEl.scrollTop = convEl.scrollHeight; |
| } |
| |
| function setStatus(text) { statusEl.textContent = text; } |
| |
| function animateWave(active) { |
| bars.forEach((bar, i) => { |
| if (active) { |
| bar.style.height = (Math.random() * 50 + 5) + 'px'; |
| } else { |
| bar.style.height = '4px'; |
| } |
| }); |
| } |
| |
| let waveInterval = null; |
| function startWaveAnim() { |
| if (waveInterval) return; |
| waveInterval = setInterval(() => animateWave(true), 80); |
| } |
| function stopWaveAnim() { |
| if (waveInterval) { clearInterval(waveInterval); waveInterval = null; } |
| animateWave(false); |
| } |
| |
| function speak(text) { |
| if (!synth) return; |
| synth.cancel(); |
| const utter = new SpeechSynthesisUtterance(text); |
| utter.rate = 1.1; |
| utter.onstart = () => { isSpeaking = true; talkBtn.classList.add('speaking'); setStatus('Speaking...'); }; |
| utter.onend = () => { isSpeaking = false; talkBtn.classList.remove('speaking'); setStatus('Click to talk'); }; |
| synth.speak(utter); |
| } |
| |
| async function sendToJarvis(text) { |
| setStatus('Thinking...'); |
| try { |
| const resp = await fetch('/v1/voice/stream', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ message: text }), |
| }); |
| const reader = resp.body.getReader(); |
| const decoder = new TextDecoder(); |
| let fullText = ''; |
| while (true) { |
| const { done, value } = await reader.read(); |
| if (done) break; |
| const chunk = decoder.decode(value); |
| const lines = chunk.split('\\n'); |
| for (const line of lines) { |
| if (line.startsWith('data: ')) { |
| const data = line.slice(6); |
| if (data === '[DONE]') continue; |
| fullText += data; |
| if (data.trim().endsWith('.') || data.trim().endsWith('!') || data.trim().endsWith('?')) { |
| speak(data.trim()); |
| } |
| } |
| } |
| } |
| addMsg('jarvis', fullText); |
| setStatus('Click to talk'); |
| } catch (e) { |
| setStatus('Error: ' + e.message); |
| } |
| } |
| |
| function toggleTalk() { |
| if (isSpeaking) { synth.cancel(); return; } |
| if (isListening) { recognition.stop(); return; } |
| |
| if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) { |
| alert('Voice not supported. Use Chrome/Edge.'); |
| return; |
| } |
| |
| const SR = window.SpeechRecognition || window.webkitSpeechRecognition; |
| recognition = new SR(); |
| recognition.continuous = false; |
| recognition.interimResults = false; |
| recognition.lang = 'en-US'; |
| |
| recognition.onstart = () => { |
| isListening = true; |
| talkBtn.classList.add('listening'); |
| setStatus('Listening...'); |
| startWaveAnim(); |
| }; |
| recognition.onresult = (e) => { |
| const text = e.results[0][0].transcript; |
| addMsg('user', text); |
| sendToJarvis(text); |
| }; |
| recognition.onerror = (e) => { setStatus('Error: ' + e.error); }; |
| recognition.onend = () => { |
| isListening = false; |
| talkBtn.classList.remove('listening'); |
| stopWaveAnim(); |
| if (!isSpeaking) setStatus('Click to talk'); |
| }; |
| recognition.start(); |
| } |
| </script> |
| </body> |
| </html>""" |
|
|