primocode 7.13.0 → 7.14.1
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/primocode.js +100 -2
- package/lib/desktop.js +31 -16
- package/lib/streaming.js +234 -101
- package/lib/ui.js +3 -3
- package/package.json +1 -1
package/bin/primocode.js
CHANGED
|
@@ -78,6 +78,53 @@ ${c.bold('Onde ele trabalha')}
|
|
|
78
78
|
${c.dim('Pasta de projeto nova só nasce quando ele vai mexer em arquivo.')}
|
|
79
79
|
`;
|
|
80
80
|
|
|
81
|
+
// ── Autopreenchimento dos comandos (estilo Claude Code) ────────────────
|
|
82
|
+
// Metade do comando já serve: Tab completa, e um prefixo único executa
|
|
83
|
+
// direto — "/str" vira "/streaming" sozinho. A lista é só a dos nomes
|
|
84
|
+
// canônicos; os apelidos (/cd, /stream…) continuam valendo por extenso.
|
|
85
|
+
const SLASH_COMANDOS = [
|
|
86
|
+
['/help', 'todos os comandos'],
|
|
87
|
+
['/project', 'lista/cria projetos em ~/primocode'],
|
|
88
|
+
['/dir', 'trabalhar numa pasta sua'],
|
|
89
|
+
['/mode', 'primocode | chat | auto'],
|
|
90
|
+
['/model', 'top | main | fast'],
|
|
91
|
+
['/attach', 'anexa arquivo ao pedido'],
|
|
92
|
+
['/detach', 'remove os anexos'],
|
|
93
|
+
['/files', 'lista os anexos'],
|
|
94
|
+
['/tudo', 'tudo que ele sabe fazer'],
|
|
95
|
+
['/tree', 'árvore do projeto'],
|
|
96
|
+
['/read', 'mostra um arquivo'],
|
|
97
|
+
['/run', 'roda um comando'],
|
|
98
|
+
['/open', 'abre no navegador'],
|
|
99
|
+
['/streaming', 'painel ao vivo — vídeo dele trabalhando'],
|
|
100
|
+
['/status', 'checa o servidor'],
|
|
101
|
+
['/name', 'como ele te chama'],
|
|
102
|
+
['/mascot', 'primo | fox | cat'],
|
|
103
|
+
['/config', 'suas preferências'],
|
|
104
|
+
['/clear', 'limpa a conversa'],
|
|
105
|
+
['/cls', 'limpa a tela'],
|
|
106
|
+
['/uninstall', 'remove o PrimoCode'],
|
|
107
|
+
['/version', 'versão'],
|
|
108
|
+
['/exit', 'sair'],
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
// O completer do readline: Tab completa o que der. Só age na palavra do
|
|
112
|
+
// comando — depois do primeiro espaço o argumento é seu, não dele.
|
|
113
|
+
function completarSlash(line) {
|
|
114
|
+
if (!line.startsWith('/') || /\s/.test(line)) return [[], line];
|
|
115
|
+
const baixa = line.toLowerCase();
|
|
116
|
+
const acertos = SLASH_COMANDOS.map(([nome]) => nome).filter((nome) => nome.startsWith(baixa));
|
|
117
|
+
return [acertos, line];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function mostrarMenuSlash() {
|
|
121
|
+
console.log('');
|
|
122
|
+
for (const [nome, desc] of SLASH_COMANDOS) {
|
|
123
|
+
console.log(` ${c.brand(ui.padRight(nome, 12))}${c.muted(desc)}`);
|
|
124
|
+
}
|
|
125
|
+
console.log(c.dim('\n Tab completa · metade do comando já serve: /str vira /streaming'));
|
|
126
|
+
}
|
|
127
|
+
|
|
81
128
|
let rl = null;
|
|
82
129
|
|
|
83
130
|
function makePrompt() {
|
|
@@ -605,7 +652,26 @@ async function handleSlash(text) {
|
|
|
605
652
|
// rl.close() em vez de process.exit(): sair na marra descarta o que
|
|
606
653
|
// ainda estava no buffer de saída.
|
|
607
654
|
case 'exit': case 'quit': if (rl) rl.close(); break;
|
|
608
|
-
default:
|
|
655
|
+
default: {
|
|
656
|
+
// "/" sozinho abre o menu; um prefixo único autopreenche e executa
|
|
657
|
+
// ("/str" → /streaming); prefixo ambíguo mostra as opções.
|
|
658
|
+
if (!cmd) { mostrarMenuSlash(); break; }
|
|
659
|
+
const casam = SLASH_COMANDOS.map(([nome]) => nome.slice(1)).filter((n) => n.startsWith(cmd.toLowerCase()));
|
|
660
|
+
if (casam.length === 1) {
|
|
661
|
+
console.log(c.dim(` → /${casam[0]}`));
|
|
662
|
+
await handleSlash('/' + casam[0] + (rest ? ' ' + rest : ''));
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
if (casam.length > 1) {
|
|
666
|
+
console.log(c.muted(` /${cmd}… pode ser:`));
|
|
667
|
+
for (const n of casam) {
|
|
668
|
+
const [, desc] = SLASH_COMANDOS.find(([nome]) => nome === '/' + n);
|
|
669
|
+
console.log(` ${c.brand(ui.padRight('/' + n, 12))}${c.dim(desc)}`);
|
|
670
|
+
}
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
console.log(c.err(`Comando desconhecido: /${cmd}. Digite / para ver o menu.`));
|
|
674
|
+
}
|
|
609
675
|
}
|
|
610
676
|
}
|
|
611
677
|
|
|
@@ -682,7 +748,7 @@ async function main() {
|
|
|
682
748
|
userName: state.config.userName,
|
|
683
749
|
pastaPadrao: state.workDirOrigem === 'padrao',
|
|
684
750
|
}));
|
|
685
|
-
rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: makePrompt() });
|
|
751
|
+
rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: makePrompt(), completer: completarSlash });
|
|
686
752
|
rl.prompt();
|
|
687
753
|
|
|
688
754
|
// Fila: o readline continua emitindo 'line' enquanto um pedido está sendo
|
|
@@ -692,6 +758,38 @@ async function main() {
|
|
|
692
758
|
let closed = false;
|
|
693
759
|
let saidBye = false;
|
|
694
760
|
|
|
761
|
+
// ── Sugestão fantasma: o autopreenchimento VISÍVEL enquanto digita ──
|
|
762
|
+
// "/str" mostra "eaming" esmaecido na frente do cursor, como o Claude
|
|
763
|
+
// Code. Seta → (ou Tab) aceita; Enter com o pedaço já executa. Desenhada
|
|
764
|
+
// DEPOIS de o readline processar a tecla (setImmediate), por cima do que
|
|
765
|
+
// sobrou da linha (\\x1b[0K limpa), e o cursor volta para o lugar.
|
|
766
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
767
|
+
let fantasma = '';
|
|
768
|
+
const desenharFantasma = () => {
|
|
769
|
+
if (busy || pendingAsk || !rl) return;
|
|
770
|
+
const linha = rl.line;
|
|
771
|
+
const noFim = rl.cursor === linha.length;
|
|
772
|
+
fantasma = '';
|
|
773
|
+
if (noFim && linha.startsWith('/') && !/\s/.test(linha) && linha.length > 1) {
|
|
774
|
+
const baixa = linha.toLowerCase();
|
|
775
|
+
const hit = SLASH_COMANDOS.map(([n]) => n).find((n) => n.startsWith(baixa) && n.length > linha.length);
|
|
776
|
+
if (hit) fantasma = hit.slice(linha.length);
|
|
777
|
+
}
|
|
778
|
+
if (!noFim) return; // cursor no meio: não mexe no que vem depois dele
|
|
779
|
+
// Limpa resto da linha (fantasma antigo) e desenha o novo, voltando o cursor.
|
|
780
|
+
process.stdout.write('\x1b[0K' + (fantasma ? `\x1b[2m${fantasma}\x1b[0m\x1b[${fantasma.length}D` : ''));
|
|
781
|
+
};
|
|
782
|
+
process.stdin.on('keypress', (ch, key) => {
|
|
783
|
+
// Seta → no fim da linha aceita a sugestão inteira.
|
|
784
|
+
if (key && key.name === 'right' && fantasma && rl && rl.cursor === rl.line.length) {
|
|
785
|
+
const resto = fantasma;
|
|
786
|
+
fantasma = '';
|
|
787
|
+
rl.write(resto);
|
|
788
|
+
}
|
|
789
|
+
setImmediate(desenharFantasma);
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
|
|
695
793
|
function sayBye() {
|
|
696
794
|
if (saidBye) return;
|
|
697
795
|
saidBye = true;
|
package/lib/desktop.js
CHANGED
|
@@ -659,25 +659,40 @@ function clickElement(nome, opts = {}) {
|
|
|
659
659
|
}
|
|
660
660
|
|
|
661
661
|
// ── Screenshot ────────────────────────────────────────
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
662
|
+
// A ferramenta do Linux é resolvida UMA vez: o vídeo do /streaming chama isto
|
|
663
|
+
// várias vezes por segundo, e um `which` por frame seria desperdício.
|
|
664
|
+
let LINUX_SHOT;
|
|
665
|
+
function linuxShot() {
|
|
666
|
+
if (LINUX_SHOT === undefined) LINUX_SHOT = firstCmd(['scrot', 'maim', 'gnome-screenshot', 'spectacle', 'import']);
|
|
667
|
+
return LINUX_SHOT;
|
|
668
|
+
}
|
|
665
669
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
670
|
+
/**
|
|
671
|
+
* O comando de shell que fotografa a tela para `filename` — ou null quando a
|
|
672
|
+
* plataforma não tem como. Existe separado do screenshot() para o painel do
|
|
673
|
+
* /streaming capturar em LOOP, assíncrono, sem travar o agente.
|
|
674
|
+
*/
|
|
675
|
+
function screenshotCommand(filename) {
|
|
676
|
+
if (PLATFORM === 'darwin') return `screencapture -x "${filename}"`;
|
|
669
677
|
if (PLATFORM === 'win32') {
|
|
670
678
|
const ps = `Add-Type -AssemblyName System.Drawing; Add-Type -AssemblyName System.Windows.Forms; $b=[System.Windows.Forms.Screen]::PrimaryScreen.Bounds; $bmp=New-Object System.Drawing.Bitmap($b.Width,$b.Height); $g=[System.Drawing.Graphics]::FromImage($bmp); $g.CopyFromScreen($b.Location,[System.Drawing.Point]::Empty,$bmp.Size); $bmp.Save('${filename.replace(/'/g, "''")}'); $g.Dispose(); $bmp.Dispose()`;
|
|
671
|
-
return
|
|
679
|
+
return pwshCmd(ps);
|
|
672
680
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
if (
|
|
676
|
-
if (
|
|
677
|
-
if (
|
|
678
|
-
if (
|
|
679
|
-
|
|
680
|
-
|
|
681
|
+
const tool = linuxShot();
|
|
682
|
+
if (tool === 'scrot') return `scrot -o "${filename}"`;
|
|
683
|
+
if (tool === 'maim') return `maim "${filename}"`;
|
|
684
|
+
if (tool === 'gnome-screenshot') return `gnome-screenshot -f "${filename}"`;
|
|
685
|
+
if (tool === 'spectacle') return `spectacle -b -n -o "${filename}"`;
|
|
686
|
+
if (tool === 'import') return `import -window root "${filename}"`; // ImageMagick
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function screenshot(saveTo) {
|
|
691
|
+
const filename = saveTo || `desktop-${Date.now()}.png`;
|
|
692
|
+
const cmd = screenshotCommand(filename);
|
|
693
|
+
if (!cmd) return { ok: false, error: 'Screenshot indisponível no Linux. Instale scrot, maim, gnome-screenshot, spectacle ou ImageMagick (import).' };
|
|
694
|
+
const r = run(cmd);
|
|
695
|
+
return r.ok ? { ok: true, path: filename, bytes: sizeOf(filename) } : r;
|
|
681
696
|
}
|
|
682
697
|
|
|
683
698
|
function sizeOf(f) {
|
|
@@ -719,7 +734,7 @@ module.exports = {
|
|
|
719
734
|
click, doubleClick, move, drag, scroll,
|
|
720
735
|
type, key,
|
|
721
736
|
openApp, listWindows, focusWindow, uiElements, clickElement,
|
|
722
|
-
screenshot, getScreenSize,
|
|
737
|
+
screenshot, screenshotCommand, getScreenSize,
|
|
723
738
|
TOOLS,
|
|
724
739
|
// Puras, exportadas para o test/desktop.test.js provar a tradução de teclas
|
|
725
740
|
// e a escolha de elemento sem depender do SO da máquina de teste.
|
package/lib/streaming.js
CHANGED
|
@@ -19,6 +19,7 @@ const http = require('http');
|
|
|
19
19
|
const fs = require('fs');
|
|
20
20
|
const path = require('path');
|
|
21
21
|
const os = require('os');
|
|
22
|
+
const { exec } = require('child_process');
|
|
22
23
|
const desktop = require('./desktop');
|
|
23
24
|
|
|
24
25
|
const DIR = path.join(os.homedir(), '.primocode', 'streaming');
|
|
@@ -44,8 +45,68 @@ const state = {
|
|
|
44
45
|
erroFoto: null,
|
|
45
46
|
ultimaFoto: 0,
|
|
46
47
|
passo: 0,
|
|
48
|
+
// ── vídeo ao vivo ──
|
|
49
|
+
liveSeq: 0, // sobe a cada frame novo; a página troca a imagem quando muda
|
|
50
|
+
liveAt: 0, // quando o último frame de vídeo foi capturado
|
|
51
|
+
viewerAt: 0, // último sinal de que alguém está com a página aberta
|
|
52
|
+
liveTimer: null,
|
|
53
|
+
liveBusy: false, // uma captura em voo por vez — sem isso cada poll da página
|
|
54
|
+
// abria OUTRA cadeia de captura e elas se multiplicavam
|
|
47
55
|
};
|
|
48
56
|
|
|
57
|
+
const LIVE = () => path.join(DIR, 'live.png');
|
|
58
|
+
const LIVE_TMP = () => path.join(DIR, 'live-tmp.png');
|
|
59
|
+
|
|
60
|
+
// ── O vídeo: um loop de captura enquanto alguém assiste ──────────────────
|
|
61
|
+
// "Vídeo" aqui é o jeito honesto de fazer sem dependência: fotografar a tela
|
|
62
|
+
// em sequência (2 a 3 quadros por segundo, conforme a máquina aguenta) e a
|
|
63
|
+
// página trocar a imagem no ritmo — como um VNC simples. O loop é assíncrono
|
|
64
|
+
// (exec, nunca execSync) para não roubar tempo do agente, escreve num tmp e
|
|
65
|
+
// renomeia (troca atômica: o navegador nunca lê um PNG pela metade), e SÓ
|
|
66
|
+
// roda enquanto a página deu sinal de vida há menos de 8s — painel fechado
|
|
67
|
+
// não custa nada.
|
|
68
|
+
function viuAgora() {
|
|
69
|
+
state.viewerAt = Date.now();
|
|
70
|
+
agendarVideo(0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function agendarVideo(ms) {
|
|
74
|
+
if (!state.on || state.liveTimer || state.liveBusy) return;
|
|
75
|
+
state.liveTimer = setTimeout(cicloVideo, ms);
|
|
76
|
+
state.liveTimer.unref();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function cicloVideo() {
|
|
80
|
+
state.liveTimer = null;
|
|
81
|
+
if (!state.on || state.liveBusy) return;
|
|
82
|
+
if (Date.now() - state.viewerAt > 8000) return; // ninguém olhando — o próximo /state religa
|
|
83
|
+
|
|
84
|
+
const cmd = desktop.screenshotCommand(LIVE_TMP());
|
|
85
|
+
if (!cmd) { if (!state.erroFoto) state.erroFoto = 'screenshot indisponível nesta máquina'; return; }
|
|
86
|
+
|
|
87
|
+
const t0 = Date.now();
|
|
88
|
+
state.liveBusy = true;
|
|
89
|
+
exec(cmd, { timeout: 8000 }, (err) => {
|
|
90
|
+
state.liveBusy = false;
|
|
91
|
+
const dur = Date.now() - t0;
|
|
92
|
+
if (!err) {
|
|
93
|
+
try {
|
|
94
|
+
// Rename é atômico; no Windows ele pode falhar se o navegador
|
|
95
|
+
// estiver lendo o arquivo NAQUELE instante — aí copia por cima.
|
|
96
|
+
try { fs.renameSync(LIVE_TMP(), LIVE()); }
|
|
97
|
+
catch { fs.copyFileSync(LIVE_TMP(), LIVE()); try { fs.unlinkSync(LIVE_TMP()); } catch {} }
|
|
98
|
+
state.liveSeq++;
|
|
99
|
+
state.liveAt = Date.now();
|
|
100
|
+
state.erroFoto = null;
|
|
101
|
+
} catch {}
|
|
102
|
+
} else if (!state.erroFoto) {
|
|
103
|
+
state.erroFoto = corta(String(err.message || err), 120);
|
|
104
|
+
}
|
|
105
|
+
// Máquina rápida ≈ 3 quadros/s; máquina lenta desacelera sozinha.
|
|
106
|
+
agendarVideo(Math.max(280, Math.min(1200, dur)));
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
49
110
|
// Ações cujo efeito aparece NA TELA — são as que merecem uma foto nova.
|
|
50
111
|
const ACOES_NA_TELA = new Set([
|
|
51
112
|
'desktop_click', 'desktop_double_click', 'desktop_click_element',
|
|
@@ -179,6 +240,20 @@ function capturar(entrada) {
|
|
|
179
240
|
|
|
180
241
|
const id = state.proximoFrame;
|
|
181
242
|
const arquivo = path.join(DIR, `frame-${id}.png`);
|
|
243
|
+
|
|
244
|
+
// Com o vídeo rodando, o quadro mais novo já está no disco — copiar é
|
|
245
|
+
// grátis e não fotografa duas vezes a mesma tela.
|
|
246
|
+
if (agora - state.liveAt < 900) {
|
|
247
|
+
try { fs.copyFileSync(LIVE(), arquivo); } catch { return null; }
|
|
248
|
+
state.proximoFrame++;
|
|
249
|
+
state.frames.push({ id, hora: entrada.hora, acao: entrada.acao, aba: entrada.aba });
|
|
250
|
+
while (state.frames.length > MAX_FRAMES) {
|
|
251
|
+
const velho = state.frames.shift();
|
|
252
|
+
try { fs.unlinkSync(path.join(DIR, `frame-${velho.id}.png`)); } catch {}
|
|
253
|
+
}
|
|
254
|
+
return id;
|
|
255
|
+
}
|
|
256
|
+
|
|
182
257
|
const r = desktop.screenshot(arquivo);
|
|
183
258
|
if (!r.ok) { if (!state.erroFoto) state.erroFoto = r.error; return null; }
|
|
184
259
|
|
|
@@ -205,13 +280,13 @@ const MASCOTE_SVG = `<svg viewBox="0 0 9 3.6" aria-hidden="true">
|
|
|
205
280
|
<g fill="#0b1220"><rect x="3" y=".35" width=".8" height=".5" rx=".1"/><rect x="5.2" y=".35" width=".8" height=".5" rx=".1"/><rect x="3.9" y="1.25" width="1.2" height=".28" rx=".14"/></g>
|
|
206
281
|
</svg>`;
|
|
207
282
|
|
|
208
|
-
// A tela
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
283
|
+
// A tela do painel, na identidade do app — a MESMA paleta do design system do
|
|
284
|
+
// CLI (lib/ui.js): primária cyan #22d3ee, neutros slate, rose para falha. A
|
|
285
|
+
// estrutura é a do desenho do dono: mascote inclinado no canto, "Primocode"
|
|
286
|
+
// no alto, a janela com as bolinhas e as abas separadas por "|", a tela
|
|
287
|
+
// embaixo. Dentro da tela roda o VÍDEO ao vivo (o loop de captura acima); as
|
|
288
|
+
// abas em cima são tudo que o agente abriu — clicáveis, cada uma com seus
|
|
289
|
+
// quadros e passos; e a barra de comando autopreenche a instrução digitando.
|
|
215
290
|
const FAVICON = 'data:image/svg+xml,' + encodeURIComponent(
|
|
216
291
|
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 9 3.6">${MASCOTE_SVG.replace(/^<svg[^>]*>|<\/svg>$/g, '')}</svg>`,
|
|
217
292
|
);
|
|
@@ -223,94 +298,97 @@ const PAGINA = `<!doctype html>
|
|
|
223
298
|
<link rel="icon" href="${FAVICON}">
|
|
224
299
|
<style>
|
|
225
300
|
* { box-sizing: border-box; margin: 0; }
|
|
226
|
-
::selection { background: rgba(
|
|
301
|
+
::selection { background: rgba(34,211,238,.28); }
|
|
227
302
|
body {
|
|
228
|
-
background: radial-gradient(
|
|
229
|
-
color: #
|
|
303
|
+
background: radial-gradient(1100px 480px at 50% -120px, #12263f 0%, #0d1729 46%, #0b1220 100%) fixed #0b1220;
|
|
304
|
+
color: #e2e8f0; font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
230
305
|
min-height: 100vh; display: flex; flex-direction: column; align-items: center; padding: 0 18px 34px;
|
|
231
306
|
}
|
|
232
307
|
|
|
233
|
-
/* ── Cabeçalho ── */
|
|
234
|
-
#topo { position: relative; width: min(1120px, 100%); padding:
|
|
235
|
-
#topo .mascote { position: absolute; left: 4px; top:
|
|
308
|
+
/* ── Cabeçalho: mascote inclinado + nome, como no desenho ── */
|
|
309
|
+
#topo { position: relative; width: min(1120px, 100%); padding: 44px 0 26px; text-align: center; }
|
|
310
|
+
#topo .mascote { position: absolute; left: 4px; top: 26px; width: 100px; height: 42px; transform: rotate(-32deg); filter: drop-shadow(0 8px 20px rgba(34,211,238,.4)); animation: flutua 3.2s ease-in-out infinite alternate; }
|
|
236
311
|
@keyframes flutua { from { translate: 0 0 } to { translate: 0 5px } }
|
|
237
312
|
#topo .mascote svg { width: 100%; height: 100%; display: block; }
|
|
238
|
-
#topo h1 { font-size: clamp(
|
|
239
|
-
#topo
|
|
240
|
-
.
|
|
313
|
+
#topo h1 { font-size: clamp(34px, 5.6vw, 54px); font-weight: 650; letter-spacing: .4px; color: #f1f5f9; text-shadow: 0 4px 28px rgba(34,211,238,.22); }
|
|
314
|
+
#topo h1 span { color: #22d3ee; }
|
|
315
|
+
#topo .vivo { position: absolute; right: 2px; top: 48px; display: inline-flex; align-items: center; gap: 8px; color: #94a3b8; font-size: 12.5px; background: rgba(15,23,42,.85); border: 1px solid #1e293b; border-radius: 999px; padding: 6px 14px; }
|
|
316
|
+
.pulso { width: 8px; height: 8px; border-radius: 50%; background: #22d3ee; box-shadow: 0 0 10px #22d3ee99; animation: pulsa 1.4s infinite; }
|
|
241
317
|
@keyframes pulsa { 50% { opacity: .3 } }
|
|
242
318
|
|
|
243
319
|
/* ── A janela ── */
|
|
244
320
|
#janela {
|
|
245
|
-
width: min(1120px, 100%); background: #000; border:
|
|
321
|
+
width: min(1120px, 100%); background: #000; border: 1px solid #24344d; border-radius: 22px;
|
|
246
322
|
overflow: hidden; display: flex; flex-direction: column;
|
|
247
|
-
box-shadow: 0
|
|
323
|
+
box-shadow: 0 30px 80px rgba(2,6,23,.75), 0 0 60px rgba(34,211,238,.07), inset 0 1px 0 rgba(226,232,240,.07);
|
|
248
324
|
}
|
|
249
|
-
#barra { position: relative; display: flex; align-items: center; background: linear-gradient(#
|
|
250
|
-
#barra .b { flex: none; margin-right:
|
|
251
|
-
#barra .b.r {
|
|
252
|
-
#barra .b.y { width: 30px; height: 17px; border-radius: 999px; background: radial-gradient(circle at 35% 30%, #ffe95c, #f5c400); }
|
|
253
|
-
#barra .b.g { width: 24px; height: 14px; border-radius: 999px; background: radial-gradient(circle at 35% 30%, #6fe589, #24b53e); }
|
|
325
|
+
#barra { position: relative; display: flex; align-items: center; background: linear-gradient(#152238, #101a2c); padding: 13px 22px; min-height: 54px; border-bottom: 1px solid #0a1120; }
|
|
326
|
+
#barra .b { flex: none; width: 13px; height: 13px; border-radius: 50%; margin-right: 9px; box-shadow: inset 0 -1.5px 3px rgba(0,0,0,.35); }
|
|
327
|
+
#barra .b.r { background: #ff5f57; } #barra .b.y { background: #febc2e; } #barra .b.g { background: #28c840; }
|
|
254
328
|
|
|
255
|
-
|
|
329
|
+
/* Abas: tudo que ele abriu, clicável, separado por | como no desenho */
|
|
330
|
+
#abas { position: absolute; left: 50%; transform: translateX(-50%); max-width: 62%; display: flex; align-items: center; gap: 11px; overflow-x: auto; white-space: nowrap; scrollbar-width: none; font-size: clamp(14px, 1.9vw, 18px); font-weight: 550; }
|
|
256
331
|
#abas::-webkit-scrollbar { display: none; }
|
|
257
|
-
.aba { border: 0; background: none; cursor: pointer; font: inherit; color:
|
|
258
|
-
.aba:hover { color:
|
|
259
|
-
.aba.ativa { color: #
|
|
260
|
-
.aba
|
|
261
|
-
.
|
|
332
|
+
.aba { position: relative; border: 0; background: none; cursor: pointer; font: inherit; color: #64748b; padding: 4px 3px 7px; transition: color .18s ease; }
|
|
333
|
+
.aba:hover { color: #cbd5e1; }
|
|
334
|
+
.aba.ativa { color: #22d3ee; text-shadow: 0 0 18px rgba(34,211,238,.5); }
|
|
335
|
+
.aba.ativa::after { content: ''; position: absolute; left: 15%; right: 15%; bottom: 0; height: 2px; border-radius: 2px; background: #22d3ee; box-shadow: 0 0 10px #22d3ee; }
|
|
336
|
+
.aba .qtd { color: #475569; font-size: .68em; vertical-align: super; margin-left: 4px; font-weight: 600; }
|
|
337
|
+
.sep { color: #334155; font-weight: 300; }
|
|
262
338
|
|
|
263
339
|
/* ── Barra de comando: a instrução autopreenchendo ── */
|
|
264
|
-
#comando { display: flex; align-items: center; gap:
|
|
265
|
-
#comando .chevron { color: #
|
|
266
|
-
#comando .texto { color: #
|
|
267
|
-
#comando .texto.vazio { color: #
|
|
268
|
-
#comando .cursor { flex: none; width: 8px; height:
|
|
340
|
+
#comando { display: flex; align-items: center; gap: 11px; background: #0a1120; border-bottom: 1px solid #060b16; padding: 11px 22px; font-family: ui-monospace, 'Cascadia Code', Consolas, monospace; font-size: 14px; min-height: 44px; }
|
|
341
|
+
#comando .chevron { color: #22d3ee; font-weight: 700; flex: none; text-shadow: 0 0 10px rgba(34,211,238,.6); }
|
|
342
|
+
#comando .texto { color: #e2e8f0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
343
|
+
#comando .texto.vazio { color: #475569; font-style: italic; }
|
|
344
|
+
#comando .cursor { flex: none; width: 8px; height: 16px; background: #22d3ee; margin-left: 2px; animation: pisca 1s steps(1) infinite; box-shadow: 0 0 8px rgba(34,211,238,.7); }
|
|
269
345
|
@keyframes pisca { 50% { opacity: 0 } }
|
|
270
|
-
#comando .hora { margin-left: auto; color: #
|
|
271
|
-
|
|
272
|
-
/* ── A tela ── */
|
|
273
|
-
#tela { position: relative; min-height:
|
|
274
|
-
#tela img { width: 100%; max-height:
|
|
275
|
-
#aviso {
|
|
276
|
-
#aviso .titulo { font-size: clamp(
|
|
277
|
-
#aviso .sub { color: #
|
|
278
|
-
#
|
|
279
|
-
#
|
|
346
|
+
#comando .hora { margin-left: auto; color: #475569; font-size: 11.5px; flex: none; }
|
|
347
|
+
|
|
348
|
+
/* ── A tela: o vídeo ao vivo ── */
|
|
349
|
+
#tela { position: relative; min-height: 440px; display: flex; align-items: center; justify-content: center; background: #000; }
|
|
350
|
+
#tela img { width: 100%; max-height: 60vh; object-fit: contain; display: none; }
|
|
351
|
+
#aviso { text-align: center; padding: 84px 24px; }
|
|
352
|
+
#aviso .titulo { font-size: clamp(19px, 3vw, 28px); font-weight: 550; color: #f1f5f9; }
|
|
353
|
+
#aviso .sub { color: #64748b; font-size: 14px; margin-top: 13px; line-height: 1.7; }
|
|
354
|
+
#badge { position: absolute; top: 14px; right: 16px; display: none; align-items: center; gap: 7px; background: rgba(8,12,22,.78); backdrop-filter: blur(6px); border: 1px solid #1e293b; color: #f1f5f9; font-size: 11.5px; font-weight: 700; letter-spacing: 1.2px; padding: 6px 12px; border-radius: 999px; }
|
|
355
|
+
#badge .rec { width: 8px; height: 8px; border-radius: 50%; background: #fb7185; box-shadow: 0 0 9px #fb7185; animation: pulsa 1.1s infinite; }
|
|
356
|
+
#carimbo { position: absolute; left: 16px; bottom: 13px; background: rgba(8,12,22,.78); backdrop-filter: blur(6px); border: 1px solid #1e293b; color: #94a3b8; font-size: 12px; padding: 5px 13px; border-radius: 999px; display: none; max-width: 82%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
357
|
+
#carimbo b { color: #22d3ee; font-weight: 600; }
|
|
280
358
|
|
|
281
359
|
/* ── Linha do tempo ── */
|
|
282
|
-
#historico { width: min(1120px, 100%); padding-top:
|
|
283
|
-
#filme { display: flex; gap:
|
|
284
|
-
.foto { flex: none; border: 1.5px solid #
|
|
285
|
-
.foto:hover { opacity: 1; transform: translateY(-2px); border-color: #
|
|
360
|
+
#historico { width: min(1120px, 100%); padding-top: 18px; }
|
|
361
|
+
#filme { display: flex; gap: 9px; padding: 4px 2px 13px; overflow-x: auto; scrollbar-width: thin; scrollbar-color: #1e293b transparent; }
|
|
362
|
+
.foto { flex: none; border: 1.5px solid #1e293b; border-radius: 9px; background: #000; cursor: pointer; padding: 0; width: 100px; height: 60px; overflow: hidden; opacity: .6; transition: transform .16s ease, opacity .16s ease, border-color .16s ease, box-shadow .16s ease; }
|
|
363
|
+
.foto:hover { opacity: 1; transform: translateY(-2px); border-color: #475569; }
|
|
286
364
|
.foto img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
287
|
-
.foto.ativa { border-color: #
|
|
288
|
-
|
|
289
|
-
#cartao { background: #
|
|
290
|
-
#cartao header { display: flex; align-items: center; justify-content: space-between; padding:
|
|
291
|
-
#cartao header span b { color: #
|
|
292
|
-
#passos { max-height:
|
|
293
|
-
#passos::-webkit-scrollbar { width: 8px; } #passos::-webkit-scrollbar-thumb { background: #
|
|
294
|
-
.item { padding: 5px
|
|
295
|
-
.item:hover { background: #
|
|
296
|
-
.item .n { color: #
|
|
297
|
-
.item .ok { color: #
|
|
298
|
-
.item .txt { flex: 1; word-break: break-word; color: #
|
|
299
|
-
.item .txt .erro { color: #
|
|
300
|
-
.item .hora { color: #
|
|
301
|
-
.item.temfoto { cursor: pointer; } .item.temfoto:hover .txt { color: #
|
|
302
|
-
.item.pedido { background: #
|
|
303
|
-
.item.pedido .txt { color: #
|
|
304
|
-
.item.pedido .chev { color: #
|
|
305
|
-
|
|
306
|
-
footer { color: #
|
|
307
|
-
footer b { color: #
|
|
365
|
+
.foto.ativa { border-color: #22d3ee; opacity: 1; box-shadow: 0 0 0 2px rgba(34,211,238,.3), 0 6px 18px rgba(2,6,23,.6); }
|
|
366
|
+
|
|
367
|
+
#cartao { background: #0f172a; border: 1px solid #1e293b; border-radius: 16px; overflow: hidden; }
|
|
368
|
+
#cartao header { display: flex; align-items: center; justify-content: space-between; padding: 11px 17px; border-bottom: 1px solid #16223a; color: #94a3b8; font-size: 13px; font-weight: 600; letter-spacing: .3px; }
|
|
369
|
+
#cartao header span b { color: #22d3ee; }
|
|
370
|
+
#passos { max-height: 236px; overflow-y: auto; padding: 5px 0 9px; scrollbar-width: thin; scrollbar-color: #1e293b transparent; }
|
|
371
|
+
#passos::-webkit-scrollbar { width: 8px; } #passos::-webkit-scrollbar-thumb { background: #1e293b; border-radius: 4px; }
|
|
372
|
+
.item { padding: 5px 17px; display: flex; gap: 10px; font-size: 13.5px; align-items: baseline; transition: background .12s ease; }
|
|
373
|
+
.item:hover { background: #131f35; }
|
|
374
|
+
.item .n { color: #475569; min-width: 3ch; text-align: right; font-variant-numeric: tabular-nums; }
|
|
375
|
+
.item .ok { color: #22d3ee; } .item .falha { color: #fb7185; }
|
|
376
|
+
.item .txt { flex: 1; word-break: break-word; color: #cbd5e1; }
|
|
377
|
+
.item .txt .erro { color: #fb7185; font-size: 12px; display: block; margin-top: 1px; }
|
|
378
|
+
.item .hora { color: #475569; font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
379
|
+
.item.temfoto { cursor: pointer; } .item.temfoto:hover .txt { color: #f1f5f9; }
|
|
380
|
+
.item.pedido { background: #0c1526; border-top: 1px solid #16223a; border-bottom: 1px solid #16223a; margin: 6px 0; padding: 8px 17px; }
|
|
381
|
+
.item.pedido .txt { color: #f1f5f9; font-weight: 600; }
|
|
382
|
+
.item.pedido .chev { color: #22d3ee; font-weight: 800; }
|
|
383
|
+
|
|
384
|
+
footer { color: #475569; font-size: 12px; padding-top: 17px; }
|
|
385
|
+
footer b { color: #64748b; font-weight: 600; }
|
|
308
386
|
</style></head>
|
|
309
387
|
<body>
|
|
310
388
|
<div id="topo">
|
|
311
389
|
<div class="mascote">${MASCOTE_SVG}</div>
|
|
312
|
-
<h1>
|
|
313
|
-
<span class="vivo"><span class="
|
|
390
|
+
<h1>Primo<span>code</span></h1>
|
|
391
|
+
<span class="vivo"><span class="pulso"></span><span id="st">conectando…</span></span>
|
|
314
392
|
</div>
|
|
315
393
|
|
|
316
394
|
<div id="janela">
|
|
@@ -325,11 +403,12 @@ const PAGINA = `<!doctype html>
|
|
|
325
403
|
<span class="hora" id="cmdhora"></span>
|
|
326
404
|
</div>
|
|
327
405
|
<section id="tela">
|
|
328
|
-
<img id="frame" alt="tela
|
|
406
|
+
<img id="frame" alt="tela do computador, ao vivo">
|
|
329
407
|
<div id="aviso">
|
|
330
|
-
<div class="titulo">
|
|
331
|
-
<div class="sub">Dê uma instrução no terminal do PrimoCode e
|
|
408
|
+
<div class="titulo">A tela do computador aparece aqui, em vídeo</div>
|
|
409
|
+
<div class="sub">Dê uma instrução no terminal do PrimoCode e assista ele trabalhando, ao vivo.<br>Cada app que ele abrir vira uma aba lá em cima — clique nelas para rever o que já foi feito.</div>
|
|
332
410
|
</div>
|
|
411
|
+
<div id="badge"><span class="rec"></span>AO VIVO</div>
|
|
333
412
|
<div id="carimbo"></div>
|
|
334
413
|
</section>
|
|
335
414
|
</div>
|
|
@@ -345,12 +424,45 @@ const PAGINA = `<!doctype html>
|
|
|
345
424
|
|
|
346
425
|
<script>
|
|
347
426
|
const VIVO = 0; // id da aba "Ao vivo"
|
|
348
|
-
let dados = { abas: [], frames: [], log: [], instrucoes: [] };
|
|
427
|
+
let dados = { abas: [], frames: [], log: [], instrucoes: [], liveSeq: 0 };
|
|
349
428
|
let abaSel = VIVO; // aba escolhida pelo usuário
|
|
350
|
-
let fotoSel = null; // frame fixado
|
|
429
|
+
let fotoSel = null; // frame fixado (null = vídeo/mais recente)
|
|
351
430
|
let ultimoEstado = '';
|
|
352
431
|
|
|
353
|
-
|
|
432
|
+
function esc(s) { return String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); }
|
|
433
|
+
|
|
434
|
+
// ── o vídeo: busca o quadro direto, desde o primeiro segundo ──
|
|
435
|
+
// Não espera o /state confirmar nada: enquanto não há seq, tenta com
|
|
436
|
+
// timestamp (um 404 só significa "primeiro quadro ainda não saiu" — tenta de
|
|
437
|
+
// novo). Assim a tela aparece assim que o painel abre, sem depender de o
|
|
438
|
+
// agente ter feito qualquer coisa.
|
|
439
|
+
let quadroNaTela = '';
|
|
440
|
+
let buscandoQuadro = false;
|
|
441
|
+
function modoVideo() { return abaSel === VIVO && fotoSel == null; }
|
|
442
|
+
function videoTick() {
|
|
443
|
+
if (modoVideo() && !buscandoQuadro) {
|
|
444
|
+
const marca = dados.liveSeq > 0 ? 's' + dados.liveSeq : 't' + Date.now();
|
|
445
|
+
if (marca !== quadroNaTela) {
|
|
446
|
+
buscandoQuadro = true;
|
|
447
|
+
const pre = new Image(); // pré-carrega fora da tela:
|
|
448
|
+
pre.onload = () => { // o quadro só entra inteiro
|
|
449
|
+
buscandoQuadro = false;
|
|
450
|
+
if (!modoVideo()) return;
|
|
451
|
+
const img = document.getElementById('frame');
|
|
452
|
+
img.src = pre.src; img.style.display = 'block';
|
|
453
|
+
document.getElementById('aviso').style.display = 'none';
|
|
454
|
+
document.getElementById('badge').style.display = 'flex';
|
|
455
|
+
document.getElementById('carimbo').style.display = 'none';
|
|
456
|
+
quadroNaTela = marca;
|
|
457
|
+
};
|
|
458
|
+
pre.onerror = () => { buscandoQuadro = false; };
|
|
459
|
+
pre.src = '/live?' + marca;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
setTimeout(videoTick, 300);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ── a instrução autopreenchendo, letra por letra ──
|
|
354
466
|
let digitando = null, instrucaoNaTela = 0;
|
|
355
467
|
function autopreencher(inst) {
|
|
356
468
|
if (!inst || inst.n === instrucaoNaTela) return;
|
|
@@ -362,14 +474,12 @@ function autopreencher(inst) {
|
|
|
362
474
|
document.getElementById('cmdhora').textContent = inst.hora;
|
|
363
475
|
let i = 0;
|
|
364
476
|
digitando = setInterval(() => {
|
|
365
|
-
i += 1 + Math.floor(Math.random() * 2);
|
|
477
|
+
i += 1 + Math.floor(Math.random() * 2); // cadência de gente digitando
|
|
366
478
|
el.textContent = inst.texto.slice(0, i);
|
|
367
479
|
if (i >= inst.texto.length) clearInterval(digitando);
|
|
368
|
-
},
|
|
480
|
+
}, 32);
|
|
369
481
|
}
|
|
370
482
|
|
|
371
|
-
function esc(s) { return String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); }
|
|
372
|
-
|
|
373
483
|
function framesDaAba(id) {
|
|
374
484
|
return id === VIVO ? dados.frames : dados.frames.filter((f) => f.aba === id);
|
|
375
485
|
}
|
|
@@ -378,46 +488,52 @@ function logDaAba(id) {
|
|
|
378
488
|
}
|
|
379
489
|
|
|
380
490
|
function render() {
|
|
381
|
-
// Abas: "Ao vivo" +
|
|
491
|
+
// Abas: "Ao vivo" + tudo que ele abriu, separadas por |
|
|
382
492
|
const nav = document.getElementById('abas');
|
|
383
493
|
const abas = [{ id: VIVO, nome: 'Ao vivo' }, ...dados.abas];
|
|
384
494
|
nav.innerHTML = abas.map((a) =>
|
|
385
495
|
'<button class="aba' + (a.id === abaSel ? ' ativa' : '') + '" data-id="' + a.id + '">' + esc(a.nome)
|
|
386
496
|
+ (a.passos ? '<span class="qtd">' + a.passos + '</span>' : '') + '</button>'
|
|
387
497
|
).join('<span class="sep">|</span>');
|
|
388
|
-
nav.querySelectorAll('.aba').forEach((b) => b.onclick = () => {
|
|
498
|
+
nav.querySelectorAll('.aba').forEach((b) => b.onclick = () => {
|
|
499
|
+
abaSel = Number(b.dataset.id); fotoSel = null; quadroNaTela = -1; render();
|
|
500
|
+
});
|
|
389
501
|
const abaObj = abas.find((a) => a.id === abaSel) || abas[0];
|
|
390
502
|
document.getElementById('abanome').textContent = abaObj.nome;
|
|
391
503
|
|
|
392
|
-
//
|
|
504
|
+
// Fora do modo vídeo (aba antiga ou foto fixada): mostra o quadro daquela aba
|
|
393
505
|
const fs = framesDaAba(abaSel);
|
|
394
|
-
const atual = fotoSel != null ? fs.find((f) => f.id === fotoSel) : fs[fs.length - 1];
|
|
395
506
|
const img = document.getElementById('frame');
|
|
396
507
|
const aviso = document.getElementById('aviso');
|
|
508
|
+
const badge = document.getElementById('badge');
|
|
397
509
|
const carimbo = document.getElementById('carimbo');
|
|
398
|
-
if (
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
img.src = '/frame?n=' + atual.id;
|
|
403
|
-
img.
|
|
510
|
+
if (!modoVideo()) {
|
|
511
|
+
const atual = fotoSel != null ? fs.find((f) => f.id === fotoSel) : fs[fs.length - 1];
|
|
512
|
+
badge.style.display = 'none';
|
|
513
|
+
if (atual) {
|
|
514
|
+
if (img.dataset.n !== 'f' + atual.id) { img.src = '/frame?n=' + atual.id; img.dataset.n = 'f' + atual.id; }
|
|
515
|
+
img.style.display = 'block'; aviso.style.display = 'none';
|
|
516
|
+
carimbo.style.display = 'block';
|
|
517
|
+
carimbo.innerHTML = '<b>#' + atual.id + '</b> · ' + atual.hora + ' · ' + esc(atual.acao)
|
|
518
|
+
+ (fotoSel != null ? ' — <b>clique na miniatura de novo para voltar</b>' : '');
|
|
519
|
+
} else {
|
|
520
|
+
img.style.display = 'none'; aviso.style.display = 'block'; carimbo.style.display = 'none';
|
|
404
521
|
}
|
|
405
|
-
img.style.display = 'block'; aviso.style.display = 'none';
|
|
406
|
-
carimbo.style.display = 'block';
|
|
407
|
-
carimbo.innerHTML = '<b>#' + atual.id + '</b> · ' + atual.hora + ' · ' + esc(atual.acao) + (fotoSel != null ? ' — <b>clique na miniatura de novo para voltar ao ao-vivo</b>' : '');
|
|
408
522
|
} else {
|
|
409
|
-
|
|
410
|
-
if (dados.erroFoto) aviso.querySelector('.sub').textContent = 'Sem
|
|
523
|
+
carimbo.style.display = 'none';
|
|
524
|
+
if (dados.erroFoto) { aviso.style.display = 'block'; aviso.querySelector('.sub').textContent = 'Sem imagem da tela: ' + dados.erroFoto; }
|
|
411
525
|
}
|
|
412
526
|
|
|
413
527
|
// Filmstrip da aba (as últimas 16)
|
|
414
528
|
const filme = document.getElementById('filme');
|
|
415
529
|
const tira = fs.slice(-16);
|
|
530
|
+
const marcada = fotoSel != null ? fotoSel : -1;
|
|
416
531
|
filme.innerHTML = tira.map((f) =>
|
|
417
|
-
'<button class="foto' + (
|
|
532
|
+
'<button class="foto' + (f.id === marcada ? ' ativa' : '') + '" data-id="' + f.id + '" title="' + esc(f.acao) + '"><img loading="lazy" src="/frame?n=' + f.id + '"></button>').join('');
|
|
418
533
|
filme.querySelectorAll('.foto').forEach((b) => b.onclick = () => {
|
|
419
534
|
const id = Number(b.dataset.id);
|
|
420
|
-
fotoSel = (fotoSel === id) ? null : id; // clicar de novo volta ao
|
|
535
|
+
fotoSel = (fotoSel === id) ? null : id; // clicar de novo volta ao vídeo
|
|
536
|
+
quadroNaTela = -1;
|
|
421
537
|
render();
|
|
422
538
|
});
|
|
423
539
|
if (fotoSel == null) filme.scrollLeft = filme.scrollWidth;
|
|
@@ -436,7 +552,7 @@ function render() {
|
|
|
436
552
|
+ '<span class="txt">' + esc(e.acao) + (e.erro ? '<span class="erro">' + esc(e.erro) + '</span>' : '') + '</span>'
|
|
437
553
|
+ '<span class="hora">' + e.hora + '</span></div>';
|
|
438
554
|
}).join('');
|
|
439
|
-
document.querySelectorAll('.item.temfoto').forEach((el) => el.onclick = () => { fotoSel = Number(el.dataset.frame); render(); });
|
|
555
|
+
document.querySelectorAll('.item.temfoto').forEach((el) => el.onclick = () => { fotoSel = Number(el.dataset.frame); quadroNaTela = -1; render(); });
|
|
440
556
|
}
|
|
441
557
|
|
|
442
558
|
async function tick() {
|
|
@@ -450,9 +566,10 @@ async function tick() {
|
|
|
450
566
|
} catch (e) {
|
|
451
567
|
document.getElementById('st').textContent = 'reconectando…';
|
|
452
568
|
}
|
|
453
|
-
setTimeout(tick,
|
|
569
|
+
setTimeout(tick, 600);
|
|
454
570
|
}
|
|
455
571
|
tick();
|
|
572
|
+
videoTick();
|
|
456
573
|
</script>
|
|
457
574
|
</body></html>`;
|
|
458
575
|
|
|
@@ -461,12 +578,13 @@ function start() {
|
|
|
461
578
|
if (state.on) return Promise.resolve(state.url);
|
|
462
579
|
try { fs.mkdirSync(DIR, { recursive: true }); } catch {}
|
|
463
580
|
// Fotos de sessões passadas não pertencem a esta linha do tempo.
|
|
464
|
-
try { for (const f of fs.readdirSync(DIR)) if (/^frame-\d
|
|
581
|
+
try { for (const f of fs.readdirSync(DIR)) if (/^(frame-\d+|live|live-tmp)\.png$/.test(f)) fs.unlinkSync(path.join(DIR, f)); } catch {}
|
|
465
582
|
|
|
466
583
|
return new Promise((resolve, reject) => {
|
|
467
584
|
const server = http.createServer((req, res) => {
|
|
468
585
|
const u = new URL(req.url || '/', 'http://x');
|
|
469
586
|
if (u.pathname === '/state') {
|
|
587
|
+
viuAgora(); // página aberta = vídeo ligado
|
|
470
588
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
471
589
|
res.end(JSON.stringify({
|
|
472
590
|
erroFoto: state.erroFoto,
|
|
@@ -474,9 +592,19 @@ function start() {
|
|
|
474
592
|
frames: state.frames,
|
|
475
593
|
log: state.log.slice(-120),
|
|
476
594
|
instrucoes: state.instrucoes.slice(-20),
|
|
595
|
+
liveSeq: state.liveSeq,
|
|
477
596
|
}));
|
|
478
597
|
return;
|
|
479
598
|
}
|
|
599
|
+
if (u.pathname === '/live') {
|
|
600
|
+
viuAgora();
|
|
601
|
+
fs.readFile(LIVE(), (err, buf) => {
|
|
602
|
+
if (err) { res.writeHead(404); res.end(); return; }
|
|
603
|
+
res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'no-store' });
|
|
604
|
+
res.end(buf);
|
|
605
|
+
});
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
480
608
|
if (u.pathname === '/frame') {
|
|
481
609
|
// O id vem de fora: só serve o arquivo se for número — nada de
|
|
482
610
|
// path traversal por aqui.
|
|
@@ -505,6 +633,10 @@ function start() {
|
|
|
505
633
|
state.server = server;
|
|
506
634
|
state.on = true;
|
|
507
635
|
state.url = `http://127.0.0.1:${server.address().port}`;
|
|
636
|
+
// O vídeo começa JÁ: quando o navegador terminar de abrir, o
|
|
637
|
+
// primeiro quadro está no ar — nada de tela vazia esperando ação.
|
|
638
|
+
state.viewerAt = Date.now();
|
|
639
|
+
agendarVideo(50);
|
|
508
640
|
resolve(state.url);
|
|
509
641
|
});
|
|
510
642
|
});
|
|
@@ -512,6 +644,7 @@ function start() {
|
|
|
512
644
|
|
|
513
645
|
function stop() {
|
|
514
646
|
if (state.server) { try { state.server.close(); } catch {} }
|
|
647
|
+
if (state.liveTimer) { clearTimeout(state.liveTimer); state.liveTimer = null; }
|
|
515
648
|
state.server = null;
|
|
516
649
|
state.on = false;
|
|
517
650
|
state.url = '';
|
package/lib/ui.js
CHANGED
|
@@ -161,9 +161,9 @@ const TIPS = [
|
|
|
161
161
|
];
|
|
162
162
|
|
|
163
163
|
const NEWS = [
|
|
164
|
-
'
|
|
165
|
-
'
|
|
166
|
-
'
|
|
164
|
+
'/streaming agora é VÍDEO ao vivo — assista ele trabalhando, aba por aba',
|
|
165
|
+
'Comandos se autopreenchem: /str + Tab (ou Enter) vira /streaming',
|
|
166
|
+
'Ele INTERAGE com o computador: janelas, botões e ícones pelo nome',
|
|
167
167
|
];
|
|
168
168
|
|
|
169
169
|
// ── Cabeçalho ────────────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.14.1",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modelos grátis. Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|