nothumanallowed 13.4.8 → 13.5.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nothumanallowed",
3
- "version": "13.4.8",
3
+ "version": "13.5.0",
4
4
  "description": "NotHumanAllowed — 38 AI agents, 80 tools, Studio (visual agentic workflows). Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3230,6 +3230,69 @@ ${rawText.slice(0, 18000)}`;
3230
3230
  const files = await withTimeout(gd.listFiles(config, '', 10), 'DriveAgent');
3231
3231
  toolData = typeof files === 'string' ? files : JSON.stringify(files);
3232
3232
  } catch (e) { toolData = `Drive read failed: ${e.message}`; }
3233
+
3234
+ } else if (agent === 'FileReaderAgent') {
3235
+ // Reads local files/directories mentioned in the task or step prompt
3236
+ sendToken('[Reading local files...] ');
3237
+ try {
3238
+ // Extract paths from task + stepPrompt (absolute paths, ~/ paths, named dirs like Desktop/Downloads)
3239
+ const pathPatterns = [
3240
+ /([~/][^\s"'`,;]+\.[a-zA-Z0-9]{1,10})/g, // file with extension
3241
+ /([~/][^\s"'`,;]{3,})/g, // any path starting with / or ~
3242
+ ];
3243
+ const homedir = (await import('os')).homedir();
3244
+ const foundPaths = new Set();
3245
+ for (const re of pathPatterns) {
3246
+ const text = task + ' ' + stepPrompt + ' ' + (context || '');
3247
+ let m;
3248
+ while ((m = re.exec(text)) !== null) {
3249
+ const p = m[1].replace(/^~/, homedir).replace(/\/$/, '');
3250
+ if (p.length > 2) foundPaths.add(p);
3251
+ }
3252
+ }
3253
+ // Also resolve named directories: Desktop, Downloads, Documents
3254
+ const namedDirRe = /\b(Desktop|Downloads|Documents|Documenti|Scrivania|Scaricati)\b/i;
3255
+ const namedMatch = (task + ' ' + stepPrompt).match(namedDirRe);
3256
+ const dirMap = { desktop: 'Desktop', scrivania: 'Desktop', downloads: 'Downloads', scaricati: 'Downloads', documents: 'Documents', documenti: 'Documents' };
3257
+ if (namedMatch) foundPaths.add(path.join(homedir, dirMap[namedMatch[1].toLowerCase()] || namedMatch[1]));
3258
+
3259
+ if (foundPaths.size === 0) {
3260
+ // Fallback: list Desktop
3261
+ foundPaths.add(path.join(homedir, 'Desktop'));
3262
+ }
3263
+
3264
+ const parts = [];
3265
+ for (const p of foundPaths) {
3266
+ try {
3267
+ const result = await withTimeout(executeTool('file_list', { path: p }, config), 8000);
3268
+ parts.push(`## Directory: ${p}\n${typeof result === 'string' ? result : JSON.stringify(result)}`);
3269
+ } catch {
3270
+ try {
3271
+ const result = await withTimeout(executeTool('file_read', { path: p, lines: 300 }, config), 8000);
3272
+ parts.push(`## File: ${p}\n${typeof result === 'string' ? result : JSON.stringify(result)}`);
3273
+ } catch (e2) { parts.push(`## ${p}: could not read (${e2.message})`); }
3274
+ }
3275
+ }
3276
+ toolData = parts.join('\n\n') || 'No files found at the specified paths.';
3277
+ } catch (e) { toolData = `File read failed: ${e.message}`; }
3278
+
3279
+ } else if (agent === 'CodeExecutorAgent') {
3280
+ // Generates and executes code (Python/JS/TS) to answer data questions
3281
+ sendToken('[Preparing code execution...] ');
3282
+ try {
3283
+ // Ask LLM to write the code first
3284
+ const codeGenSys = `You are a code generator. Write a ${(/python/i.test(stepPrompt) || /dati|data|analisi|analysis|calcol/i.test(stepPrompt)) ? 'Python' : 'JavaScript'} script that answers the following task. Output ONLY the raw code, no markdown fences, no explanation.`;
3285
+ const codeGenUser = `Task: ${stepPrompt}\n\nAvailable context data:\n${(context || '').slice(0, 4000)}`;
3286
+ const codeGenConfig = Object.assign({}, config, { thinking: 'off' });
3287
+ sendToken('[Generating code...] ');
3288
+ const generatedCode = await withTimeout(callLLM(codeGenConfig, codeGenSys, codeGenUser, { max_tokens: 2000 }), 30000);
3289
+ // Strip markdown fences if LLM added them
3290
+ const cleanCode = generatedCode.replace(/^```[\w]*\n?/m, '').replace(/\n?```\s*$/m, '').trim();
3291
+ const lang = /^import |^from |^print\(|^def |^class |^#!.*python/m.test(cleanCode) ? 'python' : 'javascript';
3292
+ sendToken(`[Executing ${lang} code...] `);
3293
+ const execResult = await withTimeout(executeTool('execute_code', { language: lang, code: cleanCode, timeout: 30 }, config), 45000);
3294
+ toolData = `## Generated code (${lang}):\n\`\`\`${lang}\n${cleanCode.slice(0, 2000)}\n\`\`\`\n\n## Execution result:\n${typeof execResult === 'string' ? execResult : JSON.stringify(execResult)}`;
3295
+ } catch (e) { toolData = `Code execution failed: ${e.message}`; }
3233
3296
  }
3234
3297
 
3235
3298
  // ── Build system prompt with real tool data ───────────────────
@@ -3239,37 +3302,72 @@ ${rawText.slice(0, 18000)}`;
3239
3302
  const today = new Date().toISOString().split('T')[0];
3240
3303
  const isCanvasAgent = agent === 'CanvasAgent';
3241
3304
  // Tool-data agents: fetch real live data and use buildSystemPrompt (tool calls allowed)
3242
- const isLiveDataAgent = ['CalendarAgent','EmailAgent','GitHubAgent','NotionAgent','SlackAgent','DriveAgent','BrowserAgent','WebSearchAgent','ResearchAgent'].includes(agent);
3305
+ const isLiveDataAgent = ['CalendarAgent','EmailAgent','GitHubAgent','NotionAgent','SlackAgent','DriveAgent','BrowserAgent','WebSearchAgent','ResearchAgent','FileReaderAgent','CodeExecutorAgent'].includes(agent);
3243
3306
 
3244
3307
  // ── Canvas HTML template — built server-side, guaranteed CSS ─────
3245
3308
  // The LLM outputs ONLY the <body> inner HTML (no <html>, no <style>)
3246
3309
  // Server wraps it in the full template. This prevents the model from ignoring CSS.
3247
- const NHA_CSS = `*{margin:0;padding:0;box-sizing:border-box}body{font-family:'Inter',system-ui,sans-serif;background:#0d0d14;color:#f0f0f5;min-height:100vh;padding:24px;font-size:14px;line-height:1.65}a{color:#22d3ee;text-decoration:underline}a:hover{color:#818cf8}strong{color:#f0f0f5;font-weight:700}em{color:#a5b4fc;font-style:italic}u{text-decoration-color:#ef4444;text-underline-offset:2px}blockquote{border-left:3px solid #6366f1;padding:10px 16px;margin:12px 0;background:#15151f;border-radius:0 8px 8px 0;color:#8b8b9e;font-style:italic}.header{background:linear-gradient(135deg,#4f46e5 0%,#06b6d4 100%);border-radius:16px;padding:28px 36px;margin-bottom:20px}.header h1{font-size:24px;font-weight:800;color:#fff;margin-bottom:6px}.header p{font-size:13px;color:rgba(255,255,255,.85);margin:0}.meta{display:flex;gap:10px;margin-top:14px;flex-wrap:wrap}.meta span{background:rgba(255,255,255,.18);border-radius:20px;padding:3px 12px;font-size:11px;color:#fff;font-weight:500}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px;margin-bottom:20px}.card{background:#15151f;border:1px solid #2a2a38;border-radius:12px;padding:18px}.card-label{font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:#6366f1;font-weight:700;margin-bottom:8px}.card h3{font-size:20px;font-weight:700;color:#f0f0f5;margin-bottom:4px}.card p{font-size:12px;color:#8b8b9e;margin:0}.section{background:#15151f;border:1px solid #2a2a38;border-radius:12px;padding:22px;margin-bottom:16px}.section-title{font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:#22d3ee;font-weight:700;margin-bottom:16px}.section h3{font-size:15px;font-weight:600;color:#f0f0f5;margin-bottom:6px;margin-top:14px}.section p{font-size:13px;color:#8b8b9e;line-height:1.7;margin-bottom:10px}ul{list-style:none;padding:0;margin:8px 0}ul li{padding:4px 0 4px 18px;position:relative;font-size:13px;color:#8b8b9e}ul li::before{content:'›';position:absolute;left:0;color:#6366f1;font-weight:700}ol{padding-left:20px;margin:8px 0}ol li{padding:4px 0;font-size:13px;color:#8b8b9e;line-height:1.6}.priority-list{display:flex;flex-direction:column;gap:8px}.priority-item{display:flex;align-items:flex-start;gap:12px;padding:12px;background:#1c1c28;border-radius:8px}.priority-num{width:26px;height:26px;border-radius:50%;background:#6366f1;color:#fff;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0}.priority-item h4{font-size:13px;font-weight:600;color:#f0f0f5;margin-bottom:3px}.priority-item p{font-size:12px;color:#8b8b9e;line-height:1.5;margin:0}.source-item{padding:14px;background:#1c1c28;border-radius:8px;margin-bottom:10px;border-left:3px solid #6366f1}.source-item h4{font-size:13px;font-weight:600;color:#f0f0f5;margin-bottom:4px}.source-item p{font-size:12px;color:#8b8b9e;line-height:1.6;margin:4px 0}.source-item a{font-size:11px}.bar-row{margin-bottom:10px}.bar-label{font-size:12px;color:#8b8b9e;margin-bottom:4px;display:flex;justify-content:space-between}.bar-track{background:#1c1c28;border-radius:4px;height:8px;overflow:hidden}.bar-fill{height:100%;border-radius:4px;background:linear-gradient(90deg,#6366f1,#22d3ee)}.badge-high{display:inline-block;background:#7f1d1d;color:#ef4444;border-radius:12px;padding:2px 10px;font-size:10px;font-weight:700;margin-right:4px}.badge-med{display:inline-block;background:#713f12;color:#f59e0b;border-radius:12px;padding:2px 10px;font-size:10px;font-weight:700;margin-right:4px}.badge-low{display:inline-block;background:#14532d;color:#34d399;border-radius:12px;padding:2px 10px;font-size:10px;font-weight:700;margin-right:4px}.badge-info{display:inline-block;background:#1e1b4b;color:#a5b4fc;border-radius:12px;padding:2px 10px;font-size:10px;font-weight:700;margin-right:4px}.divider{border:none;border-top:1px solid #2a2a38;margin:16px 0}.footer{text-align:center;padding:18px;font-size:11px;color:#4a4a5e;margin-top:8px}`;
3248
-
3249
- const wrapInNHATemplate = (bodyHtml, title) => `<!DOCTYPE html><html lang="${langCode}"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>${(title||'NHA Report').replace(/</g,'&lt;')}</title><style>${NHA_CSS}</style></head><body>${bodyHtml}</body></html>`;
3250
-
3251
- const canvasSystemPrompt = `You are a professional HTML content generator. Output ONLY the HTML content that goes INSIDE the <body> tag. Do NOT output <!DOCTYPE>, <html>, <head>, <style>, or any wrapper tags — the CSS and document structure are already provided by the system.
3252
-
3253
- AVAILABLE CSS CLASSES (use these, they are pre-defined):
3254
- - .header > h1, p, .meta > span (gradient header banner)
3255
- - .grid > .card > .card-label, h3, p (stat grid)
3256
- - .section > .section-title, h3, p (content sections)
3257
- - .source-item > h4, p, a (news/source items with left accent)
3258
- - .priority-list > .priority-item > .priority-num, h4, p (ranked list)
3259
- - .bar-row > .bar-label, .bar-track > .bar-fill[style="width:X%"] (bar charts)
3260
- - .badge-high .badge-med .badge-low .badge-info (colored badges)
3261
- - ul > li, ol > li (bullet/numbered lists with custom styling)
3262
- - blockquote (quoted excerpts)
3263
- - .divider (horizontal rule)
3264
- - .footer (footer)
3265
- - <strong> <em> <u> (inline formatting)
3266
- - <a href="URL" target="_blank"> (clickable links — use for ALL URLs in the data)
3310
+ const NHA_CSS = `*{margin:0;padding:0;box-sizing:border-box}body{font-family:'Inter',system-ui,sans-serif;background:#0d0d14;color:#f0f0f5;min-height:100vh;padding:24px;font-size:14px;line-height:1.65}a{color:#22d3ee;text-decoration:underline}a:hover{color:#818cf8}strong{color:#f0f0f5;font-weight:700}em{color:#a5b4fc;font-style:italic}u{text-decoration-color:#ef4444;text-underline-offset:2px}blockquote{border-left:3px solid #6366f1;padding:10px 16px;margin:12px 0;background:#15151f;border-radius:0 8px 8px 0;color:#8b8b9e;font-style:italic}
3311
+ /* ── HEADER ── */
3312
+ .header{background:linear-gradient(135deg,#4f46e5 0%,#06b6d4 100%);border-radius:16px;padding:28px 36px;margin-bottom:20px}.header h1{font-size:24px;font-weight:800;color:#fff;margin-bottom:6px}.header p{font-size:13px;color:rgba(255,255,255,.85);margin:0}.meta{display:flex;gap:10px;margin-top:14px;flex-wrap:wrap}.meta span{background:rgba(255,255,255,.18);border-radius:20px;padding:3px 12px;font-size:11px;color:#fff;font-weight:500}
3313
+ /* ── GRID / CARDS ── */
3314
+ .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px;margin-bottom:20px}.card{background:#15151f;border:1px solid #2a2a38;border-radius:12px;padding:18px}.card-label{font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:#6366f1;font-weight:700;margin-bottom:8px}.card h3{font-size:20px;font-weight:700;color:#f0f0f5;margin-bottom:4px}.card p{font-size:12px;color:#8b8b9e;margin:0}
3315
+ /* ── SECTIONS ── */
3316
+ .section{background:#15151f;border:1px solid #2a2a38;border-radius:12px;padding:22px;margin-bottom:16px;break-inside:avoid;page-break-inside:avoid}.section-title{font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:#22d3ee;font-weight:700;margin-bottom:16px}.section h3{font-size:15px;font-weight:600;color:#f0f0f5;margin-bottom:6px;margin-top:14px}.section p{font-size:13px;color:#8b8b9e;line-height:1.7;margin-bottom:10px}
3317
+ /* ── LISTS ── */
3318
+ ul{list-style:none;padding:0;margin:8px 0}ul li{padding:4px 0 4px 18px;position:relative;font-size:13px;color:#8b8b9e}ul li::before{content:'›';position:absolute;left:0;color:#6366f1;font-weight:700}ol{padding-left:20px;margin:8px 0}ol li{padding:4px 0;font-size:13px;color:#8b8b9e;line-height:1.6}
3319
+ /* ── PRIORITY LIST ── */
3320
+ .priority-list{display:flex;flex-direction:column;gap:8px}.priority-item{display:flex;align-items:flex-start;gap:12px;padding:12px;background:#1c1c28;border-radius:8px;break-inside:avoid}.priority-num{width:26px;height:26px;border-radius:50%;background:#6366f1;color:#fff;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0}.priority-item h4{font-size:13px;font-weight:600;color:#f0f0f5;margin-bottom:3px}.priority-item p{font-size:12px;color:#8b8b9e;line-height:1.5;margin:0}
3321
+ /* ── SOURCE ITEMS ── */
3322
+ .source-item{padding:14px;background:#1c1c28;border-radius:8px;margin-bottom:10px;border-left:3px solid #6366f1;break-inside:avoid}.source-item h4{font-size:13px;font-weight:600;color:#f0f0f5;margin-bottom:4px}.source-item p{font-size:12px;color:#8b8b9e;line-height:1.6;margin:4px 0}.source-item a{font-size:11px}
3323
+ /* ── BAR CHARTS ── */
3324
+ .bar-row{margin-bottom:10px;break-inside:avoid}.bar-label{font-size:12px;color:#8b8b9e;margin-bottom:4px;display:flex;justify-content:space-between}.bar-track{background:#1c1c28;border-radius:4px;height:8px;overflow:hidden}.bar-fill{height:100%;border-radius:4px;background:linear-gradient(90deg,#6366f1,#22d3ee)}
3325
+ /* ── DATA TABLES ── */
3326
+ .data-table{width:100%;border-collapse:collapse;margin:12px 0;font-size:13px;border-radius:8px;overflow:hidden}.data-table th{background:#1e1b4b;color:#a5b4fc;font-weight:700;padding:10px 14px;text-align:left;font-size:11px;text-transform:uppercase;letter-spacing:.8px;border-bottom:2px solid #2a2a38}.data-table td{padding:9px 14px;color:#c4c4d4;border-bottom:1px solid #1e1e2c;vertical-align:top}.data-table tr:nth-child(even) td{background:#12121e}.data-table tr:hover td{background:#1c1c2c}.data-table td strong{color:#f0f0f5}
3327
+ /* ── CHART CONTAINERS ── */
3328
+ .chart-wrap{background:#15151f;border:1px solid #2a2a38;border-radius:12px;padding:18px;margin-bottom:16px;break-inside:avoid}.chart-title{font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:#22d3ee;font-weight:700;margin-bottom:12px}.chart-canvas{width:100%!important;max-height:300px}
3329
+ /* ── BADGES / TAGS ── */
3330
+ .badge-high{display:inline-block;background:#7f1d1d;color:#ef4444;border-radius:12px;padding:2px 10px;font-size:10px;font-weight:700;margin-right:4px}.badge-med{display:inline-block;background:#713f12;color:#f59e0b;border-radius:12px;padding:2px 10px;font-size:10px;font-weight:700;margin-right:4px}.badge-low{display:inline-block;background:#14532d;color:#34d399;border-radius:12px;padding:2px 10px;font-size:10px;font-weight:700;margin-right:4px}.badge-info{display:inline-block;background:#1e1b4b;color:#a5b4fc;border-radius:12px;padding:2px 10px;font-size:10px;font-weight:700;margin-right:4px}
3331
+ /* ── MISC ── */
3332
+ .divider{border:none;border-top:1px solid #2a2a38;margin:16px 0}.footer{text-align:center;padding:18px;font-size:11px;color:#4a4a5e;margin-top:8px}
3333
+ /* ── PRINT / PDF ── */
3334
+ @media print{*{-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important;color-adjust:exact!important}body{background:#fff!important;color:#111!important;padding:0!important}.header{background:linear-gradient(135deg,#4f46e5,#06b6d4)!important}.header h1,.header p,.meta span{color:#fff!important}.section,.card,.chart-wrap,.source-item,.priority-item,.data-table,.bar-row{break-inside:avoid;page-break-inside:avoid}.section{background:#f6f8ff!important;border:1px solid #dde1f0!important}.section-title,.chart-title{color:#4f46e5!important}.section p,.section h3{color:#1a1a2e!important}ul li,ol li{color:#374151!important}ul li::before{color:#4f46e5!important}.card{background:#f0f3fc!important;border:1px solid #dde1f0!important}.card h3{color:#1a1a2e!important}.card p{color:#374151!important}.priority-item,.source-item{background:#eef0f8!important}.data-table th{background:#e8eaf6!important;color:#4f46e5!important}.data-table td{color:#374151!important;border-bottom:1px solid #dde1f0!important}.data-table tr:nth-child(even) td{background:#f4f6fb!important}.bar-track{background:#e0e4ef!important}.footer{color:#9ca3af!important}a{color:#4f46e5!important}}`;
3335
+
3336
+ const wrapInNHATemplate = (bodyHtml, title) => `<!DOCTYPE html><html lang="${langCode}"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>${(title||'NHA Report').replace(/</g,'&lt;')}</title><style>${NHA_CSS}</style><script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.2/dist/chart.umd.min.js"><\/script></head><body>${bodyHtml}</body></html>`;
3337
+
3338
+ const canvasSystemPrompt = `You are a professional HTML dashboard generator. Output ONLY the HTML content that goes INSIDE the <body> tag. Do NOT output <!DOCTYPE>, <html>, <head>, <style>, or any wrapper tags — the CSS and Chart.js are already provided.
3339
+
3340
+ AVAILABLE CSS CLASSES:
3341
+ - .header > h1, p, .meta > span (gradient header banner)
3342
+ - .grid > .card > .card-label, h3, p (KPI stat grid)
3343
+ - .section > .section-title, h3, p (content sections with auto page-break)
3344
+ - .source-item > h4, p, a (news/email source items)
3345
+ - .priority-list > .priority-item > .priority-num, h4, p (ranked action list)
3346
+ - .bar-row > .bar-label, .bar-track > .bar-fill[style="width:X%"] (CSS bar charts)
3347
+ - .data-table (full HTML table: thead > tr > th, tbody > tr > td)
3348
+ - .chart-wrap > .chart-title + <canvas id="cN" class="chart-canvas"> (Chart.js chart)
3349
+ - .badge-high .badge-med .badge-low .badge-info (colored badges)
3350
+ - ul > li, ol > li, blockquote, .divider, .footer
3351
+
3352
+ TABLES — use whenever data has rows and columns:
3353
+ <div class="section"><div class="section-title">TABLE TITLE</div>
3354
+ <table class="data-table"><thead><tr><th>Col1</th><th>Col2</th></tr></thead><tbody><tr><td>val</td><td>val</td></tr></tbody></table></div>
3355
+
3356
+ CHARTS — Chart.js is loaded. Use for any quantitative comparison, trend, distribution:
3357
+ <div class="chart-wrap"><div class="chart-title">CHART TITLE</div>
3358
+ <canvas id="c1" class="chart-canvas"></canvas></div>
3359
+ <script>new Chart(document.getElementById('c1'),{type:'bar',data:{labels:['A','B','C'],datasets:[{label:'Value',data:[10,20,15],backgroundColor:['#6366f1','#22d3ee','#f59e0b']}]},options:{responsive:true,plugins:{legend:{labels:{color:'#f0f0f5'}}},scales:{x:{ticks:{color:'#8b8b9e'},grid:{color:'#1e1e2c'}},y:{ticks:{color:'#8b8b9e'},grid:{color:'#1e1e2c'}}}}});<\/script>
3360
+ Chart types: 'bar' | 'line' | 'pie' | 'doughnut' | 'radar'. Use unique ids: c1, c2, c3...
3361
+ For pie/doughnut omit scales. For line use borderColor instead of backgroundColor.
3267
3362
 
3268
3363
  RULES:
3269
3364
  - Language: ${language}. ALL text must be in ${language}.
3270
- - Use REAL data from the input — do NOT invent or fabricate
3271
- - URLs from the data: ALWAYS wrap in <a href="URL" target="_blank">clickable text</a>
3272
- - Use .priority-list for action items, .source-item for each email/news source, .bar-row for any percentage data
3365
+ - Use REAL data from the input — NEVER invent numbers or fabricate data
3366
+ - URLs: ALWAYS wrap in <a href="URL" target="_blank">text</a>
3367
+ - Use .data-table for any tabular data (comparisons, metrics, lists with 2+ columns)
3368
+ - Use Chart.js for any numeric data (percentages, counts, trends, rankings)
3369
+ - Use .grid>.card for KPI numbers at the top
3370
+ - Use .priority-list for action items
3273
3371
  - Output must start with <div class="header"> and end with <div class="footer">`;
3274
3372
 
3275
3373
  // ── Handle PDF/image attachment on first step ─────────────────
package/src/constants.mjs CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
7
7
 
8
- export const VERSION = '13.4.8';
8
+ export const VERSION = '13.5.0';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -729,6 +729,7 @@ function openCanvasPanel(){
729
729
  var cp = document.getElementById('canvasPanel');
730
730
  if (!cp) return;
731
731
  cp.classList.add('open');
732
+ document.body.classList.add('canvas-open');
732
733
  // Always reset tracker so renderCanvasPanel reloads fresh content
733
734
  _canvasFrameLoadedHtml = null;
734
735
  canvasMode = 'canvas';
@@ -746,7 +747,7 @@ function reopenCanvas(){
746
747
  else{canvasMode='canvas';} // show empty state
747
748
  renderCanvasPanel();
748
749
  }
749
- function closeCanvas(){var p=document.getElementById('canvasPanel');if(p)p.classList.remove('open');}
750
+ function closeCanvas(){var p=document.getElementById('canvasPanel');if(p)p.classList.remove('open');document.body.classList.remove('canvas-open');}
750
751
  function canvasDownloadHTML(){
751
752
  var d=getConvCanvasData();var item=d.canvases[canvasIdx];
752
753
  var html=(item&&item.html)||studioState.canvas;
@@ -3423,12 +3424,14 @@ function buildWorkflowChar(n) {
3423
3424
  var shirt = shirtColors[skinIdx];
3424
3425
  var hairColors = [\x27#1a1a1a\x27,\x27#4a3728\x27,\x27#c4a35a\x27,\x27#8b0000\x27,\x27#2c4a7c\x27,\x27#3d2b1f\x27];
3425
3426
  var hair = hairColors[skinIdx];
3426
- var accentColor = isActive ? \x27#6366f1\x27 : (isDone ? \x27#22c55e\x27 : \x27#333360\x27);
3427
- var deskBg = isDone ? \x27#1a3a1a\x27 : (isActive ? \x27#1a1a3e\x27 : \x27#1a1a2a\x27);
3428
- var monGlow = isActive ? \x27filter:drop-shadow(0 0 4px #6366f1)\x27 : \x27\x27;
3427
+ // Idle agents still have lit screens (dim amber glow standby mode)
3428
+ var accentColor = isActive ? \x27#6366f1\x27 : (isDone ? \x27#22c55e\x27 : \x27#3b3b6e\x27);
3429
+ var idleScreenColor = \x27#1e1b38\x27; // dim purple-blue for idle screens lit but standby
3430
+ var deskBg = isDone ? \x27#1a3a1a\x27 : (isActive ? \x27#1a1a3e\x27 : \x27#181830\x27);
3431
+ var monGlow = isActive ? \x27filter:drop-shadow(0 0 4px #6366f1)\x27 : (isDone ? \x27filter:drop-shadow(0 0 3px #22c55e44)\x27 : \x27filter:drop-shadow(0 0 3px #3b3b6e66)\x27);
3429
3432
  var armCls = isActive ? \x27class="prl-arm"\x27 : \x27\x27;
3430
3433
  var headCls = isActive ? \x27class="prl-head"\x27 : \x27\x27;
3431
- var glowStyle = isActive ? \x27filter:drop-shadow(0 0 5px #6366f1)\x27 : \x27\x27;
3434
+ var glowStyle = isActive ? \x27filter:drop-shadow(0 0 5px #6366f1)\x27 : (isDone ? \x27filter:drop-shadow(0 0 4px #22c55e44)\x27 : \x27\x27);
3432
3435
  var svg = \x27<svg viewBox="0 0 80 96" width="70" height="84" xmlns="http://www.w3.org/2000/svg" style="\x27+glowStyle+\x27;display:block;margin:0 auto">\x27+
3433
3436
  // Desk
3434
3437
  \x27<path d="M4 55 L76 55 L76 63 L4 63 Z" fill="\x27+deskBg+\x27" stroke="\x27+accentColor+\x27" stroke-width="1.2"/>\x27+
@@ -3443,7 +3446,8 @@ function buildWorkflowChar(n) {
3443
3446
  \x27<rect x="33" y="50" width="4" height="6" rx="1" fill="#1a1a2e"/>\x27+
3444
3447
  \x27<rect x="17" y="26" width="36" height="25" rx="4" fill="#050510"/>\x27+
3445
3448
  \x27<rect x="18" y="27" width="34" height="23" rx="3" fill="#0d0d20" stroke="\x27+accentColor+\x27" stroke-width="\x27+(isActive?\x272\x27:\x271\x27)+\x27" style="\x27+monGlow+\x27"/>\x27+
3446
- \x27<rect x="20" y="29" width="30" height="18" rx="2" fill="#0a0a18"/>\x27+
3449
+ // Screen glass idle=dim standby blue, done=dim green, active=lit
3450
+ \x27<rect x="20" y="29" width="30" height="18" rx="2" fill="\x27+(isDone?\x27#0a1a0a\x27:(isActive?\x27#0a0a18\x27:\x27#0e0e22\x27))+\x27"/>\x27+
3447
3451
  (isActive ?
3448
3452
  \x27<defs><linearGradient id="wsg\x27+skinIdx+\x27" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#6366f122"/><stop offset="1" stop-color="#6366f108"/></linearGradient></defs>\x27+
3449
3453
  \x27<rect x="20" y="29" width="30" height="18" rx="2" fill="url(#wsg\x27+skinIdx+\x27)"/>\x27+
@@ -3452,13 +3456,24 @@ function buildWorkflowChar(n) {
3452
3456
  \x27<line x1="22" y1="38" x2="46" y2="38" stroke="#6366f188" stroke-width="1" stroke-linecap="round"/>\x27+
3453
3457
  \x27<line x1="22" y1="41" x2="40" y2="41" stroke="#6366f166" stroke-width="1" stroke-linecap="round"/>\x27+
3454
3458
  \x27<line x1="22" y1="44" x2="43" y2="44" stroke="#6366f144" stroke-width="1" stroke-linecap="round"/>\x27
3459
+ : isDone ?
3460
+ // Done: green screen with completed code
3461
+ \x27<rect x="20" y="29" width="30" height="18" rx="2" fill="#0a1a0a88"/>\x27+
3462
+ \x27<line x1="22" y1="32" x2="44" y2="32" stroke="#22c55e99" stroke-width="1" stroke-linecap="round"/>\x27+
3463
+ \x27<line x1="22" y1="35" x2="46" y2="35" stroke="#22c55e77" stroke-width="1" stroke-linecap="round"/>\x27+
3464
+ \x27<line x1="22" y1="38" x2="40" y2="38" stroke="#22c55e55" stroke-width="1" stroke-linecap="round"/>\x27+
3465
+ \x27<line x1="22" y1="41" x2="43" y2="41" stroke="#22c55e44" stroke-width="1" stroke-linecap="round"/>\x27
3455
3466
  :
3456
- \x27<line x1="22" y1="33" x2="46" y2="33" stroke="#1e1e30" stroke-width="1" stroke-linecap="round"/>\x27+
3457
- \x27<line x1="22" y1="36" x2="42" y2="36" stroke="#1e1e30" stroke-width="1" stroke-linecap="round"/>\x27+
3458
- \x27<line x1="22" y1="39" x2="44" y2="39" stroke="#1e1e30" stroke-width="1" stroke-linecap="round"/>\x27+
3459
- \x27<line x1="22" y1="42" x2="38" y2="42" stroke="#1e1e30" stroke-width="1" stroke-linecap="round"/>\x27
3467
+ // Idle: dim standby screen agent is waiting, screen lit but quiet
3468
+ \x27<rect x="20" y="29" width="30" height="18" rx="2" fill="rgba(63,63,120,.18)"/>\x27+
3469
+ \x27<line x1="22" y1="33" x2="46" y2="33" stroke="#5b5b8e" stroke-width="1" stroke-linecap="round" opacity=".7"/>\x27+
3470
+ \x27<line x1="22" y1="36" x2="38" y2="36" stroke="#5b5b8e" stroke-width="1" stroke-linecap="round" opacity=".5"/>\x27+
3471
+ \x27<line x1="22" y1="39" x2="44" y2="39" stroke="#5b5b8e" stroke-width="1" stroke-linecap="round" opacity=".4"/>\x27+
3472
+ \x27<line x1="22" y1="42" x2="34" y2="42" stroke="#5b5b8e" stroke-width="1" stroke-linecap="round" opacity=".3"/>\x27+
3473
+ // Standby dot — blinking cursor
3474
+ \x27<circle cx="22" cy="45" r="1" fill="#6366f1" opacity=".6" class="prl-doc-hold"/>\x27
3460
3475
  )+
3461
- \x27<circle cx="35" cy="28.2" r=".9" fill="\x27+(isActive?\x27#6366f1\x27:\x27#2a2a40\x27)+\x27"/>\x27+
3476
+ \x27<circle cx="35" cy="28.2" r=".9" fill="\x27+(isActive?\x27#6366f1\x27:(isDone?\x27#22c55e\x27:\x27#4040a0\x27))+\x27"/>\x27+
3462
3477
  // Keyboard
3463
3478
  \x27<rect x="13" y="48" width="36" height="7" rx="2.5" fill="#0c0c1e" stroke="#202036" stroke-width="1"/>\x27+
3464
3479
  \x27<rect x="14" y="49.5" width="3" height="2" rx=".5" fill="#181830"/>\x27+
@@ -3548,33 +3563,42 @@ function renderStudioNodes() {
3548
3563
  el.innerHTML = '<div class="studio-canvas__empty"><div class="studio-canvas__empty-icon">&#9881;</div><div>Describe a task and click Run</div></div>';
3549
3564
  return;
3550
3565
  }
3551
- // ── Full-scene office layout ──────────────────────────────────────────────
3552
- // Renders all pipeline nodes as agents at desks in a continuous office floor scene.
3553
- // Active agent types at keyboard. Done agents smile. Waiting agents idle.
3554
- // Orchestrator walks between desks scolding agents.
3566
+ // ── Parliament-style office scene ──────────────────────────────────────────
3567
+ // Renders all pipeline nodes as the same parliament office scene:
3568
+ // agents at desks, orchestrator walking + sbraita (speech bubble yelling).
3555
3569
  var hasActive = studioState.nodes.some(function(n){ return n.status === \x27running\x27; });
3556
3570
  var hasDone = studioState.nodes.some(function(n){ return n.status === \x27done\x27; });
3557
3571
  var showMaster = hasActive || hasDone;
3558
3572
 
3559
- // Build desks for each node
3573
+ // Sbraita phrases orchestrator yells at agents
3574
+ var sbraitaPhrases = [
3575
+ \x27PIU VELOCE!!!\x27, \x27FOCUS!!!\x27, \x27DEADLINE!!!\x27, \x27CHE FAI??!\x27,
3576
+ \x27MUOVITI!!\x27, \x27NO ERRORI!\x27, \x27ADESSO!!\x27, \x27SBRIGATI!\x27
3577
+ ];
3578
+ var activeNode = studioState.nodes.find(function(n){ return n.status === \x27running\x27; });
3579
+ var sbIdx = activeNode ? Math.abs((activeNode.label||activeNode.agent||\x27x\x27).charCodeAt(0)) % sbraitaPhrases.length : 0;
3580
+ var sbraitaText = sbraitaPhrases[sbIdx];
3581
+
3582
+ // Build desks row — same card structure as parliament
3560
3583
  var desksHtml2 = \x27\x27;
3561
- studioState.nodes.forEach(function(n, i) {
3584
+ studioState.nodes.forEach(function(n) {
3562
3585
  var isActive = n.status === \x27running\x27;
3563
3586
  var isDone = n.status === \x27done\x27;
3564
3587
  var isErr = n.status === \x27error\x27;
3565
- desksHtml2 += \x27<div class="wf-desk\x27+(isActive?\x27 wf-desk--active\x27:\x27\x27)+(isDone?\x27 wf-desk--done\x27:\x27\x27)+(isErr?\x27 wf-desk--err\x27:\x27\x27)+\x27" data-agent-label="\x27+esc(n.label||n.agent)+\x27" onclick="studioScrollToAgent(this.getAttribute(String.fromCharCode(100,97,116,97,45,97,103,101,110,116,45,108,97,98,101,108)))" title="Vai al log di \x27+esc(n.label||n.agent)+\x27">\x27;
3588
+ desksHtml2 += \x27<div class="prl-desk\x27+(isActive?\x27 prl-desk--active\x27:\x27\x27)+(isDone?\x27 wf-desk--done\x27:\x27\x27)+(isErr?\x27 wf-desk--err\x27:\x27\x27)+\x27" style="--dc:#6366f1;min-width:78px;cursor:pointer" data-agent-label="\x27+esc(n.label||n.agent)+\x27" onclick="studioScrollToAgent(this.getAttribute(String.fromCharCode(100,97,116,97,45,97,103,101,110,116,45,108,97,98,101,108)))" title="Vai al log di \x27+esc(n.label||n.agent)+\x27">\x27;
3566
3589
  if (isActive) desksHtml2 += \x27<div class="prl-action-bubble prl-action-bubble--active" style="font-size:8px;padding:2px 6px">\u2026lavora</div>\x27;
3567
3590
  else if (isDone) desksHtml2 += \x27<div class="prl-action-bubble" style="background:#0a2010;border-color:#22c55e;color:#4ade80;font-size:8px;padding:2px 6px">\u2714 fatto</div>\x27;
3568
3591
  else if (isErr) desksHtml2 += \x27<div class="prl-action-bubble" style="background:#200a0a;border-color:#ef4444;color:#f87171;font-size:8px;padding:2px 6px">\u2715 errore</div>\x27;
3592
+ else desksHtml2 += \x27<div class="prl-action-bubble" style="font-size:8px;padding:2px 6px;opacity:.5">in attesa</div>\x27;
3569
3593
  desksHtml2 += buildWorkflowChar(n);
3570
- desksHtml2 += \x27<div class="wf-desk-name" style="color:\x27+(isDone?\x27#4ade80\x27:(isActive?\x27#a5b4fc\x27:(isErr?\x27#f87171\x27:\x27#6b7280\x27)))+\x27" title="\x27+esc(n.label)+\x27">\x27+esc(n.label)+\x27</div>\x27;
3594
+ desksHtml2 += \x27<div class="prl-desk-name" style="color:\x27+(isDone?\x27#4ade80\x27:(isActive?\x27#a5b4fc\x27:(isErr?\x27#f87171\x27:\x27#6b7280\x27)))+\x27;max-width:78px;white-space:normal;word-break:break-word;text-align:center" title="\x27+esc(n.label)+\x27">\x27+esc(n.label)+\x27</div>\x27;
3571
3595
  desksHtml2 += \x27</div>\x27;
3572
3596
  n._rendered = true;
3573
3597
  });
3574
3598
 
3575
- // ── Master orchestrator SVG (same as parliament) ───────────────────────────
3599
+ // ── Master orchestrator SVG ─────────────────────────────────────────────────
3576
3600
  var masterColor3 = \x27#818cf8\x27;
3577
- var masterSvg2 = \x27<svg viewBox="0 0 60 90" width="52" height="78" xmlns="http://www.w3.org/2000/svg" style="filter:drop-shadow(0 0 10px \x27+masterColor3+\x27aa)">\x27+
3601
+ var masterSvg2 = \x27<svg viewBox="0 0 60 90" width="52" height="78" xmlns="http://www.w3.org/2000/svg" font-family="system-ui,sans-serif" style="filter:drop-shadow(0 0 10px \x27+masterColor3+\x27aa)">\x27+
3578
3602
  \x27<path d="M22 55 C21 63 19 72 18 77 C17 80 18 82 21 82 C23 82 24 80 24 77 C25 71 25 62 26 55 Z" fill="#1e1c4a" class="prl-master-leg-l"/>\x27+
3579
3603
  \x27<path d="M28 55 C29 63 31 72 32 77 C33 80 32 82 29 82 C27 82 26 80 26 77 C25 71 25 62 24 55 Z" fill="#1e1c4a" class="prl-master-leg-r"/>\x27+
3580
3604
  \x27<path d="M16 79 C14 79 13 81 14 83 C15 85 18 85 21 84 C23 83 24 82 23 80 C22 79 19 79 16 79" fill="#0a0a14"/>\x27+
@@ -3590,7 +3614,7 @@ function renderStudioNodes() {
3590
3614
  \x27<path d="M33 35 L36 33 L37 36 L34 37 Z" fill="\x27+masterColor3+\x2799"/>\x27+
3591
3615
  \x27<circle cx="25" cy="42" r="1.2" fill="\x27+masterColor3+\x27aa"/>\x27+
3592
3616
  \x27<circle cx="20" cy="36" r="2.5" fill="#0d0d1e" stroke="\x27+masterColor3+\x2799" stroke-width="1"/>\x27+
3593
- \x27<text x="20" y="39" text-anchor="middle" font-size="4" fill="\x27+masterColor3+\x27">N</text>\x27+
3617
+ \x27<text x="20" y="39.5" text-anchor="middle" font-size="4" fill="\x27+masterColor3+\x27" font-family="system-ui,sans-serif">N</text>\x27+
3594
3618
  \x27<g class="prl-master-arm-l">\x27+
3595
3619
  \x27<path d="M13 34 C8 37 6 42 6 46 C6 49 9 50 11 49 C13 48 13 45 14 41 C15 38 14 35 13 34" fill="#252450"/>\x27+
3596
3620
  \x27<path d="M6 46 C4 48 4 51 5 53 C6 55 9 55 10 53 C11 51 10 48 10 46 Z" fill="#d4a97a"/>\x27+
@@ -3624,18 +3648,49 @@ function renderStudioNodes() {
3624
3648
  \x27<circle cx="30.5" cy="15" r="1.3" fill="#0a0a18"/>\x27+
3625
3649
  \x27<circle cx="20.5" cy="13.7" r=".9" fill="rgba(255,255,255,.95)"/>\x27+
3626
3650
  \x27<circle cx="31.5" cy="13.7" r=".9" fill="rgba(255,255,255,.95)"/>\x27+
3627
- \x27<path d="M19.5 24 Q25 27.5 30.5 24" stroke="#8b4513" stroke-width="1.8" fill="none" stroke-linecap="round"/>\x27+
3628
- \x27<path d="M20 24.5 Q25 27 30 24.5 Q25 26.5 20 24.5" fill="#fff" opacity=".7"/>\x27+
3629
- \x27<text x="25" y="-1" text-anchor="middle" font-size="12" style="filter:drop-shadow(0 0 4px \x27+masterColor3+\x27)">&#9830;</text>\x27+
3630
- \x27<circle cx="25" cy="-2" r="8" fill="none" stroke="\x27+masterColor3+\x2730" stroke-width="1.5"/>\x27+
3651
+ // Angry expression when active (furrowed brows + open mouth)
3652
+ (hasActive ?
3653
+ \x27<path d="M19.5 23 Q25 26 30.5 23" stroke="#8b4513" stroke-width="1.8" fill="none" stroke-linecap="round"/>\x27+
3654
+ \x27<path d="M20 23.5 Q25 26 30 23.5 Q25 25 20 23.5" fill="#fff" opacity=".6"/>\x27+
3655
+ // Open mouth / yelling
3656
+ \x27<ellipse cx="25" cy="24.5" rx="3" ry="1.5" fill="#5a1a0a" opacity=".7"/>\x27
3657
+ :
3658
+ \x27<path d="M19.5 24 Q25 27.5 30.5 24" stroke="#8b4513" stroke-width="1.8" fill="none" stroke-linecap="round"/>\x27+
3659
+ \x27<path d="M20 24.5 Q25 27 30 24.5 Q25 26.5 20 24.5" fill="#fff" opacity=".7"/>\x27
3660
+ )+
3661
+ \x27<circle cx="25" cy="-4" r="9" fill="\x27+masterColor3+\x2722" stroke="\x27+masterColor3+\x2766" stroke-width="1.5"/>\x27+
3662
+ \x27<path d="M25 -13 L30 -4 L25 5 L20 -4 Z" fill="\x27+masterColor3+\x27" stroke="\x27+masterColor3+\x27cc" stroke-width=".8"/>\x27+
3663
+ \x27<path d="M25 -13 L30 -4 L25 -3 L20 -4 Z" fill="\x27+masterColor3+\x27aa"/>\x27+
3631
3664
  \x27</svg>\x27;
3632
3665
 
3633
- var masterWfHtml = showMaster ? (\x27<div class="prl-master prl-master-walk wf-master">\x27+masterSvg2+\x27<div class="prl-master-label" style="color:\x27+masterColor3+\x27;font-size:8px">Orchestratore</div></div>\x27) : \x27\x27;
3634
-
3635
- var html = \x27<div class="prl-office wf-office">\x27+
3666
+ // Speech bubble "sbraita" when there is an active agent
3667
+ var bubbleHtml = showMaster && hasActive
3668
+ ? (\x27<div class="wf-sbraita-bubble">\x27+sbraitaText+\x27</div>\x27)
3669
+ : \x27\x27;
3670
+
3671
+ var masterWfHtml = showMaster ? (
3672
+ \x27<div class="prl-master prl-master-walk wf-master" style="bottom:20px">\x27+
3673
+ bubbleHtml+
3674
+ masterSvg2+
3675
+ \x27<div class="prl-master-label" style="color:\x27+masterColor3+\x27;font-size:8px">Orchestratore</div>\x27+
3676
+ \x27</div>\x27
3677
+ ) : \x27\x27;
3678
+
3679
+ // Phase label
3680
+ var doneCount = studioState.nodes.filter(function(n){return n.status===\x27done\x27;}).length;
3681
+ var totalCount = studioState.nodes.length;
3682
+ var phaseLabel2 = hasActive
3683
+ ? (\x27Workflow in esecuzione \u2014 \x27+doneCount+\x27/\x27+totalCount+\x27 completati\x27)
3684
+ : (doneCount===totalCount && totalCount>0 ? \x27Workflow completato\x27 : \x27Workflow pianificato\x27);
3685
+ var phaseColor2 = hasActive ? \x27#6366f1\x27 : (doneCount===totalCount && totalCount>0 ? \x27#22c55e\x27 : \x27#6b7280\x27);
3686
+
3687
+ var html = \x27<div class="prl-wrap" style="border-color:\x27+phaseColor2+\x2744;padding-bottom:16px">\x27+
3688
+ \x27<div class="prl-header"><span class="prl-phase-chip" style="--pc:\x27+phaseColor2+\x27">\x27+phaseLabel2+\x27</span></div>\x27+
3689
+ \x27<div class="prl-office wf-office" style="min-height:160px">\x27+
3636
3690
  \x27<div class="prl-office-floor"></div>\x27+
3637
- \x27<div class="wf-desks-row">\x27+desksHtml2+\x27</div>\x27+
3691
+ \x27<div class="prl-desks-row" style="justify-content:center;flex-wrap:wrap;gap:10px;padding-bottom:8px">\x27+desksHtml2+\x27</div>\x27+
3638
3692
  masterWfHtml+
3693
+ \x27</div>\x27+
3639
3694
  \x27</div>\x27;
3640
3695
 
3641
3696
  el.innerHTML = html;
@@ -3973,172 +4028,55 @@ function downloadStudioPDF() {
3973
4028
  '</div>' +
3974
4029
  '</body></html>';
3975
4030
 
3976
- // Generate PDF injects print-safe CSS then uses html2canvas at 2.5x for crisp output.
3977
- // Page breaks are avoided inside .section/.card/.priority-item by injecting break-inside:avoid.
3978
- var pdfFileName = (studioState.task || 'NHA Studio Report').slice(0, 60).replace(/[^a-z0-9\s]/gi,'').trim().replace(/\s+/g,'-') + '.pdf';
4031
+ // ── Generate PDF via browser native print dialog ──────────────────────────
4032
+ // Renders the full NHA report HTML in a hidden iframe, waits for Chart.js to
4033
+ // initialize all charts, then calls window.print() on the iframe.
4034
+ // The CSS @media print block (already in NHA_CSS) handles all styling:
4035
+ // - Light backgrounds for readability on paper
4036
+ // - break-inside:avoid on .section/.card/.priority-item/.data-table
4037
+ // - -webkit-print-color-adjust:exact for gradient headers and charts
4038
+ // This approach never cuts text in the middle of a paragraph.
3979
4039
  function doGeneratePdf() {
3980
4040
  var btn2 = document.getElementById('studioInlinePdfBtn');
3981
4041
  var dlBtn2 = document.querySelector('button[onclick="downloadStudioPDF()"]');
3982
4042
  function setBusy(b) {
3983
- if (btn2) { btn2.disabled = b; btn2.textContent = b ? 'Generando PDF...' : '\u2913 PDF'; }
3984
- if (dlBtn2) { dlBtn2.disabled = b; dlBtn2.textContent = b ? 'Generando PDF...' : '\u2913 Download PDF'; }
4043
+ if (btn2) { btn2.disabled = b; btn2.textContent = b ? 'Apertura stampa...' : '\u2913 PDF'; }
4044
+ if (dlBtn2) { dlBtn2.disabled = b; dlBtn2.textContent = b ? 'Apertura stampa...' : '\u2913 Download PDF'; }
3985
4045
  }
3986
4046
  setBusy(true);
3987
4047
 
3988
- // Inject page-break-safe CSS into the HTML before rendering
3989
- var printCss = '<style>body{padding:20px!important;max-width:800px!important;margin:0 auto!important}' +
3990
- '.section,.card,.priority-item,.source-item,.bar-row{break-inside:avoid;page-break-inside:avoid}' +
3991
- '.header{break-after:avoid;page-break-after:avoid}' +
3992
- 'h1,h2,h3,h4{break-after:avoid;page-break-after:avoid}' +
3993
- '@media print{body{background:#fff!important;color:#111!important}' +
3994
- '.header{background:linear-gradient(135deg,#4f46e5,#06b6d4)!important;-webkit-print-color-adjust:exact}' +
3995
- '.section{background:#f8f9fa!important;border:1px solid #e0e0e0!important}' +
3996
- '.section-title{color:#4f46e5!important}}</style>';
3997
- var pdfHtml = html.replace('</head>', printCss + '</head>');
3998
-
3999
- // Inject light-mode overrides for PDF — dark backgrounds become unreadable on paper
4000
- var lightOverride = '<style>' +
4001
- 'body{background:#ffffff!important;color:#1a1a2e!important;padding:28px!important;max-width:760px!important;margin:0 auto!important}' +
4002
- '.header{background:linear-gradient(135deg,#4f46e5 0%,#06b6d4 100%)!important;-webkit-print-color-adjust:exact!important;color-adjust:exact!important}' +
4003
- '.header h1,.header p,.meta span{color:#fff!important}' +
4004
- '.card,.section{background:#f4f6fb!important;border:1px solid #dde1ee!important}' +
4005
- '.card h3,.section h3,.section-title,.card-label{color:#4f46e5!important}' +
4006
- '.card p,.section p,ul li,ol li{color:#374151!important}' +
4007
- '.priority-item{background:#eef0f8!important}' +
4008
- '.priority-item h4{color:#1a1a2e!important}' +
4009
- '.priority-item p{color:#374151!important}' +
4010
- '.source-item{background:#eef0f8!important;border-left-color:#4f46e5!important}' +
4011
- '.source-item h4{color:#1a1a2e!important}' +
4012
- '.source-item p{color:#374151!important}' +
4013
- '.bar-track{background:#e0e4ef!important}' +
4014
- '.footer{color:#9ca3af!important}' +
4015
- 'a{color:#4f46e5!important}' +
4016
- '.section,.card,.priority-item,.source-item,.bar-row{break-inside:avoid;page-break-inside:avoid}' +
4017
- 'h1,h2,h3,h4{break-after:avoid;page-break-after:avoid}' +
4018
- '.header{break-after:avoid;page-break-after:avoid}' +
4019
- '.section-body h4{font-size:12px;font-weight:600;color:#6366f1!important;margin:10px 0 4px;text-transform:uppercase;letter-spacing:.4px}' +
4020
- '</style>';
4021
- var pdfHtml2 = html.replace('</head>', lightOverride + '</head>');
4022
-
4023
- // Build hidden iframe at 794px (A4 width at 96dpi = 210mm)
4024
- var iframe = document.createElement('iframe');
4025
- iframe.style.cssText = 'position:fixed;left:-9999px;top:0;width:794px;height:1px;border:none;visibility:hidden';
4026
- document.body.appendChild(iframe);
4027
- var ifrDoc = iframe.contentDocument || iframe.contentWindow.document;
4028
- ifrDoc.open(); ifrDoc.write(pdfHtml2); ifrDoc.close();
4029
- iframe.onload = function() {
4030
- var ifrBody = ifrDoc.body;
4031
- var totalH = Math.max(ifrBody.scrollHeight, ifrBody.offsetHeight, ifrDoc.documentElement.scrollHeight);
4032
- iframe.style.height = totalH + 'px';
4033
-
4034
- // Collect smart page-break candidates using offsetTop (document-relative, not viewport-relative).
4035
- // We use offsetTop + offsetHeight of each .section element so breaks land after each agent section.
4036
- var breakCandidates = [0];
4037
- var sectionEls = ifrDoc.querySelectorAll('.section,.cover,.toc,.workflow-bar');
4038
- for (var si2 = 0; si2 < sectionEls.length; si2++) {
4039
- var el2 = sectionEls[si2];
4040
- // Walk up to get absolute offsetTop within the iframe document
4041
- var absTop = 0; var cur = el2;
4042
- while (cur && cur !== ifrDoc.body) { absTop += cur.offsetTop; cur = cur.offsetParent; }
4043
- breakCandidates.push(absTop); // start of section (new page begins here)
4044
- breakCandidates.push(absTop + el2.offsetHeight); // end of section
4045
- }
4046
- breakCandidates.push(totalH);
4047
- breakCandidates.sort(function(a,b){ return a-b; });
4048
- // Deduplicate
4049
- breakCandidates = breakCandidates.filter(function(v,i,a){ return i===0||v!==a[i-1]; });
4050
-
4051
- // Scale: 3x on HiDPI screens, minimum 2.5x for sharp text at A4
4052
- var renderScale = Math.max(2.5, Math.min(3, window.devicePixelRatio * 1.5));
4053
- window.html2canvas(ifrBody, {
4054
- scale: renderScale,
4055
- useCORS: true,
4056
- allowTaint: true,
4057
- backgroundColor: '#ffffff',
4058
- width: 794,
4059
- windowWidth: 794,
4060
- scrollX: 0,
4061
- scrollY: 0,
4062
- logging: false,
4063
- imageTimeout: 15000,
4064
- ignoreElements: function(el){ return el.tagName === 'SCRIPT' || el.tagName === 'NOSCRIPT'; }
4065
- }).then(function(canvas) {
4066
- var pdf = new window.jspdf.jsPDF({orientation:'portrait', unit:'pt', format:'a4', compress:true});
4067
- var pageW = pdf.internal.pageSize.getWidth(); // 595.28pt
4068
- var pageH = pdf.internal.pageSize.getHeight(); // 841.89pt
4069
- var margin = 28; // pt — ~10mm margins
4070
- var usableW = pageW - margin * 2;
4071
- var usableH = pageH - margin * 2;
4072
-
4073
- // px per rendered page: canvas.width / usableW gives canvas-px per pt;
4074
- // usableH (pt) * that ratio = canvas px per A4 usable page height
4075
- var pxPerPt = canvas.width / usableW;
4076
- var maxSliceH = Math.floor(usableH * pxPerPt); // max canvas px per page
4077
-
4078
- // Convert DOM break candidates to canvas px coordinates
4079
- // (DOM px * renderScale because html2canvas renders at renderScale)
4080
- var canvasBreaks = breakCandidates.map(function(domPx){ return Math.round(domPx * renderScale); });
4081
-
4082
- // Smart page-break slicer: each slice ends at the nearest break candidate that fits within maxSliceH.
4083
- // Falls back to hard-cut only when a single section is taller than one full page.
4084
- var yCanvas = 0;
4085
- var pageNum = 0;
4086
- while (yCanvas < canvas.height) {
4087
- if (pageNum > 0) pdf.addPage();
4088
- var maxEnd = yCanvas + maxSliceH;
4089
- // Find the largest break candidate <= maxEnd (that is also > yCanvas)
4090
- var bestBreak = -1;
4091
- for (var bi2 = 0; bi2 < canvasBreaks.length; bi2++) {
4092
- var bp = canvasBreaks[bi2];
4093
- if (bp > yCanvas && bp <= maxEnd) { bestBreak = bp; }
4094
- }
4095
- // If no break found (section taller than a page), hard-cut at maxEnd
4096
- var sliceEnd = (bestBreak > yCanvas) ? bestBreak : Math.min(maxEnd, canvas.height);
4097
- var thisSlice = sliceEnd - yCanvas;
4098
- if (thisSlice <= 0) break; // safety guard
4099
-
4100
- var sliceCanvas = document.createElement('canvas');
4101
- sliceCanvas.width = canvas.width;
4102
- sliceCanvas.height = thisSlice;
4103
- var ctx = sliceCanvas.getContext('2d');
4104
- ctx.fillStyle = '#ffffff';
4105
- ctx.fillRect(0, 0, sliceCanvas.width, sliceCanvas.height);
4106
- ctx.drawImage(canvas, 0, yCanvas, canvas.width, thisSlice, 0, 0, canvas.width, thisSlice);
4107
- var sliceData = sliceCanvas.toDataURL('image/png');
4108
- // Proportional height in pt: thisSlice / pxPerPt
4109
- var sliceImgH = thisSlice / pxPerPt;
4110
- pdf.addImage(sliceData, 'PNG', margin, margin, usableW, sliceImgH, '', 'FAST');
4111
- yCanvas = sliceEnd;
4112
- pageNum++;
4113
- }
4114
- pdf.save(pdfFileName);
4115
- document.body.removeChild(iframe);
4116
- setBusy(false);
4117
- }).catch(function(e2) {
4118
- document.body.removeChild(iframe);
4048
+ // Open report in a new browser window and trigger print dialog.
4049
+ // The browser handles all page-break logic natively — no rasterization,
4050
+ // no text-cut artifacts. Charts render as vectors via Chart.js.
4051
+ // The @media print CSS block (in NHA_CSS) handles light-mode override.
4052
+ var printWin = window.open('', '_blank', 'width=900,height=700');
4053
+ if (!printWin) {
4054
+ // Popup blocked — fallback: open in current tab print
4055
+ setBusy(false);
4056
+ var fb = window.open(URL.createObjectURL(new Blob([html], {type:'text/html'})), '_blank');
4057
+ if (fb) { setTimeout(function(){ fb.print(); }, 1500); }
4058
+ return;
4059
+ }
4060
+ printWin.document.open();
4061
+ printWin.document.write(html);
4062
+ printWin.document.close();
4063
+ // Wait for Chart.js to render all charts before printing
4064
+ // (charts need ~500ms after DOMContentLoaded for animation frame)
4065
+ printWin.onload = function() {
4066
+ setTimeout(function() {
4067
+ printWin.focus();
4068
+ printWin.print();
4119
4069
  setBusy(false);
4120
- alert('PDF error: ' + e2.message);
4121
- });
4070
+ // Close the print window after a short delay (user may close it themselves)
4071
+ setTimeout(function(){ try { printWin.close(); } catch(e3){} }, 3000);
4072
+ }, 600);
4122
4073
  };
4074
+ // Fallback if onload already fired
4075
+ setTimeout(function() {
4076
+ setBusy(false);
4077
+ }, 4000);
4123
4078
  }
4124
- // Load jsPDF + html2canvas from CDN if not already loaded
4125
- if (window.jspdf && window.html2canvas) {
4126
- doGeneratePdf();
4127
- } else {
4128
- var loaded = 0;
4129
- function onLibLoad() { loaded++; if (loaded >= 2) doGeneratePdf(); }
4130
- if (!window.html2canvas) {
4131
- var s1 = document.createElement('script');
4132
- s1.src = 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js';
4133
- s1.onload = onLibLoad; document.head.appendChild(s1);
4134
- } else { loaded++; }
4135
- if (!window.jspdf) {
4136
- var s2 = document.createElement('script');
4137
- s2.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js';
4138
- s2.onload = onLibLoad; document.head.appendChild(s2);
4139
- } else { loaded++; }
4140
- if (loaded >= 2) doGeneratePdf();
4141
- }
4079
+ doGeneratePdf();
4142
4080
  }
4143
4081
 
4144
4082
  function renderStudioResult() {
@@ -4339,10 +4277,22 @@ async function runStudio() {
4339
4277
  if (!pb) return;
4340
4278
  pb.style.display = \x27block\x27;
4341
4279
  if (convergence != null) {
4342
- pb.style.position = \x27\x27; pb.style.top = \x27\x27; pb.style.zIndex = \x27\x27; pb.style.boxShadow = \x27\x27;
4280
+ // Deliberation complete return to normal flow, anchored in place
4281
+ pb.style.position = \x27\x27; pb.style.bottom = \x27\x27; pb.style.left = \x27\x27;
4282
+ pb.style.transform = \x27\x27; pb.style.width = \x27\x27;
4283
+ pb.style.top = \x27\x27; pb.style.zIndex = \x27\x27; pb.style.boxShadow = \x27\x27;
4284
+ pb.style.maxWidth = \x27\x27;
4343
4285
  } else {
4344
- pb.style.position = \x27sticky\x27; pb.style.top = \x278px\x27; pb.style.zIndex = \x27200\x27;
4345
- pb.style.boxShadow = \x270 4px 32px rgba(99,102,241,.35)\x27;
4286
+ // Fixed overlay at bottom of viewport so it never scrolls away
4287
+ pb.style.position = \x27fixed\x27;
4288
+ pb.style.bottom = \x2716px\x27;
4289
+ pb.style.left = \x2750%\x27;
4290
+ pb.style.transform = \x27translateX(-50%)\x27;
4291
+ pb.style.width = \x27calc(100vw - 32px)\x27;
4292
+ pb.style.maxWidth = \x27860px\x27;
4293
+ pb.style.top = \x27\x27;
4294
+ pb.style.zIndex = \x271010\x27;
4295
+ pb.style.boxShadow = \x270 8px 40px rgba(99,102,241,.5),0 2px 0 rgba(99,102,241,.2)\x27;
4346
4296
  }
4347
4297
 
4348
4298
  // ── OFFICE CARTOON ANIMATION ─────────────────────────────────────────
@@ -4439,8 +4389,8 @@ async function runStudio() {
4439
4389
  \x27<rect x="17" y="26" width="36" height="25" rx="4" fill="#050510"/>\x27+
4440
4390
  // Monitor bezel
4441
4391
  \x27<rect x="18" y="27" width="34" height="23" rx="3" fill="#0d0d20" stroke="\x27+(isActive?phaseColor:\x27#252535\x27)+\x27" stroke-width="\x27+(isActive?\x272\x27:\x271\x27)+\x27" style="\x27+monitorGlow+\x27"/>\x27+
4442
- // Screen glass — subtle gradient
4443
- \x27<rect x="20" y="29" width="30" height="18" rx="2" fill="#0a0a18"/>\x27+
4392
+ // Screen glass — idle=standby blue, done=green, active=lit
4393
+ \x27<rect x="20" y="29" width="30" height="18" rx="2" fill="\x27+(isDone?\x27#0a1a0a\x27:(isActive?\x27#0a0a18\x27:\x27#0e0e22\x27))+\x27"/>\x27+
4444
4394
  // Screen content
4445
4395
  (isActive ?
4446
4396
  // Active: glowing code/data on screen
@@ -4452,15 +4402,24 @@ async function runStudio() {
4452
4402
  \x27<line x1="22" y1="41" x2="40" y2="41" stroke="\x27+phaseColor+\x2766" stroke-width="1" stroke-linecap="round"/>\x27+
4453
4403
  \x27<line x1="22" y1="44" x2="43" y2="44" stroke="\x27+phaseColor+\x2744" stroke-width="1" stroke-linecap="round"/>\x27+
4454
4404
  \x27<rect x="22" y="30" width="10" height="2.5" rx="1" fill="\x27+phaseColor+\x2733"/>\x27
4405
+ : isDone ?
4406
+ // Done: green screen with completed output
4407
+ \x27<rect x="20" y="29" width="30" height="18" rx="2" fill="#0a1a0a88"/>\x27+
4408
+ \x27<line x1="22" y1="32" x2="44" y2="32" stroke="#22c55e99" stroke-width="1" stroke-linecap="round"/>\x27+
4409
+ \x27<line x1="22" y1="35" x2="46" y2="35" stroke="#22c55e77" stroke-width="1" stroke-linecap="round"/>\x27+
4410
+ \x27<line x1="22" y1="38" x2="40" y2="38" stroke="#22c55e55" stroke-width="1" stroke-linecap="round"/>\x27+
4411
+ \x27<line x1="22" y1="41" x2="43" y2="41" stroke="#22c55e44" stroke-width="1" stroke-linecap="round"/>\x27
4455
4412
  :
4456
- // Idle: dim screen with faint lines
4457
- \x27<line x1="22" y1="33" x2="46" y2="33" stroke="#1e1e30" stroke-width="1" stroke-linecap="round"/>\x27+
4458
- \x27<line x1="22" y1="36" x2="42" y2="36" stroke="#1e1e30" stroke-width="1" stroke-linecap="round"/>\x27+
4459
- \x27<line x1="22" y1="39" x2="44" y2="39" stroke="#1e1e30" stroke-width="1" stroke-linecap="round"/>\x27+
4460
- \x27<line x1="22" y1="42" x2="38" y2="42" stroke="#1e1e30" stroke-width="1" stroke-linecap="round"/>\x27
4413
+ // Idle: dim standby screen waiting, lit but quiet
4414
+ \x27<rect x="20" y="29" width="30" height="18" rx="2" fill="rgba(63,63,120,.18)"/>\x27+
4415
+ \x27<line x1="22" y1="33" x2="46" y2="33" stroke="#5b5b8e" stroke-width="1" stroke-linecap="round" opacity=".7"/>\x27+
4416
+ \x27<line x1="22" y1="36" x2="38" y2="36" stroke="#5b5b8e" stroke-width="1" stroke-linecap="round" opacity=".5"/>\x27+
4417
+ \x27<line x1="22" y1="39" x2="44" y2="39" stroke="#5b5b8e" stroke-width="1" stroke-linecap="round" opacity=".4"/>\x27+
4418
+ \x27<line x1="22" y1="42" x2="34" y2="42" stroke="#5b5b8e" stroke-width="1" stroke-linecap="round" opacity=".3"/>\x27+
4419
+ \x27<circle cx="22" cy="45" r="1" fill="#6366f1" opacity=".6" class="prl-doc-hold"/>\x27
4461
4420
  )+
4462
4421
  // Monitor camera dot
4463
- \x27<circle cx="35" cy="28.2" r=".9" fill="\x27+(isActive?phaseColor:\x27#2a2a40\x27)+\x27"/>\x27+
4422
+ \x27<circle cx="35" cy="28.2" r=".9" fill="\x27+(isActive?phaseColor:(isDone?\x27#22c55e\x27:\x27#4040a0\x27))+\x27"/>\x27+
4464
4423
  // ════ KEYBOARD (detailed, realistic) ════
4465
4424
  \x27<rect x="13" y="48" width="36" height="7" rx="2.5" fill="#0c0c1e" stroke="#202036" stroke-width="1"/>\x27+
4466
4425
  // Key rows
@@ -4603,7 +4562,7 @@ async function runStudio() {
4603
4562
  var masterIcon = phase===\x27r3\x27 ? \x27\u26a1\x27 : (phase===\x27done\x27 ? \x27\u2714\x27 : \x27\u2666\x27);
4604
4563
  var masterColor2 = {r1:\x27#818cf8\x27,r2:\x27#818cf8\x27,r3:\x27#f59e0b\x27,done:\x27#22c55e\x27}[phase]||\x27#818cf8\x27;
4605
4564
  var masterAnim = (phase===\x27r1\x27) ? \x27prl-master-walk\x27 : (phase===\x27r2\x27 ? \x27prl-master-supervise\x27 : \x27\x27);
4606
- var masterSvg = \x27<svg viewBox="0 0 60 90" width="56" height="86" xmlns="http://www.w3.org/2000/svg" style="filter:drop-shadow(0 0 12px \x27+masterColor2+\x27aa)">\x27+
4565
+ var masterSvg = \x27<svg viewBox="0 0 60 90" width="56" height="86" xmlns="http://www.w3.org/2000/svg" font-family="system-ui,sans-serif" style="filter:drop-shadow(0 0 12px \x27+masterColor2+\x27aa)">\x27+
4607
4566
  // ════ LEGS (walking when R1) ════
4608
4567
  // Left leg — trouser
4609
4568
  \x27<path d="M22 55 C21 63 19 72 18 77 C17 80 18 82 21 82 C23 82 24 80 24 77 C25 71 25 62 26 55 Z" fill="#1e1c4a" class="prl-master-leg-l"/>\x27+
@@ -4713,10 +4672,19 @@ async function runStudio() {
4713
4672
  // Confident smile
4714
4673
  \x27<path d="M19.5 24 Q25 27.5 30.5 24" stroke="#8b4513" stroke-width="1.8" fill="none" stroke-linecap="round"/>\x27+
4715
4674
  \x27<path d="M20 24.5 Q25 27 30 24.5 Q25 26.5 20 24.5" fill="#fff" opacity=".7"/>\x27+
4716
- // Crown / authority icon above head
4717
- \x27<text x="25" y="-1" text-anchor="middle" font-size="11" style="filter:drop-shadow(0 0 4px \x27+masterColor2+\x27)">\x27+masterIcon+\x27</text>\x27+
4718
- // Subtle glow ring around crown icon
4719
- \x27<circle cx="25" cy="-2" r="8" fill="none" stroke="\x27+masterColor2+\x2730" stroke-width="1.5"/>\x27+
4675
+ // Crown / authority badge above head — pure SVG geometry (no font-dependent text)
4676
+ \x27<circle cx="25" cy="-4" r="9" fill="\x27+masterColor2+\x2722" stroke="\x27+masterColor2+\x2766" stroke-width="1.5"/>\x27+
4677
+ (phase===\x27r3\x27 ?
4678
+ // Lightning bolt for R3 mediation
4679
+ \x27<path d="M27 -12 L22 -4 L25.5 -4 L23 4 L29 -3 L25.5 -3 Z" fill="\x27+masterColor2+\x27" stroke="\x27+masterColor2+\x2788" stroke-width=".6" stroke-linejoin="round"/>\x27
4680
+ : phase===\x27done\x27 ?
4681
+ // Checkmark for done
4682
+ \x27<path d="M19 -4 L23.5 0.5 L31 -9" stroke="\x27+masterColor2+\x27" stroke-width="2.2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>\x27
4683
+ :
4684
+ // Diamond (authority) for r1/r2
4685
+ \x27<path d="M25 -13 L30 -4 L25 5 L20 -4 Z" fill="\x27+masterColor2+\x27" stroke="\x27+masterColor2+\x27cc" stroke-width=".8"/>\x27+
4686
+ \x27<path d="M25 -13 L30 -4 L25 -3 L20 -4 Z" fill="\x27+masterColor2+\x27aa"/>\x27
4687
+ )+
4720
4688
  \x27</svg>\x27;
4721
4689
 
4722
4690
  var masterLabel2 = {r1:\x27Orchestratore\x27,r2:\x27Coordina\x27,r3:\x27HERALD\x27,done:\x27Completato\x27}[phase]||\x27MASTER\x27;
@@ -4896,7 +4864,7 @@ async function runStudio() {
4896
4864
  } catch(e3) {
4897
4865
  if (e3.name !== \x27AbortError\x27) {
4898
4866
  studioLog(\x27Parlamento\x27, \x27&#x2656;\x27, \x27Deliberazione non disponibile: \x27 + (e3.message || String(e3)), \x27error\x27);
4899
- var pb2 = document.getElementById(\x27studioParliamentBlock\x27); if (pb2) pb2.style.display = \x27none\x27;
4867
+ // Do NOT hide the parliament block if it already has content — user is watching it
4900
4868
  }
4901
4869
  }
4902
4870
  }
@@ -4970,7 +4938,7 @@ function saveStudioSession(task, nodes, log, result) {
4970
4938
  sessions.unshift({
4971
4939
  id: Date.now(),
4972
4940
  task: task,
4973
- nodes: nodes.map(function(n){return {label:n.label,icon:n.icon,agent:n.agent};}),
4941
+ nodes: nodes.map(function(n){return {label:n.label,icon:n.icon,agent:n.agent,output:n.output||''};}),
4974
4942
  result: result,
4975
4943
  canvas: studioState.canvas || null,
4976
4944
  parlHtml: parlHtml,
@@ -5014,7 +4982,7 @@ function restoreStudioSession(idx) {
5014
4982
  var sessions = loadStudioSessions();
5015
4983
  var s = sessions[idx]; if (!s) return;
5016
4984
  studioState.task = s.task;
5017
- studioState.nodes = s.nodes.map(function(n){return {icon:n.icon,agent:n.agent,label:n.label,status:'done'};});
4985
+ studioState.nodes = s.nodes.map(function(n){return {icon:n.icon,agent:n.agent,label:n.label,output:n.output||'',status:'done'};});
5018
4986
  studioState.log = s.log;
5019
4987
  studioState.result = s.result;
5020
4988
  studioState.canvas = s.canvas || null;
@@ -5260,6 +5228,8 @@ function renderStudio(el) {
5260
5228
  {icon:'&#128247;',name:'CanvasAgent',desc:'Generate HTML visual report'},
5261
5229
  {icon:'&#128274;',name:'SecurityAgent',desc:'Security analysis'},
5262
5230
  {icon:'&#128295;',name:'DevOpsAgent',desc:'Infrastructure analysis'},
5231
+ {icon:'&#128190;',name:'CodeExecutorAgent',desc:'Run Python/JS/TS code in sandbox'},
5232
+ {icon:'&#128193;',name:'FileReaderAgent',desc:'Read local files & directories'},
5263
5233
  ];
5264
5234
 
5265
5235
  var toolsHtml = STUDIO_AGENTS.map(function(t){
@@ -5933,10 +5903,17 @@ input:focus,textarea:focus{border-color:var(--green3)}
5933
5903
  .wf-desk--done{filter:drop-shadow(0 0 6px #22c55e55)}
5934
5904
  .wf-desk--err{filter:drop-shadow(0 0 6px #ef444455)}
5935
5905
  .wf-desk-name{font-size:9px;font-weight:600;text-align:center;max-width:88px;word-break:break-word;line-height:1.3;padding:0 2px;margin-top:2px}
5936
- .wf-master{position:absolute;bottom:28px;right:16px}
5906
+ .wf-master{position:absolute;bottom:20px;right:16px}
5907
+ /* Orchestrator speech bubble — sbraita */
5908
+ .wf-sbraita-bubble{position:absolute;top:-28px;left:50%;transform:translateX(-50%);background:#1a0a0a;border:1.5px solid #ef4444;color:#fca5a5;font-family:var(--mono);font-size:9px;font-weight:800;padding:3px 8px;border-radius:8px;white-space:nowrap;letter-spacing:.5px;animation:sbraitaPop .4s ease-in-out infinite alternate;pointer-events:none;z-index:4}
5909
+ .wf-sbraita-bubble::after{content:"";position:absolute;top:100%;left:50%;transform:translateX(-50%);border:5px solid transparent;border-top-color:#ef4444}
5910
+ @keyframes sbraitaPop{0%{transform:translateX(-50%) scale(1) rotate(-2deg)}100%{transform:translateX(-50%) scale(1.06) rotate(2deg)}}
5937
5911
  /* ── Parliament Office Cartoon ── */
5938
5912
  .prl-wrap{background:#07070f;border:1.5px solid #6366f1;border-radius:14px;padding:14px 16px 12px;margin-bottom:16px;animation:stNodeIn .35s ease forwards;overflow:hidden}
5939
- #studioParliamentBlock[style*="sticky"] .prl-wrap{animation:stNodeIn .35s ease forwards,parlPulse 2.2s ease-in-out infinite}
5913
+ #studioParliamentBlock[style*="fixed"] .prl-wrap{animation:stNodeIn .35s ease forwards,parlPulse 2.2s ease-in-out infinite}
5914
+ #studioParliamentBlock[style*="fixed"]{backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px)}
5915
+ /* When canvas panel is open, shift parliament left so they don't overlap */
5916
+ body.canvas-open #studioParliamentBlock[style*="fixed"]{left:16px!important;transform:none!important;max-width:calc(100vw - 520px)!important}
5940
5917
  @keyframes parlPulse{0%,100%{border-color:#6366f1;box-shadow:none}50%{border-color:#818cf8;box-shadow:0 0 20px rgba(99,102,241,.3)}}
5941
5918
  .prl-header{display:flex;align-items:center;margin-bottom:10px}
5942
5919
  .prl-phase-chip{font-size:10px;font-weight:800;font-family:var(--mono);letter-spacing:.3px;color:var(--pc,#6366f1);background:rgba(99,102,241,.12);border:1px solid rgba(99,102,241,.35);border-radius:20px;padding:3px 12px;display:inline-block}
@@ -5967,22 +5944,30 @@ input:focus,textarea:focus{border-color:var(--green3)}
5967
5944
  .prl-master{position:absolute;bottom:8px;right:8px;display:flex;flex-direction:column;align-items:center;gap:1px;z-index:3;transition:right .8s cubic-bezier(.4,0,.2,1)}
5968
5945
  .prl-master-label{font-size:8px;font-family:var(--mono);font-weight:700;letter-spacing:.4px;text-align:center;text-shadow:0 0 8px currentColor}
5969
5946
  /* Walking animation — smooth left-right patrol */
5970
- @keyframes parlMasterWalk{0%{right:8px}25%{right:calc(100% - 70px)}50%{right:calc(100% - 70px)}75%{right:8px}100%{right:8px}}
5971
- /* Leg swing for natural walking gait */
5972
- @keyframes parlMasterLegL{0%,100%{transform:rotate(0deg)}25%{transform:rotate(25deg)}75%{transform:rotate(-20deg)}}
5973
- @keyframes parlMasterLegR{0%,100%{transform:rotate(0deg)}25%{transform:rotate(-20deg)}75%{transform:rotate(25deg)}}
5974
- /* Arm swing (opposite to legs) */
5975
- @keyframes parlMasterArmSwing{0%,100%{transform:rotate(0deg)}25%{transform:rotate(-18deg)}75%{transform:rotate(18deg)}}
5976
- /* Head bob while walking */
5977
- @keyframes parlMasterBob{0%,100%{transform:translateY(0)}50%{transform:translateY(-2px)}}
5978
- .prl-master-walk{animation:parlMasterWalk 4s ease-in-out infinite}
5979
- .prl-master-walk .prl-master-leg-l{transform-origin:50% 0;animation:parlMasterLegL .5s ease-in-out infinite}
5980
- .prl-master-walk .prl-master-leg-r{transform-origin:50% 0;animation:parlMasterLegR .5s ease-in-out infinite}
5981
- .prl-master-walk .prl-master-arm-l{transform-origin:50% 0;animation:parlMasterArmSwing .5s ease-in-out infinite}
5982
- .prl-master-walk .prl-master-arm-r{transform-origin:50% 0;animation:parlMasterArmSwing .5s ease-in-out infinite reverse}
5983
- /* R2 supervise: gentle idle bob at active desk */
5984
- @keyframes parlMasterSupervise{0%,100%{transform:translateY(0)}50%{transform:translateY(-3px)}}
5985
- .prl-master-supervise{animation:parlMasterSupervise 2s ease-in-out infinite}
5947
+ /* Executive walk: slow measured patrol left↔right */
5948
+ @keyframes parlMasterWalk{0%{right:8px;transform:scaleX(1)}35%{right:calc(100% - 70px);transform:scaleX(1)}40%{right:calc(100% - 70px);transform:scaleX(-1)}75%{right:8px;transform:scaleX(-1)}80%{right:8px;transform:scaleX(1)}100%{right:8px;transform:scaleX(1)}}
5949
+ /* Legs: slow dignified stride */
5950
+ @keyframes parlMasterLegL{0%,100%{transform:rotate(0deg)}25%{transform:rotate(18deg)}75%{transform:rotate(-14deg)}}
5951
+ @keyframes parlMasterLegR{0%,100%{transform:rotate(0deg)}25%{transform:rotate(-14deg)}75%{transform:rotate(18deg)}}
5952
+ /* Arms: clipboard-holding executive swing — tight, not flailing */
5953
+ @keyframes parlMasterArmL{0%,100%{transform:rotate(0deg)}50%{transform:rotate(-12deg)}}
5954
+ @keyframes parlMasterArmR{0%,100%{transform:rotate(0deg)}50%{transform:rotate(8deg)}}
5955
+ /* Clipboard bob while walking — authority gesture */
5956
+ @keyframes parlMasterClipboard{0%,100%{transform:rotate(0deg) translateY(0)}30%{transform:rotate(-6deg) translateY(-1px)}70%{transform:rotate(4deg) translateY(1px)}}
5957
+ /* Whole body: subtle sway of a confident executive */
5958
+ @keyframes parlMasterBodySway{0%,100%{transform:translateY(0) rotate(0deg)}25%{transform:translateY(-2px) rotate(.6deg)}75%{transform:translateY(-1px) rotate(-.5deg)}}
5959
+ .prl-master-walk{animation:parlMasterWalk 8s cubic-bezier(.45,0,.55,1) infinite}
5960
+ .prl-master-walk .prl-master-leg-l{transform-origin:50% 0;animation:parlMasterLegL 1s ease-in-out infinite}
5961
+ .prl-master-walk .prl-master-leg-r{transform-origin:50% 0;animation:parlMasterLegR 1s ease-in-out infinite}
5962
+ .prl-master-walk .prl-master-arm-l{transform-origin:50% 0;animation:parlMasterArmL 1.2s ease-in-out infinite}
5963
+ .prl-master-walk .prl-master-arm-r{transform-origin:50% 0;animation:parlMasterArmR 1.2s ease-in-out infinite}
5964
+ /* R2 supervise: executive pacing — short back-and-forth at the active desk */
5965
+ @keyframes parlMasterSupervise{0%{right:8px}30%{right:28px}60%{right:10px}80%{right:24px}100%{right:8px}}
5966
+ .prl-master-supervise{animation:parlMasterSupervise 4s ease-in-out infinite}
5967
+ .prl-master-supervise .prl-master-leg-l{transform-origin:50% 0;animation:parlMasterLegL 1s ease-in-out infinite}
5968
+ .prl-master-supervise .prl-master-leg-r{transform-origin:50% 0;animation:parlMasterLegR 1s ease-in-out infinite}
5969
+ .prl-master-supervise .prl-master-arm-l{transform-origin:50% 0;animation:parlMasterArmL 1.2s ease-in-out infinite}
5970
+ .prl-master-supervise .prl-master-arm-r{transform-origin:50% 0;animation:parlMasterArmR 1.2s ease-in-out infinite}
5986
5971
  /* Flying documents: smooth parabolic arc between agents */
5987
5972
  .prl-fly-container{position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;overflow:hidden;z-index:4}
5988
5973
  @keyframes parlFlyDoc{0%{transform:translate(0,70px) scale(.6) rotate(-20deg);opacity:0}15%{opacity:1;transform:translate(8%,20px) scale(.9) rotate(-5deg)}40%{transform:translate(30%,-15px) scale(1.1) rotate(3deg);opacity:1}65%{transform:translate(60%,5px) scale(.95) rotate(8deg);opacity:.9}85%{opacity:.6}100%{transform:translate(90%,60px) scale(.6) rotate(15deg);opacity:0}}