Download splitbit_llm/server/ui.py from hermescures1/splitbit-llm: direct link, hf CLI and curl.
- Browser
- Download file 12 kB
-
https://huggingface.co/hermescures1/splitbit-llm/resolve/main/splitbit_llm/server/ui.py
- Command line
-
hf download hf://hermescures1/splitbit-llm/splitbit_llm/server/ui.py
-
curl -L -o ui.py https://huggingface.co/hermescures1/splitbit-llm/resolve/main/splitbit_llm/server/ui.py
12 kB
| """Chat UI HTML for SplitBit 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>SplitBit 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>⚡ SplitBit 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 — SplitBit 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>""" | |