agents-city 0.1.0 → 0.2.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/.claude-plugin/marketplace.json +1 -1
- package/README.es.md +12 -3
- package/README.md +10 -2
- package/bin/hall.html +113 -62
- package/bin/navegador.mjs +91 -5
- package/bin/serve.py +87 -0
- package/bin/test-cities.py +45 -0
- package/bin/test-navegador.py +8 -1
- package/bin/test-seat.py +1 -1
- package/bin/test-serve.py +21 -10
- package/city/web/dist-hall/hall.js +605 -32
- package/city/web/src/bienvenida.ts +524 -0
- package/city/web/src/hall.ts +290 -45
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/channel/bus.js +1 -1
- package/plugin/channel/bus.ts +1 -1
- package/plugin/channel/runtime/codex.ts +1 -1
- package/plugin/channel/runtime-gateway.js +1 -1
- package/plugin/scripts/cities.py +34 -0
- package/plugin/scripts/reset.py +50 -14
|
@@ -1,3 +1,395 @@
|
|
|
1
|
+
// city/web/src/bienvenida.ts
|
|
2
|
+
var CLASES = [
|
|
3
|
+
[
|
|
4
|
+
"code",
|
|
5
|
+
"It writes code",
|
|
6
|
+
"Its house grows with the pull requests it merges. Mounts repositories and worktrees."
|
|
7
|
+
],
|
|
8
|
+
[
|
|
9
|
+
"knowledge",
|
|
10
|
+
"It keeps knowledge",
|
|
11
|
+
"Its house grows with the documents it writes. Needs no git at all."
|
|
12
|
+
],
|
|
13
|
+
["coordinator", "It coordinates", "Its house grows with the decisions it records."]
|
|
14
|
+
];
|
|
15
|
+
var PASOS = ["Welcome", "The work", "Your chair", "The houses", "Ready"];
|
|
16
|
+
function casita(color) {
|
|
17
|
+
return `<svg viewBox="0 0 120 108" role="img" aria-label="an isometric house" class="bvCasa">
|
|
18
|
+
<polygon points="60,10 112,40 60,70 8,40" fill="${color}" opacity=".95"/>
|
|
19
|
+
<polygon points="8,40 60,70 60,100 8,70" fill="${color}" opacity=".62"/>
|
|
20
|
+
<polygon points="112,40 60,70 60,100 112,70" fill="${color}" opacity=".40"/>
|
|
21
|
+
</svg>`;
|
|
22
|
+
}
|
|
23
|
+
var Bienvenida = class {
|
|
24
|
+
constructor(p) {
|
|
25
|
+
this.p = p;
|
|
26
|
+
}
|
|
27
|
+
paso = 0;
|
|
28
|
+
dominios = [];
|
|
29
|
+
roles = [];
|
|
30
|
+
rolesDeAgente = [];
|
|
31
|
+
repos = [];
|
|
32
|
+
dominio = "";
|
|
33
|
+
rol = "";
|
|
34
|
+
roster = [];
|
|
35
|
+
enCurso = null;
|
|
36
|
+
guardando = false;
|
|
37
|
+
buscandoRepos = false;
|
|
38
|
+
reposListos = false;
|
|
39
|
+
lienzo = null;
|
|
40
|
+
/** Draw the current step into `lienzo`. The only entry point. */
|
|
41
|
+
async pinta(lienzo) {
|
|
42
|
+
this.lienzo = lienzo;
|
|
43
|
+
if (!this.dominios.length) {
|
|
44
|
+
lienzo.innerHTML = '<p class="cargando">reading the domain packs</p>';
|
|
45
|
+
const [d, ra] = await Promise.all([
|
|
46
|
+
this.p.api("/api/domains"),
|
|
47
|
+
this.p.api("/api/roles?scope=agent")
|
|
48
|
+
]);
|
|
49
|
+
this.dominios = d.domains;
|
|
50
|
+
this.rolesDeAgente = ra.roles;
|
|
51
|
+
}
|
|
52
|
+
lienzo.innerHTML = `<div class="bv">${this.cabecera()}${await this.cuerpo()}</div>`;
|
|
53
|
+
this.enlaza(lienzo);
|
|
54
|
+
}
|
|
55
|
+
cabecera() {
|
|
56
|
+
const puntos = PASOS.map(
|
|
57
|
+
(nombre, i) => `<span class="bvPunto ${i === this.paso ? "aqui" : i < this.paso ? "hecho" : ""}"
|
|
58
|
+
title="${this.p.esc(nombre)}"><i></i>${this.p.esc(nombre)}</span>`
|
|
59
|
+
).join("");
|
|
60
|
+
return `<div class="bvPasos">${puntos}</div>`;
|
|
61
|
+
}
|
|
62
|
+
async cuerpo() {
|
|
63
|
+
switch (this.paso) {
|
|
64
|
+
case 0:
|
|
65
|
+
return this.pantallaHola();
|
|
66
|
+
case 1:
|
|
67
|
+
return this.pantallaDominio();
|
|
68
|
+
case 2:
|
|
69
|
+
return await this.pantallaRol();
|
|
70
|
+
case 3:
|
|
71
|
+
return this.pantallaAgentes();
|
|
72
|
+
default:
|
|
73
|
+
return this.pantallaFinal();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// ── 0. what this is ────────────────────────────────────────────────────────
|
|
77
|
+
pantallaHola() {
|
|
78
|
+
return `
|
|
79
|
+
<div class="bvHola">
|
|
80
|
+
<div class="bvArte">${casita("#7048e8")}${casita("#45b9a7")}${casita("#f2674a")}</div>
|
|
81
|
+
<div>
|
|
82
|
+
<span class="sub">welcome</span>
|
|
83
|
+
<h1>A city is you, and the houses around you</h1>
|
|
84
|
+
<p class="prosa">You take the chair. Around it, one <b>house</b> per worker \u2014 an
|
|
85
|
+
<b>agent</b> \u2014 each with its own window, its own role and its own corner of your
|
|
86
|
+
disk: a repository, three of them, a folder of documents, whatever it actually
|
|
87
|
+
works on. The map draws them as houses that grow with the work done in them, and
|
|
88
|
+
many houses are a city. They never talk to each other behind your back: you chair,
|
|
89
|
+
they answer.</p>
|
|
90
|
+
<p class="prosa">Everything lives as plain files in
|
|
91
|
+
<code class="mono">${this.p.esc(this.p.datos)}</code>. Nothing leaves this machine,
|
|
92
|
+
there is no account, and you can edit any of it by hand afterwards.</p>
|
|
93
|
+
<p class="prosa"><b>Four questions and you are working.</b> You can skip any of them
|
|
94
|
+
and change everything later.</p>
|
|
95
|
+
<div class="bvBotones">
|
|
96
|
+
<button class="bt ppal" data-bv="siguiente">Let\u2019s build it</button>
|
|
97
|
+
<button class="bt" data-bv="salir">I\u2019ll set it up myself</button>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>`;
|
|
101
|
+
}
|
|
102
|
+
// ── 1. the domain ─────────────────────────────────────────────────────────
|
|
103
|
+
pantallaDominio() {
|
|
104
|
+
const tarjetas = this.dominios.map(
|
|
105
|
+
(d) => `
|
|
106
|
+
<button class="bvOpcion ${this.dominio === d.id ? "elegida" : ""}" data-bv="dominio"
|
|
107
|
+
data-id="${this.p.esc(d.id)}">
|
|
108
|
+
<b>${this.p.esc(d.name)}</b><span>${this.p.esc(d.summary)}</span></button>`
|
|
109
|
+
).join("");
|
|
110
|
+
return `
|
|
111
|
+
<div class="bvPaso">
|
|
112
|
+
<span class="sub">question 1 of 4</span>
|
|
113
|
+
<h1>What kind of work happens here?</h1>
|
|
114
|
+
<p class="prosa">It decides the vocabulary, the roles you will be offered and what
|
|
115
|
+
counts as evidence in a decision. A clinic does not ship pull requests, and a law
|
|
116
|
+
firm does not measure story points. Pick the closest one \u2014 you can change it later.</p>
|
|
117
|
+
<div class="bvRejilla">${tarjetas}</div>
|
|
118
|
+
<div class="bvBotones">
|
|
119
|
+
<button class="bt" data-bv="atras">Back</button>
|
|
120
|
+
<button class="bt ppal" data-bv="siguiente" ${this.dominio ? "" : "disabled"}>Next</button>
|
|
121
|
+
</div>
|
|
122
|
+
</div>`;
|
|
123
|
+
}
|
|
124
|
+
// ── 2. your chair ─────────────────────────────────────────────────────────
|
|
125
|
+
async pantallaRol() {
|
|
126
|
+
if (!this.roles.length || this.roles[0]?.id === void 0) {
|
|
127
|
+
const r = await this.p.api(
|
|
128
|
+
"/api/roles?domain=" + encodeURIComponent(this.dominio)
|
|
129
|
+
);
|
|
130
|
+
this.roles = r.roles;
|
|
131
|
+
}
|
|
132
|
+
const tarjetas = this.roles.map(
|
|
133
|
+
(r) => `
|
|
134
|
+
<button class="bvOpcion ${this.rol === r.id ? "elegida" : ""}" data-bv="rol"
|
|
135
|
+
data-id="${this.p.esc(r.id)}">
|
|
136
|
+
<b>${this.p.esc(r.name)}</b><span>${this.p.esc(r.summary || r.trade)}</span></button>`
|
|
137
|
+
).join("");
|
|
138
|
+
return `
|
|
139
|
+
<div class="bvPaso">
|
|
140
|
+
<span class="sub">question 2 of 4</span>
|
|
141
|
+
<h1>And what are you, here?</h1>
|
|
142
|
+
<p class="prosa">You chair this city whatever you answer \u2014 this is your
|
|
143
|
+
<b>speciality</b>, not your authority. It shapes the perspective you bring to a
|
|
144
|
+
decision and the knowledge files the city writes for you. <b>Blank</b> is a real
|
|
145
|
+
answer: it means no preset knowledge.</p>
|
|
146
|
+
<div class="bvRejilla">${tarjetas}</div>
|
|
147
|
+
<div class="bvBotones">
|
|
148
|
+
<button class="bt" data-bv="atras">Back</button>
|
|
149
|
+
<button class="bt ppal" data-bv="siguiente" ${this.rol ? "" : "disabled"}>Next</button>
|
|
150
|
+
</div>
|
|
151
|
+
</div>`;
|
|
152
|
+
}
|
|
153
|
+
// ── 3. the agents ─────────────────────────────────────────────────────────
|
|
154
|
+
pantallaAgentes() {
|
|
155
|
+
this.buscaReposUnaVez();
|
|
156
|
+
if (!this.enCurso) return this.listaDeAgentes();
|
|
157
|
+
return this.formularioDeAgente(this.enCurso);
|
|
158
|
+
}
|
|
159
|
+
buscaReposUnaVez() {
|
|
160
|
+
if (this.reposListos || this.buscandoRepos) return;
|
|
161
|
+
this.buscandoRepos = true;
|
|
162
|
+
void this.p.api("/api/misrepos?user=" + encodeURIComponent(this.p.yo)).then((r) => {
|
|
163
|
+
this.repos = (r.repos ?? []).slice(0, 200);
|
|
164
|
+
}).catch(() => {
|
|
165
|
+
this.repos = [];
|
|
166
|
+
}).finally(() => {
|
|
167
|
+
this.buscandoRepos = false;
|
|
168
|
+
this.reposListos = true;
|
|
169
|
+
if (this.lienzo && this.paso === 3) void this.pinta(this.lienzo);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
listaDeAgentes() {
|
|
173
|
+
const fichas = this.roster.map(
|
|
174
|
+
(a) => `
|
|
175
|
+
<div class="bvFicha">
|
|
176
|
+
<b>${this.p.esc(a.nombre)}</b>
|
|
177
|
+
<span>${this.p.esc(a.clase)} \xB7 ${this.p.esc(a.rol)}</span>
|
|
178
|
+
<span class="de">${a.montajes.length ? this.p.esc(a.montajes.map((m) => m.split("/").pop()).join(", ")) : "nothing mounted yet"}</span>
|
|
179
|
+
</div>`
|
|
180
|
+
).join("");
|
|
181
|
+
return `
|
|
182
|
+
<div class="bvPaso">
|
|
183
|
+
<span class="sub">question 3 of 4</span>
|
|
184
|
+
<h1>${this.roster.length ? "Who else lives here?" : "Who lives in your city?"}</h1>
|
|
185
|
+
<p class="prosa">Every house holds one worker \u2014 an <b>agent</b> \u2014 with its own window,
|
|
186
|
+
its own role and its own corner of your disk. A house is <b>not</b> a repository: one
|
|
187
|
+
can answer for three services and a folder of documents at once, and a house whose
|
|
188
|
+
work is documents needs no git anywhere.</p>
|
|
189
|
+
${this.roster.length ? `<div class="bvFichas">${fichas}</div>` : ""}
|
|
190
|
+
<div class="bvBotones">
|
|
191
|
+
<button class="bt ppal" data-bv="nuevo">${this.roster.length ? "Add another house" : "Build the first house"}</button>
|
|
192
|
+
<button class="bt" data-bv="siguiente">${this.roster.length ? "That is everyone" : "Skip \u2014 just me for now"}</button>
|
|
193
|
+
</div>
|
|
194
|
+
</div>`;
|
|
195
|
+
}
|
|
196
|
+
formularioDeAgente(a) {
|
|
197
|
+
const clases = CLASES.map(
|
|
198
|
+
([id, titulo, porque]) => `
|
|
199
|
+
<button class="bvOpcion ${a.clase === id ? "elegida" : ""}" data-bv="clase" data-id="${id}">
|
|
200
|
+
<b>${titulo}</b><span>${porque}</span></button>`
|
|
201
|
+
).join("");
|
|
202
|
+
const roles = this.rolesDeAgente.map(
|
|
203
|
+
(r) => `<option value="${this.p.esc(r.id)}"${r.id === a.rol ? " selected" : ""}>${this.p.esc(
|
|
204
|
+
r.name
|
|
205
|
+
)}</option>`
|
|
206
|
+
).join("");
|
|
207
|
+
const sugeridos = this.repos.filter((r) => r.mio).slice(0, 12).map(
|
|
208
|
+
(r) => `<button class="bvChip ${a.montajes.includes(r.ruta) ? "elegida" : ""}"
|
|
209
|
+
data-bv="monta" data-ruta="${this.p.esc(r.ruta)}">${this.p.esc(r.nombre)}</button>`
|
|
210
|
+
).join("");
|
|
211
|
+
return `
|
|
212
|
+
<div class="bvPaso">
|
|
213
|
+
<span class="sub">a new house</span>
|
|
214
|
+
<h1>Who lives in it?</h1>
|
|
215
|
+
<div class="campo"><label>What do you call it?</label>
|
|
216
|
+
<input type="text" id="bvNombre" value="${this.p.esc(a.nombre)}"
|
|
217
|
+
placeholder="urgencias, api, the handbook \u2014 whatever you would say out loud"></div>
|
|
218
|
+
<label class="bvEtiqueta">What kind of work does it do?</label>
|
|
219
|
+
<div class="bvRejilla bvTres">${clases}</div>
|
|
220
|
+
<div class="campo"><label>Its role \u2014 its speciality, never authority</label>
|
|
221
|
+
<select id="bvRol">${roles}</select></div>
|
|
222
|
+
<label class="bvEtiqueta">What does it work on?</label>
|
|
223
|
+
<p class="pista">Pick any of your repositories, and add folders by hand. As many as
|
|
224
|
+
you like \u2014 this is the part that makes it an agent and not a repo.</p>
|
|
225
|
+
${sugeridos ? `<div class="bvChips">${sugeridos}</div>` : this.buscandoRepos ? '<p class="cargando">looking through your disk for repositories\u2026</p>' : '<p class="pista">No repositories found on this disk \u2014 add folders by path below.</p>'}
|
|
226
|
+
<div class="campo"><label>\u2026or a path, and Enter</label>
|
|
227
|
+
<input type="text" id="bvRuta" placeholder="~/documents/handbook"></div>
|
|
228
|
+
${a.montajes.length ? `<p class="pista">Working on: <b>${this.p.esc(a.montajes.join(", "))}</b></p>` : ""}
|
|
229
|
+
<div class="bvBotones">
|
|
230
|
+
<button class="bt" data-bv="cancela">Cancel</button>
|
|
231
|
+
<button class="bt ppal" data-bv="guarda" ${this.guardando ? "disabled" : ""}>${this.guardando ? "building\u2026" : "Build it"}</button>
|
|
232
|
+
</div>
|
|
233
|
+
</div>`;
|
|
234
|
+
}
|
|
235
|
+
// ── 4. done ───────────────────────────────────────────────────────────────
|
|
236
|
+
pantallaFinal() {
|
|
237
|
+
return `
|
|
238
|
+
<div class="bvPaso">
|
|
239
|
+
<span class="sub">ready</span>
|
|
240
|
+
<h1>${this.p.esc(this.p.ciudad)} is alive</h1>
|
|
241
|
+
<p class="prosa">Your seat is written and ${this.roster.length ? `<b>${this.roster.length}</b> house${this.roster.length === 1 ? "" : "s"} stand${this.roster.length === 1 ? "s" : ""} around it` : "the city is waiting for its first house"}. Everything you just answered is a plain file you can read and edit.</p>
|
|
242
|
+
<p class="prosa">What people usually do next:</p>
|
|
243
|
+
<div class="bvSiguientes">
|
|
244
|
+
<button class="bt ppal" data-bv="sesion">Open my session</button>
|
|
245
|
+
<button class="bt" data-bv="ir-gente">See the houses</button>
|
|
246
|
+
<button class="bt" data-bv="ir-mapa">Draw the map</button>
|
|
247
|
+
<button class="bt" data-bv="ir-puesto">Set a goal</button>
|
|
248
|
+
</div>
|
|
249
|
+
<p class="pista">A goal is optional, and the city works without one \u2014 but a round with
|
|
250
|
+
no goal is a status report, because there is nothing to argue against.</p>
|
|
251
|
+
</div>`;
|
|
252
|
+
}
|
|
253
|
+
// ── wiring ────────────────────────────────────────────────────────────────
|
|
254
|
+
enlaza(lienzo) {
|
|
255
|
+
const repinta = () => void this.pinta(lienzo);
|
|
256
|
+
lienzo.querySelectorAll("[data-bv]").forEach((el) => {
|
|
257
|
+
el.onclick = async (evento) => {
|
|
258
|
+
evento.preventDefault();
|
|
259
|
+
const que = el.dataset.bv;
|
|
260
|
+
if (que === "salir") return this.p.vete("resumen");
|
|
261
|
+
if (que === "atras") {
|
|
262
|
+
this.paso = Math.max(0, this.paso - 1);
|
|
263
|
+
return repinta();
|
|
264
|
+
}
|
|
265
|
+
if (que === "dominio") {
|
|
266
|
+
this.dominio = el.dataset.id ?? "";
|
|
267
|
+
this.roles = [];
|
|
268
|
+
this.rol = "";
|
|
269
|
+
return repinta();
|
|
270
|
+
}
|
|
271
|
+
if (que === "rol") {
|
|
272
|
+
this.rol = el.dataset.id ?? "";
|
|
273
|
+
return repinta();
|
|
274
|
+
}
|
|
275
|
+
if (que === "nuevo") {
|
|
276
|
+
this.enCurso = { nombre: "", clase: "code", rol: "blank", montajes: [] };
|
|
277
|
+
return repinta();
|
|
278
|
+
}
|
|
279
|
+
if (que === "clase" && this.enCurso) {
|
|
280
|
+
this.enCurso.clase = el.dataset.id ?? "code";
|
|
281
|
+
this.recogeFormulario(lienzo);
|
|
282
|
+
return repinta();
|
|
283
|
+
}
|
|
284
|
+
if (que === "monta" && this.enCurso) {
|
|
285
|
+
const ruta2 = el.dataset.ruta ?? "";
|
|
286
|
+
const ya = this.enCurso.montajes.indexOf(ruta2);
|
|
287
|
+
if (ya >= 0) this.enCurso.montajes.splice(ya, 1);
|
|
288
|
+
else this.enCurso.montajes.push(ruta2);
|
|
289
|
+
this.recogeFormulario(lienzo);
|
|
290
|
+
return repinta();
|
|
291
|
+
}
|
|
292
|
+
if (que === "cancela") {
|
|
293
|
+
this.enCurso = null;
|
|
294
|
+
return repinta();
|
|
295
|
+
}
|
|
296
|
+
if (que === "guarda") return void await this.guardaAgente(lienzo);
|
|
297
|
+
if (que === "siguiente") return void await this.avanza(lienzo);
|
|
298
|
+
if (que === "sesion") return void await this.abreSesion();
|
|
299
|
+
if (que === "ir-gente") return this.p.vete("gente");
|
|
300
|
+
if (que === "ir-mapa") return this.p.vete("mapa");
|
|
301
|
+
if (que === "ir-puesto") return this.p.vete("puesto");
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
const ruta = lienzo.querySelector("#bvRuta");
|
|
305
|
+
if (ruta)
|
|
306
|
+
ruta.onkeydown = (e) => {
|
|
307
|
+
if (e.key !== "Enter" || !this.enCurso) return;
|
|
308
|
+
e.preventDefault();
|
|
309
|
+
const valor = ruta.value.trim();
|
|
310
|
+
if (valor) this.enCurso.montajes.push(valor);
|
|
311
|
+
this.recogeFormulario(lienzo);
|
|
312
|
+
void this.pinta(lienzo);
|
|
313
|
+
};
|
|
314
|
+
lienzo.querySelector("#bvNombre")?.focus();
|
|
315
|
+
}
|
|
316
|
+
/** Keep what is typed when a click forces a repaint. */
|
|
317
|
+
recogeFormulario(lienzo) {
|
|
318
|
+
if (!this.enCurso) return;
|
|
319
|
+
const nombre = lienzo.querySelector("#bvNombre");
|
|
320
|
+
const rol = lienzo.querySelector("#bvRol");
|
|
321
|
+
if (nombre) this.enCurso.nombre = nombre.value;
|
|
322
|
+
if (rol) this.enCurso.rol = rol.value;
|
|
323
|
+
}
|
|
324
|
+
async guardaAgente(lienzo) {
|
|
325
|
+
this.recogeFormulario(lienzo);
|
|
326
|
+
const a = this.enCurso;
|
|
327
|
+
if (!a) return;
|
|
328
|
+
if (!a.nombre.trim()) {
|
|
329
|
+
this.p.aviso("Give it a name \u2014 it is how you will call it in its window", true);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
this.guardando = true;
|
|
333
|
+
await this.pinta(lienzo);
|
|
334
|
+
try {
|
|
335
|
+
await this.aseguraFicha();
|
|
336
|
+
const r = await this.p.api("/api/agentes", {
|
|
337
|
+
method: "POST",
|
|
338
|
+
body: JSON.stringify({ name: a.nombre.trim(), kind: a.clase, role: a.rol })
|
|
339
|
+
});
|
|
340
|
+
if (!r.ok || !r.agent) {
|
|
341
|
+
this.p.aviso(r.error || "Could not add that agent", true);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
for (const ruta of a.montajes) {
|
|
345
|
+
const m = await this.p.api("/api/montaje", {
|
|
346
|
+
method: "POST",
|
|
347
|
+
body: JSON.stringify({ agent: r.agent, add: ruta })
|
|
348
|
+
});
|
|
349
|
+
if (!m.ok) this.p.aviso(`${ruta}: ${m.error ?? "could not mount"}`, true);
|
|
350
|
+
}
|
|
351
|
+
this.roster.push({ ...a, nombre: a.nombre.trim() });
|
|
352
|
+
this.enCurso = null;
|
|
353
|
+
this.p.aviso(`${a.nombre.trim()} joined the city`);
|
|
354
|
+
await this.p.refresca();
|
|
355
|
+
} catch (e) {
|
|
356
|
+
this.p.aviso(String(e), true);
|
|
357
|
+
} finally {
|
|
358
|
+
this.guardando = false;
|
|
359
|
+
await this.pinta(lienzo);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async aseguraFicha() {
|
|
363
|
+
await this.p.api("/api/ficha", {
|
|
364
|
+
method: "POST",
|
|
365
|
+
body: JSON.stringify({ domain: this.dominio || "software", role: this.rol || "blank" })
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
async avanza(lienzo) {
|
|
369
|
+
const escribeLaFicha = this.paso === 2;
|
|
370
|
+
this.paso = Math.min(PASOS.length - 1, this.paso + 1);
|
|
371
|
+
await this.pinta(lienzo);
|
|
372
|
+
if (escribeLaFicha) {
|
|
373
|
+
await this.aseguraFicha();
|
|
374
|
+
await this.p.refresca();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async abreSesion() {
|
|
378
|
+
try {
|
|
379
|
+
const r = await this.p.api("/api/sesion", {
|
|
380
|
+
method: "POST",
|
|
381
|
+
body: JSON.stringify({ user: this.p.yo })
|
|
382
|
+
});
|
|
383
|
+
this.p.aviso(
|
|
384
|
+
r.ok && r.attach ? `Session built \u2014 attach with: ${r.attach}` : r.error ?? "Could not",
|
|
385
|
+
!r.ok
|
|
386
|
+
);
|
|
387
|
+
} catch (e) {
|
|
388
|
+
this.p.aviso(String(e), true);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
1
393
|
// city/web/src/activity.ts
|
|
2
394
|
var MAP_ACTIVITY_PROTOCOL = "agents-city-map-activity/1";
|
|
3
395
|
function isMapNavMessage(value) {
|
|
@@ -64,13 +456,21 @@ var SECCIONES = [
|
|
|
64
456
|
["resumen", "Overview"],
|
|
65
457
|
["mapa", "The map"],
|
|
66
458
|
["puesto", "My seat"],
|
|
67
|
-
["barrios", "Districts
|
|
459
|
+
["barrios", "Districts"],
|
|
68
460
|
["red", "Roads"],
|
|
69
461
|
["committee", "Committee"],
|
|
70
|
-
["gente", "
|
|
462
|
+
["gente", "Houses"],
|
|
463
|
+
["ciudades", "Cities"]
|
|
71
464
|
];
|
|
465
|
+
var primeraCarga = true;
|
|
72
466
|
async function refresca() {
|
|
73
467
|
E = await api("/api/estado");
|
|
468
|
+
if (primeraCarga) {
|
|
469
|
+
primeraCarga = false;
|
|
470
|
+
const mia = E.tarjetas.find((t) => t.user === E.yo);
|
|
471
|
+
if (!mia || (E.agents ?? []).length === 0) SECCION = "bienvenida";
|
|
472
|
+
else if (!E.mapa) SECCION = "resumen";
|
|
473
|
+
}
|
|
74
474
|
pinta();
|
|
75
475
|
pintaAvisoDeVersion();
|
|
76
476
|
pintaDemoControles();
|
|
@@ -385,13 +785,20 @@ function icono(kind) {
|
|
|
385
785
|
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 6h14v12H5zM8 10h8m-8 4h5"/></svg>';
|
|
386
786
|
}
|
|
387
787
|
function rail() {
|
|
388
|
-
|
|
788
|
+
const cuantos = (E.agents ?? []).length;
|
|
789
|
+
q("#railCiudad").innerHTML = `<span class="railEtiqueta">you are in</span>
|
|
790
|
+
<b>${esc(E.city_name)}</b>
|
|
791
|
+
<span class="railDe">${esc(E.domain)} \xB7 ${cuantos} house${cuantos === 1 ? "" : "s"}</span>`;
|
|
792
|
+
q("#rail").innerHTML = SECCIONES.filter(([id]) => id !== "ciudades").map(([id, et]) => {
|
|
389
793
|
const n = id === "gente" ? String(Object.keys(E.skills).length) : id === "committee" ? String(E.deliberations.length) : id === "red" ? String(E.roads.length) : id === "barrios" ? String(E.parcelas.length) : "";
|
|
390
794
|
return `<li class="${id === SECCION ? "aqui" : ""}" data-s="${id}">
|
|
391
795
|
<span>${et}</span>${n ? `<span class="n">${n}</span>` : ""}</li>`;
|
|
392
796
|
}).join("");
|
|
393
|
-
|
|
797
|
+
q("#railOtras").innerHTML = `<li class="${SECCION === "ciudades" ? "aqui" : ""}" data-s="ciudades">
|
|
798
|
+
<span>All cities</span><span class="n">${E.ciudades.length}</span></li>`;
|
|
799
|
+
todos("#rail li,#railOtras li").forEach((li) => {
|
|
394
800
|
li.onclick = () => {
|
|
801
|
+
document.body.classList.remove("enGuia");
|
|
395
802
|
SECCION = li.dataset.s ?? "mapa";
|
|
396
803
|
pinta();
|
|
397
804
|
};
|
|
@@ -432,33 +839,188 @@ function pinta() {
|
|
|
432
839
|
q(".cuerpo").scrollTop = 0;
|
|
433
840
|
}
|
|
434
841
|
var VISTAS = {};
|
|
842
|
+
var guia = null;
|
|
843
|
+
VISTAS.bienvenida = () => {
|
|
844
|
+
document.body.classList.add("enGuia");
|
|
845
|
+
guia ??= new Bienvenida({
|
|
846
|
+
api,
|
|
847
|
+
esc,
|
|
848
|
+
aviso: toast,
|
|
849
|
+
refresca,
|
|
850
|
+
vete: (seccion) => {
|
|
851
|
+
document.body.classList.remove("enGuia");
|
|
852
|
+
SECCION = seccion;
|
|
853
|
+
pinta();
|
|
854
|
+
},
|
|
855
|
+
ciudad: E.city_name,
|
|
856
|
+
yo: E.yo,
|
|
857
|
+
datos: corto(E.datos)
|
|
858
|
+
});
|
|
859
|
+
void guia.pinta(q("#lienzo"));
|
|
860
|
+
};
|
|
861
|
+
VISTAS.ciudades = () => {
|
|
862
|
+
const filas = E.ciudades.map(
|
|
863
|
+
(c) => `
|
|
864
|
+
<div class="fila ${c.actual ? "on" : ""}">
|
|
865
|
+
<span class="et">${esc(c.nombre)}${c.actual ? ' <b class="de">\xB7 open now</b>' : ""}</span>
|
|
866
|
+
<span class="de">${c.agentes ?? 0} house${(c.agentes ?? 0) === 1 ? "" : "s"} of its own</span>
|
|
867
|
+
<span class="de mono">${esc(corto(c.ruta))}</span>
|
|
868
|
+
<span style="margin-left:auto;display:flex;gap:6px">
|
|
869
|
+
${c.actual ? "" : `<a class="bt mini" href="?city=${encodeURIComponent(c.ruta)}">open</a>
|
|
870
|
+
<button class="bt mini archiva" data-ciudad="${esc(c.ruta)}"
|
|
871
|
+
data-nombre="${esc(c.nombre)}">archive</button>`}
|
|
872
|
+
</span>
|
|
873
|
+
</div>`
|
|
874
|
+
).join("");
|
|
875
|
+
q("#lienzo").innerHTML = `<div><span class="sub">cities</span>
|
|
876
|
+
<h1 style="margin-top:6px">Your cities</h1>
|
|
877
|
+
<p class="prosa" style="margin-top:8px">One person, several autonomous cities: each has
|
|
878
|
+
its own identity, domain, chair, <b>its own houses</b> and its own roads. A house is not
|
|
879
|
+
shared \u2014 it stands inside the city that owns it, with its workspace and mounts under that
|
|
880
|
+
city's folder \u2014 so two cities can each have a <code class="mono">docs</code> house and
|
|
881
|
+
they are two different workers. They share nothing unless you build a road between
|
|
882
|
+
them.</p></div>
|
|
883
|
+
<div class="lista"><div class="filas">${filas}</div></div>
|
|
884
|
+
<div class="campos" style="max-width:420px">
|
|
885
|
+
<div class="campo"><label>Start another city</label>
|
|
886
|
+
<input type="text" id="nuevaCiudad" placeholder="client-a, research, the book\u2026"></div>
|
|
887
|
+
<button class="bt ppal" id="creaCiudad">Create it</button>
|
|
888
|
+
</div>
|
|
889
|
+
<div class="peligro">
|
|
890
|
+
<h3>Start this city over</h3>
|
|
891
|
+
<p class="prosa">Takes <b>${esc(E.city_name)}</b> back to its first day: no seat, no
|
|
892
|
+
agents, no committee history, no map. Your repositories and document folders are
|
|
893
|
+
<b>never touched</b> \u2014 only the city that points at them.</p>
|
|
894
|
+
<button class="bt malo" id="reiniciaCiudad">Start over\u2026</button>
|
|
895
|
+
</div>
|
|
896
|
+
<p class="pista">Archiving <b>moves</b> a city into
|
|
897
|
+
<code class="mono">.backups/</code> \u2014 nothing is deleted, and you can bring it back with
|
|
898
|
+
<code class="mono">mv</code>. The city you are standing in, and the last one you own,
|
|
899
|
+
cannot be archived.</p>`;
|
|
900
|
+
enlaza();
|
|
901
|
+
const campo = q("#nuevaCiudad");
|
|
902
|
+
q("#creaCiudad").onclick = async () => {
|
|
903
|
+
const nombre = campo.value.trim();
|
|
904
|
+
if (!nombre) {
|
|
905
|
+
toast("Give it a name", true);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
const r = await api("/api/ciudades", {
|
|
909
|
+
method: "POST",
|
|
910
|
+
body: JSON.stringify({ name: nombre })
|
|
911
|
+
});
|
|
912
|
+
if (!r.ok) {
|
|
913
|
+
toast(r.error ?? "Could not create it", true);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
toast(`${nombre} created`);
|
|
917
|
+
await refresca();
|
|
918
|
+
};
|
|
919
|
+
const reiniciar = document.getElementById("reiniciaCiudad");
|
|
920
|
+
if (reiniciar)
|
|
921
|
+
reiniciar.onclick = async () => {
|
|
922
|
+
const previa = await api("/api/ciudad-reinicia", {
|
|
923
|
+
method: "POST",
|
|
924
|
+
body: "{}"
|
|
925
|
+
});
|
|
926
|
+
if (!previa.preview) {
|
|
927
|
+
toast(previa.error ?? "Could not read what a reset would do", true);
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
const p = previa.preview;
|
|
931
|
+
const texto = `Start over ${p.city}?
|
|
932
|
+
|
|
933
|
+
You lose:
|
|
934
|
+
` + p.loses.map((l) => ` \xB7 ${l}`).join("\n") + `
|
|
935
|
+
|
|
936
|
+
Right now that is ${p.agents} agent(s), ${p.roads} road(s) and ${p.deliberations} committee act(s).
|
|
937
|
+
|
|
938
|
+
You keep:
|
|
939
|
+
` + p.keeps.map((l) => ` \xB7 ${l}`).join("\n") + `
|
|
940
|
+
|
|
941
|
+
Type the city's name to confirm:`;
|
|
942
|
+
const escrito = window.prompt(texto, "");
|
|
943
|
+
if (escrito === null) return;
|
|
944
|
+
if (escrito.trim() !== p.city) {
|
|
945
|
+
toast("That is not the name \u2014 nothing was changed", true);
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
const r = await api(
|
|
949
|
+
"/api/ciudad-reinicia",
|
|
950
|
+
{ method: "POST", body: JSON.stringify({ confirm: p.city }) }
|
|
951
|
+
);
|
|
952
|
+
if (!r.ok) {
|
|
953
|
+
toast(r.error ?? "Could not reset it", true);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
toast(`${p.city} starts over \u2014 the old one is at ${r.backup ?? "its backup"}`);
|
|
957
|
+
primeraCarga = true;
|
|
958
|
+
await refresca();
|
|
959
|
+
};
|
|
960
|
+
todos(".archiva").forEach((boton) => {
|
|
961
|
+
boton.onclick = async () => {
|
|
962
|
+
const nombre = boton.dataset.nombre ?? "";
|
|
963
|
+
if (!window.confirm(`Archive ${nombre}? It moves into .backups \u2014 nothing is deleted.`))
|
|
964
|
+
return;
|
|
965
|
+
const r = await api(
|
|
966
|
+
"/api/ciudad-archiva",
|
|
967
|
+
{ method: "POST", body: JSON.stringify({ city: boton.dataset.ciudad ?? "" }) }
|
|
968
|
+
);
|
|
969
|
+
if (!r.ok) {
|
|
970
|
+
toast(r.error ?? "Could not archive it", true);
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
toast(`${nombre} archived \u2014 it is in ${r.backup ?? ".backups"}`);
|
|
974
|
+
await refresca();
|
|
975
|
+
};
|
|
976
|
+
});
|
|
977
|
+
};
|
|
435
978
|
VISTAS.resumen = () => {
|
|
436
979
|
const mia = E.tarjetas.find((t) => t.user === E.yo);
|
|
437
980
|
const sinUnidad = E.parcelas.filter((p) => p.unidad === "none" || p.unidad === "mine").length;
|
|
981
|
+
const agentes = E.agents ?? [];
|
|
438
982
|
const tareas = [
|
|
439
983
|
{
|
|
440
984
|
ok: !!mia,
|
|
441
|
-
txt: mia ? `Your
|
|
985
|
+
txt: mia ? `Your seat is taken \u2014 you are <b>${esc(mia.agent)}</b> in <b>${esc(E.domain)}</b>` : "Take your seat: the work domain and your role in it",
|
|
442
986
|
ir: "puesto"
|
|
443
987
|
},
|
|
988
|
+
{
|
|
989
|
+
ok: agentes.length > 0,
|
|
990
|
+
txt: agentes.length === 0 ? "Build the <b>houses</b> of this city \u2014 one per worker, each whole: its kind, its role, and everything it works on" : `<b>${agentes.length}</b> house${agentes.length === 1 ? "" : "s"} in this city: ${esc(
|
|
991
|
+
agentes.map((a) => a.name).join(", ")
|
|
992
|
+
)}`,
|
|
993
|
+
ir: "gente"
|
|
994
|
+
},
|
|
995
|
+
{
|
|
996
|
+
ok: agentes.length === 0 || agentes.some((a) => (a.mounts ?? []).length),
|
|
997
|
+
txt: agentes.some((a) => (a.mounts ?? []).length) ? "Every house works on something real" : "Give a house something to work on \u2014 a repository, a worktree, a folder of documents",
|
|
998
|
+
ir: "gente"
|
|
999
|
+
},
|
|
444
1000
|
{
|
|
445
1001
|
ok: !!(mia && mia.goals_defined),
|
|
446
1002
|
txt: mia && mia.goals_defined ? `Your goal: <b>${esc(mia.objetivo ? mia.objetivo.title : "")}</b>` : "Set <b>one goal</b>, with the command that measures it",
|
|
447
1003
|
ir: "puesto"
|
|
448
1004
|
},
|
|
449
1005
|
{
|
|
450
|
-
ok: E.parcelas.length
|
|
451
|
-
txt: E.parcelas.length === 0 ? "
|
|
1006
|
+
ok: E.parcelas.length === 0 || sinUnidad === 0,
|
|
1007
|
+
txt: E.parcelas.length === 0 ? "Districts appear on the map once agents have houses" : sinUnidad ? `<b>${sinUnidad}</b> of ${E.parcelas.length} houses sit in no real district \u2014 assign them` : "Every house has its district",
|
|
452
1008
|
ir: "barrios"
|
|
453
1009
|
}
|
|
454
1010
|
];
|
|
1011
|
+
const empezando = !mia || agentes.length === 0;
|
|
455
1012
|
const sesionArriba = E.tmux.includes(E.sesion);
|
|
456
1013
|
q("#lienzo").innerHTML = `
|
|
457
|
-
<div><span class="sub"
|
|
458
|
-
<h1 style="margin-top:6px">${esc(E.city_name)}</h1>
|
|
1014
|
+
<div><span class="sub">${empezando ? "welcome" : "your city"}</span>
|
|
1015
|
+
<h1 style="margin-top:6px">${empezando ? "Let\u2019s build " + esc(E.city_name) : esc(E.city_name)}</h1>
|
|
459
1016
|
<p class="de mono" style="margin-top:5px">${esc(E.address)}</p>
|
|
460
|
-
|
|
461
|
-
|
|
1017
|
+
${empezando ? `<p class="prosa" style="margin-top:8px">A city is a chair and the agents behind
|
|
1018
|
+
it. Two steps and it is alive: take your seat \u2014 the work domain and your role in
|
|
1019
|
+
it \u2014 then add the agents, each one asked for whole: what kind of work it does,
|
|
1020
|
+
its role, everything it works on, the engine that runs it and the skills it
|
|
1021
|
+
starts with. Everything below is a plain file in
|
|
1022
|
+
<code class="mono">${esc(corto(E.datos))}</code> you can also edit by hand.</p>` : `<p class="prosa" style="margin-top:8px">Domain: <b>${esc(E.domain)}</b>. Growth is
|
|
1023
|
+
counted with <code class="mono">${esc(E.grow || "nothing yet")}</code>.</p>`}</div>
|
|
462
1024
|
<div class="cifras">
|
|
463
1025
|
<div class="cifra"><b>${Object.keys(E.skills).length}</b><span>repo agents</span></div>
|
|
464
1026
|
<div class="cifra"><b>${E.parcelas.length}</b><span>houses</span></div>
|
|
@@ -543,12 +1105,17 @@ VISTAS.mapa = () => {
|
|
|
543
1105
|
};
|
|
544
1106
|
return;
|
|
545
1107
|
}
|
|
1108
|
+
const cuantos = (E.agents ?? []).length;
|
|
546
1109
|
q("#lienzo").innerHTML = `<div><span class="sub">the map</span>
|
|
547
|
-
<h1 style="margin-top:6px">
|
|
548
|
-
<p class="prosa" style="margin-top:8px">
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
1110
|
+
<h1 style="margin-top:6px">Your city, drawn</h1>
|
|
1111
|
+
<p class="prosa" style="margin-top:8px">${cuantos ? `<b>${cuantos}</b> agent${cuantos === 1 ? "" : "s"} live here, and the map shows
|
|
1112
|
+
them as houses that grow with the work they do \u2014 with the town hall in the middle
|
|
1113
|
+
when a committee is sitting, and a gate for every road out.` : `The map draws one house per agent. There are none yet, so it would be an empty
|
|
1114
|
+
plot \u2014 build one from <b>Houses</b> first.`}</p>
|
|
1115
|
+
<p class="prosa">The map is its own little server, so it is not running until you say so.
|
|
1116
|
+
The first time takes about a minute: it builds the front end and seeds a local database.
|
|
1117
|
+
After that it opens instantly, and it is yours \u2014 nothing is uploaded anywhere.</p></div>
|
|
1118
|
+
<div><button class="bt ppal" id="arrancaMapa">${cuantos ? "Draw my city" : "Draw it anyway"}</button></div>
|
|
552
1119
|
<p class="cargando" id="mapaEspera" style="display:none">baking the map \u2014 first time takes a minute</p>`;
|
|
553
1120
|
q("#arrancaMapa").onclick = async () => {
|
|
554
1121
|
q("#arrancaMapa").disabled = true;
|
|
@@ -1014,16 +1581,22 @@ VISTAS.gente = () => {
|
|
|
1014
1581
|
+ zip<input type="file" accept=".zip" data-agente="${esc(a.slug)}"></label></div>
|
|
1015
1582
|
</div>`;
|
|
1016
1583
|
};
|
|
1017
|
-
q("#lienzo").innerHTML = `<div><span class="sub">
|
|
1018
|
-
<h1 style="margin-top:6px">
|
|
1019
|
-
<p class="prosa" style="margin-top:8px">A
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1584
|
+
q("#lienzo").innerHTML = `<div><span class="sub">the houses of ${esc(E.city_name)}</span>
|
|
1585
|
+
<h1 style="margin-top:6px">Who lives in ${esc(E.city_name)}</h1>
|
|
1586
|
+
<p class="prosa" style="margin-top:8px"><b>A house is where an agent lives and works</b>,
|
|
1587
|
+
and many houses are a city \u2014 it is the same thing the map draws, growing with what that
|
|
1588
|
+
agent actually does. The card and the CLI call them <code class="mono">agents</code>;
|
|
1589
|
+
here you see their houses.</p>
|
|
1590
|
+
<p class="prosa">They belong to <b>this</b> city and only
|
|
1591
|
+
to it: each one's workspace and its mounts live inside
|
|
1592
|
+
<code class="mono">${esc(corto(E.datos))}/agents/</code>, so another city has its own
|
|
1593
|
+
people even if you give them the same names. Every agent is whole here: the kind of work
|
|
1594
|
+
it does, its role, everything it works on \u2014 any number of repositories and document
|
|
1595
|
+
folders at once \u2014 the engine and effort that run it, and the skills in its own home.</p>
|
|
1596
|
+
<p class="prosa">Every number is real: growth from what the agent actually produced,
|
|
1597
|
+
skills from live read-only recognition. Changes persist to the card and apply the next
|
|
1598
|
+
time the session opens.</p>
|
|
1599
|
+
<button class="rpgAlta" type="button" id="altaAgente">+ Build a house</button></div>
|
|
1027
1600
|
<div class="fichasRPG">
|
|
1028
1601
|
<div class="fichaRPG rpgSeat">
|
|
1029
1602
|
<div class="rpgCab">${caraRPG(E.avatars?.["seat"])}
|
|
@@ -1136,7 +1709,7 @@ VISTAS.gente = () => {
|
|
|
1136
1709
|
toast(r.error || "Could not add the agent", true);
|
|
1137
1710
|
return;
|
|
1138
1711
|
}
|
|
1139
|
-
toast(`${nombre.trim()}
|
|
1712
|
+
toast(`${nombre.trim()} has a house now \u2014 give it something to work on`);
|
|
1140
1713
|
void refresca();
|
|
1141
1714
|
} catch (e) {
|
|
1142
1715
|
toast(String(e), true);
|
|
@@ -1322,7 +1895,7 @@ window.addEventListener("message", (message) => {
|
|
|
1322
1895
|
function temaActual() {
|
|
1323
1896
|
const elegido = document.documentElement.getAttribute("data-tema");
|
|
1324
1897
|
if (elegido) return elegido === "claro" ? "light" : "dark";
|
|
1325
|
-
return
|
|
1898
|
+
return "light";
|
|
1326
1899
|
}
|
|
1327
1900
|
function tema() {
|
|
1328
1901
|
const boton = document.getElementById("temaBoton");
|
|
@@ -1333,13 +1906,13 @@ function tema() {
|
|
|
1333
1906
|
return "";
|
|
1334
1907
|
}
|
|
1335
1908
|
})();
|
|
1336
|
-
const
|
|
1909
|
+
const oscuroDelSistema = false;
|
|
1337
1910
|
const pon = (elegido) => {
|
|
1338
1911
|
const raiz = document.documentElement;
|
|
1339
1912
|
if (elegido) raiz.setAttribute("data-tema", elegido);
|
|
1340
1913
|
else raiz.removeAttribute("data-tema");
|
|
1341
|
-
const claro = elegido ? elegido === "claro" :
|
|
1342
|
-
if (boton) boton.textContent = claro ? "\
|
|
1914
|
+
const claro = elegido ? elegido === "claro" : !oscuroDelSistema;
|
|
1915
|
+
if (boton) boton.textContent = claro ? "\u263E Night" : "\u2600 Day";
|
|
1343
1916
|
const frame = document.querySelector("#cityMapFrame");
|
|
1344
1917
|
frame?.contentWindow?.postMessage(
|
|
1345
1918
|
{ type: "agents-city-map-theme/1", theme: claro ? "light" : "dark" },
|
|
@@ -1349,7 +1922,7 @@ function tema() {
|
|
|
1349
1922
|
pon(guardado);
|
|
1350
1923
|
if (boton)
|
|
1351
1924
|
boton.onclick = () => {
|
|
1352
|
-
const claroAhora = document.documentElement.getAttribute("data-tema") ? document.documentElement.getAttribute("data-tema") === "claro" :
|
|
1925
|
+
const claroAhora = document.documentElement.getAttribute("data-tema") ? document.documentElement.getAttribute("data-tema") === "claro" : !oscuroDelSistema;
|
|
1353
1926
|
const elegido = claroAhora ? "oscuro" : "claro";
|
|
1354
1927
|
try {
|
|
1355
1928
|
localStorage.setItem("hall-tema", elegido);
|