kafox-cli 0.9.8 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/kafox.js +78 -0
- package/package.json +18 -1
- package/src/agent.js +24 -0
- package/src/auth.js +25 -6
- package/src/cli.js +3 -0
- package/src/compact.js +346 -0
- package/src/gateway.js +119 -53
- package/src/mobile/auth.js +352 -0
- package/src/mobile/bridge.js +285 -0
- package/src/mobile/index.js +538 -0
- package/src/mobile/protocol.js +51 -0
- package/src/mobile/qr.js +487 -0
- package/src/mobile/server.js +297 -0
- package/src/mobile/tui-bridge.js +289 -0
- package/src/mobile/view.js +734 -0
- package/src/models.js +19 -0
- package/src/providers-cli.js +82 -0
- package/src/providers.js +87 -0
- package/src/storage.js +39 -0
- package/src/tools.js +13 -2
- package/src/tui.js +549 -44
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pagina do celular do kafox mobile (HTML+CSS+JS vanilla, sem build).
|
|
3
|
+
*
|
|
4
|
+
* NAO e um "chat": e um ESPELHO DO TERMINAL. A ideia e que o celular seja uma
|
|
5
|
+
* janela para a MESMA sessao que roda no PC. O que voce digita aqui injeta no
|
|
6
|
+
* terminal; o que o terminal produz aparece aqui como um log continuo.
|
|
7
|
+
*
|
|
8
|
+
* Modelo de render (importante):
|
|
9
|
+
* - Um BUFFER UNICO de linhas (como um `tail`), nao uma pilha de cards.
|
|
10
|
+
* - O texto do assistente em streaming CRESCE no mesmo bloco: cada chunk faz
|
|
11
|
+
* append no mesmo no (pre), nunca cria um card novo. Isso mata o efeito de
|
|
12
|
+
* "cada chunk vira uma mensagem diferente".
|
|
13
|
+
* - Pensamento/tool/erro viram LINHAS discretas prefixadas (estilo terminal),
|
|
14
|
+
* sem moldura de cartao.
|
|
15
|
+
* - Aprovacao precisa de botoes: aparece inline, empilhada no fluxo.
|
|
16
|
+
*
|
|
17
|
+
* Fluxo no browser:
|
|
18
|
+
* 1. le "#p=<pair_code>" da URL, faz POST /pair e guarda o token (sessionStorage);
|
|
19
|
+
* 2. loop de long-polling em /poll (o Cloudflare segura/aborta SSE de browser);
|
|
20
|
+
* 3. manda prompt por /prompt, aprova por /approve, para por /abort;
|
|
21
|
+
* 4. reconecta sozinho com backoff e pede so o que perdeu (since).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
function renderPage() {
|
|
25
|
+
return `<!doctype html>
|
|
26
|
+
<html lang="pt-BR">
|
|
27
|
+
<head>
|
|
28
|
+
<meta charset="utf-8" />
|
|
29
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
30
|
+
<meta name="theme-color" content="#000000" />
|
|
31
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' fill='%23000'/><text x='6' y='24' font-size='22' fill='%23fff' font-family='monospace' font-weight='bold'>K</text></svg>" />
|
|
32
|
+
<title>Kafox · mobile</title>
|
|
33
|
+
<style>
|
|
34
|
+
/* Brutalist — paleta real do site kafox.dev: preto #000, acento #bfa682,
|
|
35
|
+
cantos retos, sombras duras, tipografia mono pesada. */
|
|
36
|
+
:root {
|
|
37
|
+
--bg:#000000; --panel:#000000; --panel-2:#050505; --panel-3:#0b0b0b;
|
|
38
|
+
--fg:#ffffff; --dim:#8b8b8b; --faint:#555555; --line:#2b2b2b;
|
|
39
|
+
--neon:#bfa682; --neon-ink:#000000;
|
|
40
|
+
--ok:#3fb950; --err:#ff4444; --warn:#d29922;
|
|
41
|
+
--mono:"Cascadia Code","SFMono-Regular",Consolas,"Liberation Mono",ui-monospace,monospace;
|
|
42
|
+
--fs:14px;
|
|
43
|
+
}
|
|
44
|
+
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
|
|
45
|
+
html, body { margin:0; height:100%; background:#000; color:var(--fg); font:var(--fs)/1.55 var(--mono); }
|
|
46
|
+
::selection { background:var(--neon); color:var(--neon-ink); }
|
|
47
|
+
#app { display:flex; flex-direction:column; height:100dvh; }
|
|
48
|
+
|
|
49
|
+
/* --- barra de status estilo terminal --- */
|
|
50
|
+
header { display:flex; align-items:center; gap:8px; padding:8px 12px;
|
|
51
|
+
border-bottom:2px solid #fff; position:sticky; top:0; background:#000; z-index:2;
|
|
52
|
+
font-size:12px; text-transform:uppercase; letter-spacing:0.12em; }
|
|
53
|
+
header strong { font-weight:800; letter-spacing:-0.02em; }
|
|
54
|
+
header .dot { width:10px; height:10px; background:var(--warn); border:2px solid #fff; flex:none; }
|
|
55
|
+
header .dot.on { background:var(--neon); }
|
|
56
|
+
header .dot.off { background:var(--err); }
|
|
57
|
+
header .meta { color:var(--dim); margin-left:auto; font-size:10px; display:flex; gap:8px; align-items:center; }
|
|
58
|
+
header .meta .tag { border:1.5px solid var(--line); padding:1px 6px; }
|
|
59
|
+
.fsbtn { background:transparent; color:var(--neon); border:1.5px solid var(--line);
|
|
60
|
+
font:inherit; font-size:13px; line-height:1; padding:4px 8px; min-width:30px;
|
|
61
|
+
cursor:pointer; text-transform:none; letter-spacing:0; font-weight:800; }
|
|
62
|
+
.fsbtn:active { border-color:var(--neon); color:#fff; }
|
|
63
|
+
.fsbtn[disabled] { opacity:0.35; }
|
|
64
|
+
|
|
65
|
+
/* --- log continuo (o terminal) --- */
|
|
66
|
+
main { flex:1; overflow-y:auto; padding:12px 12px 24px; overscroll-behavior: contain;
|
|
67
|
+
-webkit-overflow-scrolling:touch; }
|
|
68
|
+
#log { min-height:100%; }
|
|
69
|
+
|
|
70
|
+
/* Cada TURNO e um bloco: prompt do usuario + saida abaixo. */
|
|
71
|
+
.turn { margin:0 0 14px; }
|
|
72
|
+
.turn:last-child { margin-bottom:0; }
|
|
73
|
+
|
|
74
|
+
/* Linha do usuario: "❯ texto" com barra acento. */
|
|
75
|
+
.u { border-left:3px solid var(--neon); padding:4px 0 4px 10px; margin:0 0 6px;
|
|
76
|
+
white-space:pre-wrap; word-break:break-word; color:#fff; }
|
|
77
|
+
.u::before { content:"❯ "; color:var(--neon); font-weight:700; }
|
|
78
|
+
|
|
79
|
+
/* Saida do assistente: um unico pre que cresce. */
|
|
80
|
+
.out { white-space:pre-wrap; word-break:break-word; margin:0; color:var(--fg); }
|
|
81
|
+
.out.cursor::after { content:"█"; color:var(--neon); margin-left:1px;
|
|
82
|
+
animation:blink 1s steps(1) infinite; }
|
|
83
|
+
@keyframes blink { 50% { opacity:0.12; } }
|
|
84
|
+
|
|
85
|
+
/* Markdown renderizado dentro da saida. */
|
|
86
|
+
.md { white-space:normal; }
|
|
87
|
+
.md p { margin:0 0 8px; }
|
|
88
|
+
.md p:last-child { margin-bottom:0; }
|
|
89
|
+
.md h1,.md h2,.md h3,.md h4 { margin:12px 0 6px; font-weight:800; letter-spacing:-0.02em; line-height:1.25; }
|
|
90
|
+
.md h1 { font-size:19px; } .md h2 { font-size:17px; } .md h3 { font-size:15px; } .md h4 { font-size:14px; color:var(--neon); }
|
|
91
|
+
.md ul,.md ol { margin:4px 0 8px; padding-left:22px; }
|
|
92
|
+
.md li { margin:2px 0; }
|
|
93
|
+
.md code { background:var(--panel-3); color:var(--neon); padding:1px 5px; font-size:12.5px;
|
|
94
|
+
border:1px solid var(--line); }
|
|
95
|
+
.md pre { background:var(--panel-3); border-left:3px solid var(--neon); padding:10px 12px;
|
|
96
|
+
overflow:auto; margin:8px 0; font-size:12.5px; max-height:50vh; }
|
|
97
|
+
.md pre code { background:none; border:none; color:#fff; padding:0; }
|
|
98
|
+
.md blockquote { border-left:3px solid var(--line); padding-left:10px; margin:8px 0; color:var(--dim); }
|
|
99
|
+
.md a { color:var(--neon); }
|
|
100
|
+
.md hr { border:none; border-top:1px solid var(--line); margin:10px 0; }
|
|
101
|
+
.md table { border-collapse:collapse; margin:8px 0; font-size:12.5px; width:100%; }
|
|
102
|
+
.md th,.md td { border:1px solid var(--line); padding:5px 8px; text-align:left; }
|
|
103
|
+
.md th { background:var(--panel-3); }
|
|
104
|
+
.md strong { font-weight:800; } .md em { font-style:italic; }
|
|
105
|
+
|
|
106
|
+
/* Linhas discretas: pensamento, tool, sistema, erro. Estilo log. */
|
|
107
|
+
.ln { display:block; white-space:pre-wrap; word-break:break-word; color:var(--dim);
|
|
108
|
+
font-size:12.5px; margin:2px 0; }
|
|
109
|
+
.ln::before { display:inline; margin-right:6px; font-weight:700; letter-spacing:0.04em; }
|
|
110
|
+
.ln.think { color:var(--faint); font-style:italic; }
|
|
111
|
+
.ln.think::before { content:"···"; color:var(--faint); }
|
|
112
|
+
.ln.tool { color:var(--warn); }
|
|
113
|
+
.ln.tool::before { content:"▸"; }
|
|
114
|
+
.ln.tool.ok { color:var(--ok); }
|
|
115
|
+
.ln.tool.err { color:var(--err); }
|
|
116
|
+
.ln.sys::before { content:"!"; color:var(--warn); }
|
|
117
|
+
.ln.sys { color:var(--warn); }
|
|
118
|
+
.ln.err::before { content:"✕"; color:var(--err); }
|
|
119
|
+
.ln.err { color:var(--err); }
|
|
120
|
+
.ln.compact::before { content:"≈"; color:var(--dim); }
|
|
121
|
+
.ln .t { color:var(--faint); }
|
|
122
|
+
.ln pre { margin:4px 0 4px 16px; padding:8px; background:var(--panel-3); color:#fff;
|
|
123
|
+
overflow:auto; font-size:12px; max-height:36vh; border-left:2px solid var(--line); }
|
|
124
|
+
.ln details { margin:2px 0 2px 16px; }
|
|
125
|
+
.ln summary { cursor:pointer; color:var(--faint); }
|
|
126
|
+
.diff-add { color:var(--ok); font-weight:700; } .diff-del { color:var(--err); font-weight:700; }
|
|
127
|
+
|
|
128
|
+
/* Aprovacao inline (precisa de toque: botoes). */
|
|
129
|
+
.approval { border:2px solid var(--neon); background:#000; padding:10px 12px; margin:8px 0;
|
|
130
|
+
box-shadow:4px 4px 0 0 var(--neon); }
|
|
131
|
+
.approval .head { font-size:12px; text-transform:uppercase; letter-spacing:0.1em; margin-bottom:6px; }
|
|
132
|
+
.approval .head strong { color:var(--neon); }
|
|
133
|
+
.approval pre { margin:0 0 10px; padding:8px; background:var(--panel-3); color:#fff;
|
|
134
|
+
overflow:auto; font-size:12px; max-height:40vh; border:1.5px solid var(--line); }
|
|
135
|
+
.approval .actions { display:flex; gap:8px; flex-wrap:wrap; }
|
|
136
|
+
button { font:inherit; font-weight:700; padding:10px 14px; border:2px solid #fff; background:#000;
|
|
137
|
+
color:#fff; flex:1; text-transform:uppercase; letter-spacing:0.08em; font-size:11px;
|
|
138
|
+
box-shadow:4px 4px 0 0 var(--line); transition:transform .12s ease, box-shadow .12s ease; }
|
|
139
|
+
button.primary { background:var(--neon); border-color:var(--neon); color:var(--neon-ink); box-shadow:4px 4px 0 0 #fff; }
|
|
140
|
+
button.danger { border-color:var(--err); color:var(--err); }
|
|
141
|
+
button:active { transform:translate(2px,2px); box-shadow:2px 2px 0 0 var(--line); }
|
|
142
|
+
|
|
143
|
+
/* --- barra de entrada estilo prompt --- */
|
|
144
|
+
footer { border-top:2px solid #fff; padding:10px; display:flex; gap:8px; align-items:flex-end;
|
|
145
|
+
position:sticky; bottom:0; background:#000; padding-bottom:calc(10px + env(safe-area-inset-bottom)); }
|
|
146
|
+
footer .caret { color:var(--neon); font-weight:800; padding-bottom:12px; flex:none; }
|
|
147
|
+
footer textarea { flex:1; resize:none; min-height:44px; max-height:120px; padding:10px;
|
|
148
|
+
border:2px solid var(--line); background:#000; color:#fff; font:inherit; }
|
|
149
|
+
footer textarea:focus { outline:none; border-color:var(--neon); }
|
|
150
|
+
footer .send { flex:none; width:auto; padding:10px 16px; }
|
|
151
|
+
|
|
152
|
+
#banner { display:none; padding:7px 12px; background:var(--err); color:#000; font-weight:700;
|
|
153
|
+
font-size:11px; text-transform:uppercase; letter-spacing:0.08em; }
|
|
154
|
+
#banner.on { background:var(--neon); }
|
|
155
|
+
#banner.on::before { content:"✓ "; }
|
|
156
|
+
|
|
157
|
+
/* Overlay de pareamento (quando nao ha token): pede o codigo digitado, caso
|
|
158
|
+
a camera do celular nao consiga ler o QR da tela do PC. */
|
|
159
|
+
#pairing { position:fixed; inset:0; background:#000; z-index:20; display:none;
|
|
160
|
+
flex-direction:column; align-items:center; justify-content:center; gap:14px; padding:24px;
|
|
161
|
+
text-align:center; }
|
|
162
|
+
#pairing.show { display:flex; }
|
|
163
|
+
#pairing h2 { font-size:16px; text-transform:uppercase; letter-spacing:0.14em; margin:0; }
|
|
164
|
+
#pairing p { color:var(--dim); font-size:12.5px; margin:0; max-width:34ch; line-height:1.6; }
|
|
165
|
+
#pairing .code { font-size:30px; font-weight:800; letter-spacing:0.34em; text-align:center;
|
|
166
|
+
border:2px solid var(--neon); background:#000; color:var(--neon); padding:14px; width:min(92vw,420px); }
|
|
167
|
+
#pairing .code:focus { outline:none; box-shadow:4px 4px 0 0 var(--neon); }
|
|
168
|
+
#pairing .err { color:var(--err); min-height:1.2em; font-size:12px; }
|
|
169
|
+
#pairing button { width:min(92vw,420px); }
|
|
170
|
+
|
|
171
|
+
/* Pensamento recolhido: mostra so a primeira linha como "resumo" clicavel. */
|
|
172
|
+
.ln.think { cursor:pointer; }
|
|
173
|
+
.ln.think .chev { color:var(--faint); margin-right:6px; }
|
|
174
|
+
.ln.think .full { display:none; white-space:pre-wrap; }
|
|
175
|
+
.ln.think.open .full { display:inline; }
|
|
176
|
+
.ln.think.open .one { display:none; }
|
|
177
|
+
</style>
|
|
178
|
+
</head>
|
|
179
|
+
<body>
|
|
180
|
+
<div id="app">
|
|
181
|
+
<header>
|
|
182
|
+
<span class="dot" id="status"></span>
|
|
183
|
+
<strong>kafox</strong>
|
|
184
|
+
<span class="meta"><span id="busy"></span><span class="tag" id="sid"></span><button class="fsbtn" id="fsMinus" type="button" aria-label="diminuir fonte">A-</button><button class="fsbtn" id="fsPlus" type="button" aria-label="aumentar fonte">A+</button></span>
|
|
185
|
+
</header>
|
|
186
|
+
<div id="banner">pc desconectado — reconectando…</div>
|
|
187
|
+
<main id="scroll"><div id="log"></div></main>
|
|
188
|
+
<footer>
|
|
189
|
+
<span class="caret">❯</span>
|
|
190
|
+
<textarea id="input" placeholder="digite pro terminal…" rows="1"></textarea>
|
|
191
|
+
<button class="send primary" id="send">↲</button>
|
|
192
|
+
<button class="send danger" id="stop" title="interromper">■</button>
|
|
193
|
+
</footer>
|
|
194
|
+
<div id="pairing">
|
|
195
|
+
<h2>conectar ao pc</h2>
|
|
196
|
+
<p>se a câmera não leu o QR, digite o código que aparece embaixo dele na tela do PC.</p>
|
|
197
|
+
<input class="code" id="paircode" inputmode="text" autocomplete="off" autocapitalize="characters"
|
|
198
|
+
spellcheck="false" placeholder="CÓDIGO" maxlength="12" />
|
|
199
|
+
<div class="err" id="pairerr"></div>
|
|
200
|
+
<button class="primary" id="pairbtn">parear</button>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
<script>
|
|
204
|
+
(function () {
|
|
205
|
+
"use strict";
|
|
206
|
+
var $ = function (id) { return document.getElementById(id); };
|
|
207
|
+
var log = $("log"), scrollEl = $("scroll"), status = $("status"), sid = $("sid"), busyEl = $("busy");
|
|
208
|
+
var input = $("input"), banner = $("banner");
|
|
209
|
+
var token = sessionStorage.getItem("kafox_token") || "";
|
|
210
|
+
var seq = 0, backoff = 1000, connected = false;
|
|
211
|
+
var wakeLock = null;
|
|
212
|
+
|
|
213
|
+
// --- estado do "terminal" ---
|
|
214
|
+
var turn = null; // bloco do turno atual (div .turn)
|
|
215
|
+
var out = null; // <pre class="out"> do assistente em streaming
|
|
216
|
+
var thought = null; // linha de pensamento aberta (pode ser detalhes)
|
|
217
|
+
|
|
218
|
+
function esc(s) {
|
|
219
|
+
return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
|
|
220
|
+
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// --- Markdown minimo, sem dependencia ---
|
|
225
|
+
// Converte o SUBconjunto de markdown que o modelo costuma usar (headers,
|
|
226
|
+
// listas, code fences, inline code, negrito/italico, links, hr, blockquote).
|
|
227
|
+
// Seguro por construcao: todo texto e escapado ANTES de aplicar as tags, e
|
|
228
|
+
// os blocos de codigo sao capturados primeiro para nada dentro deles virar
|
|
229
|
+
// HTML. Nao usa innerHTML com conteudo cru do modelo.
|
|
230
|
+
function mdInline(s) {
|
|
231
|
+
s = esc(s);
|
|
232
|
+
// code inline primeiro (protege o conteudo).
|
|
233
|
+
var codes = [];
|
|
234
|
+
s = s.replace(/\`([^\`]+)\`/g, function (_, c) { codes.push(c); return "\u0000" + (codes.length - 1) + "\u0000"; });
|
|
235
|
+
s = s.replace(/\\*\\*([^*]+)\\*\\*/g, "<strong>$1</strong>");
|
|
236
|
+
s = s.replace(/__([^_]+)__/g, "<strong>$1</strong>");
|
|
237
|
+
s = s.replace(/(^|[^*])\\*([^*\\n]+)\\*/g, "$1<em>$2</em>");
|
|
238
|
+
s = s.replace(/(^|[^_])_([^_\\n]+)_/g, "$1<em>$2</em>");
|
|
239
|
+
s = s.replace(/\\[([^\\]]+)\\]\\((https?:\\/\\/[^\\s)]+)\\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
|
|
240
|
+
s = s.replace(/\u0000(\\d+)\u0000/g, function (_, i) { return "<code>" + codes[+i] + "</code>"; });
|
|
241
|
+
return s;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function mdToHtml(src) {
|
|
245
|
+
var text = String(src == null ? "" : src).replace(/\\r\\n?/g, "\\n");
|
|
246
|
+
var html = "", i = 0;
|
|
247
|
+
var lines = text.split("\\n");
|
|
248
|
+
var inCode = false, codeBuf = [], list = null;
|
|
249
|
+
|
|
250
|
+
function closeList() { if (list) { html += "</" + list + ">"; list = null; } }
|
|
251
|
+
|
|
252
|
+
for (i = 0; i < lines.length; i++) {
|
|
253
|
+
var line = lines[i];
|
|
254
|
+
var fence = line.match(/^\\s*\`\`\`(.*)$/);
|
|
255
|
+
if (fence) {
|
|
256
|
+
if (!inCode) { closeList(); inCode = true; codeBuf = []; }
|
|
257
|
+
else { html += "<pre><code>" + esc(codeBuf.join("\\n")) + "</code></pre>"; inCode = false; }
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (inCode) { codeBuf.push(line); continue; }
|
|
261
|
+
|
|
262
|
+
var h = line.match(/^(#{1,6})\\s+(.*)$/);
|
|
263
|
+
if (h) { closeList(); var lv = Math.min(h[1].length, 4); html += "<h" + lv + ">" + mdInline(h[2]) + "</h" + lv + ">"; continue; }
|
|
264
|
+
|
|
265
|
+
if (/^\\s*([-*_])(\\s*\\1){2,}\\s*$/.test(line)) { closeList(); html += "<hr>"; continue; }
|
|
266
|
+
|
|
267
|
+
var ul = line.match(/^\\s*[-*+]\\s+(.*)$/);
|
|
268
|
+
var ol = line.match(/^\\s*\\d+[.)]\\s+(.*)$/);
|
|
269
|
+
if (ul || ol) {
|
|
270
|
+
var want = ul ? "ul" : "ol";
|
|
271
|
+
if (list && list !== want) closeList();
|
|
272
|
+
if (!list) { list = want; html += "<" + want + ">"; }
|
|
273
|
+
html += "<li>" + mdInline((ul || ol)[1]) + "</li>";
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
closeList();
|
|
277
|
+
|
|
278
|
+
var bq = line.match(/^\\s*>\\s?(.*)$/);
|
|
279
|
+
if (bq) { html += "<blockquote>" + mdInline(bq[1]) + "</blockquote>"; continue; }
|
|
280
|
+
|
|
281
|
+
if (!line.trim()) { continue; }
|
|
282
|
+
html += "<p>" + mdInline(line) + "</p>";
|
|
283
|
+
}
|
|
284
|
+
if (inCode) html += "<pre><code>" + esc(codeBuf.join("\\n")) + "</code></pre>";
|
|
285
|
+
closeList();
|
|
286
|
+
return html;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Renderiza markdown "vivo": durante o streaming fecha cercas abertas para
|
|
290
|
+
// nao piscar HTML cru enquanto o bloco de codigo ainda esta chegando.
|
|
291
|
+
function renderMd(el, src) {
|
|
292
|
+
el.innerHTML = mdToHtml(src);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Pensamento: recolhido por padrao (so a 1a linha), clicavel para expandir.
|
|
296
|
+
function makeThoughtLine(text) {
|
|
297
|
+
var t = ensureTurn();
|
|
298
|
+
var el = document.createElement("div");
|
|
299
|
+
el.className = "ln think";
|
|
300
|
+
var chev = document.createElement("span");
|
|
301
|
+
chev.className = "chev";
|
|
302
|
+
chev.textContent = "▸";
|
|
303
|
+
var one = document.createElement("span");
|
|
304
|
+
one.className = "one";
|
|
305
|
+
one.textContent = firstLine(text);
|
|
306
|
+
var full = document.createElement("span");
|
|
307
|
+
full.className = "full";
|
|
308
|
+
full.textContent = text || "";
|
|
309
|
+
el.appendChild(chev); el.appendChild(one); el.appendChild(full);
|
|
310
|
+
el._one = one; el._full = full; el._chev = chev;
|
|
311
|
+
el.onclick = function () { el.classList.toggle("open"); chev.textContent = el.classList.contains("open") ? "▾" : "▸"; };
|
|
312
|
+
t.appendChild(el);
|
|
313
|
+
stick();
|
|
314
|
+
return el;
|
|
315
|
+
}
|
|
316
|
+
function firstLine(s) {
|
|
317
|
+
var line = String(s || "").split("\\n")[0];
|
|
318
|
+
return line.length > 90 ? line.slice(0, 90) + "…" : line;
|
|
319
|
+
}
|
|
320
|
+
function growThought(el, chunk) {
|
|
321
|
+
el._full.textContent += chunk;
|
|
322
|
+
el._one.textContent = firstLine(el._full.textContent);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function nearBottom() { return scrollEl.scrollHeight - scrollEl.scrollTop - scrollEl.clientHeight < 100; }
|
|
326
|
+
function pin() { scrollEl.scrollTop = scrollEl.scrollHeight; }
|
|
327
|
+
function stick() { if (nearBottom()) pin(); }
|
|
328
|
+
|
|
329
|
+
function newTurn() {
|
|
330
|
+
turn = document.createElement("div");
|
|
331
|
+
turn.className = "turn";
|
|
332
|
+
log.appendChild(turn);
|
|
333
|
+
return turn;
|
|
334
|
+
}
|
|
335
|
+
function ensureTurn() { return turn || newTurn(); }
|
|
336
|
+
|
|
337
|
+
function userLine(text) {
|
|
338
|
+
finishOut();
|
|
339
|
+
thought = null;
|
|
340
|
+
// Um novo prompt do usuario SEMPRE abre um turno novo: separa o resultado
|
|
341
|
+
// do turno anterior do novo (cada "❯ ..." tem sua propria saida embaixo).
|
|
342
|
+
if (turn && turn.childNodes.length) newTurn();
|
|
343
|
+
var t = ensureTurn();
|
|
344
|
+
var el = document.createElement("div");
|
|
345
|
+
el.className = "u";
|
|
346
|
+
el.textContent = text == null ? "" : String(text);
|
|
347
|
+
t.appendChild(el);
|
|
348
|
+
pin();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Linha discreta (tool/think/sys/err). Devolve o elemento pra poder evoluir.
|
|
352
|
+
function line(kind, text, meta) {
|
|
353
|
+
var t = ensureTurn();
|
|
354
|
+
var el = document.createElement("div");
|
|
355
|
+
el.className = "ln " + kind;
|
|
356
|
+
if (meta) {
|
|
357
|
+
var s = document.createElement("span");
|
|
358
|
+
s.className = "t";
|
|
359
|
+
s.textContent = meta + " ";
|
|
360
|
+
el.appendChild(s);
|
|
361
|
+
}
|
|
362
|
+
var body = document.createElement("span");
|
|
363
|
+
body.textContent = text == null ? "" : String(text);
|
|
364
|
+
el.appendChild(body);
|
|
365
|
+
el._body = body;
|
|
366
|
+
t.appendChild(el);
|
|
367
|
+
stick();
|
|
368
|
+
return el;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Garante o bloco de saida do assistente (um por turno) e devolve. E um div
|
|
372
|
+
// com markdown renderizado, nao um pre: durante o stream acumulamos o texto
|
|
373
|
+
// cru e re-renderizamos o markdown a cada chunk.
|
|
374
|
+
function ensureOut() {
|
|
375
|
+
if (out) return out;
|
|
376
|
+
var t = ensureTurn();
|
|
377
|
+
out = document.createElement("div");
|
|
378
|
+
out.className = "out md cursor";
|
|
379
|
+
out._raw = "";
|
|
380
|
+
t.appendChild(out);
|
|
381
|
+
return out;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Encerra o bloco de streaming: tira o cursor E marca que o proximo token
|
|
385
|
+
// deve abrir um bloco NOVO (ex.: nova resposta depois de uma tool).
|
|
386
|
+
function finishOut() {
|
|
387
|
+
if (out) out.classList.remove("cursor");
|
|
388
|
+
out = null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function setStatus(state) {
|
|
392
|
+
status.className = "dot " + (state === "on" ? "on" : state === "off" ? "off" : "");
|
|
393
|
+
connected = state === "on";
|
|
394
|
+
if (state === "off") { banner.className = ""; banner.textContent = "pc desconectado — reconectando…"; banner.style.display = "block"; }
|
|
395
|
+
else if (state === "on") {
|
|
396
|
+
// feedback explicito de pareamento/conexao bem-sucedidos.
|
|
397
|
+
banner.className = "on";
|
|
398
|
+
banner.textContent = "conectado ao terminal do pc";
|
|
399
|
+
banner.style.display = "block";
|
|
400
|
+
clearTimeout(banner._t);
|
|
401
|
+
banner._t = setTimeout(function () { banner.style.display = "none"; }, 2600);
|
|
402
|
+
} else { banner.style.display = "none"; }
|
|
403
|
+
}
|
|
404
|
+
function setBusy(on) { busyEl.textContent = on ? "trabalhando…" : ""; }
|
|
405
|
+
|
|
406
|
+
// Base do path onde a pagina esta montada (ex.: "/" ou "/s/7789/").
|
|
407
|
+
function basePath() {
|
|
408
|
+
var p = location.pathname || "/";
|
|
409
|
+
if (p.charAt(p.length - 1) !== "/") p = p.replace(/[^/]*$/, "");
|
|
410
|
+
return p || "/";
|
|
411
|
+
}
|
|
412
|
+
var BASE = basePath();
|
|
413
|
+
function url(path) {
|
|
414
|
+
return BASE.replace(/\\/$/, "") + "/" + String(path).replace(/^\\//, "");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function pair(code) {
|
|
418
|
+
if (!code) return false;
|
|
419
|
+
var res;
|
|
420
|
+
try {
|
|
421
|
+
res = await fetch(url("pair"), {
|
|
422
|
+
method: "POST", headers: { "content-type": "application/json" },
|
|
423
|
+
body: JSON.stringify({ code: code, device: navigator.platform || "celular" }),
|
|
424
|
+
});
|
|
425
|
+
} catch (e) {
|
|
426
|
+
pairErr("sem ligação com o servidor — confira a URL");
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
if (!res.ok) { pairErr("código inválido ou expirado — gere outro no PC"); return false; }
|
|
430
|
+
var data = await res.json();
|
|
431
|
+
token = data.token;
|
|
432
|
+
sessionStorage.setItem("kafox_token", token);
|
|
433
|
+
sid.textContent = (data.session && data.session.sessionId) || "";
|
|
434
|
+
showPairing(false);
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function showPairing(on) { $("pairing").classList[on ? "add" : "remove"]("show"); }
|
|
439
|
+
function pairErr(msg) { $("pairerr").textContent = msg || ""; }
|
|
440
|
+
|
|
441
|
+
// Reconstroi o log a partir do historico (um turno por mensagem do usuario,
|
|
442
|
+
// com as respostas/tools/pensamentos abaixo).
|
|
443
|
+
function renderHistory(messages) {
|
|
444
|
+
log.innerHTML = "";
|
|
445
|
+
turn = null; out = null; thought = null;
|
|
446
|
+
(messages || []).forEach(function (m) {
|
|
447
|
+
if (m.role === "user") { userLine(m.text); }
|
|
448
|
+
else if (m.role === "assistant") {
|
|
449
|
+
var t = ensureTurn();
|
|
450
|
+
var d = document.createElement("div");
|
|
451
|
+
d.className = "out md";
|
|
452
|
+
d._raw = m.text || "";
|
|
453
|
+
renderMd(d, d._raw);
|
|
454
|
+
t.appendChild(d);
|
|
455
|
+
out = null;
|
|
456
|
+
} else if (m.role === "thinking") { var th = makeThoughtLine(m.text); }
|
|
457
|
+
else if (m.role === "system") line("err", m.text);
|
|
458
|
+
else if (m.role === "compaction") line("compact", "contexto compactado (" + (m.compaction?.before ?? "?") + " → " + (m.compaction?.after ?? "?") + ")");
|
|
459
|
+
else if (m.role === "tool" && m.tool) toolLine(m.tool, true);
|
|
460
|
+
});
|
|
461
|
+
pin();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function toolLine(tool, done) {
|
|
465
|
+
var kind = !done || tool.status === "running" ? "tool" : (tool.ok === false ? "tool err" : "tool ok");
|
|
466
|
+
var label = tool.label || tool.name || "tool";
|
|
467
|
+
var el = line(kind, label, done && tool.summary ? tool.summary + " " : (tool.summary || null));
|
|
468
|
+
if (tool.change && tool.change.diff) {
|
|
469
|
+
var pre = document.createElement("pre");
|
|
470
|
+
pre.textContent = tool.change.diff;
|
|
471
|
+
el.appendChild(pre);
|
|
472
|
+
}
|
|
473
|
+
return el;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// --- aplica um evento vindo do servidor ---
|
|
477
|
+
function applyEvent(ev) {
|
|
478
|
+
if (ev.seq) seq = Math.max(seq, ev.seq);
|
|
479
|
+
switch (ev.type) {
|
|
480
|
+
case "user":
|
|
481
|
+
// Se a TUI (PC) mandou o prompt, ainda nao temos o turno: abre um.
|
|
482
|
+
userLine(ev.text); setBusy(true); break;
|
|
483
|
+
|
|
484
|
+
case "stream-start":
|
|
485
|
+
finishOut(); setBusy(true); break;
|
|
486
|
+
|
|
487
|
+
case "token": {
|
|
488
|
+
// Append no MESMO bloco (nunca cria card por chunk) e re-renderiza o
|
|
489
|
+
// markdown acumulado. O cursor some sozinho no "done".
|
|
490
|
+
var pre = ensureOut();
|
|
491
|
+
pre._raw += ev.text;
|
|
492
|
+
renderMd(pre, pre._raw);
|
|
493
|
+
pre.classList.add("cursor");
|
|
494
|
+
stick();
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
case "thought": {
|
|
499
|
+
// Pensamento vira UMA linha recolhivel que cresce (nao N linhas).
|
|
500
|
+
if (!thought) thought = makeThoughtLine("");
|
|
501
|
+
growThought(thought, ev.text);
|
|
502
|
+
stick();
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
case "tool-start":
|
|
507
|
+
finishOut(); thought = null;
|
|
508
|
+
toolLine({ name: ev.name, label: ev.label, change: ev.change, status: "running" }, false);
|
|
509
|
+
stick(); break;
|
|
510
|
+
|
|
511
|
+
case "tool-result": {
|
|
512
|
+
thought = null;
|
|
513
|
+
var meta = (ev.ms != null ? "(" + ev.ms + "ms" + (ev.ok ? "" : " · falhou") + ") " : "");
|
|
514
|
+
line(ev.ok ? "tool ok" : "tool err", (ev.summary || ev.name || ""), meta);
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
case "approval-request":
|
|
519
|
+
finishOut(); renderApproval(ev); break;
|
|
520
|
+
|
|
521
|
+
case "approval-resolved":
|
|
522
|
+
var ap = log.querySelector('[data-ap="' + ev.id + '"]');
|
|
523
|
+
if (ap) ap.remove();
|
|
524
|
+
break;
|
|
525
|
+
|
|
526
|
+
case "done": {
|
|
527
|
+
var cur = out; // guarda antes de zerar
|
|
528
|
+
finishOut(); thought = null; setBusy(false);
|
|
529
|
+
if (ev.text) {
|
|
530
|
+
if (cur && cur._raw) {
|
|
531
|
+
cur._raw = ev.text.length > cur._raw.length ? ev.text : cur._raw;
|
|
532
|
+
renderMd(cur, cur._raw);
|
|
533
|
+
} else {
|
|
534
|
+
var o = ensureOut();
|
|
535
|
+
o._raw = ev.text;
|
|
536
|
+
renderMd(o, ev.text);
|
|
537
|
+
finishOut();
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (cur) cur.classList.remove("cursor");
|
|
541
|
+
pin(); break;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
case "aborted":
|
|
545
|
+
finishOut(); thought = null; setBusy(false); line("sys", "interrompido."); break;
|
|
546
|
+
|
|
547
|
+
case "error":
|
|
548
|
+
finishOut(); thought = null; setBusy(false); line("err", ev.text); break;
|
|
549
|
+
|
|
550
|
+
case "retry":
|
|
551
|
+
line("sys", "reconectando ao modelo… " + (ev.attempt ? "(tentativa " + ev.attempt + ")" : ""));
|
|
552
|
+
break;
|
|
553
|
+
|
|
554
|
+
case "compact":
|
|
555
|
+
line("compact", "contexto compactado (" + ev.before + " → " + ev.after + ")");
|
|
556
|
+
break;
|
|
557
|
+
|
|
558
|
+
case "subagent":
|
|
559
|
+
line("tool", "subagente " + (ev.index || "?") + "/" + (ev.total || "?") + " " + (ev.text || ev.objective || ev.phase || ""));
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function renderApproval(ev) {
|
|
565
|
+
var t = ensureTurn();
|
|
566
|
+
var el = document.createElement("div");
|
|
567
|
+
el.className = "approval";
|
|
568
|
+
el.setAttribute("data-ap", ev.id);
|
|
569
|
+
var head = document.createElement("div");
|
|
570
|
+
head.className = "head";
|
|
571
|
+
head.innerHTML = "<strong>🔐 aprovar</strong> " + esc(ev.name);
|
|
572
|
+
el.appendChild(head);
|
|
573
|
+
var detail = ev.change && ev.change.diff ? ev.change.diff : JSON.stringify(ev.args || {}, null, 2);
|
|
574
|
+
var pre = document.createElement("pre"); pre.textContent = detail; el.appendChild(pre);
|
|
575
|
+
var actions = document.createElement("div"); actions.className = "actions";
|
|
576
|
+
[["once", "aprovar", "primary"], ["always", "sempre", ""], ["deny", "negar", "danger"]].forEach(function (a) {
|
|
577
|
+
var b = document.createElement("button"); b.textContent = a[1]; b.className = a[2];
|
|
578
|
+
b.onclick = function () { decide(ev.id, a[0], el); };
|
|
579
|
+
actions.appendChild(b);
|
|
580
|
+
});
|
|
581
|
+
el.appendChild(actions);
|
|
582
|
+
t.appendChild(el); pin();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async function decide(id, decision, el) {
|
|
586
|
+
el.style.opacity = "0.6";
|
|
587
|
+
await fetch(url("approve"), {
|
|
588
|
+
method: "POST", headers: { "content-type": "application/json", "x-kafox-token": token },
|
|
589
|
+
body: JSON.stringify({ id: id, decision: decision }),
|
|
590
|
+
}).catch(function () {});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// --- conexao por long-polling (/poll) ---
|
|
594
|
+
// O Cloudflare segura/aborta respostas SSE vindas de navegadores, mas entrega
|
|
595
|
+
// normalmente uma resposta JSON que fecha sozinha. Entao fazemos um loop:
|
|
596
|
+
// pede os eventos novos (ou espera ate ~15s) e repete.
|
|
597
|
+
var aborter = null, waitTimer = null, polling = false;
|
|
598
|
+
|
|
599
|
+
function connect() {
|
|
600
|
+
if (waitTimer) { clearTimeout(waitTimer); waitTimer = null; }
|
|
601
|
+
if (polling) return;
|
|
602
|
+
polling = true; pump();
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function pump() {
|
|
606
|
+
while (polling) {
|
|
607
|
+
if (!token) { polling = false; setStatus("off"); return; }
|
|
608
|
+
var u = url("poll") + "?since=" + seq + "&t=" + encodeURIComponent(token);
|
|
609
|
+
aborter = new AbortController();
|
|
610
|
+
try {
|
|
611
|
+
var res = await fetch(u, { cache: "no-store", signal: aborter.signal });
|
|
612
|
+
if (res.status === 401) { sessionStorage.removeItem("kafox_token"); token = ""; polling = false; setStatus("off"); return; }
|
|
613
|
+
if (!res.ok) throw new Error("poll " + res.status);
|
|
614
|
+
var data = await res.json();
|
|
615
|
+
setStatus("on"); backoff = 1000;
|
|
616
|
+
(data.events || []).forEach(function (ev) { try { applyEvent(ev); } catch (_) {} });
|
|
617
|
+
if (typeof data.seq === "number") seq = Math.max(seq, data.seq);
|
|
618
|
+
} catch (err) {
|
|
619
|
+
if (aborter && aborter.signal.aborted) return;
|
|
620
|
+
setStatus("off");
|
|
621
|
+
await new Promise(function (r) { waitTimer = setTimeout(r, backoff); });
|
|
622
|
+
backoff = Math.min(backoff * 2, 15000);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async function loadState() {
|
|
628
|
+
var res = await fetch(url("state") + "?t=" + encodeURIComponent(token));
|
|
629
|
+
if (res.status === 401) { sessionStorage.removeItem("kafox_token"); token = ""; return false; }
|
|
630
|
+
if (!res.ok) return false;
|
|
631
|
+
var s = await res.json();
|
|
632
|
+
sid.textContent = s.sessionId || "";
|
|
633
|
+
seq = s.seq || 0;
|
|
634
|
+
setBusy(Boolean(s.busy));
|
|
635
|
+
renderHistory(s.messages);
|
|
636
|
+
if (s.pendingApproval) renderApproval(s.pendingApproval);
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async function send() {
|
|
641
|
+
var text = input.value.trim();
|
|
642
|
+
if (!text) return;
|
|
643
|
+
input.value = "";
|
|
644
|
+
autoGrow();
|
|
645
|
+
var res = await fetch(url("prompt"), {
|
|
646
|
+
method: "POST", headers: { "content-type": "application/json", "x-kafox-token": token },
|
|
647
|
+
body: JSON.stringify({ text: text }),
|
|
648
|
+
});
|
|
649
|
+
if (res.status === 409) line("sys", "o agente ainda está ocupado nesse turno.");
|
|
650
|
+
if (res.status === 401) line("err", "sessão expirada — escaneie o QR de novo.");
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function autoGrow() { input.style.height = "auto"; input.style.height = Math.min(input.scrollHeight, 120) + "px"; }
|
|
654
|
+
|
|
655
|
+
$("send").onclick = send;
|
|
656
|
+
$("stop").onclick = function () {
|
|
657
|
+
fetch(url("abort"), { method: "POST", headers: { "x-kafox-token": token } }).catch(function () {});
|
|
658
|
+
};
|
|
659
|
+
input.addEventListener("input", autoGrow);
|
|
660
|
+
input.addEventListener("keydown", function (e) {
|
|
661
|
+
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); }
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
async function boot() {
|
|
665
|
+
var m = location.hash.match(/p=([A-Za-z0-9]+)/);
|
|
666
|
+
if (m) {
|
|
667
|
+
await pair(m[1]);
|
|
668
|
+
history.replaceState(null, "", location.pathname);
|
|
669
|
+
}
|
|
670
|
+
if (!token) {
|
|
671
|
+
// Sem token (nem por QR, nem salvo): mostra a tela de pareamento manual.
|
|
672
|
+
showPairing(true);
|
|
673
|
+
$("paircode").focus();
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
var okState = await loadState();
|
|
677
|
+
if (!okState) { showPairing(true); $("paircode").focus(); return; }
|
|
678
|
+
connect();
|
|
679
|
+
try { if (navigator.wakeLock) wakeLock = await navigator.wakeLock.request("screen"); } catch (_) {}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Pareamento manual: o usuario digita o codigo da tela do PC.
|
|
683
|
+
async function pairManual() {
|
|
684
|
+
var code = $("paircode").value.trim().toUpperCase();
|
|
685
|
+
if (!code) { pairErr("digite o código"); return; }
|
|
686
|
+
pairErr("");
|
|
687
|
+
var ok = await pair(code);
|
|
688
|
+
if (ok) {
|
|
689
|
+
var okState = await loadState();
|
|
690
|
+
if (okState) { connect(); }
|
|
691
|
+
else pairErr("pareou, mas a sessão não respondeu — tente de novo");
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
$("pairbtn").onclick = pairManual;
|
|
695
|
+
$("paircode").addEventListener("input", function () {
|
|
696
|
+
this.value = this.value.toUpperCase().replace(/[^A-Z0-9]/g, "");
|
|
697
|
+
});
|
|
698
|
+
$("paircode").addEventListener("keydown", function (e) { if (e.key === "Enter") pairManual(); });
|
|
699
|
+
|
|
700
|
+
// --- tamanho da fonte (A- / A+) ---
|
|
701
|
+
var FS_MIN = 11, FS_MAX = 24, FS_STEP = 1, FS_DEFAULT = 14;
|
|
702
|
+
var fsPx = FS_DEFAULT;
|
|
703
|
+
try {
|
|
704
|
+
var savedFs = parseInt(localStorage.getItem("kafox_fs"), 10);
|
|
705
|
+
if (savedFs >= FS_MIN && savedFs <= FS_MAX) fsPx = savedFs;
|
|
706
|
+
} catch (_) {}
|
|
707
|
+
function applyFs() {
|
|
708
|
+
document.documentElement.style.setProperty("--fs", fsPx + "px");
|
|
709
|
+
var minus = document.getElementById("fsMinus");
|
|
710
|
+
var plus = document.getElementById("fsPlus");
|
|
711
|
+
if (minus) minus.disabled = fsPx <= FS_MIN;
|
|
712
|
+
if (plus) plus.disabled = fsPx >= FS_MAX;
|
|
713
|
+
}
|
|
714
|
+
function bumpFs(delta) { fsPx = Math.max(FS_MIN, Math.min(FS_MAX, fsPx + delta)); applyFs(); }
|
|
715
|
+
function saveFs() { try { localStorage.setItem("kafox_fs", String(fsPx)); } catch (_) {} }
|
|
716
|
+
document.getElementById("fsMinus").onclick = function () { bumpFs(-FS_STEP); saveFs(); };
|
|
717
|
+
document.getElementById("fsPlus").onclick = function () { bumpFs(FS_STEP); saveFs(); };
|
|
718
|
+
applyFs();
|
|
719
|
+
|
|
720
|
+
document.addEventListener("visibilitychange", function () {
|
|
721
|
+
if (document.visibilityState === "visible") {
|
|
722
|
+
if (navigator.wakeLock && !wakeLock) navigator.wakeLock.request("screen").then(function (l) { wakeLock = l; }).catch(function () {});
|
|
723
|
+
if (!polling && !connected) connect();
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
boot();
|
|
728
|
+
})();
|
|
729
|
+
</script>
|
|
730
|
+
</body>
|
|
731
|
+
</html>`;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
module.exports = { renderPage };
|