Download index.html from Unmid/Ana: direct link, hf CLI and curl.
- Browser
- Download file 15.7 kB
-
https://huggingface.co/spaces/Unmid/Ana/resolve/main/index.html
- Command line
-
hf download hf://spaces/Unmid/Ana/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Unmid/Ana/resolve/main/index.html
15.7 kB
| <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</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"/> | |
| <link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@400;700;900&display=swap" rel="stylesheet"/> | |
| <style> | |
| :root { | |
| --bg: #020b14; | |
| --panel: #040f1c; | |
| --border: #0a3a5c; | |
| --glow: #00aaff; | |
| --glow2: #00ffcc; | |
| --text: #c8e8ff; | |
| --dim: #3a6080; | |
| --user-bg: #001a2e; | |
| --ai-bg: #001228; | |
| --danger: #ff3860; | |
| --font-hud: 'Orbitron', monospace; | |
| --font-mono: 'Share Tech Mono', monospace; | |
| } | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| html, body { | |
| height: 100%; | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: var(--font-mono); | |
| overflow: hidden; | |
| } | |
| /* ββ animated grid background ββ */ | |
| body::before { | |
| content: ''; | |
| position: fixed; inset: 0; | |
| background-image: | |
| linear-gradient(rgba(0,170,255,.04) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(0,170,255,.04) 1px, transparent 1px); | |
| background-size: 40px 40px; | |
| animation: gridScroll 20s linear infinite; | |
| pointer-events: none; | |
| z-index: 0; | |
| } | |
| @keyframes gridScroll { | |
| from { background-position: 0 0; } | |
| to { background-position: 40px 40px; } | |
| } | |
| /* ββ scanlines ββ */ | |
| body::after { | |
| content: ''; | |
| position: fixed; inset: 0; | |
| background: repeating-linear-gradient( | |
| 0deg, | |
| transparent, | |
| transparent 2px, | |
| rgba(0,0,0,.15) 2px, | |
| rgba(0,0,0,.15) 4px | |
| ); | |
| pointer-events: none; | |
| z-index: 0; | |
| } | |
| /* ββ layout ββ */ | |
| #shell { | |
| position: relative; z-index: 1; | |
| display: flex; flex-direction: column; | |
| height: 100vh; | |
| max-width: 860px; | |
| margin: 0 auto; | |
| padding: 16px 16px 0; | |
| } | |
| /* ββ header ββ */ | |
| header { | |
| display: flex; align-items: center; justify-content: space-between; | |
| padding: 10px 18px; | |
| border: 1px solid var(--border); | |
| background: var(--panel); | |
| margin-bottom: 10px; | |
| clip-path: polygon(0 0, calc(100% - 18px) 0, 100% 18px, 100% 100%, 0 100%); | |
| } | |
| .logo { | |
| font-family: var(--font-hud); | |
| font-weight: 900; | |
| font-size: 1.3rem; | |
| letter-spacing: .15em; | |
| color: var(--glow); | |
| text-shadow: 0 0 12px var(--glow), 0 0 30px rgba(0,170,255,.3); | |
| } | |
| .logo span { color: var(--glow2); text-shadow: 0 0 12px var(--glow2); } | |
| .status-row { display: flex; gap: 12px; align-items: center; } | |
| .status-dot { | |
| width: 8px; height: 8px; border-radius: 50%; | |
| background: var(--dim); | |
| transition: background .4s, box-shadow .4s; | |
| } | |
| .status-dot.online { | |
| background: var(--glow2); | |
| box-shadow: 0 0 8px var(--glow2); | |
| animation: pulse 2s ease-in-out infinite; | |
| } | |
| @keyframes pulse { | |
| 0%,100% { opacity: 1; } | |
| 50% { opacity: .4; } | |
| } | |
| .status-label { | |
| font-size: .65rem; | |
| letter-spacing: .1em; | |
| color: var(--dim); | |
| font-family: var(--font-hud); | |
| } | |
| .status-dot.online + .status-label { color: var(--glow2); } | |
| .btn-icon { | |
| background: transparent; | |
| border: 1px solid var(--border); | |
| color: var(--dim); | |
| padding: 5px 10px; | |
| font-family: var(--font-hud); | |
| font-size: .6rem; | |
| letter-spacing: .1em; | |
| cursor: pointer; | |
| transition: all .2s; | |
| } | |
| .btn-icon:hover { | |
| border-color: var(--glow); | |
| color: var(--glow); | |
| box-shadow: 0 0 8px rgba(0,170,255,.3); | |
| } | |
| .btn-icon.active { | |
| border-color: var(--glow2); | |
| color: var(--glow2); | |
| box-shadow: 0 0 8px rgba(0,255,204,.3); | |
| } | |
| /* ββ boot screen ββ */ | |
| #boot { | |
| flex: 1; | |
| display: flex; flex-direction: column; justify-content: center; align-items: center; | |
| gap: 8px; | |
| } | |
| .boot-line { | |
| font-size: .78rem; | |
| color: var(--glow); | |
| opacity: 0; | |
| animation: fadeIn .3s forwards; | |
| } | |
| .boot-line.dim { color: var(--dim); } | |
| .boot-bar { | |
| width: 300px; height: 2px; | |
| background: var(--border); | |
| margin-top: 16px; | |
| overflow: hidden; | |
| } | |
| .boot-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--glow), var(--glow2)); | |
| width: 0%; | |
| transition: width .4s ease; | |
| box-shadow: 0 0 8px var(--glow); | |
| } | |
| @keyframes fadeIn { to { opacity: 1; } } | |
| /* ββ messages ββ */ | |
| #messages { | |
| flex: 1; | |
| overflow-y: auto; | |
| display: none; | |
| flex-direction: column; | |
| gap: 10px; | |
| padding: 4px 2px 12px; | |
| scrollbar-width: thin; | |
| scrollbar-color: var(--border) transparent; | |
| } | |
| #messages.visible { display: flex; } | |
| .msg { | |
| display: flex; | |
| gap: 10px; | |
| animation: msgIn .25s ease; | |
| } | |
| @keyframes msgIn { | |
| from { opacity: 0; transform: translateY(6px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .msg.user { justify-content: flex-end; } | |
| .avatar { | |
| width: 30px; height: 30px; flex-shrink: 0; | |
| border: 1px solid var(--border); | |
| display: flex; align-items: center; justify-content: center; | |
| font-family: var(--font-hud); | |
| font-size: .55rem; | |
| color: var(--glow); | |
| background: var(--panel); | |
| clip-path: polygon(0 0, 80% 0, 100% 20%, 100% 100%, 0 100%); | |
| } | |
| .bubble { | |
| max-width: 75%; | |
| padding: 10px 14px; | |
| font-size: .82rem; | |
| line-height: 1.55; | |
| border: 1px solid var(--border); | |
| background: var(--ai-bg); | |
| clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 0 100%); | |
| } | |
| .msg.user .bubble { | |
| background: var(--user-bg); | |
| border-color: #0a2a44; | |
| clip-path: polygon(10px 0, 100% 0, 100% 100%, 0 100%, 0 10px); | |
| color: #90c8f0; | |
| } | |
| .msg.ai .bubble { | |
| border-color: var(--border); | |
| color: var(--text); | |
| } | |
| .typing-dots span { | |
| display: inline-block; | |
| width: 5px; height: 5px; border-radius: 50%; | |
| background: var(--glow); | |
| margin: 0 2px; | |
| animation: blink 1.2s ease-in-out infinite; | |
| } | |
| .typing-dots span:nth-child(2) { animation-delay: .2s; } | |
| .typing-dots span:nth-child(3) { animation-delay: .4s; } | |
| @keyframes blink { 0%,80%,100% { opacity: .2; } 40% { opacity: 1; } } | |
| /* ββ input bar ββ */ | |
| #input-bar { | |
| display: none; | |
| gap: 8px; | |
| padding: 10px 0 14px; | |
| align-items: flex-end; | |
| } | |
| #input-bar.visible { display: flex; } | |
| #user-input { | |
| flex: 1; | |
| background: var(--panel); | |
| border: 1px solid var(--border); | |
| color: var(--text); | |
| font-family: var(--font-mono); | |
| font-size: .82rem; | |
| padding: 10px 14px; | |
| resize: none; | |
| outline: none; | |
| min-height: 42px; max-height: 140px; | |
| transition: border-color .2s, box-shadow .2s; | |
| clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 0 100%); | |
| } | |
| #user-input:focus { | |
| border-color: var(--glow); | |
| box-shadow: 0 0 12px rgba(0,170,255,.2); | |
| } | |
| #user-input::placeholder { color: var(--dim); } | |
| #send-btn { | |
| background: linear-gradient(135deg, #003a5c, #001a2e); | |
| border: 1px solid var(--glow); | |
| color: var(--glow); | |
| font-family: var(--font-hud); | |
| font-size: .65rem; | |
| letter-spacing: .12em; | |
| padding: 10px 18px; | |
| cursor: pointer; | |
| height: 42px; | |
| transition: all .2s; | |
| clip-path: polygon(0 0, 80% 0, 100% 20%, 100% 100%, 0 100%); | |
| } | |
| #send-btn:hover:not(:disabled) { | |
| background: linear-gradient(135deg, #004a7a, #002244); | |
| box-shadow: 0 0 16px rgba(0,170,255,.4); | |
| } | |
| #send-btn:disabled { opacity: .4; cursor: not-allowed; } | |
| /* ββ corner decoration ββ */ | |
| .corner-deco { | |
| position: fixed; | |
| width: 60px; height: 60px; | |
| opacity: .15; | |
| pointer-events: none; | |
| } | |
| .corner-deco.tl { top: 0; left: 0; | |
| border-top: 2px solid var(--glow); border-left: 2px solid var(--glow); } | |
| .corner-deco.br { bottom: 0; right: 0; | |
| border-bottom: 2px solid var(--glow2); border-right: 2px solid var(--glow2); } | |
| /* scrollbar */ | |
| #messages::-webkit-scrollbar { width: 4px; } | |
| #messages::-webkit-scrollbar-track { background: transparent; } | |
| #messages::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="corner-deco tl"></div> | |
| <div class="corner-deco br"></div> | |
| <div id="shell"> | |
| <!-- header --> | |
| <header> | |
| <div class="logo">J.<span>A</span>.R.V.I.S</div> | |
| <div class="status-row"> | |
| <div class="status-dot" id="status-dot"></div> | |
| <span class="status-label" id="status-label">INITIALIZING</span> | |
| <button class="btn-icon" id="tts-btn" title="Toggle voice">VOICE</button> | |
| <button class="btn-icon" id="clear-btn" title="Clear chat">CLEAR</button> | |
| <button class="btn-icon" id="save-btn" title="Save chat">SAVE</button> | |
| </div> | |
| </header> | |
| <!-- boot screen --> | |
| <div id="boot"></div> | |
| <!-- messages --> | |
| <div id="messages"></div> | |
| <!-- input --> | |
| <div id="input-bar"> | |
| <textarea id="user-input" rows="1" placeholder="Enter query..."></textarea> | |
| <button id="send-btn" disabled>SEND</button> | |
| </div> | |
| </div> | |
| <script> | |
| const $ = id => document.getElementById(id); | |
| // ββ State ββ | |
| let history = []; // [[user, assistant], ...] | |
| let ttsOn = false; | |
| let busy = false; | |
| let modelReady = false; | |
| const statusDot = $('status-dot'); | |
| const statusLabel = $('status-label'); | |
| const messagesEl = $('messages'); | |
| const inputBar = $('input-bar'); | |
| const bootEl = $('boot'); | |
| const sendBtn = $('send-btn'); | |
| const inputEl = $('user-input'); | |
| const ttsBtn = $('tts-btn'); | |
| // ββ Boot sequence ββ | |
| const bootLines = [ | |
| { text: '// INITIALIZING NEURAL CORE', dim: false }, | |
| { text: '// LOADING LANGUAGE MODEL', dim: false }, | |
| { text: '// ESTABLISHING VECTOR INDEX', dim: false }, | |
| { text: '// VOICE SYNTHESIZER STANDBY', dim: true }, | |
| { text: '// AWAITING SERVER HANDSHAKE', dim: false }, | |
| ]; | |
| async function runBoot() { | |
| const bar = document.createElement('div'); | |
| bar.className = 'boot-bar'; | |
| const fill = document.createElement('div'); | |
| fill.className = 'boot-fill'; | |
| bar.appendChild(fill); | |
| for (let i = 0; i < bootLines.length; i++) { | |
| const l = bootLines[i]; | |
| const el = document.createElement('div'); | |
| el.className = 'boot-line' + (l.dim ? ' dim' : ''); | |
| el.style.animationDelay = (i * 0.12) + 's'; | |
| el.textContent = l.text; | |
| bootEl.appendChild(el); | |
| await sleep(130); | |
| } | |
| bootEl.appendChild(bar); | |
| await sleep(100); | |
| // Poll /health until server is ready | |
| while (true) { | |
| try { | |
| const r = await fetch('/health'); | |
| if (r.ok) { | |
| const d = await r.json(); | |
| if (d.llm) break; | |
| } | |
| } catch (_) {} | |
| fill.style.width = (Math.min(parseInt(fill.style.width || '0') + 8, 85)) + '%'; | |
| await sleep(600); | |
| } | |
| fill.style.width = '100%'; | |
| await sleep(400); | |
| // Transition to chat UI | |
| bootEl.style.display = 'none'; | |
| messagesEl.classList.add('visible'); | |
| inputBar.classList.add('visible'); | |
| statusDot.classList.add('online'); | |
| statusLabel.textContent = 'ONLINE'; | |
| sendBtn.disabled = false; | |
| modelReady = true; | |
| inputEl.focus(); | |
| } | |
| // ββ Helpers ββ | |
| const sleep = ms => new Promise(r => setTimeout(r, ms)); | |
| function scrollBottom() { | |
| messagesEl.scrollTop = messagesEl.scrollHeight; | |
| } | |
| function addMsg(role, text) { | |
| const wrapper = document.createElement('div'); | |
| wrapper.className = 'msg ' + role; | |
| if (role === 'ai') { | |
| const av = document.createElement('div'); | |
| av.className = 'avatar'; | |
| av.textContent = 'AI'; | |
| wrapper.appendChild(av); | |
| } | |
| const bubble = document.createElement('div'); | |
| bubble.className = 'bubble'; | |
| bubble.textContent = text; | |
| wrapper.appendChild(bubble); | |
| if (role === 'user') { | |
| const av = document.createElement('div'); | |
| av.className = 'avatar'; | |
| av.textContent = 'YOU'; | |
| wrapper.appendChild(av); | |
| } | |
| messagesEl.appendChild(wrapper); | |
| scrollBottom(); | |
| return bubble; | |
| } | |
| function addTyping() { | |
| const wrapper = document.createElement('div'); | |
| wrapper.className = 'msg ai'; | |
| wrapper.id = 'typing'; | |
| const av = document.createElement('div'); | |
| av.className = 'avatar'; | |
| av.textContent = 'AI'; | |
| wrapper.appendChild(av); | |
| const bubble = document.createElement('div'); | |
| bubble.className = 'bubble'; | |
| bubble.innerHTML = '<div class="typing-dots"><span></span><span></span><span></span></div>'; | |
| wrapper.appendChild(bubble); | |
| messagesEl.appendChild(wrapper); | |
| scrollBottom(); | |
| return wrapper; | |
| } | |
| function removeTyping() { | |
| const t = $('typing'); | |
| if (t) t.remove(); | |
| } | |
| // ββ Send message (streaming) ββ | |
| async function send() { | |
| const text = inputEl.value.trim(); | |
| if (!text || busy || !modelReady) return; | |
| busy = true; | |
| sendBtn.disabled = true; | |
| inputEl.value = ''; | |
| inputEl.style.height = 'auto'; | |
| addMsg('user', text); | |
| const typingEl = addTyping(); | |
| let reply = ''; | |
| try { | |
| const res = await fetch('/chat/stream', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ message: text, history }) | |
| }); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| const reader = res.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let aiBubble = null; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| const raw = decoder.decode(value, { stream: true }); | |
| for (const line of raw.split('\n')) { | |
| if (!line.startsWith('data:')) continue; | |
| const payload = line.slice(5).trim(); | |
| if (payload === '[DONE]') break; | |
| try { | |
| const piece = JSON.parse(payload); | |
| if (!aiBubble) { | |
| removeTyping(); | |
| aiBubble = addMsg('ai', ''); | |
| } | |
| reply += piece; | |
| aiBubble.textContent = reply; | |
| scrollBottom(); | |
| } catch (_) {} | |
| } | |
| } | |
| } catch (err) { | |
| removeTyping(); | |
| addMsg('ai', '[Error: ' + err.message + ']'); | |
| busy = false; | |
| sendBtn.disabled = false; | |
| return; | |
| } | |
| removeTyping(); | |
| if (!reply) addMsg('ai', '[No response]'); | |
| reply = reply.trim(); | |
| if (reply) { | |
| history.push([text, reply]); | |
| if (history.length > 20) history.shift(); | |
| if (ttsOn) speakText(reply); | |
| } | |
| busy = false; | |
| sendBtn.disabled = false; | |
| inputEl.focus(); | |
| } | |
| // ββ TTS ββ | |
| async function speakText(text) { | |
| try { | |
| const res = await fetch('/tts', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ text }) | |
| }); | |
| if (!res.ok) return; | |
| const blob = await res.blob(); | |
| const url = URL.createObjectURL(blob); | |
| const audio = new Audio(url); | |
| audio.play(); | |
| } catch (_) {} | |
| } | |
| // ββ Controls ββ | |
| sendBtn.addEventListener('click', send); | |
| inputEl.addEventListener('keydown', e => { | |
| if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } | |
| }); | |
| inputEl.addEventListener('input', () => { | |
| inputEl.style.height = 'auto'; | |
| inputEl.style.height = Math.min(inputEl.scrollHeight, 140) + 'px'; | |
| }); | |
| ttsBtn.addEventListener('click', () => { | |
| ttsOn = !ttsOn; | |
| ttsBtn.classList.toggle('active', ttsOn); | |
| ttsBtn.textContent = ttsOn ? 'VOICE ON' : 'VOICE'; | |
| }); | |
| $('clear-btn').addEventListener('click', () => { | |
| history = []; | |
| messagesEl.innerHTML = ''; | |
| addMsg('ai', 'Memory cleared. How can I assist you?'); | |
| }); | |
| $('save-btn').addEventListener('click', async () => { | |
| try { | |
| const res = await fetch('/save', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ history }) | |
| }); | |
| const d = await res.json(); | |
| addMsg('ai', d.saved ? 'Chat session saved to disk.' : 'Nothing to save.'); | |
| } catch (_) { | |
| addMsg('ai', 'Save failed.'); | |
| } | |
| }); | |
| // ββ Start ββ | |
| runBoot(); | |
| </script> | |
| </body> | |
| </html> |