nio-cat 0.9.5 → 0.9.7
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 +3 -2
- package/bin/nio.js +8 -13
- package/package.json +1 -1
- package/src/agents/claude.js +4 -1
- package/src/agents/ollama.js +3 -2
- package/src/hub.js +2 -0
- package/src/notes.js +293 -0
- package/src/pair.js +3 -50
- package/src/server.js +19 -2
- package/src/usage.js +79 -0
- package/src/voice.js +21 -3
- package/src/whisper.js +148 -0
- package/web/app/app.js +256 -53
- package/web/app/demo.js +413 -0
- package/web/app/index.html +4 -2
- package/web/app/notes.css +154 -0
- package/web/app/notes.js +504 -0
- package/web/app/style.css +32 -0
- package/web/index.html +8 -3
- package/web/sw.js +2 -2
package/web/app/demo.js
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
// nio · demo: un ordinador simulat dins del navegador, per provar l'app sense instal·lar res.
|
|
2
|
+
// Respon les mateixes adreces que el programa (/api/…) a les adreces «demo:…», amb sessions,
|
|
3
|
+
// text en directe, eines i aprovacions de mentida. No surt res del navegador i es reinicia en recarregar.
|
|
4
|
+
(() => {
|
|
5
|
+
const ROOT = '/Users/tu/projectes';
|
|
6
|
+
const DEMO_USAGE = { claude: { windows: [{ label: '5 h', used: 0.32, resetsAt: Math.floor(Date.now() / 1000) + 7200 }, { label: 'setmana', used: 0.57, resetsAt: Math.floor(Date.now() / 1000) + 3 * 86400 }], at: Date.now() }, codex: { windows: [{ label: '5 h', used: 0.85, resetsAt: Math.floor(Date.now() / 1000) + 3600 }, { label: 'setmana', used: 0.17, resetsAt: Math.floor(Date.now() / 1000) + 5 * 86400 }], at: Date.now() } };
|
|
7
|
+
const AGENTS = [
|
|
8
|
+
{ id: 'claude', name: 'Claude Code', beta: false, approvals: true, noModes: false, login: '', available: true, version: 'simulat' },
|
|
9
|
+
{ id: 'codex', name: 'Codex', beta: false, approvals: true, noModes: false, login: '', available: true, version: 'simulat' },
|
|
10
|
+
];
|
|
11
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
12
|
+
const rid = () => Math.random().toString(36).slice(2, 12);
|
|
13
|
+
const err = (status, error) => Object.assign(new Error(error), { status });
|
|
14
|
+
const NOPE = 'Això no es pot fer a la demo. Instal·la nio a l’ordinador per fer-ho de debò: npx nio-cat';
|
|
15
|
+
|
|
16
|
+
const sessions = new Map(), approvals = new Map(), streams = new Set(), runs = new Map(), audit = [];
|
|
17
|
+
const out = (msg) => { const data = new TextEncoder().encode(`data: ${JSON.stringify(msg)}\n\n`); for (const c of streams) try { c.enqueue(data); } catch { streams.delete(c); } };
|
|
18
|
+
|
|
19
|
+
function summary(s) {
|
|
20
|
+
const last = [...s.events].reverse().find((e) => e.kind === 'text' || e.kind === 'user');
|
|
21
|
+
return { id: s.id, agent: s.agent, title: s.title, cwd: s.cwd, mode: s.mode, model: '', state: s.state, created: s.created, updated: s.updated, cost: s.cost || 0, last: last ? last.text.slice(0, 160) : '', error: '', pending: [...approvals.values()].filter((a) => a.session === s.id && a.state === 'pending').length };
|
|
22
|
+
}
|
|
23
|
+
const list = () => [...sessions.values()].sort((a, b) => b.updated - a.updated).map(summary);
|
|
24
|
+
const pending = () => [...approvals.values()].filter((a) => a.state === 'pending').map(({ resolve, ...a }) => a);
|
|
25
|
+
const touch = (s) => { s.updated = Date.now(); out({ type: 'session', session: summary(s) }); };
|
|
26
|
+
function push(s, kind, data = {}) {
|
|
27
|
+
const last = s.events[s.events.length - 1];
|
|
28
|
+
if (last?.open) { delete last.open; out({ type: 'event', session: s.id, event: last }); }
|
|
29
|
+
const ev = { seq: s.events.length + 1, t: Date.now(), kind, ...data };
|
|
30
|
+
s.events.push(ev); out({ type: 'event', session: s.id, event: ev }); touch(s);
|
|
31
|
+
return ev;
|
|
32
|
+
}
|
|
33
|
+
async function stream(s, text, run) {
|
|
34
|
+
const ev = push(s, 'text', { text: '', open: true });
|
|
35
|
+
for (const w of text.split(/(?<= )/)) {
|
|
36
|
+
if (run.stopped) return;
|
|
37
|
+
ev.text += w; out({ type: 'delta', session: s.id, seq: ev.seq, text: w });
|
|
38
|
+
await sleep(35 + Math.random() * 45);
|
|
39
|
+
}
|
|
40
|
+
delete ev.open; out({ type: 'event', session: s.id, event: ev }); touch(s);
|
|
41
|
+
}
|
|
42
|
+
function ask(s, tool, input) {
|
|
43
|
+
const a = { id: rid(), session: s.id, agent: s.agent, title: s.title, tool, input, reason: input.description || '', state: 'pending', created: Date.now() };
|
|
44
|
+
approvals.set(a.id, a); s.state = 'waiting';
|
|
45
|
+
push(s, 'approval', { approval: a.id, tool, input, reason: a.reason, state: 'pending' });
|
|
46
|
+
out({ type: 'approval', approval: pending().find((x) => x.id === a.id) });
|
|
47
|
+
return new Promise((r) => { a.resolve = r; });
|
|
48
|
+
}
|
|
49
|
+
function decide(id, decision) {
|
|
50
|
+
const a = approvals.get(id);
|
|
51
|
+
if (!a || a.state !== 'pending') return null;
|
|
52
|
+
a.state = decision === 'allow' ? 'allowed' : 'denied'; a.decided = Date.now(); a.by = 'Aquest navegador';
|
|
53
|
+
audit.unshift({ t: Date.now(), type: 'approval', session: a.session, agent: a.agent, title: a.title, tool: a.tool, what: a.input.command || a.input.file_path || a.tool, why: a.reason, decision: a.state, by: a.by, asked: a.created });
|
|
54
|
+
const s = sessions.get(a.session);
|
|
55
|
+
if (s) {
|
|
56
|
+
const ev = s.events.find((e) => e.kind === 'approval' && e.approval === id);
|
|
57
|
+
if (ev) { ev.state = a.state; out({ type: 'event', session: s.id, event: ev }); }
|
|
58
|
+
if (s.state === 'waiting') s.state = runs.has(s.id) ? 'running' : 'idle';
|
|
59
|
+
touch(s);
|
|
60
|
+
}
|
|
61
|
+
const { resolve, ...pub } = a;
|
|
62
|
+
out({ type: 'approval', approval: pub });
|
|
63
|
+
resolve?.(a.state === 'allowed' ? 'allow' : 'deny');
|
|
64
|
+
return pub;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Què fa l'agent simulat segons el que li demanes. Sempre: llegeix, proposa un canvi, el demana i prova.
|
|
68
|
+
function plan(text, s) {
|
|
69
|
+
const t = text.toLowerCase(), dir = s.cwd.split('/').pop();
|
|
70
|
+
if (/test|prova/.test(t)) return { intro: `Miro quins tests hi ha a ${dir} i quins fallen.`, read: 'package.json', readOut: '{\n "scripts": { "test": "node --test" }\n}', edit: { file_path: `${s.cwd}/test/usuaris.test.js`, old_string: '', new_string: "test('crea un usuari', async () => {\n const u = await crear({ nom: 'Anna' });\n assert.equal(u.nom, 'Anna');\n});", description: 'Afegeixo un test per crear usuaris' }, bash: 'npm test', bashOut: '✓ 14 tests passen (0,8 s)', end: 'Fet. He afegit el test que faltava i **tots els tests passen**.' };
|
|
71
|
+
if (/fosc|dark|color|disseny|css|estil/.test(t)) return { intro: 'Faig una ullada als estils per afegir-hi el que em demanes.', read: 'src/styles.css', readOut: ':root {\n --bg: #fff;\n --text: #1a1a1a;\n}', edit: { file_path: `${s.cwd}/src/styles.css`, old_string: ':root {\n --bg: #fff;', new_string: ':root {\n --bg: #fff;\n}\n@media (prefers-color-scheme: dark) {\n :root { --bg: #141414; --text: #eee; }', description: 'Afegeixo els colors del mode fosc' }, bash: 'npm run build', bashOut: '✓ build fet en 1,9 s', end: 'Llest. El mode fosc s’activa sol quan el sistema el fa servir. Vols que hi afegeixi un botó per canviar-lo a mà?' };
|
|
72
|
+
if (/error|bug|falla|arregla|peta/.test(t)) return { intro: 'Busco d’on ve l’error.', read: 'src/server.js', readOut: "app.get('/usuaris/:id', (req, res) => {\n res.json(db.usuaris[req.params.id].nom);\n});", edit: { file_path: `${s.cwd}/src/server.js`, old_string: 'res.json(db.usuaris[req.params.id].nom);', new_string: "const u = db.usuaris[req.params.id];\n if (!u) return res.status(404).json({ error: 'No existeix' });\n res.json(u.nom);", description: 'Retorno un 404 quan l’usuari no existeix' }, bash: 'npm test', bashOut: '✓ 12 tests passen', end: 'Trobat: quan l’usuari no existia, el servidor petava. Ara respon un **404** i els tests passen.' };
|
|
73
|
+
return { intro: `Entesos. Començo per entendre com està organitzat ${dir}.`, read: 'README.md', readOut: `# ${dir}\nProjecte d’exemple de la demo de nio.`, edit: { file_path: `${s.cwd}/NOTES.md`, old_string: '', new_string: `## Pendent\n- ${text.slice(0, 80)}`, description: 'Apunto la tasca a les notes del projecte' }, bash: 'git status --short', bashOut: ' M NOTES.md', end: 'Fet. A la versió de debò, aquí l’agent treballaria als fitxers del teu ordinador i et demanaria permís abans de cada canvi.' };
|
|
74
|
+
}
|
|
75
|
+
async function work(s, text) {
|
|
76
|
+
const run = { stopped: false }; runs.set(s.id, run);
|
|
77
|
+
const p = plan(text, s), tool = (name, input, output, error) => { const id = rid(); push(s, 'tool', { id, name, input }); logTool(s, name, input); return { id, done: () => push(s, 'tool_result', { id, name, output, error: !!error }) }; };
|
|
78
|
+
const allowed = async (name, input) => s.mode === 'tot' || (await ask(s, name, input)) === 'allow';
|
|
79
|
+
try {
|
|
80
|
+
await sleep(500); await stream(s, p.intro, run); if (run.stopped) return;
|
|
81
|
+
const r = tool('Read', { file_path: p.read }, p.readOut); await sleep(700); r.done(); if (run.stopped) return;
|
|
82
|
+
if (s.mode === 'llegir') { await stream(s, 'Treballo en mode només lectura, així que no toco res. Et proposo aquest canvi:\n\n```\n' + p.edit.new_string + '\n```', run); return; }
|
|
83
|
+
await sleep(400);
|
|
84
|
+
// Un fitxer nou és «Write»; un canvi a un fitxer que ja hi és, «Edit».
|
|
85
|
+
const [eName, eInput] = p.edit.old_string ? ['Edit', p.edit] : ['Write', { file_path: p.edit.file_path, content: p.edit.new_string, description: p.edit.description }];
|
|
86
|
+
if (!(await allowed(eName, eInput))) { if (!run.stopped) await stream(s, 'D’acord, no hi toco. Què vols que faci?', run); return; }
|
|
87
|
+
if (run.stopped) return;
|
|
88
|
+
const e = tool(eName, eInput, eName === 'Write' ? 'Fitxer creat' : 'Fitxer actualitzat'); await sleep(500); e.done();
|
|
89
|
+
if (!(await allowed('Bash', { command: p.bash, description: 'Comprovo que tot funciona' }))) { if (!run.stopped) await stream(s, 'Entesos, no ho executo. El canvi ja és fet.', run); return; }
|
|
90
|
+
if (run.stopped) return;
|
|
91
|
+
const b = tool('Bash', { command: p.bash }, p.bashOut); await sleep(1100); b.done();
|
|
92
|
+
await stream(s, p.end, run);
|
|
93
|
+
const cost = 0.02 + Math.random() * 0.08; s.cost = (s.cost || 0) + cost; push(s, 'result', { cost, ms: 6000, turns: 3 });
|
|
94
|
+
} finally {
|
|
95
|
+
runs.delete(s.id);
|
|
96
|
+
for (const a of approvals.values()) if (a.session === s.id && a.state === 'pending') decide(a.id, 'deny');
|
|
97
|
+
s.state = 'idle'; push(s, 'status', { state: 'idle', code: run.stopped ? 130 : 0 });
|
|
98
|
+
runEnded(s, run.stopped);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const logTool = (s, tool, input) => audit.unshift({ t: Date.now(), type: 'tool', session: s.id, agent: s.agent, title: s.title, tool, what: input.command || input.file_path || tool, mode: s.mode });
|
|
102
|
+
function create({ agent, cwd, title, mode }) {
|
|
103
|
+
if (!AGENTS.some((a) => a.id === agent)) throw err(400, `A la demo només hi ha ${AGENTS.map((a) => a.name).join(' i ')}`);
|
|
104
|
+
const s = { id: rid(), agent, cwd: cwd || `${ROOT}/web-botiga`, title: title || '', mode: ['llegir', 'preguntar', 'tot'].includes(mode) ? mode : 'preguntar', state: 'idle', created: Date.now(), updated: Date.now(), cost: 0, events: [] };
|
|
105
|
+
sessions.set(s.id, s); touch(s); return s;
|
|
106
|
+
}
|
|
107
|
+
function send(s, text) {
|
|
108
|
+
if (runs.has(s.id)) throw err(409, 'L’agent encara està treballant. Espera que acabi o atura’l.');
|
|
109
|
+
text = String(text || '').trim(); if (!text) throw err(400, 'El missatge és buit');
|
|
110
|
+
if (!s.title) s.title = text.replace(/\s+/g, ' ').slice(0, 60);
|
|
111
|
+
push(s, 'user', { text }); s.state = 'running'; push(s, 'status', { state: 'running' });
|
|
112
|
+
work(s, text);
|
|
113
|
+
return summary(s);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Sessions d'exemple: una d'acabada, una que espera permís i una d'antiga.
|
|
117
|
+
function seed() {
|
|
118
|
+
const ago = (min) => Date.now() - min * 60000;
|
|
119
|
+
const mk = (agent, dir, title, min, evs, extra = {}) => {
|
|
120
|
+
const s = { id: rid(), agent, cwd: `${ROOT}/${dir}`, title, mode: 'preguntar', state: 'idle', created: ago(min + 5), updated: ago(min), cost: extra.cost || 0, events: evs.map((e, i) => ({ seq: i + 1, t: ago(min + (evs.length - i) / 4), ...e })) };
|
|
121
|
+
sessions.set(s.id, s); return s;
|
|
122
|
+
};
|
|
123
|
+
mk('claude', 'web-botiga', 'Afegeix el mode fosc a la botiga', 12, [
|
|
124
|
+
{ kind: 'user', text: 'Afegeix el mode fosc a la botiga' }, { kind: 'status', state: 'running' },
|
|
125
|
+
{ kind: 'text', text: 'Miro com estan definits els colors.' },
|
|
126
|
+
{ kind: 'tool', id: 'a1', name: 'Read', input: { file_path: 'src/styles.css' } }, { kind: 'tool_result', id: 'a1', name: 'Read', output: ':root {\n --bg: #fff;\n --text: #1a1a1a;\n}', error: false },
|
|
127
|
+
{ kind: 'tool', id: 'a2', name: 'Edit', input: { file_path: `${ROOT}/web-botiga/src/styles.css`, old_string: '--bg: #fff;', new_string: '--bg: #fff;\n}\n@media (prefers-color-scheme: dark) {\n :root { --bg: #141414; }' } }, { kind: 'tool_result', id: 'a2', name: 'Edit', output: 'Fitxer actualitzat', error: false },
|
|
128
|
+
{ kind: 'tool', id: 'a3', name: 'Bash', input: { command: 'npm run build' } }, { kind: 'tool_result', id: 'a3', name: 'Bash', output: '✓ build fet en 1,9 s', error: false },
|
|
129
|
+
{ kind: 'text', text: 'Fet. El **mode fosc** s’activa sol quan el sistema el fa servir.' }, { kind: 'result', cost: 0.07, ms: 41000, turns: 4 }, { kind: 'status', state: 'idle', code: 0 },
|
|
130
|
+
], { cost: 0.07 });
|
|
131
|
+
const w = mk('codex', 'api', 'Prepara la migració de la base de dades', 2, [
|
|
132
|
+
{ kind: 'user', text: 'Prepara la migració de la base de dades' }, { kind: 'status', state: 'running' },
|
|
133
|
+
{ kind: 'text', text: 'He escrit la migració que afegeix la columna «telefon» a usuaris. Ara l’he d’executar.' },
|
|
134
|
+
]);
|
|
135
|
+
const a = { id: rid(), session: w.id, agent: 'codex', title: w.title, tool: 'Bash', input: { command: 'npm run migrate', description: 'Executo la migració a la base de dades local' }, reason: 'Executo la migració a la base de dades local', state: 'pending', created: ago(1) };
|
|
136
|
+
approvals.set(a.id, a); w.state = 'waiting'; runs.set(w.id, { stopped: false }); w.events.push({ seq: w.events.length + 1, t: ago(1), kind: 'approval', approval: a.id, tool: a.tool, input: a.input, reason: a.reason, state: 'pending' });
|
|
137
|
+
// Quan la decideixis, la sessió continua.
|
|
138
|
+
a.resolve = async (d) => {
|
|
139
|
+
const run = runs.get(w.id) || { stopped: false }; runs.set(w.id, run); w.state = 'running'; touch(w);
|
|
140
|
+
if (run.stopped) { runs.delete(w.id); w.state = 'idle'; push(w, 'status', { state: 'idle', code: 130 }); return; }
|
|
141
|
+
if (d === 'allow') { push(w, 'tool', { id: 'm1', name: 'Bash', input: { command: 'npm run migrate' } }); await sleep(900); push(w, 'tool_result', { id: 'm1', name: 'Bash', output: '✓ 1 migració aplicada', error: false }); await stream(w, 'Migració aplicada. La taula d’usuaris ja té la columna «telefon».', run); }
|
|
142
|
+
else await stream(w, 'D’acord, no l’executo. La tens a migrations/004_telefon.sql per quan vulguis.', run);
|
|
143
|
+
runs.delete(w.id); w.state = 'idle'; push(w, 'status', { state: 'idle', code: 0 });
|
|
144
|
+
};
|
|
145
|
+
mk('claude', 'api', 'Escriu tests per a /usuaris', 60 * 26, [
|
|
146
|
+
{ kind: 'user', text: 'Escriu tests per a /usuaris' }, { kind: 'text', text: 'He afegit 6 tests a test/usuaris.test.js. Tots passen.' }, { kind: 'result', cost: 0.11, ms: 52000, turns: 5 },
|
|
147
|
+
], { cost: 0.11 });
|
|
148
|
+
}
|
|
149
|
+
seed();
|
|
150
|
+
|
|
151
|
+
// ---------- Feines: cua, cadenes i programades. Cada feina obre una sessió i acaba quan l'agent acaba.
|
|
152
|
+
const jobs = [];
|
|
153
|
+
const jobOut = (j) => out({ type: 'job', job: { ...j } });
|
|
154
|
+
function runJob(j) {
|
|
155
|
+
const prev = j.after && jobs.find((x) => x.id === j.after);
|
|
156
|
+
const prompt = j.prompt.replace('{{anterior}}', prev?.result || '');
|
|
157
|
+
const s = create({ agent: j.agent, cwd: j.cwd, title: j.title, mode: j.mode });
|
|
158
|
+
Object.assign(j, { state: 'en curs', session: s.id, started: Date.now(), runAt: null });
|
|
159
|
+
jobOut(j); audit.unshift({ t: Date.now(), type: 'job', what: `Feina començada: ${j.title}`, agent: j.agent, job: j.id });
|
|
160
|
+
send(s, prompt);
|
|
161
|
+
}
|
|
162
|
+
function runEnded(s, stopped) {
|
|
163
|
+
const j = jobs.find((x) => x.session === s.id && x.state === 'en curs'); if (!j) return;
|
|
164
|
+
Object.assign(j, { state: stopped ? 'cancel·lada' : 'feta', finished: Date.now(), result: [...s.events].reverse().find((e) => e.kind === 'text')?.text || '' });
|
|
165
|
+
jobOut(j); audit.unshift({ t: Date.now(), type: 'job', what: `Feina ${stopped ? 'aturada' : 'feta'}: ${j.title}`, agent: j.agent, job: j.id });
|
|
166
|
+
if (!stopped) for (const n of jobs) if (n.after === j.id && n.state === 'pendent') runJob(n);
|
|
167
|
+
}
|
|
168
|
+
function schedule(j) {
|
|
169
|
+
if (j.state === 'pendent' && !j.after && (!j.runAt || j.runAt <= Date.now())) return runJob(j);
|
|
170
|
+
if (j.state === 'pendent' && j.runAt && j.runAt - Date.now() < 3600000) setTimeout(() => j.state === 'pendent' && runJob(j), j.runAt - Date.now());
|
|
171
|
+
}
|
|
172
|
+
function addJob(b) {
|
|
173
|
+
const prompt = String(b.prompt || '').trim(); if (!prompt) throw err(400, 'Explica què ha de fer la feina');
|
|
174
|
+
const agent = AGENTS.some((a) => a.id === b.agent) ? b.agent : 'claude';
|
|
175
|
+
if (b.after && !jobs.some((x) => x.id === b.after)) throw err(400, 'La feina anterior no existeix');
|
|
176
|
+
const at = b.at ? Number(new Date(b.at)) : null;
|
|
177
|
+
const j = { id: rid(), title: String(b.title || prompt.replace(/\s+/g, ' ').slice(0, 70)), prompt, agent, cwd: b.cwd || `${ROOT}/web-botiga`, mode: ['llegir', 'preguntar', 'tot'].includes(b.mode) ? b.mode : 'preguntar', rule: null, after: b.after || null, cron: b.cron || null, state: b.cron ? 'programada' : 'pendent', runAt: b.cron ? nextNine() : at, created: Date.now() };
|
|
178
|
+
jobs.unshift(j); jobOut(j);
|
|
179
|
+
audit.unshift({ t: Date.now(), type: 'job', what: `Feina ${j.cron ? 'programada' : 'a la cua'}: ${j.title}`, agent: j.agent, job: j.id });
|
|
180
|
+
setTimeout(() => schedule(j), 300);
|
|
181
|
+
return j;
|
|
182
|
+
}
|
|
183
|
+
// La propera vegada que toca una feina programada (a la demo, sempre el proper dia a les 9).
|
|
184
|
+
const nextNine = () => { const d = new Date(); d.setHours(9, 0, 0, 0); if (d <= new Date()) d.setDate(d.getDate() + 1); return +d; };
|
|
185
|
+
|
|
186
|
+
// ---------- Pantalla: un escriptori dibuixat aquí mateix, que respon als clics, al teclat i als arrossegaments.
|
|
187
|
+
const caps = { navegador: { installed: true, ready: true, enabled: true }, escriptori: { installed: true, ready: true, why: '', enabled: true, screenshot: true }, platform: 'darwin' };
|
|
188
|
+
const W = 1600, H = 1000;
|
|
189
|
+
const desk = {
|
|
190
|
+
ver: 1,
|
|
191
|
+
wins: [
|
|
192
|
+
{ id: 'code', x: 70, y: 70, w: 860, h: 540, title: 'styles.css — web-botiga' },
|
|
193
|
+
{ id: 'term', x: 720, y: 400, w: 780, h: 440, title: 'Terminal — api' },
|
|
194
|
+
],
|
|
195
|
+
term: ['$ npm test', '✓ 14 tests passen (0,8 s)', '$ git status --short', ' M src/styles.css'],
|
|
196
|
+
typed: '', mouse: null, ripple: 0, menu: null,
|
|
197
|
+
};
|
|
198
|
+
const bump = () => { desk.ver++; };
|
|
199
|
+
const hit = (x, y) => [...desk.wins].reverse().find((w) => x >= w.x && x <= w.x + w.w && y >= w.y && y <= w.y + w.h);
|
|
200
|
+
const front = (w) => { desk.wins = desk.wins.filter((x) => x !== w).concat(w); };
|
|
201
|
+
function input(ev) {
|
|
202
|
+
const px = (p) => [p[0] * W, p[1] * H];
|
|
203
|
+
if (ev.type === 'click' || ev.type === 'rclick') {
|
|
204
|
+
const [x, y] = px([ev.x, ev.y]); desk.mouse = [x, y]; desk.ripple = Date.now(); desk.menu = ev.type === 'rclick' ? [x, y, Date.now()] : null;
|
|
205
|
+
const w = hit(x, y); if (w) front(w);
|
|
206
|
+
} else if (ev.type === 'drag' && ev.path?.length > 1) {
|
|
207
|
+
const [x0, y0] = px(ev.path[0]), [x1, y1] = px(ev.path[ev.path.length - 1]), w = hit(x0, y0);
|
|
208
|
+
if (w && y0 - w.y < 36) { w.x = Math.max(-w.w + 80, Math.min(W - 80, w.x + x1 - x0)); w.y = Math.max(28, Math.min(H - 60, w.y + y1 - y0)); front(w); }
|
|
209
|
+
desk.mouse = [x1, y1];
|
|
210
|
+
} else if (ev.type === 'text') desk.typed = (desk.typed + ev.text).slice(-60);
|
|
211
|
+
else if (ev.type === 'key') {
|
|
212
|
+
if (ev.key === 'Backspace' || ev.key === 'BackSpace') desk.typed = desk.typed.slice(0, -1);
|
|
213
|
+
else if (ev.key === 'Enter' || ev.key === 'Return') { const cmd = desk.typed.trim(); desk.term.push(`$ ${cmd}`); if (cmd) desk.term.push(cmd === 'ls' ? 'api notes web-botiga' : cmd === 'pwd' ? ROOT : cmd === 'date' ? new Date().toString() : `demo: «${cmd.split(' ')[0]}» és una ordre simulada`); desk.typed = ''; desk.term = desk.term.slice(-12); }
|
|
214
|
+
}
|
|
215
|
+
bump();
|
|
216
|
+
return { ok: true };
|
|
217
|
+
}
|
|
218
|
+
const C = { bar: 'rgba(255,255,255,.72)', ink: '#272224', code: '#1f1d1c', term: '#141313' };
|
|
219
|
+
function rr(c, x, y, w, h, r) { c.beginPath(); c.roundRect ? c.roundRect(x, y, w, h, r) : c.rect(x, y, w, h); }
|
|
220
|
+
function paint(c) {
|
|
221
|
+
const g = c.createLinearGradient(0, 0, W, H); g.addColorStop(0, '#f6b896'); g.addColorStop(.55, '#e8a7b8'); g.addColorStop(1, '#8ea6dc');
|
|
222
|
+
c.fillStyle = g; c.fillRect(0, 0, W, H);
|
|
223
|
+
c.fillStyle = C.bar; c.fillRect(0, 0, W, 28);
|
|
224
|
+
c.fillStyle = C.ink; c.font = '600 14px -apple-system, Segoe UI, sans-serif'; c.textBaseline = 'middle';
|
|
225
|
+
c.fillText('nio', 18, 14); c.font = '14px -apple-system, Segoe UI, sans-serif';
|
|
226
|
+
let mx = 60; for (const t of ['Fitxer', 'Edita', 'Visualització', 'Finestra', 'Ajuda']) { c.fillText(t, mx, 14); mx += c.measureText(t).width + 24; }
|
|
227
|
+
const now = new Date(); c.textAlign = 'right';
|
|
228
|
+
c.fillText(now.toLocaleString('ca', { weekday: 'short', day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }), W - 18, 14); c.textAlign = 'left';
|
|
229
|
+
for (const w of desk.wins) {
|
|
230
|
+
c.save(); c.shadowColor = 'rgba(0,0,0,.28)'; c.shadowBlur = 40; c.shadowOffsetY = 14;
|
|
231
|
+
c.fillStyle = w.id === 'code' ? C.code : C.term; rr(c, w.x, w.y, w.w, w.h, 12); c.fill(); c.restore();
|
|
232
|
+
c.save(); rr(c, w.x, w.y, w.w, w.h, 12); c.clip();
|
|
233
|
+
c.fillStyle = w.id === 'code' ? '#2b2826' : '#242120'; c.fillRect(w.x, w.y, w.w, 36);
|
|
234
|
+
['#ff5f57', '#febc2e', '#28c840'].forEach((col, i) => { c.fillStyle = col; c.beginPath(); c.arc(w.x + 20 + i * 20, w.y + 18, 6, 0, 7); c.fill(); });
|
|
235
|
+
c.fillStyle = '#bdb6ae'; c.font = '13px -apple-system, Segoe UI, sans-serif'; c.textAlign = 'center'; c.fillText(w.title, w.x + w.w / 2, w.y + 18); c.textAlign = 'left';
|
|
236
|
+
c.font = '16px ui-monospace, SF Mono, Menlo, monospace';
|
|
237
|
+
if (w.id === 'code') {
|
|
238
|
+
const lines = [['#7f8c98', '/* Colors de la botiga */'], ['#e8e2d8', ':root {'], ['#9cdcfe', ' --bg: #fff;'], ['#9cdcfe', ' --text: #1a1a1a;'], ['#e8e2d8', '}'], ['', ''], ['#c586c0', '@media (prefers-color-scheme: dark) {'], ['#9cdcfe', ' :root { --bg: #141414; --text: #eee; }'], ['#e8e2d8', '}'], ['', ''], ['#d7ba7d', '.boto-comprar {'], ['#9cdcfe', ' background: var(--accent);'], ['#9cdcfe', ' border-radius: 999px;'], ['#e8e2d8', '}']];
|
|
239
|
+
lines.forEach(([col, t], i) => { c.fillStyle = '#5c5652'; c.fillText(String(i + 1).padStart(2), w.x + 16, w.y + 66 + i * 26); c.fillStyle = col; c.fillText(t, w.x + 56, w.y + 66 + i * 26); });
|
|
240
|
+
} else {
|
|
241
|
+
const lines = [...desk.term, `$ ${desk.typed}`];
|
|
242
|
+
lines.slice(-14).forEach((t, i, a) => { c.fillStyle = t.startsWith('✓') ? '#7bd88f' : t.startsWith('$') ? '#f2ede6' : '#b9b2aa'; c.fillText(t, w.x + 20, w.y + 64 + i * 26); if (i === a.length - 1 && Math.floor(Date.now() / 500) % 2) { c.fillStyle = '#f2ede6'; c.fillRect(w.x + 20 + c.measureText(t).width + 2, w.y + 54 + i * 26, 9, 20); } });
|
|
243
|
+
}
|
|
244
|
+
c.restore();
|
|
245
|
+
}
|
|
246
|
+
// Dock
|
|
247
|
+
c.fillStyle = 'rgba(255,255,255,.45)'; rr(c, W / 2 - 230, H - 82, 460, 66, 20); c.fill();
|
|
248
|
+
['#f46a3b', '#2b5bd8', '#5b6551', '#e7e66a', '#272224', '#e2382b'].forEach((col, i) => { c.fillStyle = col; rr(c, W / 2 - 205 + i * 70, H - 70, 50, 50, 12); c.fill(); });
|
|
249
|
+
if (desk.menu && Date.now() - desk.menu[2] < 2500) {
|
|
250
|
+
const [x, y] = desk.menu; c.fillStyle = 'rgba(248,246,242,.97)'; rr(c, x, y, 210, 128, 10); c.fill();
|
|
251
|
+
c.fillStyle = C.ink; c.font = '15px -apple-system, Segoe UI, sans-serif'; ['Obre', 'Copia', 'Mostra informació', 'Mou a la paperera'].forEach((t, i) => c.fillText(t, x + 16, y + 22 + i * 28));
|
|
252
|
+
}
|
|
253
|
+
if (desk.mouse) {
|
|
254
|
+
const [x, y] = desk.mouse, age = Date.now() - desk.ripple;
|
|
255
|
+
if (age < 600) { c.strokeStyle = `rgba(244,106,59,${1 - age / 600})`; c.lineWidth = 3; c.beginPath(); c.arc(x, y, 10 + age / 15, 0, 7); c.stroke(); }
|
|
256
|
+
c.fillStyle = '#fff'; c.strokeStyle = '#000'; c.lineWidth = 1.5; c.beginPath(); c.moveTo(x, y); c.lineTo(x, y + 22); c.lineTo(x + 6, y + 17); c.lineTo(x + 11, y + 27); c.lineTo(x + 15, y + 25); c.lineTo(x + 10, y + 15); c.lineTo(x + 17, y + 15); c.closePath(); c.fill(); c.stroke();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Clau de l'estat visible: si no canvia, el panell rep un 204 i no es redibuixa res.
|
|
260
|
+
const deskHash = () => `${desk.ver}-${Math.floor(Date.now() / 500)}-${Date.now() - desk.ripple < 700 ? Date.now() : 0}`;
|
|
261
|
+
async function frame(w, q) {
|
|
262
|
+
const scale = Math.min(1, Math.max(.3, (Number(w) || W) / W));
|
|
263
|
+
const cv = typeof OffscreenCanvas !== 'undefined' ? new OffscreenCanvas(Math.round(W * scale), Math.round(H * scale)) : Object.assign(document.createElement('canvas'), { width: Math.round(W * scale), height: Math.round(H * scale) });
|
|
264
|
+
const c = cv.getContext('2d'); c.scale(scale, scale); paint(c);
|
|
265
|
+
const quality = Math.min(.92, Math.max(.3, (Number(q) || 70) / 100));
|
|
266
|
+
return cv.convertToBlob ? cv.convertToBlob({ type: 'image/jpeg', quality }) : new Promise((r) => cv.toBlob(r, 'image/jpeg', quality));
|
|
267
|
+
}
|
|
268
|
+
const shots = new Map();
|
|
269
|
+
let lastShot = null;
|
|
270
|
+
async function takeShot(from) {
|
|
271
|
+
const id = rid(); shots.set(id, await frame(1600, 80));
|
|
272
|
+
lastShot = { id, t: Date.now(), from: from || 'Aquest navegador' };
|
|
273
|
+
audit.unshift({ t: Date.now(), type: 'tool', agent: 'claude', title: 'Captura de pantalla', tool: 'Bash', what: 'agent-desktop screenshot', mode: 'preguntar' });
|
|
274
|
+
return lastShot;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ---------- Activitat d'exemple: el que haurien fet els agents abans que obrissis la demo.
|
|
278
|
+
function seedExtra() {
|
|
279
|
+
const ago = (min) => Date.now() - min * 60000;
|
|
280
|
+
const s1 = [...sessions.values()].find((s) => s.agent === 'claude' && /fosc/.test(s.title));
|
|
281
|
+
const s3 = [...sessions.values()].find((s) => /tests per/.test(s.title));
|
|
282
|
+
jobs.push(
|
|
283
|
+
{ id: rid(), title: 'Revisa les dependències i obre un PR si n’hi ha de noves', prompt: 'Revisa les dependències de l’API i, si n’hi ha de noves, actualitza-les i obre un PR.', agent: 'codex', cwd: `${ROOT}/api`, mode: 'preguntar', rule: null, after: null, cron: '0 9 * * 1-5', state: 'programada', runAt: nextNine(), created: ago(60 * 48) },
|
|
284
|
+
{ id: rid(), title: 'Afegeix el mode fosc a la botiga', prompt: 'Afegeix el mode fosc a la botiga', agent: 'claude', cwd: `${ROOT}/web-botiga`, mode: 'preguntar', rule: null, after: null, cron: null, state: 'feta', session: s1?.id, created: ago(20), started: ago(18), finished: ago(12), result: 'Fet.' },
|
|
285
|
+
{ id: rid(), title: 'Escriu tests per a /usuaris', prompt: 'Escriu tests per a /usuaris', agent: 'claude', cwd: `${ROOT}/api`, mode: 'preguntar', rule: null, after: null, cron: null, state: 'feta', session: s3?.id, created: ago(60 * 27), started: ago(60 * 26 + 5), finished: ago(60 * 26), result: 'Fet.' },
|
|
286
|
+
);
|
|
287
|
+
audit.push(
|
|
288
|
+
{ t: ago(12), type: 'tool', agent: 'claude', title: 'Afegeix el mode fosc a la botiga', tool: 'Bash', what: 'npm run build', mode: 'preguntar' },
|
|
289
|
+
{ t: ago(12.5), type: 'approval', agent: 'claude', title: 'Afegeix el mode fosc a la botiga', tool: 'Edit', what: `${ROOT}/web-botiga/src/styles.css`, why: 'Afegeixo els colors del mode fosc', decision: 'allowed', by: 'iPhone', asked: ago(13) },
|
|
290
|
+
{ t: ago(13), type: 'tool', agent: 'claude', title: 'Afegeix el mode fosc a la botiga', tool: 'Read', what: 'src/styles.css', mode: 'preguntar' },
|
|
291
|
+
{ t: ago(35), type: 'tool', agent: 'claude', title: 'Comprova la botiga al navegador', tool: 'Bash', what: 'agent-browser open http://localhost:4321', mode: 'preguntar' },
|
|
292
|
+
{ t: ago(34), type: 'tool', agent: 'claude', title: 'Comprova la botiga al navegador', tool: 'Bash', what: 'agent-desktop screenshot', mode: 'preguntar' },
|
|
293
|
+
{ t: ago(60 * 5), type: 'device', what: 'iPhone s’ha connectat amb un codi', by: 'iPhone' },
|
|
294
|
+
{ t: ago(60 * 26), type: 'job', what: 'Feina feta: Escriu tests per a /usuaris', agent: 'claude' },
|
|
295
|
+
);
|
|
296
|
+
lastShot = { id: 'inicial', t: ago(34), from: 'Claude Code' };
|
|
297
|
+
}
|
|
298
|
+
seedExtra();
|
|
299
|
+
|
|
300
|
+
const now = Date.now(), NOTES = [
|
|
301
|
+
{ id: 'n1', title: 'Reunió de dimarts', text: 'Parlar amb la Marta del pressupost.\nRevisar el contracte del fuster abans de divendres.', items: null, color: 'lemon', pinned: true, archived: false, trashed: 0, labels: ['feina'], audio: [], created: now - 9e5, updated: now - 9e5 },
|
|
302
|
+
{ id: 'n2', title: 'Compra', text: '', items: [{ id: 'i1', text: 'llet', done: false }, { id: 'i2', text: 'pa de pagès', done: true }, { id: 'i3', text: 'pintura blanca', done: false }], color: 'sky', pinned: false, archived: false, trashed: 0, labels: ['casa'], audio: [], created: now - 8e5, updated: now - 8e5 },
|
|
303
|
+
{ id: 'n3', title: 'Idees per al logo', text: 'El bot que es converteix en la O. Animació suau, fons crema.', items: null, color: 'lilac', pinned: false, archived: false, trashed: 0, labels: ['nio'], audio: [], created: now - 7e5, updated: now - 7e5 },
|
|
304
|
+
];
|
|
305
|
+
const noteId = () => Math.random().toString(36).slice(2, 10);
|
|
306
|
+
const MEMORY = {
|
|
307
|
+
'preferencies.md': '# Preferències\n\n- Respon en català.\n- Fes commits petits i explica’ls bé.\n',
|
|
308
|
+
'projectes/web-botiga.md': '# web-botiga\n\nBotiga en línia feta amb Astro. Els colors són a src/styles.css.\n',
|
|
309
|
+
};
|
|
310
|
+
const DIRS = { [ROOT]: ['web-botiga', 'api', 'notes'], [`${ROOT}/api`]: ['src', 'test'], [`${ROOT}/web-botiga`]: ['public', 'src'] };
|
|
311
|
+
|
|
312
|
+
async function route(method, p, q, body) {
|
|
313
|
+
let m;
|
|
314
|
+
if (method === 'GET') {
|
|
315
|
+
if (p === '/api/info') return { name: 'nio', version: 'demo', host: 'Ordinador de demo', platform: 'darwin', user: 'tu', publicUrl: null, agents: AGENTS, roots: [ROOT], vapidKey: null, id: 'demo', device: { id: 'demo', name: 'Aquest navegador', primary: true }, demo: true, control: caps };
|
|
316
|
+
if (p === '/api/usage') return DEMO_USAGE;
|
|
317
|
+
if (p === '/api/notes') return { notes: NOTES };
|
|
318
|
+
if (p === '/api/notes/engines') return { engines: [{ id: 'ollama:qwen3:4b', label: 'qwen3:4b · 4.0B', local: true, group: 'Local (Ollama)', params: 4 }] };
|
|
319
|
+
if (p === '/api/sessions') return list();
|
|
320
|
+
if (p === '/api/approvals') return pending();
|
|
321
|
+
if ((m = p.match(/^\/api\/sessions\/([\w-]+)$/))) { const s = sessions.get(m[1]); if (!s) throw err(404, 'Aquesta sessió no existeix'); const since = Number(q.get('since') || 0); return { session: summary(s), events: s.events.filter((e) => e.seq > since) }; }
|
|
322
|
+
if (/^\/api\/sessions\/[\w-]+\/commands$/.test(p)) return [];
|
|
323
|
+
if (/^\/api\/sessions\/[\w-]+\/git$/.test(p)) return { repo: false };
|
|
324
|
+
if (p === '/api/projects') return [{ path: ROOT, name: 'projectes', root: true, git: false }, ...DIRS[ROOT].map((n) => ({ path: `${ROOT}/${n}`, name: n, root: false, git: n !== 'notes' }))];
|
|
325
|
+
if (p === '/api/fs') { const d = (q.get('path') || ROOT).replace(/\/+$/, '') || ROOT; return { path: d, parent: d === '/Users/tu' ? null : d.replace(/\/[^/]+$/, '') || null, home: '/Users/tu', git: false, dirs: (DIRS[d] || []).map((n) => ({ name: n, path: `${d}/${n}`, hidden: false })) }; }
|
|
326
|
+
if (p === '/api/memory') return { dir: '~/.nio/memory', files: Object.entries(MEMORY).map(([path, c]) => ({ path, size: c.length, updated: Date.now() - 86400000 })) };
|
|
327
|
+
if ((m = p.match(/^\/api\/memory\/f\/(.+)$/))) { const path = decodeURIComponent(m[1]); if (!(path in MEMORY)) throw err(404, 'Aquest fitxer no existeix'); return { path, content: MEMORY[path], updated: Date.now() - 86400000 }; }
|
|
328
|
+
if (p === '/api/memory/search') { const t = (q.get('q') || '').toLowerCase(), r = []; for (const [path, c] of Object.entries(MEMORY)) c.split('\n').forEach((l, i) => l.toLowerCase().includes(t) && t && r.push({ path, line: i + 1, text: l })); return r; }
|
|
329
|
+
if (p === '/api/jobs') return { jobs: jobs.map((j) => ({ ...j })), routing: [], max: 2 };
|
|
330
|
+
if (p === '/api/audit') return audit.filter((e) => !q.get('type') || e.type === q.get('type')).sort((a, b) => b.t - a.t).slice(0, 300);
|
|
331
|
+
if (p === '/api/devices') return [{ id: 'demo', name: 'Aquest navegador', primary: true, created: null, lastSeen: Date.now(), current: true }, { id: 'iphone', name: 'iPhone', primary: false, created: Date.now() - 3 * 86400000, lastSeen: Date.now() - 5 * 3600000 }];
|
|
332
|
+
if (p === '/api/voice') return { providers: {}, stt: null, tts: null, speak: false, lang: 'ca', voice: {}, demo: true };
|
|
333
|
+
if (p === '/api/control') return caps;
|
|
334
|
+
if (p === '/api/screen') return { last: lastShot, enabled: caps.escriptori.enabled || caps.navegador.enabled };
|
|
335
|
+
if (p === '/api/desktop/status') return { w: W, h: H, screen: true, accessibility: true, platform: 'darwin', input: true, capture: true, enabled: caps.escriptori.enabled };
|
|
336
|
+
if (p === '/api/telegram') return { configured: false, bot: null, running: false, allowed: [], pending: [], agent: null, cwd: ROOT, mode: 'preguntar', notify: true, voice: false };
|
|
337
|
+
if (p === '/api/ollama') return { available: false, host: '', models: [], running: [], pulling: [] };
|
|
338
|
+
if (p === '/api/ollama/models' || p === '/api/notices' || p === '/api/ssh/hosts') return [];
|
|
339
|
+
if (p === '/api/github') return { available: false, authed: false };
|
|
340
|
+
}
|
|
341
|
+
if (method === 'POST') {
|
|
342
|
+
if (p === '/api/sessions') { const s = create(body); if (body.text) send(s, body.text); return summary(s); }
|
|
343
|
+
if ((m = p.match(/^\/api\/sessions\/([\w-]+)\/message$/))) { const s = sessions.get(m[1]); if (!s) throw err(404, 'Aquesta sessió no existeix'); return send(s, body.text); }
|
|
344
|
+
if ((m = p.match(/^\/api\/sessions\/([\w-]+)\/stop$/))) { const r = runs.get(m[1]), s = sessions.get(m[1]); if (!r) return { stopped: false }; r.stopped = true; for (const a of approvals.values()) if (a.session === m[1] && a.state === 'pending') decide(a.id, 'deny'); if (s) push(s, 'info', { text: 'Has aturat l’agent' }); return { stopped: true }; }
|
|
345
|
+
if ((m = p.match(/^\/api\/approvals\/([\w-]+)$/))) { const a = decide(m[1], body.decision === 'allow' ? 'allow' : 'deny'); if (!a) throw err(404, 'Aquesta petició ja s’ha resolt'); return a; }
|
|
346
|
+
if (p === '/api/notes') { const t = Date.now(), n = { id: noteId(), title: '', text: '', items: null, color: '', pinned: false, archived: false, trashed: 0, labels: [], audio: [], created: t, updated: t, ...body }; NOTES.unshift(n); return n; }
|
|
347
|
+
if (p === '/api/notes/ai') { const n = NOTES.find((x) => x.id === body.id), t = (n?.text || '').split('\n').filter(Boolean); return { engine: 'ollama:qwen3:4b', local: true, action: body.action, result: { text: t.map((x) => `- ${x}`).join('\n') || '- (res a resumir)', items: t, title: n?.title || 'Nota', labels: ['demo'] } }; }
|
|
348
|
+
if (p === '/api/jobs') return addJob(body);
|
|
349
|
+
if ((m = p.match(/^\/api\/jobs\/([\w-]+)\/(run|cancel)$/))) {
|
|
350
|
+
const j = jobs.find((x) => x.id === m[1]); if (!j) throw err(404, 'Aquesta feina no existeix');
|
|
351
|
+
if (m[2] === 'run') { if (j.cron) { const c = { ...j, id: rid(), cron: null, state: 'pendent', created: Date.now() }; jobs.unshift(c); runJob(c); } else runJob(j); return j; }
|
|
352
|
+
if (j.state === 'en curs') { const r = runs.get(j.session); if (r) r.stopped = true; }
|
|
353
|
+
Object.assign(j, { state: 'cancel·lada', finished: Date.now() }); jobOut(j); return j;
|
|
354
|
+
}
|
|
355
|
+
if (p === '/api/screen') return takeShot('Aquest navegador');
|
|
356
|
+
if (p === '/api/desktop/session') return { ok: true };
|
|
357
|
+
if (p === '/api/desktop/input') { if (!caps.escriptori.enabled) throw err(400, 'Activa primer «Apps de l’escriptori».'); return input(body); }
|
|
358
|
+
if (p === '/api/panic') { let n = 0; for (const [id, r] of runs) { r.stopped = true; n++; const s = sessions.get(id); if (s) push(s, 'info', { text: 'Aturat des de l’aturada d’emergència' }); } for (const a of approvals.values()) if (a.state === 'pending') decide(a.id, 'deny'); return { stopped: n }; }
|
|
359
|
+
if (p === '/api/voice/stt') throw err(400, 'A la demo no hi ha micròfon. Amb nio instal·lat, dictes amb Whisper, gratis, a l’ordinador.');
|
|
360
|
+
}
|
|
361
|
+
if ((m = p.match(/^\/api\/notes\/([\w-]+)$/))) {
|
|
362
|
+
const n = NOTES.find((x) => x.id === m[1]); if (!n) throw err(404, 'Aquesta nota no existeix');
|
|
363
|
+
if (method === 'PATCH') { Object.assign(n, body, { updated: Date.now() }); if ('trashed' in body) n.trashed = body.trashed ? Date.now() : 0; return n; }
|
|
364
|
+
if (method === 'DELETE') { if (n.trashed || q.get('forever') === '1') NOTES.splice(NOTES.indexOf(n), 1); else n.trashed = Date.now(); return { ok: true }; }
|
|
365
|
+
}
|
|
366
|
+
if ((m = p.match(/^\/api\/sessions\/([\w-]+)$/))) {
|
|
367
|
+
const s = sessions.get(m[1]); if (!s) throw err(404, 'Aquesta sessió no existeix');
|
|
368
|
+
if (method === 'PATCH') { if (['llegir', 'preguntar', 'tot'].includes(body.mode)) s.mode = body.mode; if (typeof body.title === 'string') s.title = body.title.slice(0, 120); touch(s); return summary(s); }
|
|
369
|
+
if (method === 'DELETE') { runs.get(s.id) && (runs.get(s.id).stopped = true); sessions.delete(s.id); out({ type: 'removed', session: s.id }); return { ok: true }; }
|
|
370
|
+
}
|
|
371
|
+
if (method === 'DELETE' && (m = p.match(/^\/api\/jobs\/([\w-]+)$/))) { const i = jobs.findIndex((x) => x.id === m[1]); if (i >= 0) jobs.splice(i, 1); out({ type: 'job', removed: m[1] }); return { ok: true }; }
|
|
372
|
+
if (method === 'PUT' && p === '/api/control') { for (const k of ['navegador', 'escriptori']) if (typeof body[k] === 'boolean') caps[k].enabled = body[k]; return { ...caps, notes: [] }; }
|
|
373
|
+
if ((m = p.match(/^\/api\/memory\/f\/(.+)$/))) {
|
|
374
|
+
if (method === 'PUT') { MEMORY[decodeURIComponent(m[1])] = String(body.content ?? ''); return { ok: true }; }
|
|
375
|
+
if (method === 'DELETE') { delete MEMORY[decodeURIComponent(m[1])]; return { ok: true }; }
|
|
376
|
+
}
|
|
377
|
+
throw err(400, NOPE);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const json = (status, data) => new Response(JSON.stringify(data), { status, headers: { 'content-type': 'application/json' } });
|
|
381
|
+
const realFetch = window.fetch.bind(window);
|
|
382
|
+
window.fetch = async (input, opts = {}) => {
|
|
383
|
+
const href = typeof input === 'string' ? input : input?.url || String(input);
|
|
384
|
+
if (!href.startsWith('demo:')) return realFetch(input, opts);
|
|
385
|
+
const u = new URL(href.replace(/^demo:[^/]*/, 'http://demo')), method = (opts.method || 'GET').toUpperCase();
|
|
386
|
+
if (u.pathname === '/api/stream') {
|
|
387
|
+
let ctl;
|
|
388
|
+
const body = new ReadableStream({
|
|
389
|
+
start(c) { ctl = c; streams.add(c); c.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ type: 'hello', sessions: list(), approvals: pending(), usage: DEMO_USAGE })}\n\n`)); },
|
|
390
|
+
cancel() { streams.delete(ctl); },
|
|
391
|
+
});
|
|
392
|
+
opts.signal?.addEventListener('abort', () => { streams.delete(ctl); try { ctl.close(); } catch { /* ja tancat */ } });
|
|
393
|
+
return new Response(body, { status: 200, headers: { 'content-type': 'text/event-stream' } });
|
|
394
|
+
}
|
|
395
|
+
if (u.pathname === '/api/desktop/frame') {
|
|
396
|
+
if (!caps.escriptori.enabled) return json(400, { error: 'Activa primer «Apps de l’escriptori».' });
|
|
397
|
+
const h = deskHash(), hd = { 'x-hash': h, 'x-screen-w': String(W), 'x-screen-h': String(H), 'x-blank': '0' };
|
|
398
|
+
if (u.searchParams.get('h') === h) return new Response(null, { status: 204, headers: hd });
|
|
399
|
+
return new Response(await frame(u.searchParams.get('w'), u.searchParams.get('q')), { status: 200, headers: { ...hd, 'content-type': 'image/jpeg' } });
|
|
400
|
+
}
|
|
401
|
+
let sm;
|
|
402
|
+
if ((sm = u.pathname.match(/^\/api\/screen\/shot\/([\w-]+)$/))) {
|
|
403
|
+
if (!shots.has(sm[1])) shots.set(sm[1], await frame(1600, 80));
|
|
404
|
+
return new Response(shots.get(sm[1]), { status: 200, headers: { 'content-type': 'image/jpeg' } });
|
|
405
|
+
}
|
|
406
|
+
await sleep(60);
|
|
407
|
+
let body = {};
|
|
408
|
+
try { body = typeof opts.body === 'string' ? JSON.parse(opts.body) : {}; } catch { /* àudio o res */ }
|
|
409
|
+
try { return json(200, await route(method, u.pathname, u.searchParams, body)); }
|
|
410
|
+
catch (e) { return json(e.status || 500, { error: e.message }); }
|
|
411
|
+
};
|
|
412
|
+
window.NioDemo = { url: 'demo:nio', isDemo: (m) => !!m && String(m.url).startsWith('demo:') };
|
|
413
|
+
})();
|
package/web/app/index.html
CHANGED
|
@@ -17,14 +17,14 @@
|
|
|
17
17
|
<meta property="og:url" content="https://www.nio.cat/app">
|
|
18
18
|
<meta property="og:title" content="nio // Panell">
|
|
19
19
|
<meta property="og:description" content="Dona feina als agents d’IA del teu ordinador des del mòbil, segueix-los en directe i aprova cada pas.">
|
|
20
|
-
<meta property="og:image" content="https://www.nio.cat/og.
|
|
20
|
+
<meta property="og:image" content="https://www.nio.cat/og.jpg">
|
|
21
21
|
<meta property="og:image:width" content="1200">
|
|
22
22
|
<meta property="og:image:height" content="630">
|
|
23
23
|
<meta property="og:image:alt" content="nio">
|
|
24
24
|
<meta name="twitter:card" content="summary_large_image">
|
|
25
25
|
<meta name="twitter:title" content="nio // Panell">
|
|
26
26
|
<meta name="twitter:description" content="Dona feina als agents d’IA del teu ordinador des del mòbil, segueix-los en directe i aprova cada pas.">
|
|
27
|
-
<meta name="twitter:image" content="https://www.nio.cat/og.
|
|
27
|
+
<meta name="twitter:image" content="https://www.nio.cat/og.jpg">
|
|
28
28
|
<link rel="manifest" href="/manifest.webmanifest">
|
|
29
29
|
<meta name="format-detection" content="telephone=no">
|
|
30
30
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
<link rel="stylesheet" href="/bot.css">
|
|
36
36
|
<link rel="stylesheet" href="/board.css">
|
|
37
37
|
<link rel="stylesheet" href="/app/style.css">
|
|
38
|
+
<link rel="stylesheet" href="/app/notes.css">
|
|
38
39
|
</head>
|
|
39
40
|
<body>
|
|
40
41
|
<header class="mbar">
|
|
@@ -72,6 +73,7 @@
|
|
|
72
73
|
|
|
73
74
|
<script src="/bot.js"></script>
|
|
74
75
|
<script src="/board.js"></script>
|
|
76
|
+
<script src="/app/demo.js"></script>
|
|
75
77
|
<script src="/app/app.js" type="module"></script>
|
|
76
78
|
</body>
|
|
77
79
|
</html>
|