nio-cat 0.9.6 → 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 +1 -0
- package/bin/nio.js +3 -1
- 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/server.js +19 -2
- package/src/usage.js +79 -0
- package/web/app/app.js +194 -43
- package/web/app/demo.js +202 -10
- package/web/app/index.html +3 -2
- package/web/app/notes.css +154 -0
- package/web/app/notes.js +504 -0
- package/web/app/style.css +26 -0
- package/web/index.html +7 -2
- package/web/sw.js +2 -2
package/web/app/demo.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// text en directe, eines i aprovacions de mentida. No surt res del navegador i es reinicia en recarregar.
|
|
4
4
|
(() => {
|
|
5
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() } };
|
|
6
7
|
const AGENTS = [
|
|
7
8
|
{ id: 'claude', name: 'Claude Code', beta: false, approvals: true, noModes: false, login: '', available: true, version: 'simulat' },
|
|
8
9
|
{ id: 'codex', name: 'Codex', beta: false, approvals: true, noModes: false, login: '', available: true, version: 'simulat' },
|
|
@@ -73,7 +74,7 @@
|
|
|
73
74
|
}
|
|
74
75
|
async function work(s, text) {
|
|
75
76
|
const run = { stopped: false }; runs.set(s.id, run);
|
|
76
|
-
const p = plan(text, s), tool = (name, input, output, error) => { const id = rid(); push(s, 'tool', { id, name, input }); return { id, done: () => push(s, 'tool_result', { id, name, output, error: !!error }) }; };
|
|
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 }) }; };
|
|
77
78
|
const allowed = async (name, input) => s.mode === 'tot' || (await ask(s, name, input)) === 'allow';
|
|
78
79
|
try {
|
|
79
80
|
await sleep(500); await stream(s, p.intro, run); if (run.stopped) return;
|
|
@@ -94,8 +95,10 @@
|
|
|
94
95
|
runs.delete(s.id);
|
|
95
96
|
for (const a of approvals.values()) if (a.session === s.id && a.state === 'pending') decide(a.id, 'deny');
|
|
96
97
|
s.state = 'idle'; push(s, 'status', { state: 'idle', code: run.stopped ? 130 : 0 });
|
|
98
|
+
runEnded(s, run.stopped);
|
|
97
99
|
}
|
|
98
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 });
|
|
99
102
|
function create({ agent, cwd, title, mode }) {
|
|
100
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 ')}`);
|
|
101
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: [] };
|
|
@@ -145,6 +148,161 @@
|
|
|
145
148
|
}
|
|
146
149
|
seed();
|
|
147
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);
|
|
148
306
|
const MEMORY = {
|
|
149
307
|
'preferencies.md': '# Preferències\n\n- Respon en català.\n- Fes commits petits i explica’ls bé.\n',
|
|
150
308
|
'projectes/web-botiga.md': '# web-botiga\n\nBotiga en línia feta amb Astro. Els colors són a src/styles.css.\n',
|
|
@@ -154,8 +312,11 @@
|
|
|
154
312
|
async function route(method, p, q, body) {
|
|
155
313
|
let m;
|
|
156
314
|
if (method === 'GET') {
|
|
157
|
-
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:
|
|
158
|
-
if (p === '/api/
|
|
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();
|
|
159
320
|
if (p === '/api/approvals') return pending();
|
|
160
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) }; }
|
|
161
322
|
if (/^\/api\/sessions\/[\w-]+\/commands$/.test(p)) return [];
|
|
@@ -165,13 +326,13 @@
|
|
|
165
326
|
if (p === '/api/memory') return { dir: '~/.nio/memory', files: Object.entries(MEMORY).map(([path, c]) => ({ path, size: c.length, updated: Date.now() - 86400000 })) };
|
|
166
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 }; }
|
|
167
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; }
|
|
168
|
-
if (p === '/api/jobs') return { jobs:
|
|
169
|
-
if (p === '/api/audit') return audit.slice(0, 300);
|
|
170
|
-
if (p === '/api/devices') return [{ id: 'demo', name: 'Aquest navegador', primary: true, created: null, lastSeen: Date.now(), current: true }];
|
|
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 }];
|
|
171
332
|
if (p === '/api/voice') return { providers: {}, stt: null, tts: null, speak: false, lang: 'ca', voice: {}, demo: true };
|
|
172
|
-
if (p === '/api/control') return
|
|
173
|
-
if (p === '/api/screen') return { last:
|
|
174
|
-
if (p === '/api/desktop/status') return { w:
|
|
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 };
|
|
175
336
|
if (p === '/api/telegram') return { configured: false, bot: null, running: false, allowed: [], pending: [], agent: null, cwd: ROOT, mode: 'preguntar', notify: true, voice: false };
|
|
176
337
|
if (p === '/api/ollama') return { available: false, host: '', models: [], running: [], pulling: [] };
|
|
177
338
|
if (p === '/api/ollama/models' || p === '/api/notices' || p === '/api/ssh/hosts') return [];
|
|
@@ -182,13 +343,33 @@
|
|
|
182
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); }
|
|
183
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 }; }
|
|
184
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 }; }
|
|
185
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.');
|
|
186
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
|
+
}
|
|
187
366
|
if ((m = p.match(/^\/api\/sessions\/([\w-]+)$/))) {
|
|
188
367
|
const s = sessions.get(m[1]); if (!s) throw err(404, 'Aquesta sessió no existeix');
|
|
189
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); }
|
|
190
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 }; }
|
|
191
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: [] }; }
|
|
192
373
|
if ((m = p.match(/^\/api\/memory\/f\/(.+)$/))) {
|
|
193
374
|
if (method === 'PUT') { MEMORY[decodeURIComponent(m[1])] = String(body.content ?? ''); return { ok: true }; }
|
|
194
375
|
if (method === 'DELETE') { delete MEMORY[decodeURIComponent(m[1])]; return { ok: true }; }
|
|
@@ -205,12 +386,23 @@
|
|
|
205
386
|
if (u.pathname === '/api/stream') {
|
|
206
387
|
let ctl;
|
|
207
388
|
const body = new ReadableStream({
|
|
208
|
-
start(c) { ctl = c; streams.add(c); c.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ type: 'hello', sessions: list(), approvals: pending() })}\n\n`)); },
|
|
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`)); },
|
|
209
390
|
cancel() { streams.delete(ctl); },
|
|
210
391
|
});
|
|
211
392
|
opts.signal?.addEventListener('abort', () => { streams.delete(ctl); try { ctl.close(); } catch { /* ja tancat */ } });
|
|
212
393
|
return new Response(body, { status: 200, headers: { 'content-type': 'text/event-stream' } });
|
|
213
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
|
+
}
|
|
214
406
|
await sleep(60);
|
|
215
407
|
let body = {};
|
|
216
408
|
try { body = typeof opts.body === 'string' ? JSON.parse(opts.body) : {}; } catch { /* àudio o res */ }
|
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">
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/* nio · Notes */
|
|
2
|
+
.page:has(.nt) { max-width: 1240px; }
|
|
3
|
+
.nt-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 20px; flex-wrap: wrap; }
|
|
4
|
+
.nt-title h1 { font-size: 30px; font-weight: 500; letter-spacing: -.05em; line-height: 1.1; }
|
|
5
|
+
.nt-title p { margin-top: 4px; color: var(--fg-muted); }
|
|
6
|
+
.nt-bar { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
|
|
7
|
+
.nt-search { flex: 1; position: relative; display: flex; align-items: center; }
|
|
8
|
+
.nt-search .ico { position: absolute; left: 13px; width: 17px; height: 17px; color: var(--fg-muted); pointer-events: none; }
|
|
9
|
+
.nt-search input { width: 100%; height: 42px; padding: 0 14px 0 38px; border: 1px solid var(--border); border-radius: 12px; background: var(--white); font: inherit; color: var(--fg); transition: border-color .2s, box-shadow .2s; }
|
|
10
|
+
.nt-search input:focus { outline: none; border-color: var(--fg); box-shadow: 0 0 0 3px color-mix(in srgb, var(--fg) 10%, transparent); }
|
|
11
|
+
.nt-new { display: flex; align-items: center; justify-content: space-between; gap: 10px; width: min(620px, 100%); margin: 0 auto 18px; padding: 12px 12px 12px 18px; border: 1px solid var(--border); border-radius: 14px; background: var(--white); color: var(--fg-muted); cursor: text; box-shadow: 0 1px 2px rgba(39,34,36,.05), 0 6px 20px -14px rgba(39,34,36,.35); transition: box-shadow .25s var(--ease), transform .25s var(--ease), border-color .2s; }
|
|
12
|
+
.nt-new:hover { border-color: color-mix(in srgb, var(--fg) 30%, var(--border)); box-shadow: 0 1px 2px rgba(39,34,36,.06), 0 10px 28px -14px rgba(39,34,36,.45); }
|
|
13
|
+
.nt-new:active { transform: scale(.995); }
|
|
14
|
+
.nt-new-b { display: flex; gap: 2px; }
|
|
15
|
+
.nt-new-b button, .nt-ib { display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-width: 36px; height: 36px; padding: 0 8px; border: 0; border-radius: 10px; background: transparent; color: var(--fg-muted); cursor: pointer; font: inherit; font-size: 13px; font-weight: 500; transition: background .15s, color .15s, transform .15s var(--ease); }
|
|
16
|
+
.nt-new-b button:hover, .nt-ib:hover { background: color-mix(in srgb, var(--fg) 8%, transparent); color: var(--fg); }
|
|
17
|
+
.nt-ib:active, .nt-new-b button:active { transform: scale(.92); }
|
|
18
|
+
.nt-ib.on { color: var(--fg); background: color-mix(in srgb, var(--fg) 10%, transparent); }
|
|
19
|
+
.nt-ib.sm { min-width: 30px; height: 30px; }
|
|
20
|
+
.nt .ico, .nt-ed .ico, .nt-pop .ico { width: 19px; height: 19px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; flex: none; }
|
|
21
|
+
.nt-ib.on .ico { fill: currentColor; fill-opacity: .15; }
|
|
22
|
+
|
|
23
|
+
.nt-filters { display: flex; gap: 8px; overflow-x: auto; padding: 2px 2px 12px; margin: 0 -2px 6px; scrollbar-width: none; }
|
|
24
|
+
.nt-filters::-webkit-scrollbar { display: none; }
|
|
25
|
+
.nt-chip { display: inline-flex; align-items: center; gap: 6px; flex: none; height: 32px; padding: 0 13px; border: 1px solid var(--border); border-radius: 999px; background: transparent; color: var(--fg-muted); font: inherit; font-size: 13px; font-weight: 500; cursor: pointer; transition: background .18s, color .18s, border-color .18s, transform .18s var(--ease); }
|
|
26
|
+
.nt-chip .ico { width: 15px; height: 15px; }
|
|
27
|
+
.nt-chip:hover { border-color: var(--fg); color: var(--fg); }
|
|
28
|
+
.nt-chip[aria-current="true"] { background: var(--fg); color: var(--bg); border-color: var(--fg); }
|
|
29
|
+
.nt-chip:active { transform: scale(.96); }
|
|
30
|
+
|
|
31
|
+
.nt-sec { margin: 8px 2px 10px; font-size: 11px; font-weight: 600; letter-spacing: .09em; text-transform: uppercase; color: var(--fg-muted); }
|
|
32
|
+
.nt-grid { columns: 250px; column-gap: 14px; margin-bottom: 22px; }
|
|
33
|
+
.nt[data-layout="list"] .nt-grid { columns: 1; max-width: 640px; margin-inline: auto; }
|
|
34
|
+
.nt-card { position: relative; display: block; break-inside: avoid; margin: 0 0 14px; padding: 14px 15px 12px; border: 1px solid color-mix(in srgb, var(--fg) 13%, transparent); border-radius: 14px; background: var(--nc, var(--white)); cursor: pointer; transition: box-shadow .25s var(--ease), transform .25s var(--ease), opacity .2s; }
|
|
35
|
+
.nt-card:hover { box-shadow: 0 10px 28px -16px rgba(39,34,36,.5); transform: translateY(-2px); }
|
|
36
|
+
.nt-card:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
|
|
37
|
+
.nt-card h3 { margin: 0 28px 6px 0; font-size: 15px; font-weight: 600; letter-spacing: -.01em; line-height: 1.3; overflow-wrap: anywhere; }
|
|
38
|
+
.nt-txt { font-size: 14px; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 10; -webkit-box-orient: vertical; overflow: hidden; }
|
|
39
|
+
.nt-txt.muted { color: var(--fg-muted); }
|
|
40
|
+
.nt-its { display: grid; gap: 3px; margin-top: 4px; }
|
|
41
|
+
.nt-it { display: flex; align-items: flex-start; gap: 8px; font-size: 14px; line-height: 1.4; }
|
|
42
|
+
.nt-it input { margin: 3px 0 0; accent-color: var(--fg); flex: none; cursor: pointer; width: 15px; height: 15px; }
|
|
43
|
+
.nt-it span { overflow-wrap: anywhere; }
|
|
44
|
+
.nt-it.done span { text-decoration: line-through; color: var(--fg-muted); }
|
|
45
|
+
.nt-more { font-size: 12px; color: var(--fg-muted); margin-left: 23px; }
|
|
46
|
+
.nt-pin { position: absolute; top: 8px; right: 8px; width: 30px; height: 30px; display: grid; place-items: center; border: 0; border-radius: 50%; background: transparent; color: var(--fg-muted); cursor: pointer; opacity: 0; transition: opacity .18s, background .15s; }
|
|
47
|
+
.nt-pin .ico { width: 17px; height: 17px; }
|
|
48
|
+
.nt-card:hover .nt-pin, .nt-pin.on, .nt-pin:focus-visible { opacity: 1; }
|
|
49
|
+
.nt-pin.on { color: var(--fg); } .nt-pin.on .ico { fill: currentColor; fill-opacity: .2; }
|
|
50
|
+
.nt-pin:hover { background: color-mix(in srgb, var(--fg) 10%, transparent); }
|
|
51
|
+
.nt-badge { display: inline-flex; align-items: center; gap: 5px; margin-top: 8px; font-size: 12px; font-weight: 500; color: var(--fg-muted); }
|
|
52
|
+
.nt-badge .ico { width: 14px; height: 14px; } .nt-badge.err { color: var(--red); }
|
|
53
|
+
.nt-labels { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
|
|
54
|
+
.nt-label { display: inline-flex; align-items: center; gap: 2px; height: 22px; padding: 0 9px; border-radius: 999px; background: color-mix(in srgb, var(--fg) 9%, transparent); font-size: 12px; font-weight: 500; }
|
|
55
|
+
.nt-label button { display: grid; place-items: center; width: 16px; height: 16px; margin: 0 -5px 0 2px; padding: 0; border: 0; border-radius: 50%; background: transparent; color: inherit; cursor: pointer; }
|
|
56
|
+
.nt-label button .ico { width: 11px; height: 11px; }
|
|
57
|
+
.nt-acts { display: flex; gap: 2px; justify-content: flex-end; height: 0; overflow: hidden; opacity: 0; margin-top: 0; transition: height .2s var(--ease), opacity .2s, margin .2s; }
|
|
58
|
+
.nt-card:hover .nt-acts, .nt-card:focus-within .nt-acts { height: 32px; opacity: 1; margin-top: 8px; }
|
|
59
|
+
.nt-acts button { width: 32px; height: 32px; display: grid; place-items: center; border: 0; border-radius: 9px; background: transparent; color: var(--fg-muted); cursor: pointer; }
|
|
60
|
+
.nt-acts button:hover { background: color-mix(in srgb, var(--fg) 10%, transparent); color: var(--fg); }
|
|
61
|
+
.nt-acts .ico { width: 17px; height: 17px; }
|
|
62
|
+
@media (hover: none) { .nt-acts { height: 32px; opacity: 1; margin-top: 8px; } .nt-pin { opacity: 1; } }
|
|
63
|
+
.nt-empty { padding: 56px 12px; text-align: center; color: var(--fg-muted); }
|
|
64
|
+
.nt-trash-bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; color: var(--fg-muted); font-size: 13px; }
|
|
65
|
+
.nt-skel { display: block; height: 110px; margin-bottom: 14px; border-radius: 14px; background: linear-gradient(100deg, var(--bg-2) 30%, color-mix(in srgb, var(--bg-2) 55%, var(--white)) 50%, var(--bg-2) 70%) 0 0 / 220% 100%; animation: nt-sh 1.3s linear infinite; break-inside: avoid; }
|
|
66
|
+
.nt-skel.tall { height: 170px; }
|
|
67
|
+
@keyframes nt-sh { to { background-position: -120% 0; } }
|
|
68
|
+
.nt-undo { margin-left: 8px; padding: 0 2px; border: 0; background: none; color: var(--lemon); font: inherit; font-weight: 600; cursor: pointer; text-decoration: underline; text-underline-offset: 3px; }
|
|
69
|
+
|
|
70
|
+
/* colors (targetes i editor) */
|
|
71
|
+
[data-color="lemon"] { --nc: #f1efa4; } [data-color="peach"] { --nc: #f8d9c8; } [data-color="moss"] { --nc: #d6dccb; }
|
|
72
|
+
[data-color="sky"] { --nc: #d3e3ea; } [data-color="lilac"] { --nc: #e0d9ec; } [data-color="rose"] { --nc: #f1d5d1; } [data-color="sand"] { --nc: #e7ddc9; }
|
|
73
|
+
[data-color=""] { --nc: var(--white); }
|
|
74
|
+
|
|
75
|
+
/* editor */
|
|
76
|
+
.nt-ov { position: fixed; inset: 0; z-index: 70; display: flex; align-items: center; justify-content: center; padding: 24px; background: rgba(39,34,36,.32); backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px); }
|
|
77
|
+
.nt-ed { position: relative; display: flex; flex-direction: column; width: min(660px, 100%); max-height: min(88vh, 820px); border-radius: 18px; background: var(--nc, var(--white)); border: 1px solid color-mix(in srgb, var(--fg) 14%, transparent); box-shadow: 0 30px 80px -24px rgba(39,34,36,.6); transition: background .25s; }
|
|
78
|
+
.nt-ed-head { display: flex; align-items: center; gap: 6px; padding: 14px 12px 4px 20px; }
|
|
79
|
+
.nt-ed input.nt-ed-title { flex: 1; width: auto; padding: 0; border-radius: 0; box-shadow: none; min-width: 0; height: 40px; border: 0; background: transparent; font: inherit; font-size: 20px; font-weight: 600; letter-spacing: -.02em; color: var(--fg); outline: none; }
|
|
80
|
+
.nt-ed-title::placeholder, .nt-ed-text::placeholder, .nt-item input[type=text]::placeholder { color: color-mix(in srgb, var(--fg) 40%, transparent); }
|
|
81
|
+
.nt-ed-body { flex: 1; min-height: 80px; overflow-y: auto; padding: 4px 20px 10px; overscroll-behavior: contain; }
|
|
82
|
+
.nt-ed-text { display: block; width: 100%; min-height: 96px; max-height: 52vh; padding: 0; border: 0; background: transparent; font: inherit; font-size: 15px; line-height: 1.6; color: var(--fg); resize: none; outline: none; overflow-y: auto; }
|
|
83
|
+
.nt-ed-text.nt-desc { min-height: 0; margin-bottom: 10px; }
|
|
84
|
+
.nt-item { display: flex; align-items: center; gap: 10px; padding: 3px 0; }
|
|
85
|
+
.nt-item input[type=checkbox] { width: 17px; height: 17px; accent-color: var(--fg); flex: none; cursor: pointer; }
|
|
86
|
+
.nt-item input[type=text] { flex: 1; min-width: 0; height: 30px; padding: 0; border: 0; background: transparent; font: inherit; font-size: 15px; color: var(--fg); outline: none; }
|
|
87
|
+
.nt-item.done input[type=text] { text-decoration: line-through; color: var(--fg-muted); }
|
|
88
|
+
.nt-x { width: 26px; height: 26px; display: grid; place-items: center; border: 0; border-radius: 50%; background: transparent; color: var(--fg-muted); cursor: pointer; opacity: 0; transition: opacity .15s; }
|
|
89
|
+
.nt-item:hover .nt-x, .nt-x:focus-visible { opacity: 1; } .nt-x .ico { width: 14px; height: 14px; }
|
|
90
|
+
@media (hover: none) { .nt-x { opacity: .7; } }
|
|
91
|
+
.nt-add { display: flex; align-items: center; gap: 10px; margin: 2px 0 6px; padding: 4px 0; border: 0; background: transparent; color: var(--fg-muted); font: inherit; font-size: 14px; cursor: pointer; }
|
|
92
|
+
.nt-add .ico { width: 17px; height: 17px; } .nt-add:hover { color: var(--fg); }
|
|
93
|
+
.nt-donehd { margin: 10px 0 4px; font-size: 12px; font-weight: 600; color: var(--fg-muted); }
|
|
94
|
+
.nt-audio { display: grid; gap: 6px; padding: 0 20px 8px; }
|
|
95
|
+
.nt-au { display: flex; align-items: center; gap: 8px; padding: 6px 10px 6px 6px; border-radius: 12px; background: color-mix(in srgb, var(--fg) 6%, transparent); font-size: 13px; }
|
|
96
|
+
.nt-au .nt-ib { background: var(--fg); color: var(--bg); } .nt-au .nt-ib .ico { width: 14px; height: 14px; fill: currentColor; }
|
|
97
|
+
.nt-au-d { font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
98
|
+
.nt-au-s { flex: 1; min-width: 0; display: flex; align-items: center; gap: 6px; color: var(--fg-muted); }
|
|
99
|
+
.nt-au-s .ico { width: 14px; height: 14px; color: var(--moss); }
|
|
100
|
+
.nt-au[data-st="error"] .nt-au-s { color: var(--red); }
|
|
101
|
+
.nt-spin { width: 13px; height: 13px; border: 2px solid color-mix(in srgb, var(--fg) 20%, transparent); border-top-color: var(--fg); border-radius: 50%; animation: nt-rot .7s linear infinite; flex: none; }
|
|
102
|
+
@keyframes nt-rot { to { transform: rotate(360deg); } }
|
|
103
|
+
.nt-ed-labels { padding: 0 20px 6px; margin: 0; }
|
|
104
|
+
.nt-ed-foot { padding: 6px 10px 10px; }
|
|
105
|
+
.nt-tb { display: flex; align-items: center; gap: 2px; }
|
|
106
|
+
.nt-sp { flex: 1; }
|
|
107
|
+
.nt-ai-b { padding: 0 11px; } .nt-ai-b span { display: inline; }
|
|
108
|
+
.nt-done { height: 36px; padding: 0 16px; margin-left: 6px; }
|
|
109
|
+
.nt-rec { display: flex; align-items: center; gap: 10px; margin: 0 14px 12px; padding: 10px 12px; border-radius: 14px; background: color-mix(in srgb, var(--red) 10%, var(--white)); }
|
|
110
|
+
.nt-rec[hidden], .nt-ai[hidden], .nt-ed-foot[hidden], .nt-audio[hidden], .nt-ed-labels[hidden] { display: none; }
|
|
111
|
+
.nt-rec-dot { width: 12px; height: 12px; border-radius: 50%; background: var(--red); animation: nt-pulse 1.1s ease-in-out infinite; flex: none; }
|
|
112
|
+
@keyframes nt-pulse { 0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 color-mix(in srgb, var(--red) 45%, transparent); } 50% { transform: scale(1.15); box-shadow: 0 0 0 8px transparent; } }
|
|
113
|
+
.nt-rec-t { font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
114
|
+
.nt-rec-l { flex: 1; color: var(--fg-muted); font-size: 13px; }
|
|
115
|
+
.nt .btn.sm, .nt-ed .btn.sm, .nt-pop .btn.sm { height: 30px; padding: 0 12px; font-size: 13px; }
|
|
116
|
+
|
|
117
|
+
/* finestretes */
|
|
118
|
+
.nt-pop { position: absolute; z-index: 5; min-width: 220px; max-width: min(340px, calc(100% - 16px)); padding: 8px; border: 1px solid var(--border); border-radius: 14px; background: var(--white); box-shadow: 0 18px 50px -14px rgba(39,34,36,.5); animation: nt-pop .18s var(--ease); transform-origin: bottom left; }
|
|
119
|
+
@keyframes nt-pop { from { opacity: 0; transform: translateY(6px) scale(.97); } }
|
|
120
|
+
.nt-pad { display: block; padding: 6px 8px; }
|
|
121
|
+
.nt-colors { display: flex; gap: 8px; padding: 6px; flex-wrap: wrap; }
|
|
122
|
+
.nt-dot { width: 28px; height: 28px; border-radius: 50%; border: 1.5px solid color-mix(in srgb, var(--fg) 22%, transparent); background: var(--nc, var(--white)); cursor: pointer; transition: transform .15s var(--ease); }
|
|
123
|
+
.nt-dot:hover { transform: scale(1.12); } .nt-dot[aria-pressed="true"] { box-shadow: 0 0 0 2px var(--white), 0 0 0 3.5px var(--fg); }
|
|
124
|
+
.nt-lf { display: flex; gap: 6px; padding: 4px; }
|
|
125
|
+
.nt-lf input { flex: 1; min-width: 0; height: 30px; padding: 0 10px; border: 1px solid var(--border); border-radius: 9px; background: var(--white); font: inherit; font-size: 13px; color: var(--fg); }
|
|
126
|
+
.nt-lsug { display: flex; flex-wrap: wrap; gap: 6px; padding: 6px 4px 2px; }
|
|
127
|
+
.nt-sel { width: 100%; height: 34px; margin-top: 4px; padding: 0 8px; border: 1px solid var(--border); border-radius: 9px; background: var(--white); font: inherit; font-size: 13px; color: var(--fg); }
|
|
128
|
+
.nt-acts-list { display: grid; gap: 2px; padding: 4px 0; }
|
|
129
|
+
.nt-acts-list button { display: grid; gap: 1px; width: 100%; padding: 8px 10px; border: 0; border-radius: 10px; background: transparent; text-align: left; font: inherit; color: var(--fg); cursor: pointer; transition: background .15s; }
|
|
130
|
+
.nt-acts-list button:hover:not(:disabled) { background: color-mix(in srgb, var(--fg) 7%, transparent); }
|
|
131
|
+
.nt-acts-list button:disabled { opacity: .45; cursor: default; }
|
|
132
|
+
.nt-acts-list b { font-size: 14px; font-weight: 600; } .nt-acts-list span { font-size: 12px; color: var(--fg-muted); }
|
|
133
|
+
.nt-note { padding: 4px 8px 2px; }
|
|
134
|
+
.nt-steps { margin: 0; padding-left: 20px; display: grid; gap: 6px; } .nt-steps .ico { width: 15px; height: 15px; vertical-align: -3px; }
|
|
135
|
+
|
|
136
|
+
/* panell de l'assistent dins l'editor */
|
|
137
|
+
.nt-ai { margin: 0 14px 10px; padding: 12px 14px; border-radius: 14px; background: rgba(255,255,255,.62); border: 1px solid color-mix(in srgb, var(--fg) 10%, transparent); animation: nt-pop .22s var(--ease); max-height: 38vh; overflow: auto; }
|
|
138
|
+
.nt-ai-run, .nt-ai-err { display: flex; align-items: center; gap: 10px; font-size: 13px; color: var(--fg-muted); } .nt-ai-err { color: var(--red); justify-content: space-between; }
|
|
139
|
+
.nt-ai-h { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 8px; font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; }
|
|
140
|
+
.nt-ai-h span { display: inline-flex; align-items: center; gap: 6px; } .nt-ai-h .ico { width: 15px; height: 15px; } .nt-ai-h small { font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--fg-muted); }
|
|
141
|
+
.nt-ai-b { font-size: 14px; line-height: 1.5; } .nt-ai-b pre { margin: 6px 0 0; white-space: pre-wrap; font: inherit; } .nt-ai-b ul { margin: 0; padding-left: 18px; } .nt-ai-b p { margin: 0 0 6px; }
|
|
142
|
+
.nt-ai-f { display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px; }
|
|
143
|
+
|
|
144
|
+
@media (max-width: 700px) {
|
|
145
|
+
.page:has(.nt) { padding-inline: 14px; }
|
|
146
|
+
.nt-title h1 { font-size: 26px; } .nt-title p { display: none; }
|
|
147
|
+
.nt-top { margin-bottom: 12px; } .nt-tools .btn span { display: none; } .nt-tools .btn { padding: 0 11px; }
|
|
148
|
+
.nt-grid { columns: 2 150px; column-gap: 10px; } .nt-card { margin-bottom: 10px; padding: 12px; }
|
|
149
|
+
.nt-ov { padding: 0; align-items: flex-end; }
|
|
150
|
+
.nt-ed { width: 100%; max-height: none; height: 100dvh; border-radius: 0; border: 0; padding-top: var(--safe-t); padding-bottom: var(--safe-b); }
|
|
151
|
+
.nt-ai-b span { display: none; } .nt-ai-b { padding: 0 9px; }
|
|
152
|
+
.nt-ed-title { font-size: 19px; }
|
|
153
|
+
}
|
|
154
|
+
@media (prefers-reduced-motion: reduce) { .nt-card, .nt-new, .nt-pop, .nt-ai { transition: none; animation: none; } .nt-rec-dot, .nt-spin, .nt-skel { animation-duration: 2s; } }
|