primocode 9.8.0-beta.2 → 9.8.0-beta.21
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/README.md +33 -6
- package/app/ansi.js +55 -0
- package/app/app.css +482 -281
- package/app/app.js +888 -315
- package/app/index.html +176 -101
- package/app/primo-marca.js +58 -1
- package/bin/primocode.js +697 -118
- package/comandos/explicar.md +14 -0
- package/comandos/publicar.md +10 -0
- package/comandos/revisar.md +13 -0
- package/lib/act.js +130 -26
- package/lib/api.js +8 -1
- package/lib/app.js +317 -74
- package/lib/browser.js +79 -3
- package/lib/catalogo.js +11 -3
- package/lib/checkpoint-git.js +218 -0
- package/lib/checkpoint.js +249 -0
- package/lib/claude-engine.js +25 -3
- package/lib/comandos-extra.js +481 -0
- package/lib/comandos.js +104 -0
- package/lib/config.js +21 -4
- package/lib/estudio/acervo-site.js +157 -0
- package/lib/estudio/assets.js +77 -3
- package/lib/estudio/cinema.js +354 -0
- package/lib/estudio/composicao.js +470 -0
- package/lib/estudio/edicao.js +450 -0
- package/lib/estudio/impacto.js +263 -0
- package/lib/estudio/index.js +769 -5
- package/lib/estudio/plano.js +5 -2
- package/lib/estudio/remotion.js +228 -23
- package/lib/estudio/slides.js +34 -30
- package/lib/fala.js +95 -1
- package/lib/local/cerebro.json +361 -180
- package/lib/local/entrada.js +18 -8
- package/lib/local/motor-cli.js +1 -1
- package/lib/mencoes.js +98 -0
- package/lib/offline.js +168 -0
- package/lib/painel.js +291 -0
- package/lib/perigo.js +256 -0
- package/lib/pre-leitura.js +118 -0
- package/lib/repeticao.js +4 -2
- package/lib/skills.js +6 -1
- package/lib/tempos.js +62 -0
- package/lib/ui.js +171 -17
- package/package.json +3 -2
- package/skills/apresentacao-pptx/SKILL.md +52 -0
- package/skills/cartaz-de-arte/LICENSE.txt +202 -0
- package/skills/cartaz-de-arte/SKILL.md +47 -0
- package/skills/criar-skill/LICENSE.txt +202 -0
- package/skills/criar-skill/SKILL.md +49 -0
- package/skills/depurar-de-verdade/LICENSE.txt +21 -0
- package/skills/depurar-de-verdade/SKILL.md +14 -5
- package/skills/design-de-verdade/LICENSE.txt +177 -0
- package/skills/design-de-verdade/SKILL.md +42 -0
- package/skills/documento-word/SKILL.md +57 -0
- package/skills/interface-revisada/SKILL.md +47 -0
- package/skills/me-ensina/LICENSE.txt +21 -0
- package/skills/me-ensina/SKILL.md +27 -0
- package/skills/me-entrevista/LICENSE.txt +21 -0
- package/skills/me-entrevista/SKILL.md +27 -0
- package/skills/motion-de-primeira/SKILL.md +1 -1
- package/skills/pdf-bonito/SKILL.md +48 -0
- package/skills/planilha-excel/SKILL.md +47 -0
- package/skills/react-sem-dor/SKILL.md +48 -0
- package/skills/revisao-antes-de-entregar/LICENSE.txt +21 -0
- package/skills/revisao-antes-de-entregar/SKILL.md +6 -1
- package/skills/site-com-motion/SKILL.md +39 -0
- package/skills/temas-prontos/LICENSE.txt +202 -0
- package/skills/temas-prontos/SKILL.md +50 -0
- package/skills/testar-o-site/LICENSE.txt +202 -0
- package/skills/testar-o-site/SKILL.md +46 -0
- package/skills/video-no-remotion/SKILL.md +48 -0
- package/studio/narrar.py +111 -10
- package/studio/rostos.py +52 -0
package/app/app.js
CHANGED
|
@@ -1,444 +1,1017 @@
|
|
|
1
|
-
/* A janela conversando com o
|
|
1
|
+
/* A janela do PrimoCode conversando com o agente.
|
|
2
2
|
*
|
|
3
|
-
* Nada de framework: é a mesma regra do resto do projeto
|
|
4
|
-
* custa — a página tem
|
|
3
|
+
* Nada de framework: é a mesma regra do resto do projeto (zero dependência),
|
|
4
|
+
* e aqui ela nem custa — a página tem uma barra lateral, uma conversa, um
|
|
5
|
+
* compositor e um painel, ligados por um fluxo de eventos (SSE).
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* A conversa chega ESTRUTURADA (evento `agente`, vindo de lib/fala.js): a
|
|
8
|
+
* fala do Primo vira texto, cada ferramenta vira uma linha cinza e o
|
|
9
|
+
* resultado dela fecha essa linha. O terminal cru continua chegando
|
|
10
|
+
* (`saida`) e mora atrás do botão "Terminal".
|
|
10
11
|
*/
|
|
11
12
|
(function () {
|
|
12
13
|
"use strict";
|
|
13
14
|
|
|
14
|
-
/* A chave chega na URL
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
var CHAVE =
|
|
15
|
+
/* A chave chega na URL na primeira abertura. Sai de lá assim que é lida —
|
|
16
|
+
numa aba comum ficaria à mostra no histórico — mas antes vai para o
|
|
17
|
+
sessionStorage: sem isso, um F5 matava a janela. */
|
|
18
|
+
var busca = new URLSearchParams(location.search);
|
|
19
|
+
var CHAVE = busca.get("k") || "";
|
|
20
|
+
var ABRIR_PAINEL = busca.get("painel") || "";
|
|
19
21
|
try {
|
|
20
22
|
if (CHAVE) sessionStorage.setItem("primo-chave", CHAVE);
|
|
21
23
|
else CHAVE = sessionStorage.getItem("primo-chave") || "";
|
|
22
24
|
} catch (e) { /* sem sessionStorage: vale só esta abertura */ }
|
|
23
25
|
if (location.search) history.replaceState(null, "", location.pathname);
|
|
24
26
|
|
|
25
|
-
var
|
|
26
|
-
var
|
|
27
|
-
var
|
|
28
|
-
var
|
|
29
|
-
var
|
|
30
|
-
var
|
|
31
|
-
var
|
|
27
|
+
var $ = function (id) { return document.getElementById(id); };
|
|
28
|
+
var app = $("app");
|
|
29
|
+
var rolo = $("rolo");
|
|
30
|
+
var chat = $("chat");
|
|
31
|
+
var vazio = $("vazio");
|
|
32
|
+
var campo = $("campo");
|
|
33
|
+
var forma = $("forma");
|
|
34
|
+
var btnEnviar = $("btnEnviar");
|
|
35
|
+
var estado = {};
|
|
32
36
|
var ocupado = false;
|
|
33
37
|
|
|
38
|
+
var pego = function (k, p) { try { return localStorage.getItem(k) || p; } catch (e) { return p; } };
|
|
39
|
+
var guardo = function (k, v) { try { localStorage.setItem(k, v); } catch (e) {} };
|
|
40
|
+
|
|
34
41
|
function pedir(rota, corpo) {
|
|
35
42
|
return fetch(rota, {
|
|
36
43
|
method: corpo === undefined ? "GET" : "POST",
|
|
37
44
|
headers: { "Content-Type": "application/json", "X-Primo-Chave": CHAVE },
|
|
38
45
|
body: corpo === undefined ? undefined : JSON.stringify(corpo),
|
|
46
|
+
}).then(function (r) {
|
|
47
|
+
return r.json().catch(function () { return { ok: false, error: "resposta ilegível" }; })
|
|
48
|
+
.then(function (d) { d = d || {}; d.__status = r.status; return d; });
|
|
39
49
|
});
|
|
40
50
|
}
|
|
41
51
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return rolo.scrollTop + rolo.clientHeight >= rolo.scrollHeight - 60;
|
|
52
|
+
function el(tag, classe, texto) {
|
|
53
|
+
var e = document.createElement(tag);
|
|
54
|
+
if (classe) e.className = classe;
|
|
55
|
+
if (texto !== undefined) e.textContent = texto;
|
|
56
|
+
return e;
|
|
48
57
|
}
|
|
49
|
-
function
|
|
50
|
-
|
|
58
|
+
function icone(nome, classe) {
|
|
59
|
+
var ns = "http://www.w3.org/2000/svg";
|
|
60
|
+
var s = document.createElementNS(ns, "svg");
|
|
61
|
+
s.setAttribute("class", "ic" + (classe ? " " + classe : ""));
|
|
62
|
+
s.setAttribute("aria-hidden", "true");
|
|
63
|
+
var u = document.createElementNS(ns, "use");
|
|
64
|
+
u.setAttribute("href", "#i-" + nome);
|
|
65
|
+
s.appendChild(u);
|
|
66
|
+
return s;
|
|
51
67
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
var b = document.getElementById("vazio");
|
|
55
|
-
if (b) b.hidden = true;
|
|
56
|
-
// A tira das mensagens nasce escondida para a tela vazia poder ocupar o
|
|
57
|
-
// meio. Esconder uma sem mostrar a outra deixaria a janela em branco na
|
|
58
|
-
// primeira mensagem — e nada na tela é o pior jeito de responder.
|
|
59
|
-
var t = document.getElementById("conversa");
|
|
60
|
-
if (t) t.hidden = false;
|
|
68
|
+
function fugir(s) {
|
|
69
|
+
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
61
70
|
}
|
|
62
71
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
72
|
+
/* ── os rótulos que a pessoa lê ──────────────────────────────────────
|
|
73
|
+
Os modos e os níveis são os do CLI (lib/perigo.js, lib/effort.js); os
|
|
74
|
+
nomes aqui são os que quem não programa entende. */
|
|
75
|
+
var PERMISSOES = {
|
|
76
|
+
auto: { rotulo: "Pergunta antes do perigoso", icone: "escudo",
|
|
77
|
+
desc: "Faz o trabalho e só pede licença antes do que não tem volta" },
|
|
78
|
+
perguntar: { rotulo: "Pergunta sempre", icone: "mao",
|
|
79
|
+
desc: "Pede licença antes de cada comando e de cada arquivo" },
|
|
80
|
+
livre: { rotulo: "Acesso completo", icone: "perigo",
|
|
81
|
+
desc: "Nunca pergunta, nem antes de apagar ou forçar push" },
|
|
82
|
+
};
|
|
83
|
+
var ESFORCOS = [
|
|
84
|
+
{ nome: "low", rotulo: "Rápido", desc: "tarefa simples, resposta curta" },
|
|
85
|
+
{ nome: "medium", rotulo: "Equilíbrio", desc: "o padrão do dia a dia" },
|
|
86
|
+
{ nome: "high", rotulo: "Capricho", desc: "capricha e pesquisa referências" },
|
|
87
|
+
{ nome: "xhigh", rotulo: "Esperto", desc: "o mais capaz, contexto enorme" },
|
|
88
|
+
{ nome: "max", rotulo: "Máximo", desc: "pesquisa a fundo antes de escrever" },
|
|
89
|
+
{ nome: "ultracode", rotulo: "Ultra", desc: "tudo no talo" },
|
|
90
|
+
];
|
|
91
|
+
function esforcoDe(nome) {
|
|
92
|
+
for (var i = 0; i < ESFORCOS.length; i++) if (ESFORCOS[i].nome === nome) return ESFORCOS[i];
|
|
93
|
+
return ESFORCOS[1];
|
|
68
94
|
}
|
|
69
95
|
|
|
70
|
-
/* ──
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
var
|
|
96
|
+
/* ── o menu flutuante ────────────────────────────────────────────────
|
|
97
|
+
Um só, reaproveitado por todos os botões com ⌄: projeto, permissão,
|
|
98
|
+
esforço e o nome do app. Abre para cima quando o botão está embaixo —
|
|
99
|
+
o compositor fica colado no rodapé, e um menu para baixo sairia da tela. */
|
|
100
|
+
var menu = $("menu");
|
|
101
|
+
var menuDono = null;
|
|
102
|
+
|
|
103
|
+
function fecharMenu() {
|
|
104
|
+
menu.hidden = true;
|
|
105
|
+
menu.innerHTML = "";
|
|
106
|
+
if (menuDono) menuDono.setAttribute("aria-expanded", "false");
|
|
107
|
+
menuDono = null;
|
|
108
|
+
}
|
|
75
109
|
|
|
76
|
-
function
|
|
77
|
-
|
|
110
|
+
function abrirMenu(dono, itens) {
|
|
111
|
+
if (menuDono === dono) { fecharMenu(); return; }
|
|
112
|
+
fecharMenu();
|
|
113
|
+
menuDono = dono;
|
|
114
|
+
dono.setAttribute("aria-expanded", "true");
|
|
115
|
+
itens.forEach(function (it) {
|
|
116
|
+
if (it.sep) { menu.appendChild(el("div", "menu-sep")); return; }
|
|
117
|
+
if (it.titulo) { menu.appendChild(el("div", "menu-titulo", it.titulo)); return; }
|
|
118
|
+
if (it.entrada) {
|
|
119
|
+
var f = el("form", "menu-entrada");
|
|
120
|
+
var i = el("input");
|
|
121
|
+
i.id = it.entrada.id; i.placeholder = it.entrada.placeholder; i.maxLength = 60;
|
|
122
|
+
var b = el("button", "", it.entrada.botao);
|
|
123
|
+
b.type = "submit";
|
|
124
|
+
f.appendChild(i); f.appendChild(b);
|
|
125
|
+
f.onsubmit = function (ev) {
|
|
126
|
+
ev.preventDefault();
|
|
127
|
+
if (!i.value.trim()) return;
|
|
128
|
+
fecharMenu();
|
|
129
|
+
it.entrada.acao(i.value.trim());
|
|
130
|
+
};
|
|
131
|
+
menu.appendChild(f);
|
|
132
|
+
setTimeout(function () { i.focus(); }, 0);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
var botao = el("button", "menu-item" + (it.classe ? " " + it.classe : ""));
|
|
136
|
+
botao.type = "button";
|
|
137
|
+
botao.setAttribute("role", "menuitem");
|
|
138
|
+
if (it.icone) botao.appendChild(icone(it.icone));
|
|
139
|
+
var rot = el("span", "rot", it.rotulo);
|
|
140
|
+
if (it.desc) rot.appendChild(el("span", "", it.desc));
|
|
141
|
+
botao.appendChild(rot);
|
|
142
|
+
if (it.marcado) botao.appendChild(el("span", "marcado", "✓"));
|
|
143
|
+
botao.onclick = function () { fecharMenu(); it.acao(); };
|
|
144
|
+
menu.appendChild(botao);
|
|
145
|
+
});
|
|
146
|
+
menu.hidden = false;
|
|
147
|
+
var r = dono.getBoundingClientRect();
|
|
148
|
+
var m = menu.getBoundingClientRect();
|
|
149
|
+
var esq = Math.min(r.left, window.innerWidth - m.width - 8);
|
|
150
|
+
var embaixo = r.top > window.innerHeight / 2;
|
|
151
|
+
menu.style.left = Math.max(8, esq) + "px";
|
|
152
|
+
menu.style.top = (embaixo ? Math.max(8, r.top - m.height - 6) : r.bottom + 6) + "px";
|
|
153
|
+
var primeiro = menu.querySelector("button, input");
|
|
154
|
+
if (primeiro && !itens.some(function (x) { return x.entrada; })) primeiro.focus();
|
|
78
155
|
}
|
|
79
156
|
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
var m;
|
|
84
|
-
ANSI.lastIndex = 0;
|
|
157
|
+
document.addEventListener("mousedown", function (ev) {
|
|
158
|
+
if (!menu.hidden && !menu.contains(ev.target) && menuDono && !menuDono.contains(ev.target)) fecharMenu();
|
|
159
|
+
});
|
|
85
160
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
161
|
+
/* ── a conversa ──────────────────────────────────────────────────────── */
|
|
162
|
+
|
|
163
|
+
function noFim() { return rolo.scrollTop + rolo.clientHeight >= rolo.scrollHeight - 80; }
|
|
164
|
+
// Só acompanha se a pessoa JÁ estava embaixo: puxar a tela enquanto
|
|
165
|
+
// alguém lê o que passou é a coisa mais irritante que um chat ao vivo faz.
|
|
166
|
+
function acompanhar(estava) { if (estava) rolo.scrollTop = rolo.scrollHeight; }
|
|
167
|
+
|
|
168
|
+
function mostrarConversa() { vazio.hidden = true; chat.hidden = false; }
|
|
169
|
+
function limparConversa() {
|
|
170
|
+
chat.innerHTML = "";
|
|
171
|
+
chat.hidden = true;
|
|
172
|
+
vazio.hidden = false;
|
|
173
|
+
fala = null; grupo = null; pendentes = [];
|
|
174
|
+
trabalhando = null;
|
|
175
|
+
if (ocupado) mostrarTrabalhando();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* Markdown pequeno e SEGURO: tudo é escapado antes, e só depois o que é
|
|
179
|
+
marcação vira tag. A resposta vem do modelo, e o modelo leu páginas da
|
|
180
|
+
internet — um <script> que ele repetisse não pode virar script aqui. */
|
|
181
|
+
function markdown(texto) {
|
|
182
|
+
var blocos = [];
|
|
183
|
+
var t = String(texto || "").replace(/\r/g, "");
|
|
184
|
+
t = t.replace(/```[^\n]*\n([\s\S]*?)(```|$)/g, function (_, codigo) {
|
|
185
|
+
blocos.push("<pre><code>" + fugir(codigo.replace(/\n$/, "")) + "</code></pre>");
|
|
186
|
+
return "\u0000" + (blocos.length - 1) + "\u0000";
|
|
187
|
+
});
|
|
188
|
+
var linhas = t.split("\n");
|
|
189
|
+
var fora = [];
|
|
190
|
+
var lista = null;
|
|
191
|
+
var para = [];
|
|
192
|
+
function inline(s) {
|
|
193
|
+
return fugir(s)
|
|
194
|
+
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
|
195
|
+
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
|
196
|
+
.replace(/(^|[\s(])\*([^*\s][^*]*)\*/g, "$1<em>$2</em>");
|
|
91
197
|
}
|
|
92
|
-
function
|
|
93
|
-
|
|
198
|
+
function fecharPara() { if (para.length) { fora.push("<p>" + para.map(inline).join("<br>") + "</p>"); para = []; } }
|
|
199
|
+
function fecharLista() { if (lista) { fora.push("</" + lista + ">"); lista = null; } }
|
|
200
|
+
linhas.forEach(function (l) {
|
|
201
|
+
var bloco = /^\u0000(\d+)\u0000$/.exec(l.trim());
|
|
202
|
+
var item = /^\s*[-*•]\s+(.*)$/.exec(l);
|
|
203
|
+
var num = /^\s*\d+[.)]\s+(.*)$/.exec(l);
|
|
204
|
+
var tit = /^\s*#{1,6}\s+(.*)$/.exec(l);
|
|
205
|
+
if (bloco) { fecharPara(); fecharLista(); fora.push(blocos[Number(bloco[1])]); return; }
|
|
206
|
+
if (item || num) {
|
|
207
|
+
fecharPara();
|
|
208
|
+
var tipo = item ? "ul" : "ol";
|
|
209
|
+
if (lista !== tipo) { fecharLista(); fora.push("<" + tipo + ">"); lista = tipo; }
|
|
210
|
+
fora.push("<li>" + inline((item || num)[1]) + "</li>");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (tit) { fecharPara(); fecharLista(); fora.push("<h4>" + inline(tit[1]) + "</h4>"); return; }
|
|
214
|
+
if (!l.trim()) { fecharPara(); fecharLista(); return; }
|
|
215
|
+
fecharLista();
|
|
216
|
+
para.push(l);
|
|
217
|
+
});
|
|
218
|
+
fecharPara(); fecharLista();
|
|
219
|
+
return fora.join("").replace(/\u0000(\d+)\u0000/g, function (_, i) { return blocos[Number(i)]; });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
var fala = null; // o bloco de texto do Primo que está chegando aos pedaços
|
|
223
|
+
var grupo = null; // o grupo de linhas de ação em andamento
|
|
224
|
+
var pendentes = []; // ações esperando o resultado
|
|
225
|
+
var trabalhando = null; // o "Trabalhando…" do fim da conversa
|
|
226
|
+
|
|
227
|
+
function anexar(no) {
|
|
228
|
+
mostrarConversa();
|
|
229
|
+
if (trabalhando && trabalhando.parentNode === chat) chat.insertBefore(no, trabalhando);
|
|
230
|
+
else chat.appendChild(no);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function mostrarTrabalhando() {
|
|
234
|
+
if (trabalhando) return;
|
|
235
|
+
trabalhando = el("div", "trabalhando");
|
|
236
|
+
trabalhando.appendChild(el("span", "pulso"));
|
|
237
|
+
trabalhando.appendChild(el("span", "brilho", "Trabalhando"));
|
|
238
|
+
mostrarConversa();
|
|
239
|
+
chat.appendChild(trabalhando);
|
|
240
|
+
}
|
|
241
|
+
function esconderTrabalhando() {
|
|
242
|
+
if (trabalhando && trabalhando.parentNode) trabalhando.parentNode.removeChild(trabalhando);
|
|
243
|
+
trabalhando = null;
|
|
244
|
+
// O que ficou sem resultado (parou no meio) não pode girar para sempre.
|
|
245
|
+
pendentes.forEach(function (p) { var r = p.querySelector(".rodando"); if (r) r.remove(); });
|
|
246
|
+
pendentes = [];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function escreverVoce(texto) {
|
|
250
|
+
fala = null; grupo = null;
|
|
251
|
+
var linha = el("div", "msg-voce");
|
|
252
|
+
linha.appendChild(el("div", "balao", texto));
|
|
253
|
+
anexar(linha);
|
|
254
|
+
rolo.scrollTop = rolo.scrollHeight;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function blocoDeFala() {
|
|
258
|
+
if (!fala) {
|
|
259
|
+
grupo = null;
|
|
260
|
+
fala = el("div", "msg-primo");
|
|
261
|
+
fala.__texto = "";
|
|
262
|
+
anexar(fala);
|
|
94
263
|
}
|
|
264
|
+
return fala;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
var ICONES = { ler: "ler", escrever: "arquivo", editar: "editar", rodar: "rodar", buscar: "busca",
|
|
268
|
+
web: "web", computador: "computador", plano: "plano", agente: "agente", outro: "barra" };
|
|
95
269
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
270
|
+
function linhaDeAcao(e) {
|
|
271
|
+
var estava = noFim();
|
|
272
|
+
fala = null;
|
|
273
|
+
if (!grupo) { grupo = el("div", "acoes"); anexar(grupo); }
|
|
274
|
+
var caixa = el("div", "acao" + (e.prof ? " fundo" : ""));
|
|
275
|
+
var linha = el("button", "acao-linha");
|
|
276
|
+
linha.type = "button";
|
|
277
|
+
linha.appendChild(icone(ICONES[e.icone] || "barra"));
|
|
278
|
+
var texto = el("span", "acao-texto");
|
|
279
|
+
texto.appendChild(el("b", "", e.verbo || e.nome || "Ação"));
|
|
280
|
+
if (e.alvo) texto.appendChild(document.createTextNode(" " + e.alvo));
|
|
281
|
+
linha.appendChild(texto);
|
|
282
|
+
var meta = el("span", "acao-meta");
|
|
283
|
+
meta.appendChild(el("span", "rodando"));
|
|
284
|
+
linha.appendChild(meta);
|
|
285
|
+
linha.appendChild(icone("chev", "acao-seta"));
|
|
286
|
+
caixa.appendChild(linha);
|
|
287
|
+
caixa.__meta = meta;
|
|
288
|
+
caixa.__linha = linha;
|
|
289
|
+
caixa.__texto = texto;
|
|
290
|
+
caixa.__nome = e.nome;
|
|
291
|
+
grupo.appendChild(caixa);
|
|
292
|
+
pendentes.push(caixa);
|
|
293
|
+
acompanhar(estava);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function fecharAcao(r) {
|
|
297
|
+
var caixa = pendentes.pop();
|
|
298
|
+
if (!caixa) return;
|
|
299
|
+
var meta = caixa.__meta;
|
|
300
|
+
meta.innerHTML = "";
|
|
301
|
+
if (r.nome === "write_file" && r.criado) caixa.__texto.firstChild.textContent = "Criou";
|
|
302
|
+
if (r.ok === false) {
|
|
303
|
+
meta.appendChild(el("span", "falhou", "✗ " + (r.resumo || "falhou").slice(0, 80)));
|
|
304
|
+
if (r.resumo && r.resumo.length > 80) r.detalhe = r.resumo;
|
|
305
|
+
} else {
|
|
306
|
+
if (r.mais || r.menos) {
|
|
307
|
+
meta.appendChild(el("span", "mais", "+" + (r.mais || 0)));
|
|
308
|
+
meta.appendChild(el("span", "menos", "−" + (r.menos || 0)));
|
|
309
|
+
} else if (r.resumo) {
|
|
310
|
+
meta.appendChild(el("span", "", r.resumo));
|
|
109
311
|
}
|
|
312
|
+
if (r.codigo !== undefined) meta.appendChild(el("span", r.codigo === 0 ? "ok" : "falhou", r.codigo === 0 ? "✓" : "exit " + r.codigo));
|
|
313
|
+
}
|
|
314
|
+
if (r.detalhe) {
|
|
315
|
+
var linha = caixa.__linha;
|
|
316
|
+
linha.classList.add("abre");
|
|
317
|
+
linha.setAttribute("aria-expanded", "false");
|
|
318
|
+
linha.onclick = function () {
|
|
319
|
+
var aberta = caixa.classList.toggle("aberta");
|
|
320
|
+
linha.setAttribute("aria-expanded", aberta ? "true" : "false");
|
|
321
|
+
var d = caixa.querySelector(".acao-detalhe");
|
|
322
|
+
if (aberta && !d) {
|
|
323
|
+
d = el("pre", "acao-detalhe");
|
|
324
|
+
String(r.detalhe).split("\n").forEach(function (l, i) {
|
|
325
|
+
var s = el("span", l.charAt(0) === "+" && caixa.__nome !== "run_shell" ? "l-mais"
|
|
326
|
+
: l.charAt(0) === "-" && caixa.__nome !== "run_shell" ? "l-menos" : "", (i ? "\n" : "") + l);
|
|
327
|
+
d.appendChild(s);
|
|
328
|
+
});
|
|
329
|
+
caixa.appendChild(d);
|
|
330
|
+
} else if (d) d.hidden = !aberta;
|
|
331
|
+
};
|
|
110
332
|
}
|
|
111
|
-
var resto = texto.slice(ultimo);
|
|
112
|
-
if (resto) fora += abrir() + fugir(resto) + fechar();
|
|
113
|
-
return fora;
|
|
114
333
|
}
|
|
115
334
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
335
|
+
var agendado = null;
|
|
336
|
+
function redesenharFala() {
|
|
337
|
+
if (agendado) return;
|
|
338
|
+
agendado = requestAnimationFrame(function () {
|
|
339
|
+
agendado = null;
|
|
340
|
+
if (fala) { var estava = noFim(); fala.innerHTML = markdown(fala.__texto); acompanhar(estava); }
|
|
341
|
+
});
|
|
342
|
+
}
|
|
121
343
|
|
|
122
|
-
function
|
|
344
|
+
function aoAgente(e) {
|
|
123
345
|
var estava = noFim();
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
346
|
+
switch (e.tipo) {
|
|
347
|
+
case "pedaco": {
|
|
348
|
+
var b = blocoDeFala();
|
|
349
|
+
b.__texto += e.texto;
|
|
350
|
+
redesenharFala();
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
case "fim":
|
|
354
|
+
if (fala) fala.innerHTML = markdown(fala.__texto);
|
|
355
|
+
fala = null;
|
|
356
|
+
break;
|
|
357
|
+
case "resposta": {
|
|
358
|
+
fala = null;
|
|
359
|
+
var r = blocoDeFala();
|
|
360
|
+
r.__texto = e.texto;
|
|
361
|
+
r.innerHTML = markdown(e.texto);
|
|
362
|
+
fala = null;
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
case "pensamento":
|
|
366
|
+
fala = null; grupo = null;
|
|
367
|
+
anexar(el("div", "pensamento", e.texto));
|
|
368
|
+
break;
|
|
369
|
+
case "aviso":
|
|
370
|
+
fala = null; grupo = null;
|
|
371
|
+
anexar(el("div", "aviso-msg", e.texto));
|
|
372
|
+
break;
|
|
373
|
+
case "acao": linhaDeAcao(e); return;
|
|
374
|
+
case "resultado": fecharAcao(e); return;
|
|
129
375
|
}
|
|
130
|
-
bloco.insertAdjacentHTML("beforeend", paraHtml(texto, corrente));
|
|
131
376
|
acompanhar(estava);
|
|
132
377
|
}
|
|
133
378
|
|
|
134
|
-
function
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
var caixa = document.createElement("div");
|
|
140
|
-
caixa.textContent = texto;
|
|
141
|
-
linha.appendChild(caixa);
|
|
142
|
-
conversa.appendChild(linha);
|
|
143
|
-
bloco = null; // a próxima saída começa bloco novo
|
|
144
|
-
corrente = { cor: null, forte: false };
|
|
379
|
+
function escreverComando(texto) {
|
|
380
|
+
fala = null; grupo = null;
|
|
381
|
+
var pre = el("pre", "saida-comando");
|
|
382
|
+
pre.innerHTML = window.PrimoAnsi.paraHtml(String(texto).replace(/^\n+|\n+$/g, ""), { cor: null, forte: false });
|
|
383
|
+
anexar(pre);
|
|
145
384
|
acompanhar(true);
|
|
146
|
-
void estava;
|
|
147
385
|
}
|
|
148
386
|
|
|
149
387
|
function escreverErro(texto) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
388
|
+
fala = null; grupo = null;
|
|
389
|
+
anexar(el("div", "aviso-msg erro", texto));
|
|
390
|
+
acompanhar(true);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function reproduzir(lista) {
|
|
394
|
+
chat.innerHTML = "";
|
|
395
|
+
fala = null; grupo = null; pendentes = []; trabalhando = null;
|
|
396
|
+
if (!lista || !lista.length) { limparConversa(); return; }
|
|
397
|
+
lista.forEach(function (x) { tratar(x.evento, x.dados); });
|
|
398
|
+
// Na história, ação sem resultado já passou: nada fica girando.
|
|
399
|
+
pendentes.forEach(function (p) { var r = p.querySelector(".rodando"); if (r) r.remove(); });
|
|
400
|
+
pendentes = [];
|
|
401
|
+
if (ocupado) mostrarTrabalhando();
|
|
402
|
+
rolo.scrollTop = rolo.scrollHeight;
|
|
157
403
|
}
|
|
158
404
|
|
|
159
|
-
|
|
405
|
+
function tratar(evento, dados) {
|
|
406
|
+
if (evento === "voce") escreverVoce(dados);
|
|
407
|
+
else if (evento === "agente") aoAgente(dados);
|
|
408
|
+
else if (evento === "comando") escreverComando(dados);
|
|
409
|
+
else if (evento === "erro") escreverErro(dados);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/* ── o terminal ──────────────────────────────────────────────────────── */
|
|
413
|
+
|
|
414
|
+
var terminal = $("terminal");
|
|
415
|
+
var terminalSaida = $("terminalSaida");
|
|
416
|
+
var corTerminal = { cor: null, forte: false };
|
|
417
|
+
function escreverTerminal(texto) {
|
|
418
|
+
var estava = terminalSaida.scrollTop + terminalSaida.clientHeight >= terminalSaida.scrollHeight - 40;
|
|
419
|
+
terminalSaida.insertAdjacentHTML("beforeend", window.PrimoAnsi.paraHtml(texto, corTerminal));
|
|
420
|
+
// O terminal só guarda o fim: centenas de milhares de linhas deixariam
|
|
421
|
+
// a janela lenta sem ninguém estar olhando.
|
|
422
|
+
while (terminalSaida.childNodes.length > 6000) terminalSaida.removeChild(terminalSaida.firstChild);
|
|
423
|
+
if (estava) terminalSaida.scrollTop = terminalSaida.scrollHeight;
|
|
424
|
+
}
|
|
425
|
+
function alternarTerminal(mostrar) {
|
|
426
|
+
if (mostrar === undefined) mostrar = terminal.hidden;
|
|
427
|
+
terminal.hidden = !mostrar;
|
|
428
|
+
rolo.hidden = mostrar;
|
|
429
|
+
$("navTerminal").setAttribute("aria-pressed", mostrar ? "true" : "false");
|
|
430
|
+
if (mostrar) terminalSaida.scrollTop = terminalSaida.scrollHeight;
|
|
431
|
+
else campo.focus();
|
|
432
|
+
}
|
|
433
|
+
$("navTerminal").onclick = function () { alternarTerminal(); };
|
|
434
|
+
$("fecharTerminal").onclick = function () { alternarTerminal(false); };
|
|
435
|
+
|
|
436
|
+
/* ── o estado ────────────────────────────────────────────────────────── */
|
|
437
|
+
|
|
438
|
+
function iniciais(nome) {
|
|
439
|
+
var p = String(nome || "").trim().split(/\s+/).filter(Boolean);
|
|
440
|
+
if (!p.length) return "PC";
|
|
441
|
+
return ((p[0][0] || "") + (p.length > 1 ? p[p.length - 1][0] : "")).toUpperCase();
|
|
442
|
+
}
|
|
160
443
|
|
|
161
444
|
function pintarEstado(e) {
|
|
162
445
|
if (!e) return;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
446
|
+
estado = e;
|
|
447
|
+
var projeto = e.projeto || "";
|
|
448
|
+
$("projetoNome").textContent = projeto || "Escolher projeto";
|
|
449
|
+
$("btnProjeto").classList.toggle("tem", !!projeto);
|
|
450
|
+
$("topoTitulo").textContent = projeto;
|
|
451
|
+
document.title = (projeto ? projeto + " · " : "") + "PrimoCode";
|
|
452
|
+
|
|
453
|
+
var modo = PERMISSOES[e.permissoes] ? e.permissoes : "auto";
|
|
454
|
+
var perm = $("btnPermissao");
|
|
455
|
+
perm.dataset.modo = modo;
|
|
456
|
+
$("permRotulo").textContent = PERMISSOES[modo].rotulo;
|
|
457
|
+
$("permIcone").firstChild.setAttribute("href", "#i-" + PERMISSOES[modo].icone);
|
|
458
|
+
perm.title = PERMISSOES[modo].desc + " (/permissoes)";
|
|
459
|
+
|
|
460
|
+
var esf = esforcoDe(e.esforco);
|
|
461
|
+
$("esforcoRotulo").textContent = esf.rotulo;
|
|
462
|
+
$("esforcoNome").textContent = esf.nome;
|
|
463
|
+
|
|
464
|
+
var mic = $("btnMic");
|
|
465
|
+
mic.setAttribute("aria-pressed", e.voz ? "true" : "false");
|
|
466
|
+
mic.setAttribute("aria-label", e.voz ? "Desligar o modo voz" : "Ligar o modo voz");
|
|
467
|
+
|
|
468
|
+
$("nomeUsuario").textContent = e.usuario || "Você";
|
|
469
|
+
$("avatar").textContent = iniciais(e.usuario || "Primo Code");
|
|
470
|
+
|
|
471
|
+
if (e.mascote !== pintarEstado.mascote && window.PrimoMarca) {
|
|
472
|
+
pintarEstado.mascote = e.mascote;
|
|
473
|
+
$("mascote").innerHTML = window.PrimoMarca.linha({ mascote: e.mascote, tamanho: 56 });
|
|
474
|
+
$("mascotePainel").innerHTML = window.PrimoMarca.linha({ mascote: e.mascote, tamanho: 40 });
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
var caixa = $("anexos");
|
|
478
|
+
caixa.innerHTML = "";
|
|
479
|
+
(e.anexos || []).forEach(function (nome) {
|
|
480
|
+
var a = el("span", "anexo");
|
|
481
|
+
a.appendChild(icone("arquivo"));
|
|
482
|
+
a.appendChild(el("span", "", nome));
|
|
483
|
+
var x = el("button");
|
|
484
|
+
x.type = "button";
|
|
485
|
+
x.setAttribute("aria-label", "Tirar " + nome);
|
|
486
|
+
x.appendChild(icone("fechar"));
|
|
487
|
+
x.onclick = function () { pedir("/api/desanexar", { nome: nome }).then(function (d) { if (d.estado) pintarEstado(d.estado); }); };
|
|
488
|
+
a.appendChild(x);
|
|
489
|
+
caixa.appendChild(a);
|
|
490
|
+
});
|
|
491
|
+
caixa.hidden = !(e.anexos || []).length;
|
|
492
|
+
if (e.ocupado !== undefined) pintarOcupado(e.ocupado);
|
|
493
|
+
marcarProjetoAtual();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function atualizarEnviar() {
|
|
497
|
+
if (ocupado) {
|
|
498
|
+
btnEnviar.disabled = false;
|
|
499
|
+
btnEnviar.classList.add("parar");
|
|
500
|
+
btnEnviar.setAttribute("aria-label", "Parar");
|
|
501
|
+
btnEnviar.title = "Parar (Esc Esc)";
|
|
502
|
+
btnEnviar.firstChild.firstChild.setAttribute("href", "#i-pare");
|
|
503
|
+
} else {
|
|
504
|
+
btnEnviar.classList.remove("parar");
|
|
505
|
+
btnEnviar.disabled = !campo.value.trim();
|
|
506
|
+
btnEnviar.setAttribute("aria-label", "Enviar");
|
|
507
|
+
btnEnviar.title = "Enviar (Enter)";
|
|
508
|
+
btnEnviar.firstChild.firstChild.setAttribute("href", "#i-seta");
|
|
509
|
+
}
|
|
168
510
|
}
|
|
169
511
|
|
|
512
|
+
var avisar = pego("primo-avisar", "") === "1";
|
|
170
513
|
function pintarOcupado(v) {
|
|
514
|
+
var era = ocupado;
|
|
171
515
|
ocupado = !!v;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
que já saiu ao ligar — então uma queda de rede local não deixa buraco na
|
|
186
|
-
conversa. */
|
|
516
|
+
if (ocupado) mostrarTrabalhando(); else esconderTrabalhando();
|
|
517
|
+
campo.placeholder = ocupado ? "Ele está trabalhando. Pode ir escrevendo o próximo" : "Peça qualquer coisa";
|
|
518
|
+
atualizarEnviar();
|
|
519
|
+
// Avisa só quando a janela está escondida: com ela na frente, a pessoa já viu.
|
|
520
|
+
if (era && !ocupado && avisar && document.hidden && window.Notification && Notification.permission === "granted") {
|
|
521
|
+
try { new Notification("PrimoCode", { body: "Terminei. Volte para ver o que eu fiz." }); } catch (e) {}
|
|
522
|
+
}
|
|
523
|
+
if (era && !ocupado) { carregarRecentes(); carregarProjetos(); }
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/* ── o fluxo ─────────────────────────────────────────────────────────
|
|
527
|
+
EventSource reconecta sozinho, e o servidor manda a história ao ligar —
|
|
528
|
+
uma queda de rede local não deixa buraco na conversa. */
|
|
187
529
|
function ligar() {
|
|
188
530
|
var fonte = new EventSource("/api/fluxo?k=" + encodeURIComponent(CHAVE));
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
531
|
+
var ouvir = function (nome, fn) {
|
|
532
|
+
fonte.addEventListener(nome, function (ev) { var d; try { d = JSON.parse(ev.data); } catch (e) { return; } fn(d); });
|
|
533
|
+
};
|
|
534
|
+
ouvir("estado", pintarEstado);
|
|
535
|
+
ouvir("ocupado", pintarOcupado);
|
|
536
|
+
ouvir("historia", reproduzir);
|
|
537
|
+
ouvir("limpar", limparConversa);
|
|
538
|
+
ouvir("terminal", function (t) { terminalSaida.innerHTML = ""; corTerminal = { cor: null, forte: false }; escreverTerminal(t); });
|
|
539
|
+
ouvir("saida", escreverTerminal);
|
|
540
|
+
ouvir("voce", function (d) { tratar("voce", d); });
|
|
541
|
+
ouvir("agente", function (d) { tratar("agente", d); });
|
|
542
|
+
ouvir("comando", function (d) { tratar("comando", d); });
|
|
543
|
+
ouvir("erro", function (d) { tratar("erro", d); });
|
|
544
|
+
ouvir("arquivos", aoMudarArquivos);
|
|
195
545
|
}
|
|
196
546
|
|
|
197
|
-
/* ── mandar
|
|
547
|
+
/* ── mandar ──────────────────────────────────────────────────────────── */
|
|
198
548
|
|
|
199
549
|
function enviar(texto) {
|
|
200
|
-
texto = (texto || "").trim();
|
|
550
|
+
texto = String(texto || "").trim();
|
|
201
551
|
if (!texto) return;
|
|
202
552
|
pedir("/api/enviar", { texto: texto });
|
|
203
553
|
campo.value = "";
|
|
204
554
|
ajustarAltura();
|
|
555
|
+
atualizarEnviar();
|
|
205
556
|
}
|
|
557
|
+
function parar() { pedir("/api/parar", {}); }
|
|
206
558
|
|
|
207
559
|
function ajustarAltura() {
|
|
208
560
|
campo.style.height = "auto";
|
|
209
|
-
campo.style.height = Math.min(campo.scrollHeight, window.innerHeight * 0.
|
|
561
|
+
campo.style.height = Math.min(campo.scrollHeight, window.innerHeight * 0.4) + "px";
|
|
210
562
|
}
|
|
211
563
|
|
|
212
564
|
forma.addEventListener("submit", function (ev) {
|
|
213
565
|
ev.preventDefault();
|
|
214
|
-
|
|
566
|
+
if (ocupado && !campo.value.trim()) parar();
|
|
567
|
+
else enviar(campo.value);
|
|
215
568
|
});
|
|
216
|
-
|
|
217
|
-
campo.addEventListener("input", ajustarAltura);
|
|
569
|
+
campo.addEventListener("input", function () { ajustarAltura(); atualizarEnviar(); });
|
|
218
570
|
campo.addEventListener("keydown", function (ev) {
|
|
219
|
-
if (ev.key === "Enter" && !ev.shiftKey && !ev.
|
|
571
|
+
if (ev.key === "Enter" && !ev.shiftKey && !ev.isComposing) {
|
|
220
572
|
ev.preventDefault();
|
|
221
573
|
enviar(campo.value);
|
|
222
574
|
}
|
|
223
575
|
});
|
|
224
576
|
|
|
225
|
-
|
|
226
|
-
|
|
577
|
+
/* Esc Esc para, como no terminal. Um Esc só fecha o que estiver aberto
|
|
578
|
+
(menu, diálogo) — parar o agente por um toque errado jogaria fora um
|
|
579
|
+
minuto de trabalho. */
|
|
580
|
+
var ultimoEsc = 0;
|
|
227
581
|
document.addEventListener("keydown", function (ev) {
|
|
228
|
-
if (ev.key === "Escape"
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
582
|
+
if (ev.key === "Escape") {
|
|
583
|
+
if (!menu.hidden) { fecharMenu(); return; }
|
|
584
|
+
var agora = Date.now();
|
|
585
|
+
if (ocupado && agora - ultimoEsc < 800) { parar(); ultimoEsc = 0; return; }
|
|
586
|
+
ultimoEsc = agora;
|
|
587
|
+
return;
|
|
233
588
|
}
|
|
589
|
+
if ((ev.ctrlKey || ev.metaKey) && (ev.key === "b" || ev.key === "B")) { ev.preventDefault(); alternarLado(); return; }
|
|
590
|
+
if ((ev.ctrlKey || ev.metaKey) && (ev.key === "j" || ev.key === "J")) { ev.preventDefault(); alternarTerminal(); return; }
|
|
591
|
+
// Tecla de conteúdo devolve o foco ao campo: num chat não se clica para escrever.
|
|
592
|
+
var alvo = document.activeElement;
|
|
593
|
+
var escrevendo = alvo && (alvo.tagName === "INPUT" || alvo.tagName === "TEXTAREA");
|
|
594
|
+
if (ev.key.length === 1 && !ev.metaKey && !ev.ctrlKey && !ev.altKey && !escrevendo && !$("dlgAjuda").open) campo.focus();
|
|
234
595
|
});
|
|
235
596
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
597
|
+
/* ── anexar ──────────────────────────────────────────────────────────
|
|
598
|
+
O navegador não entrega o CAMINHO de um arquivo, só o conteúdo. Então o
|
|
599
|
+
conteúdo sobe para o servidor e entra em state.attachedFiles, igual ao
|
|
600
|
+
/attach do terminal. */
|
|
601
|
+
var arquivo = $("arquivo");
|
|
602
|
+
$("btnAnexar").onclick = function () { arquivo.click(); };
|
|
603
|
+
arquivo.onchange = function () { subir(arquivo.files); arquivo.value = ""; };
|
|
604
|
+
|
|
605
|
+
function subir(lista) {
|
|
606
|
+
Array.prototype.forEach.call(lista || [], function (f) {
|
|
607
|
+
if (f.size > 2 * 1024 * 1024) { escreverErro(f.name + " passa de 2 MB. Anexe um arquivo menor ou peça pelo caminho."); return; }
|
|
608
|
+
var leitor = new FileReader();
|
|
609
|
+
leitor.onload = function () {
|
|
610
|
+
var base64 = String(leitor.result).split(",")[1] || "";
|
|
611
|
+
pedir("/api/anexar", { nome: f.name, base64: base64 }).then(function (d) {
|
|
612
|
+
if (d.estado) pintarEstado(d.estado);
|
|
613
|
+
else escreverErro(d.error || "Não consegui anexar " + f.name + ".");
|
|
614
|
+
});
|
|
615
|
+
};
|
|
616
|
+
leitor.readAsDataURL(f);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
247
619
|
|
|
248
|
-
Duas portas, então. Soltar o arquivo escreve o nome no campo com uma
|
|
249
|
-
frase que o agente entende ("o arquivo X, que está em..."), e soltar TEXTO
|
|
250
|
-
— que é o que o Finder e o Explorer dão ao arrastar com a tecla de atalho,
|
|
251
|
-
e o que todo terminal recebe — entra como caminho de verdade. */
|
|
252
620
|
var conta = 0;
|
|
253
|
-
window.addEventListener("dragenter", function (ev) {
|
|
254
|
-
ev.preventDefault(); conta++; document.body.classList.add("soltando");
|
|
255
|
-
});
|
|
621
|
+
window.addEventListener("dragenter", function (ev) { ev.preventDefault(); conta++; document.body.classList.add("soltando"); });
|
|
256
622
|
window.addEventListener("dragover", function (ev) { ev.preventDefault(); });
|
|
257
|
-
window.addEventListener("dragleave", function () {
|
|
258
|
-
if (--conta <= 0) { conta = 0; document.body.classList.remove("soltando"); }
|
|
259
|
-
});
|
|
623
|
+
window.addEventListener("dragleave", function () { if (--conta <= 0) { conta = 0; document.body.classList.remove("soltando"); } });
|
|
260
624
|
window.addEventListener("drop", function (ev) {
|
|
261
625
|
ev.preventDefault();
|
|
262
626
|
conta = 0;
|
|
263
627
|
document.body.classList.remove("soltando");
|
|
264
|
-
|
|
628
|
+
var dt = ev.dataTransfer;
|
|
629
|
+
if (dt.files && dt.files.length) { subir(dt.files); return; }
|
|
630
|
+
// Texto solto (o caminho que o Finder dá com a tecla de atalho) entra no campo.
|
|
265
631
|
var texto = "";
|
|
266
|
-
try { texto =
|
|
267
|
-
if (
|
|
268
|
-
texto = Array.prototype.map.call(ev.dataTransfer.files, function (f) {
|
|
269
|
-
// Sem caminho: o nome, e o aviso de onde procurar. É o que dá para
|
|
270
|
-
// dizer com honestidade daqui.
|
|
271
|
-
return f.name;
|
|
272
|
-
}).join(" ");
|
|
273
|
-
if (texto) {
|
|
274
|
-
texto = "o arquivo " + texto + " (procure na pasta de Downloads ou na "
|
|
275
|
-
+ "Área de Trabalho; se não achar, me peça o caminho completo)";
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
if (!texto) return;
|
|
279
|
-
campo.value = (campo.value ? campo.value.trimEnd() + " " : "") + texto + " ";
|
|
280
|
-
campo.focus();
|
|
281
|
-
ajustarAltura();
|
|
632
|
+
try { texto = dt.getData("text/plain") || ""; } catch (e) {}
|
|
633
|
+
if (texto) { campo.value = (campo.value ? campo.value.trimEnd() + " " : "") + texto + " "; ajustarAltura(); atualizarEnviar(); campo.focus(); }
|
|
282
634
|
});
|
|
283
635
|
|
|
284
|
-
/* ──
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
636
|
+
/* ── os controles do compositor ─────────────────────────────────────── */
|
|
637
|
+
|
|
638
|
+
$("btnPermissao").onclick = function () {
|
|
639
|
+
var atual = estado.permissoes || "auto";
|
|
640
|
+
abrirMenu(this, [{ titulo: "Quando ele pede licença" }].concat(["auto", "perguntar", "livre"].map(function (m) {
|
|
641
|
+
return {
|
|
642
|
+
rotulo: PERMISSOES[m].rotulo, desc: PERMISSOES[m].desc, icone: PERMISSOES[m].icone,
|
|
643
|
+
marcado: m === atual, classe: m === "livre" ? "laranja" : "",
|
|
644
|
+
acao: function () { pedir("/api/permissoes", { modo: m }).then(function (d) { if (d.estado) pintarEstado(d.estado); }); },
|
|
645
|
+
};
|
|
646
|
+
})));
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
$("btnEsforco").onclick = function () {
|
|
650
|
+
var atual = estado.esforco || "medium";
|
|
651
|
+
abrirMenu(this, [{ titulo: "Esforço do agente" }].concat(ESFORCOS.map(function (n) {
|
|
652
|
+
return {
|
|
653
|
+
rotulo: n.rotulo + " · " + n.nome, desc: n.desc, marcado: n.nome === atual,
|
|
654
|
+
acao: function () { pedir("/api/esforco", { nivel: n.nome }).then(function (d) { if (d.estado) pintarEstado(d.estado); }); },
|
|
655
|
+
};
|
|
656
|
+
})));
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
$("btnMic").onclick = function () {
|
|
660
|
+
// O /voice ouve pelo microfone DO COMPUTADOR (o "ei, primo"), não pela
|
|
661
|
+
// página — por isso é um liga/desliga, e o terminal conta o progresso.
|
|
662
|
+
pedir("/api/voz", { ligar: !estado.voz }).then(function (d) { if (d.estado) pintarEstado(d.estado); });
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
/* ── projetos ────────────────────────────────────────────────────────── */
|
|
666
|
+
|
|
667
|
+
var projetos = [];
|
|
668
|
+
function entrar(nome) {
|
|
669
|
+
return pedir("/api/projeto", { nome: nome }).then(function (d) {
|
|
670
|
+
if (d.estado) { pintarEstado(d.estado); carregarProjetos(); recarregarPainel(true); }
|
|
671
|
+
else escreverErro(d.error || "Não consegui entrar no projeto.");
|
|
299
672
|
});
|
|
300
673
|
}
|
|
301
|
-
function
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (qual === "sistema") {
|
|
307
|
-
raiz.setAttribute("data-tema",
|
|
308
|
-
window.matchMedia("(prefers-color-scheme: dark)").matches ? "escuro" : "claro");
|
|
309
|
-
} else {
|
|
310
|
-
raiz.setAttribute("data-tema", qual);
|
|
311
|
-
}
|
|
312
|
-
marcarEscolha("temas", "tema", qual);
|
|
674
|
+
function criarProjeto(nome) {
|
|
675
|
+
return pedir("/api/projeto", { novo: nome }).then(function (d) {
|
|
676
|
+
if (d.estado) { pintarEstado(d.estado); carregarProjetos(); recarregarPainel(true); campo.focus(); }
|
|
677
|
+
else escreverErro(d.error || "Não consegui criar a pasta.");
|
|
678
|
+
});
|
|
313
679
|
}
|
|
314
|
-
function
|
|
315
|
-
|
|
316
|
-
|
|
680
|
+
function menuDeProjetos(dono) {
|
|
681
|
+
var itens = [{ titulo: "Projetos em ~/primocode" }];
|
|
682
|
+
projetos.slice(0, 12).forEach(function (p) {
|
|
683
|
+
itens.push({ rotulo: p.nome, icone: "pasta", marcado: p.nome === estado.projeto, acao: function () { entrar(p.nome); } });
|
|
684
|
+
});
|
|
685
|
+
if (!projetos.length) itens.push({ titulo: "Nenhum projeto ainda" });
|
|
686
|
+
itens.push({ sep: true });
|
|
687
|
+
itens.push({ entrada: { id: "nomeNovaPasta", placeholder: "Nova pasta…", botao: "Criar", acao: criarProjeto } });
|
|
688
|
+
abrirMenu(dono, itens);
|
|
689
|
+
}
|
|
690
|
+
$("btnProjeto").onclick = function () { menuDeProjetos(this); };
|
|
691
|
+
$("btnNovoProjeto").onclick = function () {
|
|
692
|
+
abrirMenu(this, [{ titulo: "Novo projeto em ~/primocode" },
|
|
693
|
+
{ entrada: { id: "nomeNovoProjeto", placeholder: "Nome da pasta", botao: "Criar", acao: criarProjeto } }]);
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
function marcarProjetoAtual() {
|
|
697
|
+
Array.prototype.forEach.call($("listaProjetos").querySelectorAll(".item"), function (b) {
|
|
698
|
+
b.setAttribute("aria-current", b.dataset.nome === estado.projeto ? "true" : "false");
|
|
699
|
+
});
|
|
317
700
|
}
|
|
318
701
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
702
|
+
function carregarProjetos() {
|
|
703
|
+
return pedir("/api/projetos").then(function (d) {
|
|
704
|
+
if (!d.ok) return;
|
|
705
|
+
projetos = d.projetos || [];
|
|
706
|
+
var caixa = $("listaProjetos");
|
|
707
|
+
caixa.innerHTML = "";
|
|
708
|
+
if (!projetos.length) { caixa.appendChild(el("div", "vazio-lista", "Nenhum projeto")); return; }
|
|
709
|
+
projetos.slice(0, 8).forEach(function (p) {
|
|
710
|
+
var b = el("button", "item");
|
|
711
|
+
b.type = "button";
|
|
712
|
+
b.dataset.nome = p.nome;
|
|
713
|
+
b.appendChild(icone("pasta"));
|
|
714
|
+
b.appendChild(el("span", "titulo", p.nome));
|
|
715
|
+
b.onclick = function () { entrar(p.nome); };
|
|
716
|
+
caixa.appendChild(b);
|
|
717
|
+
});
|
|
718
|
+
marcarProjetoAtual();
|
|
719
|
+
filtrar();
|
|
720
|
+
});
|
|
329
721
|
}
|
|
330
722
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
723
|
+
/* ── recentes ────────────────────────────────────────────────────────
|
|
724
|
+
Com conta, as conversas da nuvem (as mesmas do /chats e do celular):
|
|
725
|
+
clicar continua dali. Sem conta, os últimos pedidos deste computador:
|
|
726
|
+
clicar põe o pedido de volta no campo, para repetir ou ajustar. */
|
|
727
|
+
function quando(ms) {
|
|
728
|
+
if (!ms) return "";
|
|
729
|
+
var d = new Date(ms);
|
|
334
730
|
if (isNaN(d)) return "";
|
|
335
731
|
var dias = Math.floor((Date.now() - d.getTime()) / 86400000);
|
|
336
732
|
if (dias <= 0) return d.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
|
|
337
733
|
if (dias === 1) return "ontem";
|
|
338
|
-
if (dias < 7) return dias + "
|
|
734
|
+
if (dias < 7) return dias + " d";
|
|
339
735
|
return d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit" });
|
|
340
736
|
}
|
|
341
737
|
|
|
342
738
|
var conversaAberta = null;
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
pedir("/api/conversas").then(function (r) { return r.json(); }).then(function (d) {
|
|
348
|
-
if (!d || !d.ok || !d.conversas || !d.conversas.length) return; // deixa o texto de vazio
|
|
739
|
+
function carregarRecentes() {
|
|
740
|
+
return pedir("/api/recentes").then(function (d) {
|
|
741
|
+
if (!d.ok) return;
|
|
742
|
+
var caixa = $("listaRecentes");
|
|
349
743
|
caixa.innerHTML = "";
|
|
350
|
-
d.
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
b
|
|
354
|
-
|
|
355
|
-
// textContent, nunca innerHTML: o título
|
|
356
|
-
//
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
744
|
+
var itens = d.itens || [];
|
|
745
|
+
if (!itens.length) { caixa.appendChild(el("div", "vazio-lista", "Nenhuma conversa ainda")); return; }
|
|
746
|
+
itens.slice(0, 20).forEach(function (c) {
|
|
747
|
+
var b = el("button", "item");
|
|
748
|
+
b.type = "button";
|
|
749
|
+
// textContent, nunca innerHTML: o título foi escrito por gente, e um
|
|
750
|
+
// <img onerror> ali viraria script na janela que MANDA NO AGENTE.
|
|
751
|
+
b.appendChild(el("span", "titulo", c.titulo || "Conversa"));
|
|
752
|
+
b.title = c.titulo || "";
|
|
753
|
+
if (d.fonte === "nuvem") {
|
|
754
|
+
b.setAttribute("aria-current", c.id === conversaAberta ? "true" : "false");
|
|
755
|
+
b.onclick = function () { abrirConversa(c); };
|
|
756
|
+
} else {
|
|
757
|
+
b.onclick = function () { campo.value = c.titulo; ajustarAltura(); atualizarEnviar(); campo.focus(); };
|
|
758
|
+
}
|
|
363
759
|
caixa.appendChild(b);
|
|
364
760
|
});
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
function abrirConversa(id) {
|
|
369
|
-
conversaAberta = id;
|
|
370
|
-
pedir("/api/conversa/" + encodeURIComponent(id)).then(function (r) { return r.json(); })
|
|
371
|
-
.then(function (d) {
|
|
372
|
-
if (!d || !d.ok) return;
|
|
373
|
-
voltarAoVazio();
|
|
374
|
-
limparBoasVindas();
|
|
375
|
-
var tira = document.getElementById("conversa");
|
|
376
|
-
(d.mensagens || []).forEach(function (m) {
|
|
377
|
-
var linha = document.createElement("div");
|
|
378
|
-
if (m.papel === "user") {
|
|
379
|
-
linha.className = "voce";
|
|
380
|
-
var dentro = document.createElement("div");
|
|
381
|
-
dentro.textContent = m.texto;
|
|
382
|
-
linha.appendChild(dentro);
|
|
383
|
-
} else {
|
|
384
|
-
linha.className = "saida";
|
|
385
|
-
linha.textContent = m.texto;
|
|
386
|
-
}
|
|
387
|
-
tira.appendChild(linha);
|
|
388
|
-
});
|
|
389
|
-
rolo.scrollTop = rolo.scrollHeight;
|
|
390
|
-
carregarConversas();
|
|
391
|
-
}).catch(function () {});
|
|
761
|
+
filtrar();
|
|
762
|
+
});
|
|
392
763
|
}
|
|
393
764
|
|
|
394
|
-
function
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
conversaAberta =
|
|
398
|
-
|
|
399
|
-
carregarConversas();
|
|
765
|
+
function abrirConversa(c) {
|
|
766
|
+
pedir("/api/conversa/abrir", { id: c.id, titulo: c.titulo }).then(function (d) {
|
|
767
|
+
if (!d.ok) { escreverErro(d.error || "Não consegui abrir a conversa."); return; }
|
|
768
|
+
conversaAberta = c.id;
|
|
769
|
+
carregarRecentes();
|
|
400
770
|
campo.focus();
|
|
401
|
-
};
|
|
771
|
+
});
|
|
772
|
+
}
|
|
402
773
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
var para = document.getElementById(k + "Config");
|
|
412
|
-
if (de && para) para.textContent = de.textContent || "—";
|
|
774
|
+
/* A busca filtra o que já está na barra — recentes e projetos — sem ir ao
|
|
775
|
+
servidor: são no máximo vinte linhas. */
|
|
776
|
+
var campoBusca = $("campoBusca");
|
|
777
|
+
function filtrar() {
|
|
778
|
+
var q = (campoBusca.value || "").trim().toLowerCase();
|
|
779
|
+
["listaProjetos", "listaRecentes"].forEach(function (id) {
|
|
780
|
+
Array.prototype.forEach.call($(id).querySelectorAll(".item"), function (b) {
|
|
781
|
+
b.hidden = !!q && b.textContent.toLowerCase().indexOf(q) < 0;
|
|
413
782
|
});
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
campoBusca.addEventListener("input", filtrar);
|
|
786
|
+
$("btnBusca").onclick = function () {
|
|
787
|
+
var caixa = $("busca");
|
|
788
|
+
caixa.hidden = !caixa.hidden;
|
|
789
|
+
if (!caixa.hidden) campoBusca.focus(); else { campoBusca.value = ""; filtrar(); }
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
var btnAvisos = $("btnAvisos");
|
|
793
|
+
btnAvisos.setAttribute("aria-pressed", avisar ? "true" : "false");
|
|
794
|
+
btnAvisos.onclick = function () {
|
|
795
|
+
avisar = !avisar;
|
|
796
|
+
guardo("primo-avisar", avisar ? "1" : "");
|
|
797
|
+
btnAvisos.setAttribute("aria-pressed", avisar ? "true" : "false");
|
|
798
|
+
if (avisar && window.Notification && Notification.permission === "default") {
|
|
799
|
+
try { Notification.requestPermission(); } catch (e) {}
|
|
800
|
+
}
|
|
801
|
+
};
|
|
417
802
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
803
|
+
$("btnNovo").onclick = function () {
|
|
804
|
+
pedir("/api/novo-chat", {}).then(function (d) {
|
|
805
|
+
if (!d.ok) { escreverErro(d.error || "Não deu para começar outro chat agora."); return; }
|
|
806
|
+
conversaAberta = null;
|
|
807
|
+
limparConversa();
|
|
808
|
+
alternarTerminal(false);
|
|
809
|
+
carregarRecentes();
|
|
810
|
+
campo.focus();
|
|
811
|
+
});
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
/* ── o nome do app, a ajuda e o tema ─────────────────────────────────── */
|
|
815
|
+
|
|
816
|
+
var CHAVE_TEMA = "primocode_tema";
|
|
817
|
+
function aplicarTema(qual) {
|
|
818
|
+
// "sistema" não é um terceiro tema: é seguir o aparelho. Guardar
|
|
819
|
+
// "escuro" no lugar faria a janela ignorar o modo claro para sempre.
|
|
820
|
+
var real = qual === "sistema"
|
|
821
|
+
? (window.matchMedia("(prefers-color-scheme: light)").matches ? "claro" : "escuro")
|
|
822
|
+
: qual;
|
|
823
|
+
document.documentElement.setAttribute("data-tema", real);
|
|
824
|
+
}
|
|
825
|
+
window.matchMedia("(prefers-color-scheme: light)").addEventListener("change", function () {
|
|
826
|
+
if (pego(CHAVE_TEMA, "escuro") === "sistema") aplicarTema("sistema");
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
var dlgAjuda = $("dlgAjuda");
|
|
830
|
+
function abrirAjuda() { if (!dlgAjuda.open) dlgAjuda.showModal(); }
|
|
831
|
+
$("btnAjuda").onclick = abrirAjuda;
|
|
832
|
+
$("navComandos").onclick = abrirAjuda;
|
|
833
|
+
$("fecharAjuda").onclick = function () { dlgAjuda.close(); };
|
|
834
|
+
$("listaComandos").onclick = function (ev) {
|
|
835
|
+
var b = ev.target.closest("[data-cmd]");
|
|
836
|
+
if (!b) return;
|
|
837
|
+
dlgAjuda.close();
|
|
838
|
+
campo.value = b.dataset.cmd;
|
|
839
|
+
ajustarAltura(); atualizarEnviar();
|
|
840
|
+
campo.focus();
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
$("btnMarca").onclick = function () {
|
|
844
|
+
var tema = pego(CHAVE_TEMA, "escuro");
|
|
845
|
+
var escolher = function (t) { return function () { guardo(CHAVE_TEMA, t); aplicarTema(t); }; };
|
|
846
|
+
abrirMenu(this, [
|
|
847
|
+
{ titulo: "Tema" },
|
|
848
|
+
{ rotulo: "Escuro", marcado: tema === "escuro", acao: escolher("escuro") },
|
|
849
|
+
{ rotulo: "Claro", marcado: tema === "claro", acao: escolher("claro") },
|
|
850
|
+
{ rotulo: "Do sistema", marcado: tema === "sistema", acao: escolher("sistema") },
|
|
851
|
+
{ sep: true },
|
|
852
|
+
{ rotulo: "Atalhos e comandos", icone: "ajuda", acao: abrirAjuda },
|
|
853
|
+
{ rotulo: "Terminal", icone: "terminal", acao: function () { alternarTerminal(true); } },
|
|
854
|
+
{ sep: true },
|
|
855
|
+
{ titulo: "PrimoCode " + (estado.versao ? "v" + estado.versao : "") },
|
|
856
|
+
]);
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
/* ── a barra lateral ──────────────────────────────────────────────────
|
|
860
|
+
Em tela larga ela fica aberta e a escolha é lembrada; abaixo de 960px
|
|
861
|
+
ela flutua por cima da conversa, então começa fechada e fecha sozinha
|
|
862
|
+
depois de uma escolha. */
|
|
863
|
+
var estreita = window.matchMedia("(max-width: 960px)");
|
|
864
|
+
function alternarLado(abrir) {
|
|
865
|
+
var aberta = !app.classList.contains("sem-lado");
|
|
866
|
+
if (abrir === undefined) abrir = !aberta;
|
|
867
|
+
app.classList.toggle("sem-lado", !abrir);
|
|
868
|
+
if (!estreita.matches) guardo("primo-lado", abrir ? "aberto" : "fechado");
|
|
869
|
+
}
|
|
870
|
+
$("btnLado").onclick = function () { alternarLado(false); };
|
|
871
|
+
$("btnLadoAbrir").onclick = function () { alternarLado(true); };
|
|
872
|
+
function ajustarLado() {
|
|
873
|
+
if (estreita.matches) app.classList.add("sem-lado");
|
|
874
|
+
else app.classList.toggle("sem-lado", pego("primo-lado", "aberto") === "fechado");
|
|
875
|
+
}
|
|
876
|
+
estreita.addEventListener("change", ajustarLado);
|
|
877
|
+
$("lado").addEventListener("click", function (ev) {
|
|
878
|
+
if (estreita.matches && ev.target.closest(".item, #btnNovo, #navTerminal, #navPrevia")) alternarLado(false);
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
/* ── o painel: Prévia e Arquivos ─────────────────────────────────────
|
|
882
|
+
A prévia é o index.html do projeto, servido pela rota com a chave no
|
|
883
|
+
caminho (os arquivos relativos da página precisam dela), num iframe
|
|
884
|
+
SEM allow-same-origin: o código da prévia não fala com a API. */
|
|
885
|
+
var painel = $("painel");
|
|
886
|
+
var previa = $("previa");
|
|
887
|
+
var aba = "previa";
|
|
888
|
+
var entrada = null;
|
|
889
|
+
var itensArvore = [];
|
|
890
|
+
var abertos = {};
|
|
891
|
+
var mudadosAgora = {};
|
|
892
|
+
var arquivoAberto = null;
|
|
893
|
+
|
|
894
|
+
function abrirPainel(qual) {
|
|
895
|
+
painel.hidden = false;
|
|
896
|
+
$("btnPainel").setAttribute("aria-pressed", "true");
|
|
897
|
+
if (qual) escolherAba(qual);
|
|
898
|
+
recarregarPainel(true);
|
|
899
|
+
}
|
|
900
|
+
function fecharPainel() {
|
|
901
|
+
painel.hidden = true;
|
|
902
|
+
$("btnPainel").setAttribute("aria-pressed", "false");
|
|
903
|
+
previa.removeAttribute("src");
|
|
904
|
+
}
|
|
905
|
+
$("btnPainel").onclick = function () { if (painel.hidden) abrirPainel(); else fecharPainel(); };
|
|
906
|
+
$("fecharPainel").onclick = fecharPainel;
|
|
907
|
+
$("navPrevia").onclick = function () { abrirPainel("previa"); };
|
|
908
|
+
$("btnRecarregar").onclick = function () { recarregarPainel(true); };
|
|
909
|
+
|
|
910
|
+
function escolherAba(qual) {
|
|
911
|
+
aba = qual;
|
|
912
|
+
$("abaPrevia").setAttribute("aria-selected", qual === "previa" ? "true" : "false");
|
|
913
|
+
$("abaArquivos").setAttribute("aria-selected", qual === "arquivos" ? "true" : "false");
|
|
914
|
+
$("corpoPrevia").hidden = qual !== "previa";
|
|
915
|
+
$("corpoArquivos").hidden = qual !== "arquivos";
|
|
916
|
+
}
|
|
917
|
+
$("abaPrevia").onclick = function () { escolherAba("previa"); recarregarPainel(true); };
|
|
918
|
+
$("abaArquivos").onclick = function () { escolherAba("arquivos"); recarregarPainel(false); };
|
|
919
|
+
|
|
920
|
+
function recarregarPainel(previaTambem) {
|
|
921
|
+
if (painel.hidden) return;
|
|
922
|
+
pedir("/api/arquivos/arvore").then(function (d) {
|
|
923
|
+
itensArvore = d.ok ? d.itens || [] : [];
|
|
924
|
+
entrada = d.ok ? d.entrada : null;
|
|
925
|
+
desenharArvore();
|
|
926
|
+
if (previaTambem || aba === "previa") atualizarPrevia();
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function atualizarPrevia() {
|
|
931
|
+
var tem = !!entrada;
|
|
932
|
+
$("previaVazia").hidden = tem;
|
|
933
|
+
previa.hidden = !tem;
|
|
934
|
+
if (!tem) { previa.removeAttribute("src"); return; }
|
|
935
|
+
var caminho = entrada.split("/").map(encodeURIComponent).join("/");
|
|
936
|
+
// O `?v=` força o navegador a buscar de novo: o mesmo endereço no src
|
|
937
|
+
// não recarrega o iframe.
|
|
938
|
+
previa.src = "/api/previa/" + CHAVE + "/" + caminho + "?v=" + Date.now();
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
var espera = null;
|
|
942
|
+
function aoMudarArquivos(d) {
|
|
943
|
+
(d.mudados || []).forEach(function (m) { mudadosAgora[m] = Date.now(); });
|
|
944
|
+
if (painel.hidden) return;
|
|
945
|
+
// O agente escreve em rajada (três arquivos num segundo): recarregar a
|
|
946
|
+
// cada um faria a prévia piscar. Espera o silêncio.
|
|
947
|
+
clearTimeout(espera);
|
|
948
|
+
espera = setTimeout(function () {
|
|
949
|
+
recarregarPainel(aba === "previa");
|
|
950
|
+
if (arquivoAberto && (d.mudados || []).indexOf(arquivoAberto) >= 0) lerArquivo(arquivoAberto);
|
|
951
|
+
}, 350);
|
|
952
|
+
}
|
|
428
953
|
|
|
429
|
-
|
|
430
|
-
|
|
954
|
+
function desenharArvore() {
|
|
955
|
+
var caixa = $("arvore");
|
|
956
|
+
caixa.innerHTML = "";
|
|
957
|
+
if (!itensArvore.length) {
|
|
958
|
+
caixa.appendChild(el("div", "vazio-lista", estado.projeto ? "Pasta vazia" : "Escolha um projeto para ver os arquivos"));
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
itensArvore.forEach(function (it) {
|
|
962
|
+
var partes = it.caminho.split("/");
|
|
963
|
+
// Some quem está dentro de uma pasta fechada.
|
|
964
|
+
for (var i = 1; i < partes.length; i++) {
|
|
965
|
+
if (!abertos[partes.slice(0, i).join("/")]) return;
|
|
966
|
+
}
|
|
967
|
+
var b = el("button", "no");
|
|
968
|
+
b.type = "button";
|
|
969
|
+
b.setAttribute("role", "treeitem");
|
|
970
|
+
b.style.paddingLeft = (8 + (partes.length - 1) * 14) + "px";
|
|
971
|
+
if (it.tipo === "pasta") {
|
|
972
|
+
b.setAttribute("aria-expanded", abertos[it.caminho] ? "true" : "false");
|
|
973
|
+
b.appendChild(icone("pasta"));
|
|
974
|
+
b.onclick = function () { abertos[it.caminho] = !abertos[it.caminho]; desenharArvore(); };
|
|
975
|
+
} else {
|
|
976
|
+
b.appendChild(icone("arquivo"));
|
|
977
|
+
b.setAttribute("aria-selected", it.caminho === arquivoAberto ? "true" : "false");
|
|
978
|
+
if (mudadosAgora[it.caminho] > Date.now() - 60000) b.classList.add("mudou");
|
|
979
|
+
b.onclick = function () { lerArquivo(it.caminho); };
|
|
980
|
+
}
|
|
981
|
+
b.appendChild(el("span", "nome", partes[partes.length - 1]));
|
|
982
|
+
caixa.appendChild(b);
|
|
431
983
|
});
|
|
432
984
|
}
|
|
433
985
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
986
|
+
function lerArquivo(caminho) {
|
|
987
|
+
arquivoAberto = caminho;
|
|
988
|
+
desenharArvore();
|
|
989
|
+
$("visorNome").textContent = caminho;
|
|
990
|
+
var visor = $("visor");
|
|
991
|
+
pedir("/api/arquivos/ler?p=" + encodeURIComponent(caminho)).then(function (d) {
|
|
992
|
+
visor.innerHTML = "";
|
|
993
|
+
if (!d.ok) { visor.textContent = " " + (d.error || "não abriu"); return; }
|
|
994
|
+
if (d.binario) { visor.textContent = " Arquivo binário (imagem, áudio…). Não dá para mostrar como texto."; return; }
|
|
995
|
+
if (d.grande) { visor.textContent = " Arquivo grande demais para mostrar aqui (mais de 1 MB)."; return; }
|
|
996
|
+
var frag = document.createDocumentFragment();
|
|
997
|
+
String(d.conteudo || "").split("\n").forEach(function (l) { frag.appendChild(el("span", "ln", l || " ")); });
|
|
998
|
+
visor.appendChild(frag);
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/* ── ligar tudo ──────────────────────────────────────────────────────── */
|
|
439
1003
|
|
|
440
|
-
|
|
441
|
-
|
|
1004
|
+
aplicarTema(pego(CHAVE_TEMA, "escuro"));
|
|
1005
|
+
ajustarLado();
|
|
1006
|
+
if (window.PrimoMarca) {
|
|
1007
|
+
$("mascote").innerHTML = window.PrimoMarca.linha({ tamanho: 56 });
|
|
1008
|
+
$("mascotePainel").innerHTML = window.PrimoMarca.linha({ tamanho: 40 });
|
|
1009
|
+
}
|
|
1010
|
+
pedir("/api/estado").then(function (d) { if (d.__status === 200) pintarEstado(d); }).catch(function () {});
|
|
1011
|
+
carregarProjetos();
|
|
1012
|
+
carregarRecentes();
|
|
442
1013
|
ligar();
|
|
1014
|
+
if (ABRIR_PAINEL === "previa" || ABRIR_PAINEL === "arquivos") abrirPainel(ABRIR_PAINEL);
|
|
1015
|
+
atualizarEnviar();
|
|
443
1016
|
campo.focus();
|
|
444
1017
|
})();
|