Download vlm/webconsole/webui/index.html from StrikeRobot/Go2-Nav_System: direct link, hf CLI and curl.
- Browser
- Download file 7.08 kB
-
https://huggingface.co/StrikeRobot/Go2-Nav_System/resolve/main/vlm/webconsole/webui/index.html
- Command line
-
hf download hf://StrikeRobot/Go2-Nav_System/vlm/webconsole/webui/index.html
-
curl -L -o index.html https://huggingface.co/StrikeRobot/Go2-Nav_System/resolve/main/vlm/webconsole/webui/index.html
7.08 kB
| <html lang="vi"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>VLM Console · Unitree Go2</title> | |
| <style> | |
| :root { --glass: rgba(255,255,255,0.08); --line: rgba(255,255,255,0.15); } | |
| * { box-sizing: border-box; } | |
| body { | |
| margin: 0; height: 100vh; font-family: "Segoe UI", system-ui, sans-serif; | |
| color: #eef; overflow: hidden; | |
| background: radial-gradient(1200px 800px at 10% 10%, #1b2a5e, transparent), | |
| radial-gradient(1000px 700px at 90% 90%, #3a1b5e, transparent), | |
| linear-gradient(135deg, #0a0e27, #0e1430); | |
| } | |
| .app { display: grid; grid-template-columns: 1.2fr 1fr; gap: 18px; | |
| height: 100vh; padding: 18px; } | |
| .panel { | |
| background: var(--glass); border: 1px solid var(--line); | |
| border-radius: 22px; backdrop-filter: blur(18px); | |
| -webkit-backdrop-filter: blur(18px); | |
| box-shadow: 0 8px 40px rgba(0,0,0,0.4); overflow: hidden; | |
| display: flex; flex-direction: column; | |
| } | |
| .panel-head { padding: 16px 20px; display: flex; align-items: center; | |
| justify-content: space-between; border-bottom: 1px solid var(--line); } | |
| .panel-head h1 { font-size: 16px; margin: 0; letter-spacing: .5px; font-weight: 600; } | |
| .badge { font-size: 12px; padding: 5px 12px; border-radius: 999px; | |
| display: flex; align-items: center; gap: 7px; } | |
| .dot { width: 9px; height: 9px; border-radius: 50%; } | |
| .badge.on { background: rgba(0,255,150,.12); color: #7fffc4; } | |
| .badge.on .dot { background: #2bff9c; box-shadow: 0 0 10px #2bff9c; } | |
| .badge.off { background: rgba(255,80,80,.12); color: #ff9d9d; } | |
| .badge.off .dot { background: #ff5b5b; box-shadow: 0 0 10px #ff5b5b; } | |
| .video-wrap { flex: 1; display: flex; align-items: center; justify-content: center; | |
| padding: 16px; } | |
| .video-wrap img { max-width: 100%; max-height: 100%; border-radius: 14px; | |
| border: 1px solid var(--line); } | |
| .chat { flex: 1; overflow-y: auto; padding: 18px; display: flex; | |
| flex-direction: column; gap: 14px; } | |
| .msg { max-width: 88%; padding: 12px 16px; border-radius: 16px; line-height: 1.5; | |
| font-size: 14px; white-space: pre-wrap; animation: rise .25s ease; } | |
| @keyframes rise { from { opacity: 0; transform: translateY(8px); } } | |
| .msg.user { align-self: flex-end; background: linear-gradient(135deg,#5b8cff,#8a5bff); | |
| color: #fff; border-bottom-right-radius: 4px; } | |
| .msg.bot { align-self: flex-start; background: rgba(255,255,255,0.06); | |
| border: 1px solid var(--line); border-bottom-left-radius: 4px; } | |
| .msg img { display: block; margin-top: 10px; max-width: 100%; border-radius: 12px; } | |
| .cursor { display: inline-block; width: 8px; height: 15px; background: #8a5bff; | |
| margin-left: 2px; animation: blink 1s steps(2) infinite; vertical-align: -2px; } | |
| @keyframes blink { 50% { opacity: 0; } } | |
| .composer { padding: 16px; border-top: 1px solid var(--line); display: flex; gap: 10px; } | |
| .composer input { | |
| flex: 1; background: rgba(0,0,0,0.25); border: 1px solid var(--line); | |
| border-radius: 14px; padding: 13px 16px; color: #eef; font-size: 14px; outline: none; | |
| } | |
| .composer input:focus { border-color: #8a5bff; box-shadow: 0 0 0 3px rgba(138,91,255,.2); } | |
| .composer button { | |
| background: linear-gradient(135deg,#5b8cff,#8a5bff); border: 0; color: #fff; | |
| border-radius: 14px; padding: 0 22px; font-size: 14px; font-weight: 600; | |
| cursor: pointer; transition: transform .1s, filter .2s; | |
| } | |
| .composer button:hover { filter: brightness(1.1); } | |
| .composer button:active { transform: scale(.96); } | |
| .composer button:disabled { opacity: .5; cursor: not-allowed; } | |
| .empty { margin: auto; color: rgba(255,255,255,.4); font-size: 14px; text-align: center; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <section class="panel"> | |
| <div class="panel-head"> | |
| <h1>🐾 GO2 · LIVE CAMERA</h1> | |
| <span id="badge" class="badge off"><span class="dot"></span><span id="badge-txt">…</span></span> | |
| </div> | |
| <div class="video-wrap"><img id="feed" src="/video_feed" alt="live feed"></div> | |
| </section> | |
| <section class="panel"> | |
| <div class="panel-head"><h1>💬 VLM REASONING</h1></div> | |
| <div id="chat" class="chat"> | |
| <div class="empty">Nhập lệnh để hỏi VLM về cảnh robot đang nhìn thấy.</div> | |
| </div> | |
| <form id="composer" class="composer"> | |
| <input id="prompt" placeholder="vd: tìm điện thoại, mô tả cảnh, có người không?" autocomplete="off"> | |
| <button id="send" type="submit">Gửi</button> | |
| </form> | |
| </section> | |
| </div> | |
| <script> | |
| const chat = document.getElementById('chat'); | |
| const form = document.getElementById('composer'); | |
| const input = document.getElementById('prompt'); | |
| const sendBtn = document.getElementById('send'); | |
| const badge = document.getElementById('badge'); | |
| const badgeTxt = document.getElementById('badge-txt'); | |
| async function pollStatus() { | |
| try { | |
| const s = await (await fetch('/status')).json(); | |
| const on = s.connected; | |
| badge.className = 'badge ' + (on ? 'on' : 'off'); | |
| badgeTxt.textContent = on ? 'CONNECTED' : (s.mock ? 'MOCK MODE' : 'DISCONNECTED'); | |
| } catch (e) { badge.className = 'badge off'; badgeTxt.textContent = 'OFFLINE'; } | |
| } | |
| pollStatus(); setInterval(pollStatus, 3000); | |
| function clearEmpty() { const e = chat.querySelector('.empty'); if (e) e.remove(); } | |
| function addMsg(cls) { | |
| clearEmpty(); | |
| const d = document.createElement('div'); d.className = 'msg ' + cls; | |
| chat.appendChild(d); chat.scrollTop = chat.scrollHeight; return d; | |
| } | |
| let ws, busy = false; | |
| function connectWS() { | |
| ws = new WebSocket(`ws://${location.host}/ws`); | |
| ws.onclose = () => setTimeout(connectWS, 1500); | |
| } | |
| connectWS(); | |
| form.addEventListener('submit', (e) => { | |
| e.preventDefault(); | |
| const prompt = input.value.trim(); | |
| if (!prompt || busy || ws.readyState !== 1) return; | |
| addMsg('user').textContent = prompt; | |
| input.value = ''; | |
| busy = true; sendBtn.disabled = true; | |
| const bot = addMsg('bot'); | |
| const span = document.createElement('span'); bot.appendChild(span); | |
| const cursor = document.createElement('span'); cursor.className = 'cursor'; | |
| bot.appendChild(cursor); | |
| ws.onmessage = (ev) => { | |
| const m = JSON.parse(ev.data); | |
| if (m.type === 'token') { | |
| span.textContent += m.text; chat.scrollTop = chat.scrollHeight; | |
| } else if (m.type === 'image') { | |
| const img = new Image(); img.src = 'data:image/jpeg;base64,' + m.data; | |
| bot.appendChild(img); chat.scrollTop = chat.scrollHeight; | |
| } else if (m.type === 'busy') { | |
| span.textContent = '⏳ Đang xử lý lệnh trước, thử lại sau.'; | |
| finish(bot, cursor); | |
| } else if (m.type === 'error') { | |
| span.textContent += '\n⚠️ ' + m.message; finish(bot, cursor); | |
| } else if (m.type === 'done') { | |
| finish(bot, cursor); | |
| } | |
| }; | |
| ws.send(JSON.stringify({ prompt })); | |
| }); | |
| function finish(bot, cursor) { | |
| cursor.remove(); busy = false; sendBtn.disabled = false; input.focus(); | |
| } | |
| </script> | |
| </body> | |
| </html> | |