thanhnc19's picture
Upload folder using huggingface_hub
15b8951 verified
Raw
History Blame Contribute Delete
7.08 kB
<!doctype html>
<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>