Spaces:
Running
Running
File size: 10,683 Bytes
e26c434 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | <!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>
|