mac-compute-space / app /static /dashboard.html
josephrw's picture
Upload folder using huggingface_hub
c4916b2 verified
Raw
History Blame Contribute Delete
25.5 kB
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PhoneGPU — 2050</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@300;400;500&display=swap"
rel="stylesheet">
<style>
:root {
--bg: #07070a;
--bg-elevated: #0f0f14;
--bg-glass: rgba(15, 15, 20, 0.7);
--border: rgba(255, 255, 255, 0.06);
--border-hover: rgba(255, 255, 255, 0.12);
--text: #e8e8ec;
--text-muted: #6b6b78;
--accent: #00f0ff;
--accent-dim: rgba(0, 240, 255, 0.15);
--accent-glow: rgba(0, 240, 255, 0.4);
--violet: #a78bfa;
--violet-dim: rgba(167, 139, 250, 0.15);
--success: #34d399;
--warning: #fbbf24;
--danger: #f87171;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
font-family: 'Inter', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
}
/* Ambient background */
body::before {
content: '';
position: fixed;
inset: 0;
background:
radial-gradient(ellipse 80% 50% at 20% 40%, rgba(0, 240, 255, 0.03) 0%, transparent 50%),
radial-gradient(ellipse 60% 40% at 80% 60%, rgba(167, 139, 250, 0.03) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
/* Grid pattern */
body::after {
content: '';
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(255, 255, 255, 0.015) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.015) 1px, transparent 1px);
background-size: 60px 60px;
pointer-events: none;
z-index: 0;
}
.container {
position: relative;
z-index: 1;
max-width: 1280px;
margin: 0 auto;
padding: 32px 24px;
}
/* Typography */
h1 {
font-size: 28px;
font-weight: 700;
letter-spacing: -0.5px;
}
h2 {
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--text-muted);
}
.mono {
font-family: 'JetBrains Mono', monospace;
}
/* Cards */
.card {
background: var(--bg-glass);
backdrop-filter: blur(20px) saturate(1.2);
border: 1px solid var(--border);
border-radius: 16px;
padding: 20px;
transition: border-color 0.3s, box-shadow 0.3s;
}
.card:hover {
border-color: var(--border-hover);
}
.card-glow {
box-shadow: 0 0 0 1px var(--border), 0 0 40px -10px var(--accent-dim);
}
.card-glow-violet {
box-shadow: 0 0 0 1px var(--border), 0 0 40px -10px var(--violet-dim);
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
border-radius: 10px;
border: none;
font-family: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
letter-spacing: 0.3px;
}
.btn-primary {
background: linear-gradient(135deg, var(--accent), #00c8d6);
color: #000;
box-shadow: 0 0 20px -5px var(--accent-glow);
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 0 30px -5px var(--accent-glow);
}
.btn-primary:active {
transform: translateY(0);
}
.btn-ghost {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
}
.btn-ghost:hover {
border-color: var(--border-hover);
background: rgba(255, 255, 255, 0.03);
}
.btn:disabled {
opacity: 0.4;
cursor: not-allowed;
transform: none !important;
}
/* Status indicators */
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
display: inline-block;
margin-right: 6px;
box-shadow: 0 0 6px currentColor;
}
.status-online {
background: var(--success);
color: var(--success);
}
.status-offline {
background: var(--danger);
color: var(--danger);
}
.status-warn {
background: var(--warning);
color: var(--warning);
}
/* Progress bars */
.progress-track {
height: 3px;
background: rgba(255, 255, 255, 0.05);
border-radius: 2px;
overflow: hidden;
margin-top: 8px;
}
.progress-fill {
height: 100%;
border-radius: 2px;
background: linear-gradient(90deg, var(--accent), var(--violet));
transition: width 0.5s ease;
box-shadow: 0 0 8px var(--accent-glow);
}
/* Inputs */
.input,
.textarea,
.select {
width: 100%;
padding: 10px 14px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--border);
border-radius: 10px;
color: var(--text);
font-family: inherit;
font-size: 13px;
outline: none;
transition: all 0.2s;
}
.input:focus,
.textarea:focus,
.select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-dim);
}
.textarea {
min-height: 80px;
resize: vertical;
}
.select {
cursor: pointer;
}
/* Grid */
.grid {
display: grid;
gap: 16px;
}
.grid-4 {
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
.grid-2 {
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}
.grid-3 {
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
/* Animations */
@keyframes fadeUp {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse-glow {
0%,
100% {
box-shadow: 0 0 20px -5px var(--accent-glow);
}
50% {
box-shadow: 0 0 35px -5px var(--accent-glow);
}
}
.animate-fade-up {
animation: fadeUp 0.5s ease both;
}
.animate-pulse-glow {
animation: pulse-glow 3s ease-in-out infinite;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 4px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
}
/* Utility */
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.gap-2 {
gap: 8px;
}
.gap-3 {
gap: 12px;
}
.gap-4 {
gap: 16px;
}
.text-muted {
color: var(--text-muted);
}
.text-accent {
color: var(--accent);
}
.text-violet {
color: var(--violet);
}
.text-xs {
font-size: 11px;
}
.text-sm {
font-size: 13px;
}
.text-lg {
font-size: 18px;
}
.text-2xl {
font-size: 24px;
}
.font-mono {
font-family: 'JetBrains Mono', monospace;
}
.font-medium {
font-weight: 500;
}
.font-semibold {
font-weight: 600;
}
.font-bold {
font-weight: 700;
}
.mt-2 {
margin-top: 8px;
}
.mt-3 {
margin-top: 12px;
}
.mt-4 {
margin-top: 16px;
}
.mb-1 {
margin-bottom: 4px;
}
.mb-2 {
margin-bottom: 8px;
}
.mb-3 {
margin-bottom: 12px;
}
.mb-4 {
margin-bottom: 16px;
}
.p-3 {
padding: 12px;
}
.p-4 {
padding: 16px;
}
.rounded-lg {
border-radius: 10px;
}
.rounded-xl {
border-radius: 16px;
}
.w-full {
width: 100%;
}
.overflow-auto {
overflow: auto;
}
.max-h-64 {
max-height: 256px;
}
.space-y-2>*+* {
margin-top: 8px;
}
.space-y-3>*+* {
margin-top: 12px;
}
.space-y-4>*+* {
margin-top: 16px;
}
.hidden {
display: none;
}
/* Receipt card */
.receipt-card {
background: linear-gradient(135deg, rgba(0, 240, 255, 0.03), rgba(167, 139, 250, 0.03));
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px;
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
}
/* Job card */
.job-card {
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px;
transition: all 0.2s;
}
.job-card:hover {
border-color: var(--border-hover);
background: rgba(255, 255, 255, 0.03);
}
.job-status {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.job-status-completed {
background: rgba(52, 211, 153, 0.1);
color: var(--success);
}
.job-status-running {
background: rgba(0, 240, 255, 0.1);
color: var(--accent);
}
.job-status-queued {
background: rgba(255, 255, 255, 0.05);
color: var(--text-muted);
}
.job-status-failed {
background: rgba(248, 113, 113, 0.1);
color: var(--danger);
}
/* Live terminal */
.terminal {
background: #050508;
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
line-height: 1.6;
min-height: 200px;
max-height: 300px;
overflow-y: auto;
}
.terminal-line {
opacity: 0;
animation: fadeUp 0.3s ease both;
}
.terminal-prompt {
color: var(--accent);
margin-right: 8px;
}
.terminal-timestamp {
color: var(--text-muted);
margin-right: 8px;
}
/* Capability tag */
.cap-tag {
display: inline-flex;
align-items: center;
padding: 3px 10px;
border-radius: 6px;
font-size: 11px;
font-weight: 500;
background: var(--accent-dim);
color: var(--accent);
border: 1px solid rgba(0, 240, 255, 0.2);
}
/* Scanline effect for 2050 feel */
.scanlines {
position: relative;
}
.scanlines::after {
content: '';
position: absolute;
inset: 0;
background: repeating-linear-gradient(0deg,
transparent,
transparent 2px,
rgba(0, 0, 0, 0.03) 2px,
rgba(0, 0, 0, 0.03) 4px);
pointer-events: none;
border-radius: inherit;
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<header class="flex items-center justify-between mb-8 animate-fade-up" style="animation-delay:0s">
<div class="flex items-center gap-3">
<div
style="width:36px;height:36px;border-radius:10px;background:linear-gradient(135deg,var(--accent),var(--violet));display:flex;align-items:center;justify-content:center;box-shadow:0 0 20px -5px var(--accent-glow)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2.5"
stroke-linecap="round" stroke-linejoin="round">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
</svg>
</div>
<div>
<h1 style="font-size:18px">PhoneGPU</h1>
<div class="text-xs text-muted" style="letter-spacing:2px;text-transform:uppercase">Edge Compute · 2050</div>
</div>
</div>
<div class="flex items-center gap-3">
<div id="hfStatus" class="flex items-center text-xs text-muted">
<span class="status-dot status-offline"></span>HF Space
</div>
<button id="btnCreateSession" class="btn btn-primary animate-pulse-glow">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M12 5v14M5 12h14" />
</svg>
New Session
</button>
</div>
</header>
<!-- Metrics -->
<div class="grid grid-4 mb-6">
<div class="card animate-fade-up" style="animation-delay:0.05s">
<div class="text-xs text-muted mb-1" style="text-transform:uppercase;letter-spacing:1px">Workers Online</div>
<div class="text-2xl font-bold mono" id="metricWorkers">0</div>
<div class="progress-track">
<div class="progress-fill" id="barWorkers" style="width:0%"></div>
</div>
</div>
<div class="card animate-fade-up" style="animation-delay:0.1s">
<div class="text-xs text-muted mb-1" style="text-transform:uppercase;letter-spacing:1px">Jobs Completed</div>
<div class="text-2xl font-bold mono" id="metricJobs">0</div>
<div class="progress-track">
<div class="progress-fill" id="barJobs" style="width:0%"></div>
</div>
</div>
<div class="card animate-fade-up" style="animation-delay:0.15s">
<div class="text-xs text-muted mb-1" style="text-transform:uppercase;letter-spacing:1px">Avg Latency</div>
<div class="text-2xl font-bold mono" id="metricLatency"></div>
<div class="progress-track">
<div class="progress-fill" id="barLatency" style="width:0%"></div>
</div>
</div>
<div class="card card-glow animate-fade-up" style="animation-delay:0.2s">
<div class="text-xs text-accent mb-1" style="text-transform:uppercase;letter-spacing:1px">Total Tokens</div>
<div class="text-2xl font-bold mono text-accent" id="metricTokens">0</div>
<div class="progress-track">
<div class="progress-fill" style="width:100%"></div>
</div>
</div>
</div>
<!-- Main grid -->
<div class="grid grid-2">
<!-- Session & Job Builder -->
<div class="space-y-4">
<div class="card card-glow animate-fade-up" style="animation-delay:0.25s">
<h2 class="mb-3">Session</h2>
<div id="sessionPanel">
<div class="text-sm text-muted mb-3">No active session. Create one to begin.</div>
<button id="btnCreateSession2" class="btn btn-primary w-full">Initialize Compute Session</button>
</div>
<div id="sessionActive" class="hidden">
<div class="flex items-center justify-between mb-2">
<span class="font-mono text-sm" id="sessionId"></span>
<span class="job-status job-status-running">Active</span>
</div>
<div class="text-xs text-muted mb-3">Scan QR with iPhone to pair worker</div>
<img id="qrCode" src=""
style="width:160px;height:160px;border-radius:12px;display:block;margin:0 auto;image-rendering:pixelated"
alt="Scan with iPhone">
<div class="mt-3 p-3 rounded-lg"
style="background:rgba(0,240,255,0.03);border:1px solid rgba(0,240,255,0.1)">
<div class="text-xs text-accent mb-1">Join URL</div>
<div class="font-mono text-xs" id="workerUrl" style="word-break:break-all"></div>
</div>
</div>
</div>
<div class="card animate-fade-up" style="animation-delay:0.3s">
<h2 class="mb-3">Submit Job</h2>
<div class="space-y-3">
<div class="flex gap-2">
<button class="btn btn-ghost flex-1 job-type-btn active" data-type="text_embedding"
style="font-size:11px;padding:8px">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
Embedding
</button>
<button class="btn btn-ghost flex-1 job-type-btn" data-type="image_classification"
style="font-size:11px;padding:8px">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" />
<circle cx="8.5" cy="8.5" r="1.5" />
<path d="M21 15l-5-5L5 21" />
</svg>
Vision
</button>
<button class="btn btn-ghost flex-1 job-type-btn" data-type="privacy_redaction"
style="font-size:11px;padding:8px">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg>
Privacy
</button>
</div>
<textarea id="jobPayload" class="textarea" placeholder="Enter text prompt..."></textarea>
<div class="flex gap-2">
<button class="btn btn-ghost flex-1 privacy-btn active" data-mode="raw_input_remote"
style="font-size:11px;padding:6px 12px">Standard</button>
<button class="btn btn-ghost flex-1 privacy-btn" data-mode="hash_only_remote"
style="font-size:11px;padding:6px 12px">Hash</button>
<button class="btn btn-ghost flex-1 privacy-btn" data-mode="local_only_result_only"
style="font-size:11px;padding:6px 12px">Private</button>
</div>
<button id="btnSubmitJob" class="btn btn-primary w-full" disabled>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M22 2L11 13M22 2l-7 20-5-5-5 5 2-20 20-2z" />
</svg>
Dispatch to Edge
</button>
</div>
</div>
</div>
<!-- Live Terminal & Workers -->
<div class="space-y-4">
<div class="card card-glow-violet animate-fade-up scanlines" style="animation-delay:0.25s">
<div class="flex items-center justify-between mb-3">
<h2>Live Stream</h2>
<div class="flex items-center gap-2">
<span class="status-dot status-online"></span>
<span class="text-xs text-muted">Listening</span>
</div>
</div>
<div id="terminal" class="terminal">
<div class="terminal-line"><span class="terminal-prompt"></span><span
class="terminal-timestamp">INIT</span>PhoneGPU Edge Compute initialized</div>
<div class="terminal-line"><span class="terminal-prompt"></span><span
class="terminal-timestamp">WAIT</span>Waiting for session...</div>
</div>
</div>
<div class="card animate-fade-up" style="animation-delay:0.3s">
<h2 class="mb-3">Workers</h2>
<div id="workersList" class="space-y-2">
<div class="text-sm text-muted">No workers connected</div>
</div>
</div>
<div class="card animate-fade-up" style="animation-delay:0.35s">
<h2 class="mb-3">Capabilities</h2>
<div class="flex flex-wrap gap-2">
<span class="cap-tag">iphone.text.embedding</span>
<span class="cap-tag">iphone.image.classify</span>
<span class="cap-tag">iphone.privacy.redact</span>
<span class="cap-tag">mac.llm.generate</span>
<span class="cap-tag">mac.embed.batch</span>
</div>
</div>
</div>
</div>
<!-- Receipts & Jobs -->
<div class="mt-6">
<div class="card animate-fade-up" style="animation-delay:0.4s">
<h2 class="mb-3">Recent Jobs & Receipts</h2>
<div id="jobsList" class="space-y-2 max-h-64 overflow-auto">
<div class="text-sm text-muted">No jobs yet</div>
</div>
</div>
</div>
</div>
<script>
// State
let sessionId = null;
let sessionSecret = null;
let ws = null;
let selectedJobType = 'text_embedding';
let selectedPrivacy = 'raw_input_remote';
let jobs = [];
let receipts = [];
let workers = [];
// DOM refs
const $ = (id) => document.getElementById(id);
const terminal = $('terminal');
function log(msg, type = 'INFO') {
const line = document.createElement('div');
line.className = 'terminal-line';
line.innerHTML = `<span class="terminal-prompt">⟩</span><span class="terminal-timestamp">${type}</span>${msg}`;
terminal.appendChild(line);
terminal.scrollTop = terminal.scrollHeight;
}
// Session creation
async function createSession() {
try {
const res = await fetch('/api/session', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) });
const data = await res.json();
sessionId = data.session_id;
sessionSecret = data.secret;
$('sessionPanel').classList.add('hidden');
$('sessionActive').classList.remove('hidden');
$('sessionId').textContent = sessionId.slice(0, 16) + '...';
$('workerUrl').textContent = data.worker_join_url;
$('qrCode').src = 'data:image/png;base64,' + data.qr_code_base64;
log(`Session ${sessionId.slice(0, 8)} created`, 'OK');
$('hfStatus').innerHTML = '<span class="status-dot status-online"></span>HF Space';
connectWebSocket();
} catch (e) {
log('Failed to create session: ' + e.message, 'ERR');
}
}
$('btnCreateSession').onclick = createSession;
$('btnCreateSession2').onclick = createSession;
// Job type selection
document.querySelectorAll('.job-type-btn').forEach(btn => {
btn.onclick = () => {
document.querySelectorAll('.job-type-btn').forEach(b => {
b.style.borderColor = '';
b.style.background = '';
});
btn.style.borderColor = 'var(--accent)';
btn.style.background = 'var(--accent-dim)';
selectedJobType = btn.dataset.type;
};
});
document.querySelector('.job-type-btn[data-type="text_embedding"]').click();
// Privacy selection
document.querySelectorAll('.privacy-btn').forEach(btn => {
btn.onclick = () => {
document.querySelectorAll('.privacy-btn').forEach(b => {
b.style.borderColor = '';
b.style.background = '';
});
btn.style.borderColor = 'var(--accent)';
btn.style.background = 'var(--accent-dim)';
selectedPrivacy = btn.dataset.mode;
};
});
document.querySelector('.privacy-btn[data-mode="raw_input_remote"]').click();
// Submit job
$('btnSubmitJob').onclick = async () => {
if (!sessionId) { log('No session', 'ERR'); return; }
const payload = $('jobPayload').value;
if (!payload.trim()) return;
try {
const p = selectedJobType === 'image_classification' ? { image: payload } : { text: payload };
const res = await fetch(`/api/session/${sessionId}/job`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ job_type: selectedJobType, payload: p, privacy_mode: selectedPrivacy }),
});
const data = await res.json();
log(`Job ${data.job_id.slice(0, 8)} dispatched → ${selectedJobType}`, 'SEND');
$('jobPayload').value = '';
} catch (e) {
log('Dispatch failed: ' + e.message, 'ERR');
}
};
// WebSocket
function connectWebSocket() {
if (!sessionId || ws) return;
const url = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws/session/${sessionId}`;
ws = new WebSocket(url);
ws.onopen = () => {
log('WebSocket connected', 'NET');
ws.send(JSON.stringify({ op: 'client_hello', session_id: sessionId }));
};
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.op === 'job_result') {
log(`Job ${msg.job_id.slice(0, 8)} completed`, 'DONE');
fetchJobs();
} else if (msg.op === 'stream_chunk') {
log(`Token: ${msg.chunk?.slice(0, 20)}...`, 'STREAM');
} else if (msg.op === 'session_update') {
log('Session updated', 'INFO');
}
};
ws.onclose = () => { ws = null; log('WebSocket closed', 'NET'); };
ws.onerror = () => log('WebSocket error', 'ERR');
}
// Polling
async function fetchJobs() {
if (!sessionId) return;
try {
const res = await fetch(`/api/session/${sessionId}`);
if (res.ok) {
const data = await res.json();
$('metricWorkers').textContent = data.worker_count;
$('barWorkers').style.width = Math.min(data.worker_count * 20, 100) + '%';
}
} catch (e) { }
}
setInterval(fetchJobs, 3000);
// Keyboard shortcut
document.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
$('btnSubmitJob').click();
}
});
</script>
</body>
</html>