nothumanallowed 13.2.28 → 13.2.30
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 +1 -1
- package/src/commands/ui.mjs +174 -188
- package/src/constants.mjs +1 -1
- package/src/services/web-ui.mjs +47 -40
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "13.2.
|
|
3
|
+
"version": "13.2.30",
|
|
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": {
|
package/src/commands/ui.mjs
CHANGED
|
@@ -2544,54 +2544,85 @@ export async function cmdUI(args) {
|
|
|
2544
2544
|
if (!task) { sendJSON(res, 400, { error: 'task required' }); logRequest(method, pathname, 400, Date.now() - start); return; }
|
|
2545
2545
|
|
|
2546
2546
|
const plannerLang = (() => { const LANG_MAP2 = {en:'English',it:'Italian',es:'Spanish',fr:'French',de:'German',pt:'Portuguese',zh:'Chinese',ja:'Japanese',ar:'Arabic',hi:'Hindi',ru:'Russian',nl:'Dutch',pl:'Polish',tr:'Turkish',ko:'Korean'}; const lc = (config?.language||'it').slice(0,2); return LANG_MAP2[lc]||'Italian'; })();
|
|
2547
|
-
const planPrompt = `You are a workflow planner. Task: "${task}"
|
|
2548
|
-
Language: ${plannerLang}. All prompts must be in ${plannerLang}.
|
|
2549
2547
|
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2548
|
+
// ── Fast keyword-based planning (no LLM call needed for common patterns) ──────
|
|
2549
|
+
// Detect task intent from keywords — avoids sending the full task through SENTINEL
|
|
2550
|
+
const taskLow = task.toLowerCase();
|
|
2551
|
+
const hasEmail = /email|mail|inbox|posta/i.test(taskLow);
|
|
2552
|
+
const hasCalendar = /calendar|agenda|calendari|eventi|schedule/i.test(taskLow);
|
|
2553
|
+
const hasSearch = /cerca|search|notizie|news|ultime|latest|web|internet/i.test(taskLow);
|
|
2554
|
+
const hasCanvas = /html|dashboard|visua|report|grafico|chart/i.test(taskLow);
|
|
2555
|
+
const hasGitHub = /github|git|issue|pr|pull request/i.test(taskLow);
|
|
2556
|
+
const hasSlack = /slack|channel|messag/i.test(taskLow);
|
|
2557
|
+
const hasNotion = /notion|note|page/i.test(taskLow);
|
|
2558
|
+
const hasBriefing = /briefing|analisi|analizza|summary|sommario|riassunto|riepiloga/i.test(taskLow);
|
|
2559
|
+
const hasFinance = /finance|mercato|market|stock|trading|finanz/i.test(taskLow);
|
|
2560
|
+
const hasSecurity = /security|sicurezza|vulnerabilit|audit|pentest/i.test(taskLow);
|
|
2561
|
+
|
|
2562
|
+
// Extract a clean short search query from the task (to avoid SENTINEL flagging long task strings)
|
|
2563
|
+
const extractSearchQuery = (t) => {
|
|
2564
|
+
// Try to find an explicit search topic after keywords like "cerca", "search", "notizie su"
|
|
2565
|
+
const m = t.match(/(?:cerca|search|find|ricerca|notizie su|news about|latest on|aggiornamenti su|ultime su)\s+(.{5,80}?)(?:\s+(?:e |and |per |for |poi |then )|[,\n]|$)/i);
|
|
2566
|
+
if (m) return m[1].trim();
|
|
2567
|
+
// Strip leading instruction prefix (everything before the first colon)
|
|
2568
|
+
const stripped = t.replace(/^[^:]+:\s*/,'').split(/[,\n]/)[0].slice(0,100).trim();
|
|
2569
|
+
return stripped || t.slice(0,80).trim();
|
|
2570
|
+
};
|
|
2571
|
+
const searchQuery = extractSearchQuery(task);
|
|
2572
|
+
|
|
2573
|
+
// Build plan directly from keywords — reliable, fast, no SENTINEL risk
|
|
2574
|
+
const buildKeywordPlan = () => {
|
|
2575
|
+
const steps = [];
|
|
2576
|
+
if (hasEmail) steps.push({icon:'\u{1F4E7}',agent:'EmailAgent',label:plannerLang==='Italian'?'Controlla email':'Check emails',prompt:'Read the latest unread emails and identify urgent items, deadlines, and required actions'});
|
|
2577
|
+
if (hasCalendar) steps.push({icon:'\u{1F4C5}',agent:'CalendarAgent',label:plannerLang==='Italian'?'Rivedi calendario':'Review calendar',prompt:'Check today\'s events and identify any scheduling conflicts or important meetings'});
|
|
2578
|
+
if (hasGitHub) steps.push({icon:'\u{1F4BB}',agent:'GitHubAgent',label:'GitHub',prompt:'Read open issues and pull requests, identify what needs attention'});
|
|
2579
|
+
if (hasSlack) steps.push({icon:'\u{1F4AC}',agent:'SlackAgent',label:'Slack',prompt:'Check recent Slack messages and identify important conversations'});
|
|
2580
|
+
if (hasNotion) steps.push({icon:'\u{1F4DD}',agent:'NotionAgent',label:'Notion',prompt:'Search Notion for relevant pages and notes'});
|
|
2581
|
+
if (hasSearch || (!hasEmail && !hasCalendar && !hasGitHub && !hasSlack)) {
|
|
2582
|
+
steps.push({icon:'\u{1F50D}',agent:'WebSearchAgent',label:plannerLang==='Italian'?'Ricerca web':'Web search',prompt:searchQuery});
|
|
2583
|
+
}
|
|
2584
|
+
if (hasSecurity) {
|
|
2585
|
+
steps.push({icon:'\u{1F6E1}',agent:'cassandra',label:plannerLang==='Italian'?'Analisi rischi':'Risk analysis',prompt:'Analyze the data and identify security risks and recommendations'});
|
|
2586
|
+
} else if (hasFinance) {
|
|
2587
|
+
steps.push({icon:'\u{1F4B0}',agent:'mercury',label:plannerLang==='Italian'?'Analisi finanziaria':'Financial analysis',prompt:'Analyze the financial data and market information'});
|
|
2588
|
+
} else if (hasBriefing || steps.length > 0) {
|
|
2589
|
+
steps.push({icon:'\u{1F4F0}',agent:'HERALD',label:plannerLang==='Italian'?'Analisi e briefing':'Analysis & briefing',prompt:'Based on ALL the data collected by the previous steps, write a complete executive briefing with priorities, findings, and strategic recommendations. Do NOT invent data — only use what was provided.'});
|
|
2590
|
+
}
|
|
2591
|
+
if (hasCanvas) steps.push({icon:'\u{1F4CA}',agent:'CanvasAgent',label:plannerLang==='Italian'?'Dashboard HTML':'HTML Dashboard',prompt:'Create a professional HTML dashboard report with the briefing data'});
|
|
2592
|
+
return steps;
|
|
2593
|
+
};
|
|
2560
2594
|
|
|
2561
|
-
|
|
2562
|
-
|
|
2595
|
+
// Use keyword plan directly if it covers the task well enough
|
|
2596
|
+
// Only fall back to LLM planning for genuinely ambiguous tasks
|
|
2597
|
+
const keywordSteps = buildKeywordPlan();
|
|
2598
|
+
const taskIsComplex = !hasEmail && !hasCalendar && !hasSearch && !hasGitHub && !hasSlack && !hasBriefing && keywordSteps.length <= 1;
|
|
2563
2599
|
|
|
2564
2600
|
try {
|
|
2565
|
-
// Force thinking OFF for planner — we need deterministic JSON, not reasoning chains
|
|
2566
|
-
const planConfig = Object.assign({}, config, { thinking: 'off' });
|
|
2567
|
-
const planRaw = await callLLM(planConfig, 'You are a JSON workflow planner. Output ONLY valid JSON. No thinking, no explanation, no markdown.', planPrompt, { max_tokens: 1500 });
|
|
2568
|
-
process.stderr.write('[STUDIO PLAN RAW] ' + planRaw.slice(0, 400) + '\n');
|
|
2569
2601
|
let steps;
|
|
2570
|
-
|
|
2571
|
-
//
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
const
|
|
2580
|
-
const
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
if (hasCanvas) steps.push({icon:'📊',agent:'CanvasAgent',label:'Dashboard HTML',prompt:task});
|
|
2602
|
+
if (!taskIsComplex) {
|
|
2603
|
+
// Use keyword plan directly — no LLM, no SENTINEL risk
|
|
2604
|
+
process.stderr.write('[STUDIO PLAN KEYWORD] steps=' + keywordSteps.length + '\n');
|
|
2605
|
+
steps = keywordSteps;
|
|
2606
|
+
} else {
|
|
2607
|
+
// Task is ambiguous — use LLM planner with sanitized short description
|
|
2608
|
+
const shortTask = task.slice(0, 200).replace(/[`'"]/g, ' ');
|
|
2609
|
+
const plannerLangStr = plannerLang;
|
|
2610
|
+
const planPrompt = `Workflow planner. Goal: ${shortTask}\nLanguage: ${plannerLangStr}.\nOutput ONLY JSON:\n{"steps":[{"icon":"EMOJI","agent":"AGENT_NAME","label":"LABEL","prompt":"INSTRUCTION"}]}\nAgents: WebSearchAgent, EmailAgent, CalendarAgent, HERALD, ORACLE, ATHENA, CASSANDRA, MERCURY, QUILL, CanvasAgent (last, only if visual needed). 2-5 steps.`;
|
|
2611
|
+
const planConfig = Object.assign({}, config, { thinking: 'off' });
|
|
2612
|
+
const planRaw = await callLLM(planConfig, 'Output ONLY valid JSON. No explanation.', planPrompt, { max_tokens: 800 });
|
|
2613
|
+
process.stderr.write('[STUDIO PLAN LLM RAW] ' + planRaw.slice(0, 400) + '\n');
|
|
2614
|
+
try {
|
|
2615
|
+
let clean = planRaw;
|
|
2616
|
+
let prev = '';
|
|
2617
|
+
while (prev !== clean) { prev = clean; clean = clean.replace(/<think>[\s\S]*?<\/think>/g, ''); }
|
|
2618
|
+
clean = clean.trim().replace(/^```[\w]*\r?\n?/,'').replace(/\r?\n?```$/,'').trim();
|
|
2619
|
+
const jsonMatch = clean.match(/\{[\s\S]*\}/);
|
|
2620
|
+
const parsed = JSON.parse(jsonMatch ? jsonMatch[0] : clean);
|
|
2621
|
+
steps = parsed.steps;
|
|
2622
|
+
} catch (parseErr) {
|
|
2623
|
+
process.stderr.write('[STUDIO PLAN PARSE ERR] ' + parseErr.message + '\n');
|
|
2624
|
+
steps = keywordSteps;
|
|
2625
|
+
}
|
|
2595
2626
|
}
|
|
2596
2627
|
if (!Array.isArray(steps) || !steps.length) {
|
|
2597
2628
|
sendJSON(res, 500, { error: 'Empty workflow plan' });
|
|
@@ -2663,8 +2694,22 @@ Example output:
|
|
|
2663
2694
|
} else if (agent === 'WebSearchAgent' || agent === 'ResearchAgent') {
|
|
2664
2695
|
sendToken('[Searching the web and reading pages...] ');
|
|
2665
2696
|
try {
|
|
2697
|
+
// Extract a concise search query from the step prompt (avoid sending the whole task as query)
|
|
2698
|
+
// The planner should provide a short query, but if not, extract key terms
|
|
2699
|
+
let searchQuery = stepPrompt;
|
|
2700
|
+
// If the prompt is very long (> 120 chars), extract the core search terms
|
|
2701
|
+
if (searchQuery.length > 120) {
|
|
2702
|
+
// Try to extract a meaningful short query
|
|
2703
|
+
const keywordMatch = searchQuery.match(/(?:cerca|search|find|ricerca|notizie su|news about|latest on|aggiornamenti su)\s+(.{5,80}?)(?:\s+(?:e|and|per|for|poi|then)|$)/i);
|
|
2704
|
+
if (keywordMatch) {
|
|
2705
|
+
searchQuery = keywordMatch[1].trim();
|
|
2706
|
+
} else {
|
|
2707
|
+
// Take first meaningful clause before comma/period
|
|
2708
|
+
searchQuery = searchQuery.split(/[,\.\n]/)[0].slice(0, 100).trim();
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2666
2711
|
// Use deep search: fetches and reads top 3 pages for real content
|
|
2667
|
-
const searchResult = await withTimeout(executeTool('web_search', { query:
|
|
2712
|
+
const searchResult = await withTimeout(executeTool('web_search', { query: searchQuery, deep: true }, config), 25000);
|
|
2668
2713
|
toolData = typeof searchResult === 'string' ? searchResult : JSON.stringify(searchResult);
|
|
2669
2714
|
} catch (e) { toolData = `Web search failed: ${e.message}`; }
|
|
2670
2715
|
|
|
@@ -2726,124 +2771,78 @@ Example output:
|
|
|
2726
2771
|
// Tool-data agents: fetch real live data and use buildSystemPrompt (tool calls allowed)
|
|
2727
2772
|
const isLiveDataAgent = ['CalendarAgent','EmailAgent','GitHubAgent','NotionAgent','SlackAgent','DriveAgent','BrowserAgent','WebSearchAgent','ResearchAgent'].includes(agent);
|
|
2728
2773
|
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
-
|
|
2742
|
-
-
|
|
2743
|
-
-
|
|
2744
|
-
-
|
|
2745
|
-
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
<
|
|
2751
|
-
<
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
em{color:#a5b4fc;font-style:italic}
|
|
2760
|
-
u{text-decoration-color:#ef4444}
|
|
2761
|
-
blockquote{border-left:3px solid #6366f1;padding:8px 16px;margin:10px 0;background:#15151f;border-radius:0 8px 8px 0;color:#8b8b9e;font-style:italic}
|
|
2762
|
-
.header{background:linear-gradient(135deg,#4f46e5 0%,#06b6d4 100%);border-radius:16px;padding:28px 36px;margin-bottom:20px}
|
|
2763
|
-
.header h1{font-size:26px;font-weight:800;color:#fff;margin-bottom:6px}
|
|
2764
|
-
.header p{font-size:13px;color:rgba(255,255,255,0.85);margin:0}
|
|
2765
|
-
.meta{display:flex;gap:10px;margin-top:14px;flex-wrap:wrap}
|
|
2766
|
-
.meta span{background:rgba(255,255,255,0.18);border-radius:20px;padding:3px 12px;font-size:11px;color:#fff;font-weight:500}
|
|
2767
|
-
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px;margin-bottom:20px}
|
|
2768
|
-
.card{background:#15151f;border:1px solid #2a2a38;border-radius:12px;padding:18px}
|
|
2769
|
-
.card-label{font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:#6366f1;font-weight:700;margin-bottom:8px}
|
|
2770
|
-
.card h3{font-size:18px;font-weight:700;color:#f0f0f5;margin-bottom:4px}
|
|
2771
|
-
.card p{font-size:12px;color:#8b8b9e;margin:0}
|
|
2772
|
-
.section{background:#15151f;border:1px solid #2a2a38;border-radius:12px;padding:22px;margin-bottom:16px}
|
|
2773
|
-
.section-title{font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:#22d3ee;font-weight:700;margin-bottom:16px}
|
|
2774
|
-
.section h3{font-size:15px;font-weight:600;color:#f0f0f5;margin-bottom:6px;margin-top:14px}
|
|
2775
|
-
.section p{font-size:13px;color:#8b8b9e;line-height:1.7;margin-bottom:10px}
|
|
2776
|
-
ul{list-style:none;padding:0;margin:8px 0}
|
|
2777
|
-
ul li{padding:4px 0 4px 18px;position:relative;font-size:13px;color:#8b8b9e}
|
|
2778
|
-
ul li::before{content:'›';position:absolute;left:0;color:#6366f1;font-weight:700}
|
|
2779
|
-
ol{padding-left:20px;margin:8px 0}
|
|
2780
|
-
ol li{padding:4px 0;font-size:13px;color:#8b8b9e;line-height:1.6}
|
|
2781
|
-
.priority-list{display:flex;flex-direction:column;gap:8px}
|
|
2782
|
-
.priority-item{display:flex;align-items:flex-start;gap:12px;padding:12px;background:#1c1c28;border-radius:8px}
|
|
2783
|
-
.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;margin-top:1px}
|
|
2784
|
-
.priority-item h4{font-size:13px;font-weight:600;color:#f0f0f5;margin-bottom:3px}
|
|
2785
|
-
.priority-item p{font-size:12px;color:#8b8b9e;line-height:1.5;margin:0}
|
|
2786
|
-
.source-item{padding:14px;background:#1c1c28;border-radius:8px;margin-bottom:10px;border-left:3px solid #6366f1}
|
|
2787
|
-
.source-item h4{font-size:13px;font-weight:600;color:#f0f0f5;margin-bottom:4px}
|
|
2788
|
-
.source-item p{font-size:12px;color:#8b8b9e;line-height:1.6;margin:4px 0}
|
|
2789
|
-
.source-item a{font-size:11px}
|
|
2790
|
-
.bar-row{margin-bottom:10px}
|
|
2791
|
-
.bar-label{font-size:12px;color:#8b8b9e;margin-bottom:4px;display:flex;justify-content:space-between}
|
|
2792
|
-
.bar-track{background:#1c1c28;border-radius:4px;height:8px;overflow:hidden}
|
|
2793
|
-
.bar-fill{height:100%;border-radius:4px;background:linear-gradient(90deg,#6366f1,#22d3ee)}
|
|
2794
|
-
.badge-high{display:inline-block;background:#7f1d1d;color:#ef4444;border-radius:12px;padding:2px 10px;font-size:10px;font-weight:700;margin-right:4px}
|
|
2795
|
-
.badge-med{display:inline-block;background:#713f12;color:#f59e0b;border-radius:12px;padding:2px 10px;font-size:10px;font-weight:700;margin-right:4px}
|
|
2796
|
-
.badge-low{display:inline-block;background:#14532d;color:#34d399;border-radius:12px;padding:2px 10px;font-size:10px;font-weight:700;margin-right:4px}
|
|
2797
|
-
.badge-info{display:inline-block;background:#1e1b4b;color:#a5b4fc;border-radius:12px;padding:2px 10px;font-size:10px;font-weight:700;margin-right:4px}
|
|
2798
|
-
.divider{border:none;border-top:1px solid #2a2a38;margin:16px 0}
|
|
2799
|
-
.footer{text-align:center;padding:18px;font-size:11px;color:#4a4a5e;margin-top:8px}
|
|
2800
|
-
</style>
|
|
2801
|
-
</head>
|
|
2802
|
-
<body>
|
|
2803
|
-
|
|
2804
|
-
[HEADER — gradient card with h1 title, subtitle p, and .meta spans for date/stats]
|
|
2805
|
-
[GRID — 3-4 .card stat boxes with real numbers from the data]
|
|
2806
|
-
[SECTIONS — use .section with .section-title, then real content with h3, p, ul/ol, .source-item for URLs, .priority-list for ranked items, .bar-row for visual charts]
|
|
2807
|
-
[For every URL in the data: wrap in <a href="..." target="_blank"> as a clickable hyperlink]
|
|
2808
|
-
[FOOTER — "NHA Studio · ${today}"]
|
|
2809
|
-
|
|
2810
|
-
Output ONLY the full HTML. Replace all bracketed instructions above with real HTML content.`;
|
|
2774
|
+
// ── Canvas HTML template — built server-side, guaranteed CSS ─────
|
|
2775
|
+
// The LLM outputs ONLY the <body> inner HTML (no <html>, no <style>)
|
|
2776
|
+
// Server wraps it in the full template. This prevents the model from ignoring CSS.
|
|
2777
|
+
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}`;
|
|
2778
|
+
|
|
2779
|
+
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,'<')}</title><style>${NHA_CSS}</style></head><body>${bodyHtml}</body></html>`;
|
|
2780
|
+
|
|
2781
|
+
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.
|
|
2782
|
+
|
|
2783
|
+
AVAILABLE CSS CLASSES (use these, they are pre-defined):
|
|
2784
|
+
- .header > h1, p, .meta > span (gradient header banner)
|
|
2785
|
+
- .grid > .card > .card-label, h3, p (stat grid)
|
|
2786
|
+
- .section > .section-title, h3, p (content sections)
|
|
2787
|
+
- .source-item > h4, p, a (news/source items with left accent)
|
|
2788
|
+
- .priority-list > .priority-item > .priority-num, h4, p (ranked list)
|
|
2789
|
+
- .bar-row > .bar-label, .bar-track > .bar-fill[style="width:X%"] (bar charts)
|
|
2790
|
+
- .badge-high .badge-med .badge-low .badge-info (colored badges)
|
|
2791
|
+
- ul > li, ol > li (bullet/numbered lists with custom styling)
|
|
2792
|
+
- blockquote (quoted excerpts)
|
|
2793
|
+
- .divider (horizontal rule)
|
|
2794
|
+
- .footer (footer)
|
|
2795
|
+
- <strong> <em> <u> (inline formatting)
|
|
2796
|
+
- <a href="URL" target="_blank"> (clickable links — use for ALL URLs in the data)
|
|
2797
|
+
|
|
2798
|
+
RULES:
|
|
2799
|
+
- Language: ${language}. ALL text must be in ${language}.
|
|
2800
|
+
- Use REAL data from the input — do NOT invent or fabricate
|
|
2801
|
+
- URLs from the data: ALWAYS wrap in <a href="URL" target="_blank">clickable text</a>
|
|
2802
|
+
- Use .priority-list for action items, .source-item for each email/news source, .bar-row for any percentage data
|
|
2803
|
+
- Output must start with <div class="header"> and end with <div class="footer">`;
|
|
2811
2804
|
|
|
2812
2805
|
let sysPrompt, userMsg;
|
|
2813
2806
|
|
|
2814
2807
|
if (isCanvasAgent) {
|
|
2815
2808
|
sysPrompt = canvasSystemPrompt;
|
|
2816
|
-
userMsg = `
|
|
2809
|
+
userMsg = `Create a professional dashboard report for this data. Output ONLY the inner HTML body content (starting with <div class="header">):\n\n${context.slice(0, 10000)}`;
|
|
2817
2810
|
} else if (isLiveDataAgent) {
|
|
2818
|
-
// These agents fetched real data — use
|
|
2819
|
-
const agentInstruction = `You are ${agent}, a specialist AI agent inside NHA Studio.
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2811
|
+
// These agents fetched real data — use a focused prompt (no tool definitions to avoid JSON output)
|
|
2812
|
+
const agentInstruction = `You are ${agent}, a specialist AI agent inside NHA Studio. Today is ${today}. Respond entirely in ${language}.
|
|
2813
|
+
|
|
2814
|
+
CRITICAL: Do NOT invent, hallucinate, or add any data not present in the DATA sections below. ONLY use the exact data provided.
|
|
2815
|
+
Do NOT output JSON, tool calls, or code blocks. Write in plain text with markdown headers.
|
|
2816
|
+
|
|
2817
|
+
${toolData ? `## DATA FROM TOOLS:\n${toolData.slice(0, 6000)}\n` : '## DATA: No data was retrieved by this agent.\n'}
|
|
2818
|
+
${context ? `## OUTPUT FROM PREVIOUS AGENTS:\n${context.slice(0, 4000)}\n` : ''}
|
|
2819
|
+
|
|
2820
|
+
Your task: ${stepPrompt}`;
|
|
2821
|
+
sysPrompt = agentInstruction;
|
|
2824
2822
|
userMsg = toolData
|
|
2825
|
-
? `Summarize the data above
|
|
2823
|
+
? `Summarize and analyze the REAL data above. Do not add anything not present in the data.`
|
|
2826
2824
|
: context
|
|
2827
|
-
? `Based on the previous
|
|
2825
|
+
? `Based ONLY on the previous agent outputs above, complete: ${stepPrompt}`
|
|
2828
2826
|
: stepPrompt;
|
|
2829
2827
|
} else {
|
|
2830
2828
|
// All other agents (WriterAgent, DataAnalystAgent, specialist agents, etc.)
|
|
2831
2829
|
// Use a focused prompt with NO TOOL_DEFINITIONS to prevent JSON/tool-call output
|
|
2830
|
+
const hasRealData = !!(toolData || context);
|
|
2832
2831
|
sysPrompt = `You are ${agent}, a specialist AI agent inside NHA Studio. Today is ${today}. You MUST respond entirely in ${language}.
|
|
2833
2832
|
|
|
2834
2833
|
CRITICAL RULES:
|
|
2835
2834
|
- Do NOT output JSON, tool calls, function calls, or code blocks
|
|
2836
|
-
-
|
|
2837
|
-
-
|
|
2838
|
-
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
? `
|
|
2846
|
-
: stepPrompt
|
|
2835
|
+
- NEVER invent, fabricate, or hallucinate data, events, emails, meetings, or news
|
|
2836
|
+
- ONLY use the EXACT data provided in the DATA sections below — if no data is provided, say so clearly
|
|
2837
|
+
- Do NOT add fictional examples, placeholder content, or generic suggestions not grounded in the data
|
|
2838
|
+
- Write in plain prose, structured with markdown headers (##) and bullet points (-)
|
|
2839
|
+
- Be thorough and specific — this is for an executive briefing based on REAL data only
|
|
2840
|
+
|
|
2841
|
+
${toolData ? `## LIVE DATA FROM TOOLS:\n${toolData.slice(0, 6000)}\n` : '## LIVE DATA: No tool data was fetched for this step.\n'}
|
|
2842
|
+
${context ? `## OUTPUT FROM PREVIOUS AGENTS:\n${context.slice(0, 6000)}\n` : ''}`;
|
|
2843
|
+
userMsg = hasRealData
|
|
2844
|
+
? `Based ONLY on the real data above, complete this task: ${stepPrompt}`
|
|
2845
|
+
: `No real data is available. State clearly that no data was retrieved and explain what would be needed: ${stepPrompt}`;
|
|
2847
2846
|
}
|
|
2848
2847
|
|
|
2849
2848
|
// ── Stream LLM response ───────────────────────────────────────
|
|
@@ -2876,50 +2875,37 @@ ${context ? `## CONTEXT FROM PREVIOUS AGENTS:\n${context.slice(0, 5000)}\n` : ''
|
|
|
2876
2875
|
}
|
|
2877
2876
|
|
|
2878
2877
|
if (isCanvasAgent) {
|
|
2879
|
-
let
|
|
2880
|
-
// Strip thinking tags
|
|
2881
|
-
|
|
2882
|
-
//
|
|
2883
|
-
const mdMatch =
|
|
2884
|
-
if (mdMatch)
|
|
2885
|
-
//
|
|
2886
|
-
const
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
if (
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
const
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Report</title><style>
|
|
2906
|
-
*{box-sizing:border-box;margin:0;padding:0}
|
|
2907
|
-
body{font-family:system-ui,-apple-system,sans-serif;background:#f8fafc;color:#1e293b;padding:0}
|
|
2908
|
-
.header{background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;padding:48px 40px;margin-bottom:32px}
|
|
2909
|
-
.header h1{font-size:1.8em;font-weight:700;margin-bottom:8px}
|
|
2910
|
-
.header p{opacity:.85;font-size:1em}
|
|
2911
|
-
.content{max-width:900px;margin:0 auto;padding:0 32px 48px}
|
|
2912
|
-
.card{background:#fff;border-radius:12px;padding:28px;margin-bottom:20px;box-shadow:0 1px 3px rgba(0,0,0,.08),0 1px 2px rgba(0,0,0,.05);border:1px solid #e2e8f0}
|
|
2913
|
-
.card h2{color:#6366f1;font-size:1.05em;font-weight:700;margin-bottom:12px;padding-bottom:10px;border-bottom:1px solid #f1f5f9}
|
|
2914
|
-
.card p{color:#475569;line-height:1.75;margin-bottom:8px}
|
|
2915
|
-
.card p:last-child{margin-bottom:0}
|
|
2916
|
-
</style></head><body>
|
|
2917
|
-
<div class="header"><h1>${reportTitle}</h1><p>Report generated by NHA Studio</p></div>
|
|
2918
|
-
<div class="content">${cardsHtml || '<div class="card"><p>' + safeContext + '</p></div>'}</div>
|
|
2919
|
-
</body></html>`;
|
|
2878
|
+
let bodyHtml = fullOutput.trim();
|
|
2879
|
+
// Strip thinking tags
|
|
2880
|
+
bodyHtml = bodyHtml.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
|
|
2881
|
+
// Strip markdown code fences
|
|
2882
|
+
const mdMatch = bodyHtml.match(/```html?\s*([\s\S]*?)```/i);
|
|
2883
|
+
if (mdMatch) bodyHtml = mdMatch[1].trim();
|
|
2884
|
+
// If model returned full HTML despite instructions, extract body content
|
|
2885
|
+
const bodyTagMatch = bodyHtml.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
|
2886
|
+
if (bodyTagMatch) bodyHtml = bodyTagMatch[1].trim();
|
|
2887
|
+
// If model returned <!DOCTYPE (full doc), extract everything after <body> open tag
|
|
2888
|
+
else if (bodyHtml.includes('<!DOCTYPE') || bodyHtml.includes('<html')) {
|
|
2889
|
+
const bodyStart = bodyHtml.search(/<body[^>]*>/i);
|
|
2890
|
+
if (bodyStart >= 0) bodyHtml = bodyHtml.slice(bodyStart).replace(/<body[^>]*>/i, '').replace(/<\/body>[\s\S]*/i, '').trim();
|
|
2891
|
+
}
|
|
2892
|
+
// Fallback: if LLM output is empty or has no HTML tags, build body from context using markdown→HTML conversion
|
|
2893
|
+
if (!bodyHtml || !bodyHtml.includes('<')) {
|
|
2894
|
+
const reportTitle = task.slice(0, 80).replace(/</g,'<');
|
|
2895
|
+
const sections = context.split(/\n#{1,3} |(?=\n\n)/).filter(s => s.trim()).slice(0, 12);
|
|
2896
|
+
bodyHtml = `<div class="header"><h1>${reportTitle}</h1><p>NHA Studio Report \u00b7 ${today}</p><div class="meta"><span>${today}</span></div></div>` +
|
|
2897
|
+
sections.map(s => {
|
|
2898
|
+
const lines = s.replace(/\*\*/g,'').replace(/\*/g,'').trim().split('\n').filter(Boolean);
|
|
2899
|
+
const title = lines[0] || '';
|
|
2900
|
+
const body = lines.slice(1).map(l => `<p>${l.replace(/</g,'<')}</p>`).join('');
|
|
2901
|
+
return `<div class="section"><div class="section-title">${title.replace(/</g,'<')}</div>${body}</div>`;
|
|
2902
|
+
}).join('') +
|
|
2903
|
+
`<div class="footer">NHA Studio \u00b7 ${today}</div>`;
|
|
2920
2904
|
}
|
|
2905
|
+
// Always wrap in the guaranteed NHA dark CSS template
|
|
2906
|
+
const finalHtml = wrapInNHATemplate(bodyHtml, task.slice(0, 60));
|
|
2921
2907
|
sendToken('\n\n[Report generato]');
|
|
2922
|
-
sendEvent({ canvas:
|
|
2908
|
+
sendEvent({ canvas: finalHtml });
|
|
2923
2909
|
}
|
|
2924
2910
|
|
|
2925
2911
|
// Estimate token usage (aprox: 1 token ≈ 4 chars)
|
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.2.
|
|
8
|
+
export const VERSION = '13.2.30';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
package/src/services/web-ui.mjs
CHANGED
|
@@ -3218,7 +3218,14 @@ function runStudioStep(idx, node, task, context, stepDef, signal) {
|
|
|
3218
3218
|
if (ev.token) {
|
|
3219
3219
|
// Tool status tokens (start with '[') shown in dim color, LLM output streamed normally
|
|
3220
3220
|
var isStatus = ev.token.charAt(0) === '[' && ev.token.indexOf(']') > 0 && ev.token.length < 80;
|
|
3221
|
-
if (!isStatus)
|
|
3221
|
+
if (!isStatus) {
|
|
3222
|
+
var tok = ev.token;
|
|
3223
|
+
// Insert space between tokens if missing (Qwen3/Liara emits tokens without separators)
|
|
3224
|
+
if (output && tok && !/[\s\n]$/.test(output) && !/^[\s\n.,;:!?)\]}\u2019'"]/.test(tok)) {
|
|
3225
|
+
tok = ' ' + tok;
|
|
3226
|
+
}
|
|
3227
|
+
output += tok;
|
|
3228
|
+
}
|
|
3222
3229
|
// Update live log entry
|
|
3223
3230
|
var entries = document.querySelectorAll('.studio-log-entry');
|
|
3224
3231
|
var last = entries[entries.length - 1];
|
|
@@ -3298,44 +3305,44 @@ function renderStudio(el) {
|
|
|
3298
3305
|
|
|
3299
3306
|
// 38 specialist agents sidebar
|
|
3300
3307
|
var SPECIALIST_AGENTS = [
|
|
3301
|
-
{icon:'
|
|
3302
|
-
{icon:'
|
|
3303
|
-
{icon:'
|
|
3304
|
-
{icon:'
|
|
3305
|
-
{icon:'
|
|
3306
|
-
{icon:'
|
|
3307
|
-
{icon:'
|
|
3308
|
-
{icon:'
|
|
3309
|
-
{icon:'
|
|
3310
|
-
{icon:'
|
|
3311
|
-
{icon:'
|
|
3312
|
-
{icon:'
|
|
3313
|
-
{icon:'
|
|
3314
|
-
{icon:'
|
|
3315
|
-
{icon:'
|
|
3316
|
-
{icon:'
|
|
3317
|
-
{icon:'
|
|
3318
|
-
{icon:'
|
|
3319
|
-
{icon:'
|
|
3320
|
-
{icon:'
|
|
3321
|
-
{icon:'
|
|
3322
|
-
{icon:'
|
|
3323
|
-
{icon:'
|
|
3324
|
-
{icon:'
|
|
3325
|
-
{icon:'
|
|
3326
|
-
{icon:'
|
|
3327
|
-
{icon:'
|
|
3328
|
-
{icon:'
|
|
3329
|
-
{icon:'
|
|
3330
|
-
{icon:'
|
|
3331
|
-
{icon:'
|
|
3332
|
-
{icon:'
|
|
3333
|
-
{icon:'
|
|
3334
|
-
{icon:'
|
|
3335
|
-
{icon:'
|
|
3336
|
-
{icon:'
|
|
3337
|
-
{icon:'
|
|
3338
|
-
{icon:'
|
|
3308
|
+
{icon:'\u{1F6E1}',name:'saber',desc:'Security audits, pentest, OWASP'},
|
|
3309
|
+
{icon:'\u{1F50D}',name:'zero',desc:'Vulnerability & dependency audit'},
|
|
3310
|
+
{icon:'\u2713',name:'veritas',desc:'Fact-checking & hallucination detection'},
|
|
3311
|
+
{icon:'\u{1F52C}',name:'ade',desc:'Full security review, forensics'},
|
|
3312
|
+
{icon:'\u{1F512}',name:'heimdall',desc:'OAuth, JWT, RBAC design'},
|
|
3313
|
+
{icon:'\u{1F4BB}',name:'jarvis',desc:'Full-stack architecture & API'},
|
|
3314
|
+
{icon:'\u2699',name:'forge',desc:'CI/CD, deployment, infra'},
|
|
3315
|
+
{icon:'\u{1F527}',name:'pipe',desc:'Build systems, Airflow, automation'},
|
|
3316
|
+
{icon:'\u{1F4DF}',name:'shell',desc:'Shell scripts, CLI tools'},
|
|
3317
|
+
{icon:'\u{1F41B}',name:'glitch',desc:'Debugging & root cause'},
|
|
3318
|
+
{icon:'\u{1F4CA}',name:'oracle',desc:'Data analysis, stats, ML'},
|
|
3319
|
+
{icon:'\u{1F9EE}',name:'logos',desc:'Logic, proofs, formal reasoning'},
|
|
3320
|
+
{icon:'\u{1F5FA}',name:'atlas',desc:'Terraform, CloudFormation, IaC'},
|
|
3321
|
+
{icon:'\u{1F30D}',name:'cartographer',desc:'Geo data, mapping, routing'},
|
|
3322
|
+
{icon:'\u270D',name:'scheherazade',desc:'Docs, tutorials, blog posts'},
|
|
3323
|
+
{icon:'\u{1F4DD}',name:'quill',desc:'Posts, summaries, abstracts'},
|
|
3324
|
+
{icon:'\u{1F3A8}',name:'muse',desc:'Creative brainstorming & ideation'},
|
|
3325
|
+
{icon:'\u{1F58C}',name:'murasaki',desc:'UI/UX design, accessibility'},
|
|
3326
|
+
{icon:'\u{1F517}',name:'hermes',desc:'Kafka, RabbitMQ, event-driven'},
|
|
3327
|
+
{icon:'\u{1F50C}',name:'link',desc:'Community, reputation, engagement'},
|
|
3328
|
+
{icon:'\u{1F310}',name:'mercury',desc:'Finance, market, ROI analysis'},
|
|
3329
|
+
{icon:'\u2638',name:'shogun',desc:'Kubernetes, Helm, pod security'},
|
|
3330
|
+
{icon:'\u{1F504}',name:'flux',desc:'GitOps, rollback planning'},
|
|
3331
|
+
{icon:'\u23F0',name:'cron',desc:'GitHub Actions, GitLab CI'},
|
|
3332
|
+
{icon:'\u{1F30E}',name:'babel',desc:'API integration, microservices'},
|
|
3333
|
+
{icon:'\u{1F5E3}',name:'polyglot',desc:'i18n, localization, translation'},
|
|
3334
|
+
{icon:'\u{1F4E2}',name:'herald',desc:'News analysis, trend detection'},
|
|
3335
|
+
{icon:'\u{1F4E1}',name:'echo',desc:'Content repurposing: blog\u2192social'},
|
|
3336
|
+
{icon:'\u26A1',name:'macro',desc:'Batch processing, data migration'},
|
|
3337
|
+
{icon:'\u{1F525}',name:'prometheus',desc:'Strategy, architecture trade-offs'},
|
|
3338
|
+
{icon:'\u26A0',name:'cassandra',desc:'Risk prediction, worst-case analysis'},
|
|
3339
|
+
{icon:'\u{1F9E0}',name:'athena',desc:'Tech evaluation, benchmarks'},
|
|
3340
|
+
{icon:'\u{1F441}',name:'sauron',desc:'Performance profiling, bottlenecks'},
|
|
3341
|
+
{icon:'\u{1F3BC}',name:'conductor',desc:'Workflow orchestration'},
|
|
3342
|
+
{icon:'\u{1F9ED}',name:'navi',desc:'Data profiling, schema inference'},
|
|
3343
|
+
{icon:'\u{1F4C8}',name:'edi',desc:'A/B testing, hypothesis testing'},
|
|
3344
|
+
{icon:'\u26C8',name:'tempest',desc:'Climate, weather, environmental'},
|
|
3345
|
+
{icon:'\u{1F37D}',name:'epicure',desc:'Recipes, nutrition, dietary'},
|
|
3339
3346
|
];
|
|
3340
3347
|
var specialistHtml = SPECIALIST_AGENTS.map(function(t){
|
|
3341
3348
|
var ic = t.icon;
|
|
@@ -3392,7 +3399,7 @@ function renderStudio(el) {
|
|
|
3392
3399
|
|
|
3393
3400
|
'<div style="display:flex;align-items:center;gap:8px;margin:8px 0">' +
|
|
3394
3401
|
'<div id="studioTokenBar" style="font-size:10px;color:var(--dim);font-family:var(--mono);flex:1"></div>' +
|
|
3395
|
-
'<button id="studioCanvasBtn" onclick="
|
|
3402
|
+
'<button id="studioCanvasBtn" onclick="openCanvasPanel()" style="display:none;font-size:12px;padding:5px 14px;background:var(--greendim);border:1px solid var(--green3);border-radius:6px;color:var(--green);cursor:pointer;font-weight:700">■ ' + t('canvas_open') + '</button>' +
|
|
3396
3403
|
'</div>' +
|
|
3397
3404
|
'<div class="studio-canvas" id="studioNodes"></div>' +
|
|
3398
3405
|
'<div class="studio-log" id="studioLog" style="display:none"></div>' +
|