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>