Spaces:
Paused
Paused
| <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 ; | |
| } | |
| /* 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> |