josephrw's picture
Upload folder using huggingface_hub
c4916b2 verified
Raw
History Blame Contribute Delete
11.6 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="theme-color" content="#0f172a">
<title>Membra Chat</title>
<link rel="manifest" href="/static/manifest.webmanifest">
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
-webkit-tap-highlight-color: transparent;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro', sans-serif;
background: #0f172a;
color: #e2e8f0;
height: 100dvh;
display: flex;
flex-direction: column;
overflow: hidden;
}
#header {
padding: 12px 16px;
background: #1e293b;
border-bottom: 1px solid #334155;
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
#header .logo {
width: 28px;
height: 28px;
border-radius: 6px;
background: linear-gradient(135deg, #06b6d4, #3b82f6);
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
#header h1 {
font-size: 16px;
font-weight: 600;
}
#header .status {
margin-left: auto;
font-size: 11px;
padding: 3px 8px;
border-radius: 10px;
background: #334155;
}
#header .status.connected {
background: #064e3b;
color: #34d399;
}
#header .status.compute {
background: #1e3a5f;
color: #60a5fa;
}
#messages {
flex: 1;
overflow-y: auto;
padding: 12px 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
.msg {
max-width: 90%;
padding: 10px 14px;
border-radius: 14px;
font-size: 14px;
line-height: 1.5;
word-wrap: break-word;
animation: slideIn 0.2s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.msg.user {
align-self: flex-end;
background: #3b82f6;
color: #fff;
border-bottom-right-radius: 4px;
}
.msg.assistant {
align-self: flex-start;
background: #1e293b;
color: #e2e8f0;
border-bottom-left-radius: 4px;
}
.msg.system {
align-self: center;
background: #334155;
color: #94a3b8;
font-size: 11px;
padding: 6px 12px;
}
.msg .meta {
font-size: 10px;
opacity: 0.6;
margin-top: 4px;
}
.msg .meta .device {
color: #34d399;
}
.msg .meta .latency {
color: #fbbf24;
}
#inputArea {
padding: 10px 12px;
background: #1e293b;
border-top: 1px solid #334155;
display: flex;
gap: 8px;
align-items: flex-end;
flex-shrink: 0;
}
#promptInput {
flex: 1;
background: #0f172a;
border: 1px solid #334155;
border-radius: 20px;
padding: 10px 14px;
color: #e2e8f0;
font-size: 15px;
outline: none;
resize: none;
min-height: 20px;
max-height: 120px;
font-family: inherit;
}
#sendBtn {
width: 40px;
height: 40px;
border-radius: 50%;
background: #3b82f6;
color: #fff;
border: none;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
#sendBtn:disabled {
background: #334155;
}
.typing {
display: flex;
gap: 3px;
padding: 8px 0;
}
.typing span {
width: 6px;
height: 6px;
background: #94a3b8;
border-radius: 50%;
animation: bounce 1s infinite;
}
.typing span:nth-child(2) {
animation-delay: 0.15s;
}
.typing span:nth-child(3) {
animation-delay: 0.3s;
}
@keyframes bounce {
0%,
80%,
100% {
transform: translateY(0);
}
40% {
transform: translateY(-4px);
}
}
#setup {
padding: 20px;
}
#setup input,
#setup button {
width: 100%;
padding: 12px;
border-radius: 10px;
border: 1px solid #334155;
background: #1e293b;
color: #e2e8f0;
font-size: 15px;
margin-top: 8px;
}
#setup button {
background: #3b82f6;
border: none;
font-weight: 600;
}
.metric-bar {
height: 4px;
background: #334155;
border-radius: 2px;
margin-top: 4px;
overflow: hidden;
}
.metric-bar .fill {
height: 100%;
border-radius: 2px;
transition: width 0.3s;
}
#computeBadge {
position: fixed;
top: 60px;
right: 12px;
z-index: 100;
background: #1e293b;
border: 1px solid #334155;
border-radius: 8px;
padding: 8px 12px;
font-size: 11px;
display: none;
}
#computeBadge.show {
display: block;
}
#computeBadge .node {
color: #34d399;
}
</style>
</head>
<body>
<div id="setup" style="display:none;">
<h2 style="margin-bottom:8px;">Connect to Session</h2>
<p style="color:#94a3b8;font-size:13px;margin-bottom:12px;">
Enter your Space session ID or scan the QR code from your MacBook.
</p>
<input type="text" id="sessionInput" placeholder="Session ID (e.g., sess_abc123)">
<input type="text" id="tokenInput" placeholder="Worker Token">
<button onclick="connectSession()">Connect</button>
</div>
<div id="header">
<div class="logo">M</div>
<h1>Membra</h1>
<span id="connStatus" class="status">Offline</span>
</div>
<div id="computeBadge">
<div>Compute: <span class="node" id="computeNode">MacBook</span></div>
<div class="metric-bar">
<div class="fill" id="computeBar" style="width:0%;background:#34d399;"></div>
</div>
</div>
<div id="messages"></div>
<div id="inputArea">
<textarea id="promptInput" rows="1" placeholder="Message Membra..."
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMessage();}"></textarea>
<button id="sendBtn" onclick="sendMessage()">&#10148;</button>
</div>
<script>
let ws = null;
let sessionId = null;
let token = null;
let msgId = 0;
let streamingMsg = null;
function getParams() {
const p = new URLSearchParams(location.search);
return { sid: p.get('session_id'), tok: p.get('token') };
}
function connectSession() {
sessionId = document.getElementById('sessionInput').value.trim();
token = document.getElementById('tokenInput').value.trim();
startConnection();
}
function startConnection() {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
const url = `${proto}//${location.host}/ws/gbridge/client/${sessionId}`;
ws = new WebSocket(url);
ws.onopen = () => {
updateStatus('connected', 'Connected');
addSystem('Connected to session ' + sessionId);
ws.send(JSON.stringify({ op: 'client_hello', device: 'iphone' }));
};
ws.onmessage = (e) => {
try { handleMessage(JSON.parse(e.data)); } catch (err) { }
};
ws.onclose = () => {
updateStatus('offline', 'Offline');
addSystem('Disconnected. Retrying...');
setTimeout(startConnection, 3000);
};
ws.onerror = () => {
updateStatus('offline', 'Error');
};
}
function handleMessage(data) {
if (data.op === 'job_stream') {
if (!streamingMsg) {
streamingMsg = createMsg('assistant', '');
}
streamingMsg.textContent += data.chunk || '';
scrollToBottom();
} else if (data.op === 'job_complete') {
if (streamingMsg) {
const meta = document.createElement('div');
meta.className = 'meta';
meta.innerHTML = `<span class="device">${data.worker || 'MacBook'}</span> 路 <span class="latency">${data.latency_ms || 0}ms</span> 路 ${data.tokens || 0} tokens 路 ${(data.tps || 0).toFixed(1)} tok/s`;
streamingMsg.appendChild(meta);
streamingMsg = null;
}
updateComputeBadge(data.worker || 'MacBook', 0);
document.getElementById('sendBtn').disabled = false;
} else if (data.op === 'job_assigned') {
updateComputeBadge(data.worker || 'MacBook', 30);
} else if (data.op === 'error') {
addSystem('Error: ' + (data.error || 'Unknown'));
document.getElementById('sendBtn').disabled = false;
} else if (data.op === 'client_welcome') {
addSystem('Welcome! Type a prompt to start inference on your MacBook.');
}
}
function sendMessage() {
const input = document.getElementById('promptInput');
const text = input.value.trim();
if (!text || !ws || ws.readyState !== 1) return;
input.value = '';
input.style.height = 'auto';
createMsg('user', text);
document.getElementById('sendBtn').disabled = true;
// Show typing indicator
const typing = document.createElement('div');
typing.id = 'typing';
typing.className = 'msg system';
typing.innerHTML = '<div class="typing"><span></span><span></span><span></span></div>';
document.getElementById('messages').appendChild(typing);
scrollToBottom();
ws.send(JSON.stringify({
op: 'chat_submit',
prompt: text,
max_tokens: 512,
temperature: 0.7,
job_type: 'chat_completion'
}));
}
function createMsg(role, text) {
const el = document.createElement('div');
el.className = 'msg ' + role;
if (text) el.textContent = text;
document.getElementById('messages').appendChild(el);
scrollToBottom();
return el;
}
function addSystem(text) {
const t = document.getElementById('typing');
if (t) t.remove();
createMsg('system', text);
}
function updateStatus(cls, text) {
const el = document.getElementById('connStatus');
el.className = 'status ' + cls;
el.textContent = text;
}
function updateComputeBadge(node, load) {
const badge = document.getElementById('computeBadge');
badge.classList.add('show');
document.getElementById('computeNode').textContent = node;
document.getElementById('computeBar').style.width = load + '%';
}
function scrollToBottom() {
const m = document.getElementById('messages');
m.scrollTop = m.scrollHeight;
}
// Auto-connect from URL params
const params = getParams();
if (params.sid && params.tok) {
sessionId = params.sid;
token = params.tok;
document.getElementById('setup').style.display = 'none';
startConnection();
} else {
document.getElementById('setup').style.display = 'block';
document.getElementById('header').style.display = 'none';
document.getElementById('messages').style.display = 'none';
document.getElementById('inputArea').style.display = 'none';
}
// Auto-resize textarea
document.getElementById('promptInput').addEventListener('input', function () {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
});
</script>
</body>
</html>