zero-tvm / share.html
abgunaydin's picture
sync from github@30b8f47
e26c434
Raw
History Blame Contribute Delete
10.7 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Share a model — Zero-TVM</title>
<meta name="description" content="Serve a model running in this tab to another device over an encrypted WebRTC data channel. No install on either end.">
<meta name="robots" content="noindex">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/fonts.css">
<link rel="stylesheet" href="/tokens.css">
<link rel="stylesheet" href="/chat-ui.css">
<style>
/* Page chrome only — the conversational surface (palette, messages,
composer, FAB) comes from chat-ui.css, shared verbatim with the chat
page so the remote conversation looks exactly like the local one. */
* { box-sizing: border-box; margin: 0; }
html, body { height: 100%; background: var(--bg); color: var(--text);
font-family: var(--font); font-size: 15px; line-height: 1.6;
-webkit-font-smoothing: antialiased; }
button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
a { color: var(--accent); text-decoration: none; }
.icon { width: 18px; height: 18px; stroke: currentColor; stroke-width: 2; fill: none; stroke-linecap: round; stroke-linejoin: round; }
.top { display: flex; align-items: center; gap: .75rem; padding: .65rem 1rem;
border-bottom: 1px solid var(--border); }
.top h1 { font-size: 1rem; font-weight: 600; }
.top a.back { font-size: .8rem; color: var(--text-dim); }
.badge { display: inline-flex; align-items: center; gap: 6px; font-size: .72rem;
padding: 3px 10px; border: 1px solid var(--border); border-radius: 999px;
color: var(--muted); margin-left: auto; }
.badge .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--muted); }
.badge.loading { color: #fbbf24; } .badge.loading .dot { background: #fbbf24; }
.badge.ready { color: var(--accent); border-color: rgba(16,185,129,.3); } .badge.ready .dot { background: var(--accent); }
.badge.error { color: var(--danger); } .badge.error .dot { background: var(--danger); }
/* host view */
.wrap { max-width: 780px; margin: 0 auto; padding: 1.25rem 1rem 3rem; }
#progress-wrap { display: none; margin: 1rem 0; border: 1px solid var(--border);
border-radius: 10px; padding: 1rem; background: var(--panel); }
#progress-wrap.active { display: block; }
.progress-track { height: 6px; background: var(--panel-2); border-radius: 3px; overflow: hidden; margin: .6rem 0; }
#progress-bar { height: 100%; width: 0%; background: var(--accent); transition: width .2s; }
#progress-status { font-size: .85rem; } #progress-detail { font-size: .72rem; color: var(--muted); }
#progress-log { font-size: .7rem; color: var(--muted); max-height: 9rem; overflow: auto;
white-space: pre-wrap; margin-top: .5rem; font-family: var(--mono); }
#loading-error { color: var(--danger); font-size: .8rem; margin-top: .5rem; }
/* Consent gate — shown by share.ts before the first weight byte, and only
when the model is not already cached on this device. */
#share-gate { border: 1px solid var(--border); border-radius: 12px; background: var(--panel);
color: var(--text); width: calc(100vw - 2rem); max-width: 30rem; padding: 1.5rem 1.6rem; font: inherit; }
#share-gate::backdrop { background: rgba(8,8,10,.82); backdrop-filter: blur(10px); }
#share-gate h2 { font-size: 1.05rem; font-weight: 600; margin-bottom: .7rem; }
#share-gate p { font-size: .84rem; line-height: 1.6; color: var(--text-dim); margin-bottom: .7rem; }
#share-gate p.warn { color: var(--warn, #d9a441); }
#share-gate p.fine { font-size: .74rem; color: var(--muted); }
#share-gate .acts { display: flex; gap: .8rem; align-items: center; justify-content: flex-end;
margin-top: 1.1rem; }
#share-gate .acts a { font-size: .82rem; color: var(--text-dim); }
#share-gate .acts button { background: var(--accent); color: #04140d; border-radius: 8px;
font-weight: 600; padding: .6rem 1.15rem; font: inherit;
font-weight: 600; cursor: pointer; border: 0; }
section.card { border: 1px solid var(--border); border-radius: 10px; background: var(--panel);
padding: 1rem 1.1rem; margin: 1rem 0; }
section.card h2 { font-size: .78rem; color: var(--muted); font-weight: 600;
text-transform: uppercase; letter-spacing: .06em; margin-bottom: .6rem; }
.linkrow { display: flex; gap: .5rem; align-items: center; }
.linkrow input { flex: 1; background: var(--bg); color: var(--text); border: 1px solid var(--border-2);
border-radius: 8px; padding: .55rem .7rem; font: inherit; font-size: .8rem;
font-family: var(--mono); }
.linkrow button { background: var(--accent); color: #04140d; border-radius: 8px;
font-weight: 600; padding: .55rem 1rem; }
.hint { font-size: .74rem; color: var(--muted); margin-top: .55rem; }
label.opt { display: flex; align-items: center; gap: .5rem; font-size: .82rem; margin-top: .8rem; cursor: pointer; }
label.opt input { accent-color: var(--accent); width: 15px; height: 15px; }
#req-log { list-style: none; display: flex; flex-direction: column; gap: .4rem; padding: 0; }
#req-log li { font-size: .78rem; border-left: 2px solid var(--border-2); padding: .15rem .6rem; color: var(--muted); }
#req-log li .who { color: var(--accent-2); font-family: var(--mono); } #req-log li .st { color: var(--accent); }
/* guest view = the chat shell */
#guest-view { display: flex; flex-direction: column; height: 100dvh; }
#guest-view .top { flex: none; }
#local-copy { max-width: var(--msg-max); margin: 1rem auto; }
#local-copy button { background: var(--accent); color: #04140d; border-radius: 8px;
font-weight: 600; padding: .5rem 1rem; margin-top: .6rem; }
#local-copy button:disabled { opacity: .5; cursor: default; }
.hidden { display: none !important; }
</style>
<script type="module" crossorigin src="/assets/share-TXmFQ6ZAWJ.js"></script>
<link rel="modulepreload" crossorigin href="/assets/model-registry-BoK-ObgACo.js">
<link rel="modulepreload" crossorigin href="/assets/preload-helper-ckwbz45pO3.js">
<link rel="modulepreload" crossorigin href="/assets/chat-ui-CQAxV1po6S.js">
<link rel="modulepreload" crossorigin href="/assets/room-host-C6R3zseuU5.js">
</head>
<body>
<!-- ─── HOST ─── -->
<div id="host-view" class="hidden">
<header class="top">
<a class="back" href="/">← zerotvm.com</a>
<h1 id="page-title">Share a model</h1>
<span class="badge" id="badge"><span class="dot"></span><span id="badge-text">Waiting</span></span>
</header>
<div class="wrap">
<div id="loading-error-top" style="color: var(--danger); font-size: .85rem; margin: .8rem 0; display: none;"></div>
<div id="progress-wrap">
<div id="progress-status">Starting…</div>
<div class="progress-track"><div id="progress-bar"></div></div>
<div id="progress-detail">0%</div>
<pre id="progress-log"></pre>
<div id="loading-error"></div>
</div>
<section class="card">
<h2>Room link</h2>
<div class="linkrow">
<input id="share-link" readonly value="…" aria-label="Room link">
<button id="copy-link" type="button">Copy</button>
</div>
<p class="hint">Anyone with this link chats with the model running in THIS tab,
over an encrypted peer-to-peer data channel. Keep the tab open and the
machine awake; close the tab to end the room.</p>
</section>
<!-- Its own card: a helper stage removes the room-link card above but
needs this MORE than a host does, being a background tab all day. -->
<section class="card">
<label class="opt">
<input type="checkbox" id="awake">
Keep this tab awake — screen wake-lock + a silent audio track, so the
browser throttles generation less when the tab is in the background
</label>
</section>
<section class="card">
<h2>Room</h2>
<p class="hint" id="room-stats">1 machine serving · 0 guests connected</p>
</section>
<section class="card">
<h2>Requests</h2>
<ul id="req-log"><li id="req-empty">No guests yet.</li></ul>
</section>
</div>
</div>
<!-- ─── GUEST — the same shell as zero-tvm.html's chat ─── -->
<div id="guest-view" class="hidden">
<header class="top">
<a class="back" href="/">← zerotvm.com</a>
<h1 id="guest-model">Remote model</h1>
<span class="hint" id="room-count"></span>
<span class="badge" id="guest-badge"><span class="dot"></span><span id="guest-badge-text">Connecting</span></span>
</header>
<main class="chat-main" id="chat-main">
<div class="chat-inner">
<!-- Shown only when the host has a weight cache this device could copy. -->
<section class="card hidden" id="local-copy">
<h2>Run it on this device</h2>
<p class="hint" id="lc-status"></p>
<button id="lc-btn" type="button"></button>
</section>
<div id="messages"></div>
</div>
<button class="scroll-fab" id="scroll-fab" title="Scroll to bottom" aria-label="Scroll to bottom">
<svg class="icon" viewBox="0 0 24 24"><path d="M12 5v14M19 12l-7 7-7-7"/></svg>
</button>
</main>
<div class="composer-wrap">
<form class="composer" id="composer">
<textarea id="inp" rows="1" placeholder="Connecting to the host…" aria-label="Message" disabled></textarea>
<button class="composer-btn" data-variant="send" id="btn" type="submit" disabled aria-label="Send">
<svg class="icon" viewBox="0 0 24 24"><path d="M5 12l14-7-7 14-2-5-5-2z"/></svg>
</button>
<button class="composer-btn" data-variant="stop" id="stop-btn" type="button" hidden aria-label="Stop">
<svg class="icon" viewBox="0 0 24 24" fill="currentColor" stroke="none"><rect x="7" y="7" width="10" height="10" rx="2"/></svg>
</button>
</form>
<div class="composer-hint">
<kbd>Enter</kbd> to send ·
<span id="guest-status" style="color:var(--text-dim)">Waiting for the host.</span>
</div>
</div>
</div>
</body>
</html>