squidclaw 3.0.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/ai/gateway.js +8 -1
- package/lib/ai/smart-router.js +77 -0
- package/lib/tools/dashboard-pro.js +340 -0
- package/lib/tools/pptx-pro.js +686 -0
- package/lib/tools/router.js +36 -55
- package/package.json +1 -1
package/lib/ai/gateway.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { logger } from '../core/logger.js';
|
|
7
|
+
import { getRouteForTask } from './smart-router.js';
|
|
7
8
|
import { MODEL_MAP, MODEL_PRICING } from '../core/config.js';
|
|
8
9
|
|
|
9
10
|
export class AIGateway {
|
|
@@ -27,7 +28,13 @@ export class AIGateway {
|
|
|
27
28
|
* Send a chat completion request with auto-fallback
|
|
28
29
|
*/
|
|
29
30
|
async chat(messages, options = {}) {
|
|
30
|
-
|
|
31
|
+
// Smart routing: use best model for the task
|
|
32
|
+
let routedModel = options.model;
|
|
33
|
+
if (!routedModel && options.taskHint) {
|
|
34
|
+
const route = getRouteForTask(options.taskHint, options.toolName, this.config);
|
|
35
|
+
if (route) routedModel = route.model;
|
|
36
|
+
}
|
|
37
|
+
const model = routedModel || this.config.ai?.defaultModel;
|
|
31
38
|
const fallbackChain = options.fallbackChain || this.config.ai?.fallbackChain || [];
|
|
32
39
|
const modelsToTry = [model, ...fallbackChain].filter(Boolean);
|
|
33
40
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🦑 Smart AI Router
|
|
3
|
+
* Routes to the best model based on task type
|
|
4
|
+
*
|
|
5
|
+
* - Creative content (PPT, reports, stories) → Gemini (free + creative)
|
|
6
|
+
* - Conversation, reasoning → Claude (smart)
|
|
7
|
+
* - Quick tasks, translations → Flash models (fast + cheap)
|
|
8
|
+
* - Code, analysis → Claude or GPT-4o
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { logger } from '../core/logger.js';
|
|
12
|
+
|
|
13
|
+
const TASK_ROUTES = {
|
|
14
|
+
// Creative content → Gemini
|
|
15
|
+
presentation: { provider: 'google', model: 'gemini-2.5-flash', reason: 'creative content' },
|
|
16
|
+
pptx: { provider: 'google', model: 'gemini-2.5-flash', reason: 'presentation' },
|
|
17
|
+
dashboard: { provider: 'google', model: 'gemini-2.5-flash', reason: 'visual content' },
|
|
18
|
+
report: { provider: 'google', model: 'gemini-2.5-flash', reason: 'report generation' },
|
|
19
|
+
story: { provider: 'google', model: 'gemini-2.5-flash', reason: 'creative writing' },
|
|
20
|
+
article: { provider: 'google', model: 'gemini-2.5-flash', reason: 'article writing' },
|
|
21
|
+
email_draft: { provider: 'google', model: 'gemini-2.5-flash', reason: 'email draft' },
|
|
22
|
+
excel: { provider: 'google', model: 'gemini-2.5-flash', reason: 'data generation' },
|
|
23
|
+
pdf: { provider: 'google', model: 'gemini-2.5-flash', reason: 'document' },
|
|
24
|
+
|
|
25
|
+
// Quick tasks → Flash (fast)
|
|
26
|
+
translate: { provider: 'google', model: 'gemini-2.0-flash', reason: 'translation' },
|
|
27
|
+
summarize: { provider: 'google', model: 'gemini-2.0-flash', reason: 'summarization' },
|
|
28
|
+
weather: { provider: 'google', model: 'gemini-2.0-flash', reason: 'simple lookup' },
|
|
29
|
+
|
|
30
|
+
// Default stays on primary model
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Detect task type from message + tool context
|
|
34
|
+
function detectTask(message, toolName) {
|
|
35
|
+
const lower = (message || '').toLowerCase();
|
|
36
|
+
|
|
37
|
+
// Explicit tool-based routing
|
|
38
|
+
if (toolName) {
|
|
39
|
+
if (['pptx', 'pptx_slides', 'pptx_pro', 'presentation', 'powerpoint'].includes(toolName)) return 'presentation';
|
|
40
|
+
if (['dashboard', 'canvas_dashboard'].includes(toolName)) return 'dashboard';
|
|
41
|
+
if (['excel', 'xlsx'].includes(toolName)) return 'excel';
|
|
42
|
+
if (['pdf'].includes(toolName)) return 'pdf';
|
|
43
|
+
if (['translate'].includes(toolName)) return 'translate';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Message-based detection
|
|
47
|
+
if (lower.match(/\b(presentation|ppt|powerpoint|slides|عرض تقديمي|شرائح)\b/)) return 'presentation';
|
|
48
|
+
if (lower.match(/\b(dashboard|لوحة)\b/)) return 'dashboard';
|
|
49
|
+
if (lower.match(/\b(report|تقرير)\b/)) return 'report';
|
|
50
|
+
if (lower.match(/\b(story|قصة|حكاية)\b/)) return 'story';
|
|
51
|
+
if (lower.match(/\b(article|مقال)\b/)) return 'article';
|
|
52
|
+
if (lower.match(/\b(translate|ترجم)\b/)) return 'translate';
|
|
53
|
+
if (lower.match(/\b(summarize|summary|لخص|ملخص)\b/)) return 'summarize';
|
|
54
|
+
if (lower.match(/\b(excel|spreadsheet|جدول)\b/)) return 'excel';
|
|
55
|
+
|
|
56
|
+
return null; // Use default model
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getRouteForTask(message, toolName, config) {
|
|
60
|
+
const task = detectTask(message, toolName);
|
|
61
|
+
if (!task) return null;
|
|
62
|
+
|
|
63
|
+
const route = TASK_ROUTES[task];
|
|
64
|
+
if (!route) return null;
|
|
65
|
+
|
|
66
|
+
// Check if provider is available
|
|
67
|
+
const providers = config?.ai?.providers || {};
|
|
68
|
+
if (!providers[route.provider]?.key) {
|
|
69
|
+
logger.debug('smart-router', `${route.provider} not available for ${task}, using default`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
logger.info('smart-router', `Routing "${task}" → ${route.provider}/${route.model} (${route.reason})`);
|
|
74
|
+
return { provider: route.provider, model: route.model, task, reason: route.reason };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { detectTask, TASK_ROUTES };
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🦑 Dashboard PRO — Professional visual dashboards
|
|
3
|
+
* Renders beautiful dashboards as images via Puppeteer
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Auto-layout from markdown/data
|
|
7
|
+
* - Multiple chart types (CSS-based, no Chart.js needed)
|
|
8
|
+
* - Stats cards with gradients
|
|
9
|
+
* - Tables with alternating rows
|
|
10
|
+
* - Progress bars
|
|
11
|
+
* - KPI indicators (up/down arrows)
|
|
12
|
+
* - Dark/light themes
|
|
13
|
+
* - Responsive grid
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { mkdirSync, writeFileSync, existsSync } from 'fs';
|
|
17
|
+
import { join } from 'path';
|
|
18
|
+
import { logger } from '../core/logger.js';
|
|
19
|
+
|
|
20
|
+
const OUTPUT_DIR = '/tmp/squidclaw-dashboard';
|
|
21
|
+
|
|
22
|
+
const DASH_THEMES = {
|
|
23
|
+
dark: {
|
|
24
|
+
bg: '#0d1117', card: '#161b22', border: '#30363d', text: '#c9d1d9', subtle: '#8b949e',
|
|
25
|
+
accent: '#58a6ff', accent2: '#3fb950', warning: '#d29922', danger: '#f85149',
|
|
26
|
+
gradient: 'linear-gradient(135deg, #58a6ff 0%, #3fb950 100%)',
|
|
27
|
+
},
|
|
28
|
+
light: {
|
|
29
|
+
bg: '#f6f8fa', card: '#ffffff', border: '#d0d7de', text: '#24292f', subtle: '#656d76',
|
|
30
|
+
accent: '#0969da', accent2: '#1a7f37', warning: '#9a6700', danger: '#cf222e',
|
|
31
|
+
gradient: 'linear-gradient(135deg, #0969da 0%, #1a7f37 100%)',
|
|
32
|
+
},
|
|
33
|
+
saudi: {
|
|
34
|
+
bg: '#fafdf7', card: '#ffffff', border: '#bbf7d0', text: '#14532d', subtle: '#6b7280',
|
|
35
|
+
accent: '#006c35', accent2: '#c8a951', warning: '#b8860b', danger: '#dc2626',
|
|
36
|
+
gradient: 'linear-gradient(135deg, #006c35 0%, #c8a951 100%)',
|
|
37
|
+
},
|
|
38
|
+
ocean: {
|
|
39
|
+
bg: '#0a192f', card: '#112240', border: '#1e3a5f', text: '#ccd6f6', subtle: '#8892b0',
|
|
40
|
+
accent: '#64ffda', accent2: '#5eead4', warning: '#fbbf24', danger: '#f87171',
|
|
41
|
+
gradient: 'linear-gradient(135deg, #64ffda 0%, #5eead4 100%)',
|
|
42
|
+
},
|
|
43
|
+
neon: {
|
|
44
|
+
bg: '#0a0a0a', card: '#1a1a1a', border: '#333', text: '#e0e0e0', subtle: '#888',
|
|
45
|
+
accent: '#00ff88', accent2: '#00ccff', warning: '#ffcc00', danger: '#ff3366',
|
|
46
|
+
gradient: 'linear-gradient(135deg, #00ff88 0%, #00ccff 100%)',
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function buildDashboardHtml(data, theme = 'dark') {
|
|
51
|
+
const t = DASH_THEMES[theme] || DASH_THEMES.dark;
|
|
52
|
+
const chartColors = [t.accent, t.accent2, t.warning, t.danger, '#bc8cff', '#f778ba', '#a5d6ff', '#7ee787'];
|
|
53
|
+
|
|
54
|
+
let html = `<!DOCTYPE html><html><head><meta charset="UTF-8"><style>
|
|
55
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
56
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: ${t.bg}; color: ${t.text}; padding: 28px; min-width: 900px; }
|
|
57
|
+
.header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px; padding-bottom: 16px; border-bottom: 2px solid ${t.border}; }
|
|
58
|
+
.header h1 { font-size: 26px; font-weight: 700; background: ${t.gradient}; -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
|
59
|
+
.header .date { font-size: 12px; color: ${t.subtle}; }
|
|
60
|
+
.grid { display: grid; gap: 16px; margin-bottom: 16px; }
|
|
61
|
+
.grid-2 { grid-template-columns: 1fr 1fr; }
|
|
62
|
+
.grid-3 { grid-template-columns: 1fr 1fr 1fr; }
|
|
63
|
+
.grid-4 { grid-template-columns: 1fr 1fr 1fr 1fr; }
|
|
64
|
+
.card { background: ${t.card}; border: 1px solid ${t.border}; border-radius: 12px; padding: 20px; position: relative; overflow: hidden; }
|
|
65
|
+
.card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: ${t.gradient}; }
|
|
66
|
+
.card h3 { font-size: 13px; color: ${t.subtle}; font-weight: 500; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
67
|
+
|
|
68
|
+
/* Stats */
|
|
69
|
+
.stat-card { text-align: center; padding: 24px 16px; }
|
|
70
|
+
.stat-icon { font-size: 28px; margin-bottom: 8px; }
|
|
71
|
+
.stat-value { font-size: 32px; font-weight: 800; background: ${t.gradient}; -webkit-background-clip: text; -webkit-text-fill-color: transparent; line-height: 1.1; }
|
|
72
|
+
.stat-label { font-size: 12px; color: ${t.subtle}; margin-top: 6px; font-weight: 500; }
|
|
73
|
+
.stat-change { font-size: 11px; margin-top: 4px; }
|
|
74
|
+
.stat-change.up { color: ${t.accent2}; }
|
|
75
|
+
.stat-change.down { color: ${t.danger}; }
|
|
76
|
+
|
|
77
|
+
/* Bar chart */
|
|
78
|
+
.bar-row { display: flex; align-items: center; margin: 8px 0; }
|
|
79
|
+
.bar-label { width: 100px; font-size: 12px; color: ${t.subtle}; flex-shrink: 0; }
|
|
80
|
+
.bar-track { flex: 1; height: 28px; background: ${t.bg}; border-radius: 6px; overflow: hidden; position: relative; }
|
|
81
|
+
.bar-fill { height: 100%; border-radius: 6px; display: flex; align-items: center; padding-left: 10px; transition: width 0.5s ease; }
|
|
82
|
+
.bar-value { font-size: 11px; color: #fff; font-weight: 700; }
|
|
83
|
+
|
|
84
|
+
/* Donut chart (CSS) */
|
|
85
|
+
.donut-container { display: flex; align-items: center; gap: 24px; }
|
|
86
|
+
.donut { width: 160px; height: 160px; border-radius: 50%; position: relative; flex-shrink: 0; }
|
|
87
|
+
.donut-hole { position: absolute; top: 20%; left: 20%; width: 60%; height: 60%; border-radius: 50%; background: ${t.card}; display: flex; align-items: center; justify-content: center; flex-direction: column; }
|
|
88
|
+
.donut-total { font-size: 22px; font-weight: 800; color: ${t.text}; }
|
|
89
|
+
.donut-subtitle { font-size: 10px; color: ${t.subtle}; }
|
|
90
|
+
.legend { display: flex; flex-direction: column; gap: 8px; }
|
|
91
|
+
.legend-item { display: flex; align-items: center; gap: 8px; font-size: 12px; }
|
|
92
|
+
.legend-dot { width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0; }
|
|
93
|
+
|
|
94
|
+
/* Table */
|
|
95
|
+
table { width: 100%; border-collapse: collapse; }
|
|
96
|
+
th { background: ${t.bg}; color: ${t.accent}; padding: 10px 12px; text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 2px solid ${t.border}; }
|
|
97
|
+
td { padding: 10px 12px; font-size: 12px; border-bottom: 1px solid ${t.border}; }
|
|
98
|
+
tr:nth-child(even) { background: ${t.bg}40; }
|
|
99
|
+
tr:hover { background: ${t.accent}10; }
|
|
100
|
+
|
|
101
|
+
/* Progress */
|
|
102
|
+
.progress-row { display: flex; align-items: center; gap: 12px; margin: 10px 0; }
|
|
103
|
+
.progress-label { width: 120px; font-size: 12px; }
|
|
104
|
+
.progress-track { flex: 1; height: 10px; background: ${t.bg}; border-radius: 5px; overflow: hidden; }
|
|
105
|
+
.progress-fill { height: 100%; border-radius: 5px; }
|
|
106
|
+
.progress-pct { font-size: 12px; font-weight: 700; width: 40px; text-align: right; }
|
|
107
|
+
|
|
108
|
+
/* Section */
|
|
109
|
+
.section-title { font-size: 15px; font-weight: 700; color: ${t.text}; margin: 20px 0 12px; padding-left: 12px; border-left: 3px solid ${t.accent}; }
|
|
110
|
+
|
|
111
|
+
/* Footer */
|
|
112
|
+
.footer { text-align: center; margin-top: 20px; padding-top: 12px; border-top: 1px solid ${t.border}; font-size: 10px; color: ${t.subtle}; }
|
|
113
|
+
</style></head><body>`;
|
|
114
|
+
|
|
115
|
+
// Header
|
|
116
|
+
html += `<div class="header"><h1>${data.title || 'Dashboard'}</h1><div class="date">${data.subtitle || new Date().toLocaleString()}</div></div>`;
|
|
117
|
+
|
|
118
|
+
// Render sections
|
|
119
|
+
for (const section of (data.sections || [])) {
|
|
120
|
+
if (section.heading) {
|
|
121
|
+
html += `<div class="section-title">${section.heading}</div>`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Stats
|
|
125
|
+
if (section.type === 'stats' && section.items) {
|
|
126
|
+
const cols = Math.min(section.items.length, 4);
|
|
127
|
+
html += `<div class="grid grid-${cols}">`;
|
|
128
|
+
for (const stat of section.items) {
|
|
129
|
+
const changeClass = stat.change ? (stat.change.startsWith('+') || stat.change.startsWith('↑') ? 'up' : 'down') : '';
|
|
130
|
+
html += `<div class="card stat-card">
|
|
131
|
+
<div class="stat-icon">${stat.icon || '📊'}</div>
|
|
132
|
+
<div class="stat-value">${stat.value}</div>
|
|
133
|
+
<div class="stat-label">${stat.label}</div>
|
|
134
|
+
${stat.change ? `<div class="stat-change ${changeClass}">${stat.change}</div>` : ''}
|
|
135
|
+
</div>`;
|
|
136
|
+
}
|
|
137
|
+
html += `</div>`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Bar chart
|
|
141
|
+
if (section.type === 'bar' && section.items) {
|
|
142
|
+
const max = Math.max(...section.items.map(i => i.value));
|
|
143
|
+
html += `<div class="card"><h3>${section.title || 'Chart'}</h3>`;
|
|
144
|
+
section.items.forEach((item, idx) => {
|
|
145
|
+
const pct = (item.value / max * 100).toFixed(0);
|
|
146
|
+
const color = item.color || chartColors[idx % chartColors.length];
|
|
147
|
+
html += `<div class="bar-row">
|
|
148
|
+
<div class="bar-label">${item.label}</div>
|
|
149
|
+
<div class="bar-track"><div class="bar-fill" style="width:${pct}%;background:${color}"><span class="bar-value">${item.value}</span></div></div>
|
|
150
|
+
</div>`;
|
|
151
|
+
});
|
|
152
|
+
html += `</div>`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Donut/Pie
|
|
156
|
+
if ((section.type === 'donut' || section.type === 'pie') && section.items) {
|
|
157
|
+
const total = section.items.reduce((s, i) => s + i.value, 0);
|
|
158
|
+
let rotation = 0;
|
|
159
|
+
const gradParts = [];
|
|
160
|
+
section.items.forEach((item, idx) => {
|
|
161
|
+
const pct = item.value / total * 360;
|
|
162
|
+
const color = item.color || chartColors[idx % chartColors.length];
|
|
163
|
+
gradParts.push(`${color} ${rotation}deg ${rotation + pct}deg`);
|
|
164
|
+
rotation += pct;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
html += `<div class="card"><h3>${section.title || 'Distribution'}</h3>
|
|
168
|
+
<div class="donut-container">
|
|
169
|
+
<div class="donut" style="background:conic-gradient(${gradParts.join(',')})">
|
|
170
|
+
<div class="donut-hole"><div class="donut-total">${total}</div><div class="donut-subtitle">Total</div></div>
|
|
171
|
+
</div>
|
|
172
|
+
<div class="legend">`;
|
|
173
|
+
section.items.forEach((item, idx) => {
|
|
174
|
+
const color = item.color || chartColors[idx % chartColors.length];
|
|
175
|
+
html += `<div class="legend-item"><div class="legend-dot" style="background:${color}"></div>${item.label}: <strong>${item.value}</strong> (${(item.value / total * 100).toFixed(0)}%)</div>`;
|
|
176
|
+
});
|
|
177
|
+
html += `</div></div></div>`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Table
|
|
181
|
+
if (section.type === 'table' && section.rows) {
|
|
182
|
+
html += `<div class="card"><h3>${section.title || 'Data'}</h3><table>`;
|
|
183
|
+
if (section.headers) {
|
|
184
|
+
html += `<tr>${section.headers.map(h => `<th>${h}</th>`).join('')}</tr>`;
|
|
185
|
+
}
|
|
186
|
+
for (const row of section.rows) {
|
|
187
|
+
html += `<tr>${row.map(c => `<td>${c}</td>`).join('')}</tr>`;
|
|
188
|
+
}
|
|
189
|
+
html += `</table></div>`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Progress bars
|
|
193
|
+
if (section.type === 'progress' && section.items) {
|
|
194
|
+
html += `<div class="card"><h3>${section.title || 'Progress'}</h3>`;
|
|
195
|
+
section.items.forEach((item, idx) => {
|
|
196
|
+
const color = item.color || chartColors[idx % chartColors.length];
|
|
197
|
+
html += `<div class="progress-row">
|
|
198
|
+
<div class="progress-label">${item.label}</div>
|
|
199
|
+
<div class="progress-track"><div class="progress-fill" style="width:${item.value}%;background:${color}"></div></div>
|
|
200
|
+
<div class="progress-pct" style="color:${color}">${item.value}%</div>
|
|
201
|
+
</div>`;
|
|
202
|
+
});
|
|
203
|
+
html += `</div>`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Two cards side by side
|
|
207
|
+
if (section.type === 'grid-2' && section.left && section.right) {
|
|
208
|
+
html += `<div class="grid grid-2">`;
|
|
209
|
+
html += `<div class="card"><h3>${section.left.title || ''}</h3>${section.left.html || ''}</div>`;
|
|
210
|
+
html += `<div class="card"><h3>${section.right.title || ''}</h3>${section.right.html || ''}</div>`;
|
|
211
|
+
html += `</div>`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Footer
|
|
216
|
+
html += `<div class="footer">Squidclaw AI 🦑 · Generated ${new Date().toLocaleString()}</div>`;
|
|
217
|
+
html += `</body></html>`;
|
|
218
|
+
|
|
219
|
+
return html;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Smart parser: markdown → dashboard data
|
|
223
|
+
function parseMarkdownToDashboard(content, title) {
|
|
224
|
+
const sections = [];
|
|
225
|
+
const lines = content.split('\n').map(l => l.trim()).filter(Boolean);
|
|
226
|
+
let current = null;
|
|
227
|
+
|
|
228
|
+
for (const line of lines) {
|
|
229
|
+
if (line.startsWith('## ') || line.startsWith('# ')) {
|
|
230
|
+
if (current) sections.push(current);
|
|
231
|
+
current = { heading: line.replace(/^#+\s*/, ''), type: null, items: [], rows: [], headers: null, title: '' };
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (!current) current = { type: null, items: [], rows: [], headers: null, title: '' };
|
|
235
|
+
|
|
236
|
+
if (line.match(/^\[stats?\]/i)) { current.type = 'stats'; continue; }
|
|
237
|
+
if (line.match(/^\[bar\s*chart?\]/i) || line.match(/^\[chart\]/i)) { current.type = 'bar'; continue; }
|
|
238
|
+
if (line.match(/^\[donut|pie\]/i)) { current.type = 'donut'; continue; }
|
|
239
|
+
if (line.match(/^\[table\]/i)) { current.type = 'table'; continue; }
|
|
240
|
+
if (line.match(/^\[progress\]/i)) { current.type = 'progress'; continue; }
|
|
241
|
+
|
|
242
|
+
// Stats line: - icon value — label (change)
|
|
243
|
+
if ((current.type === 'stats' || !current.type) && line.startsWith('-')) {
|
|
244
|
+
const m = line.match(/^-\s*(.+?)\s+(.+?)\s*[—–-]\s*(.+?)(?:\s*\((.+?)\))?$/);
|
|
245
|
+
if (m) {
|
|
246
|
+
current.type = 'stats';
|
|
247
|
+
current.items.push({ icon: m[1], value: m[2], label: m[3].trim(), change: m[4] || null });
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Chart/bar: - label: value
|
|
253
|
+
if ((current.type === 'bar' || current.type === 'donut') && line.startsWith('-')) {
|
|
254
|
+
const m = line.match(/^-\s*(.+?):\s*(\d+)/);
|
|
255
|
+
if (m) { current.items.push({ label: m[1], value: parseInt(m[2]) }); continue; }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Progress: - label: value%
|
|
259
|
+
if (current.type === 'progress' && line.startsWith('-')) {
|
|
260
|
+
const m = line.match(/^-\s*(.+?):\s*(\d+)%?/);
|
|
261
|
+
if (m) { current.items.push({ label: m[1], value: parseInt(m[2]) }); continue; }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Table
|
|
265
|
+
if (line.startsWith('|') && line.endsWith('|')) {
|
|
266
|
+
if (line.match(/^\|[\s-:|]+\|$/)) continue;
|
|
267
|
+
const cells = line.split('|').filter(c => c.trim()).map(c => c.trim());
|
|
268
|
+
if (!current.headers) { current.headers = cells; current.type = 'table'; }
|
|
269
|
+
else current.rows.push(cells);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Auto-detect chart data from plain "- label: number"
|
|
274
|
+
if (line.startsWith('-') && !current.type) {
|
|
275
|
+
const m = line.match(/^-\s*(.+?):\s*(\d+)/);
|
|
276
|
+
if (m) { current.type = 'bar'; current.items.push({ label: m[1], value: parseInt(m[2]) }); continue; }
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (current) sections.push(current);
|
|
280
|
+
|
|
281
|
+
return { title, sections };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export async function renderDashboard(input) {
|
|
285
|
+
mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
286
|
+
|
|
287
|
+
let data, theme;
|
|
288
|
+
if (typeof input === 'string') {
|
|
289
|
+
// Parse markdown
|
|
290
|
+
const parts = input.split('|');
|
|
291
|
+
const title = parts[0]?.trim() || 'Dashboard';
|
|
292
|
+
theme = parts[1]?.trim() || 'dark';
|
|
293
|
+
const content = parts.slice(2).join('|') || parts.slice(1).join('|');
|
|
294
|
+
data = parseMarkdownToDashboard(content, title);
|
|
295
|
+
} else {
|
|
296
|
+
data = input.data || input;
|
|
297
|
+
theme = input.theme || 'dark';
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const dashHtml = buildDashboardHtml(data, theme);
|
|
301
|
+
|
|
302
|
+
// Try Puppeteer
|
|
303
|
+
try {
|
|
304
|
+
const puppeteer = await import('puppeteer-core');
|
|
305
|
+
const paths = ['/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium-browser', '/usr/bin/chromium', '/snap/bin/chromium'];
|
|
306
|
+
let execPath = null;
|
|
307
|
+
for (const p of paths) { if (existsSync(p)) { execPath = p; break; } }
|
|
308
|
+
|
|
309
|
+
if (execPath) {
|
|
310
|
+
const browser = await puppeteer.default.launch({
|
|
311
|
+
executablePath: execPath, headless: 'new',
|
|
312
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
|
|
313
|
+
});
|
|
314
|
+
const page = await browser.newPage();
|
|
315
|
+
await page.setViewport({ width: 1000, height: 800 });
|
|
316
|
+
await page.setContent(dashHtml, { waitUntil: 'networkidle0', timeout: 10000 });
|
|
317
|
+
const height = await page.evaluate(() => document.body.scrollHeight);
|
|
318
|
+
await page.setViewport({ width: 1000, height: Math.min(height + 40, 3000) });
|
|
319
|
+
const screenshot = await page.screenshot({ type: 'png', fullPage: true });
|
|
320
|
+
await browser.close();
|
|
321
|
+
|
|
322
|
+
const filename = 'dashboard_' + Date.now() + '.png';
|
|
323
|
+
const filepath = join(OUTPUT_DIR, filename);
|
|
324
|
+
writeFileSync(filepath, screenshot);
|
|
325
|
+
|
|
326
|
+
logger.info('dashboard', `Rendered: ${filepath}`);
|
|
327
|
+
return { filepath, filename, buffer: screenshot, html: dashHtml };
|
|
328
|
+
}
|
|
329
|
+
} catch (err) {
|
|
330
|
+
logger.warn('dashboard', 'Puppeteer failed: ' + err.message);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Fallback: save HTML
|
|
334
|
+
const filename = 'dashboard_' + Date.now() + '.html';
|
|
335
|
+
const filepath = join(OUTPUT_DIR, filename);
|
|
336
|
+
writeFileSync(filepath, dashHtml);
|
|
337
|
+
return { filepath, filename, html: dashHtml };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export { buildDashboardHtml, parseMarkdownToDashboard, DASH_THEMES };
|
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🦑 PPTX PRO — Smart Presentation Engine
|
|
3
|
+
*
|
|
4
|
+
* Takes raw content and auto-generates professional multi-slide decks.
|
|
5
|
+
* AI just says what it wants → engine handles layout, design, charts, animations.
|
|
6
|
+
*
|
|
7
|
+
* Smart features:
|
|
8
|
+
* - Auto-detects content type (stats, table, chart, quote, timeline, comparison)
|
|
9
|
+
* - Markdown → proper slides conversion
|
|
10
|
+
* - Professional layouts with consistent branding
|
|
11
|
+
* - Master slide designs with gradients, shapes, accents
|
|
12
|
+
* - Auto icon assignment
|
|
13
|
+
* - Smart color palettes
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import PptxGenJS from 'pptxgenjs';
|
|
17
|
+
import { mkdirSync, existsSync } from 'fs';
|
|
18
|
+
import { join } from 'path';
|
|
19
|
+
import { logger } from '../core/logger.js';
|
|
20
|
+
|
|
21
|
+
const OUTPUT_DIR = '/tmp/squidclaw-pptx';
|
|
22
|
+
|
|
23
|
+
// ── Professional Themes ──
|
|
24
|
+
|
|
25
|
+
const THEMES = {
|
|
26
|
+
executive: {
|
|
27
|
+
bg: '0D1117', accent: '58A6FF', accent2: '3FB950', text: 'FFFFFF', subtle: '8B949E',
|
|
28
|
+
gradStart: '0D1117', gradEnd: '161B22', cardBg: '161B22', border: '30363D',
|
|
29
|
+
},
|
|
30
|
+
corporate: {
|
|
31
|
+
bg: 'FFFFFF', accent: '1B4F72', accent2: '2E86C1', text: '2C3E50', subtle: '7F8C8D',
|
|
32
|
+
gradStart: '1B4F72', gradEnd: '2E86C1', cardBg: 'F8F9FA', border: 'DEE2E6',
|
|
33
|
+
},
|
|
34
|
+
saudi: {
|
|
35
|
+
bg: 'FFFFFF', accent: '006C35', accent2: 'C8A951', text: '1A1A2E', subtle: '6B7280',
|
|
36
|
+
gradStart: '006C35', gradEnd: '004D25', cardBg: 'F0FDF4', border: 'BBF7D0',
|
|
37
|
+
},
|
|
38
|
+
modern: {
|
|
39
|
+
bg: '1A1A2E', accent: 'E94560', accent2: '0F3460', text: 'EAEAEA', subtle: '9CA3AF',
|
|
40
|
+
gradStart: '16213E', gradEnd: '1A1A2E', cardBg: '16213E', border: '374151',
|
|
41
|
+
},
|
|
42
|
+
ocean: {
|
|
43
|
+
bg: '0A192F', accent: '64FFDA', accent2: '5EEAD4', text: 'CCD6F6', subtle: '8892B0',
|
|
44
|
+
gradStart: '0A192F', gradEnd: '112240', cardBg: '112240', border: '1E3A5F',
|
|
45
|
+
},
|
|
46
|
+
gradient: {
|
|
47
|
+
bg: '0F0C29', accent: 'F7971E', accent2: 'FFD200', text: 'FFFFFF', subtle: 'B0B0B0',
|
|
48
|
+
gradStart: '302B63', gradEnd: '24243E', cardBg: '1E1E3F', border: '4A4A7A',
|
|
49
|
+
},
|
|
50
|
+
minimal: {
|
|
51
|
+
bg: 'FAFAFA', accent: '111111', accent2: '555555', text: '111111', subtle: '999999',
|
|
52
|
+
gradStart: '111111', gradEnd: '333333', cardBg: 'FFFFFF', border: 'E5E5E5',
|
|
53
|
+
},
|
|
54
|
+
fire: {
|
|
55
|
+
bg: '1A0000', accent: 'FF4500', accent2: 'FF8C00', text: 'FFFFFF', subtle: 'CC9999',
|
|
56
|
+
gradStart: '8B0000', gradEnd: '1A0000', cardBg: '2D0000', border: '661111',
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ── Auto Icons ──
|
|
61
|
+
const TOPIC_ICONS = {
|
|
62
|
+
revenue: '💰', money: '💰', gdp: '💰', profit: '💰', income: '💰', cost: '💰', price: '💰', budget: '💰', finance: '💰',
|
|
63
|
+
growth: '📈', increase: '📈', rise: '📈', trend: '📈', up: '📈',
|
|
64
|
+
users: '👥', people: '👥', team: '👥', employees: '👥', population: '👥', workforce: '👥',
|
|
65
|
+
time: '⏱️', speed: '⚡', fast: '⚡', performance: '⚡',
|
|
66
|
+
global: '🌍', world: '🌍', international: '🌍', country: '🌍',
|
|
67
|
+
tech: '💻', software: '💻', digital: '💻', ai: '🤖', data: '📊',
|
|
68
|
+
security: '🔒', safe: '🔒', protect: '🛡️',
|
|
69
|
+
health: '🏥', medical: '🏥', hospital: '🏥',
|
|
70
|
+
education: '🎓', school: '🎓', university: '🎓', training: '🎓',
|
|
71
|
+
oil: '⛽', energy: '⚡', power: '⚡',
|
|
72
|
+
tourism: '✈️', travel: '✈️', visitors: '✈️',
|
|
73
|
+
construction: '🏗️', building: '🏗️', infrastructure: '🏗️',
|
|
74
|
+
investment: '📊', fdi: '📊', invest: '📊',
|
|
75
|
+
target: '🎯', goal: '🎯', objective: '🎯',
|
|
76
|
+
success: '✅', complete: '✅', done: '✅', achievement: '🏆',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function autoIcon(text) {
|
|
80
|
+
const lower = text.toLowerCase();
|
|
81
|
+
for (const [key, icon] of Object.entries(TOPIC_ICONS)) {
|
|
82
|
+
if (lower.includes(key)) return icon;
|
|
83
|
+
}
|
|
84
|
+
return '📌';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Smart Content Parser ──
|
|
88
|
+
|
|
89
|
+
function parseMarkdownToSlides(content) {
|
|
90
|
+
const slides = [];
|
|
91
|
+
const lines = content.split('\n').map(l => l.trim()).filter(Boolean);
|
|
92
|
+
|
|
93
|
+
let currentSlide = null;
|
|
94
|
+
let currentSection = null;
|
|
95
|
+
|
|
96
|
+
for (let i = 0; i < lines.length; i++) {
|
|
97
|
+
const line = lines[i];
|
|
98
|
+
|
|
99
|
+
// H1/H2 = new slide
|
|
100
|
+
if (line.startsWith('# ') || line.startsWith('## ')) {
|
|
101
|
+
if (currentSlide) slides.push(currentSlide);
|
|
102
|
+
const title = line.replace(/^#+\s*/, '').replace(/\*\*/g, '');
|
|
103
|
+
currentSlide = { title, type: 'content', body: [], stats: [], table: [], chart: [], bullets: [], timeline: [], comparisons: [], quote: null };
|
|
104
|
+
currentSection = 'body';
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!currentSlide) {
|
|
109
|
+
currentSlide = { title: 'Overview', type: 'content', body: [], stats: [], table: [], chart: [], bullets: [], timeline: [], comparisons: [], quote: null };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// [stats] marker
|
|
113
|
+
if (line.match(/^\[stats?\]/i)) { currentSection = 'stats'; continue; }
|
|
114
|
+
if (line.match(/^\[table\]/i)) { currentSection = 'table'; continue; }
|
|
115
|
+
if (line.match(/^\[chart\]/i)) { currentSection = 'chart'; continue; }
|
|
116
|
+
if (line.match(/^\[timeline\]/i)) { currentSection = 'timeline'; continue; }
|
|
117
|
+
if (line.match(/^\[compare|comparison\]/i)) { currentSection = 'comparison'; continue; }
|
|
118
|
+
if (line.match(/^\[quote\]/i)) { currentSection = 'quote'; continue; }
|
|
119
|
+
|
|
120
|
+
// Stats: - icon value — label OR - label: value
|
|
121
|
+
if (currentSection === 'stats' && line.startsWith('-')) {
|
|
122
|
+
const m = line.match(/^-\s*(.+?)\s+(.+?)\s*[—–-]\s*(.+)/) || line.match(/^-\s*(.+?):\s*(.+)/);
|
|
123
|
+
if (m) {
|
|
124
|
+
if (m[3]) {
|
|
125
|
+
currentSlide.stats.push({ icon: m[1].trim(), value: m[2].trim(), label: m[3].trim() });
|
|
126
|
+
} else {
|
|
127
|
+
currentSlide.stats.push({ icon: autoIcon(m[1]), value: m[2].trim(), label: m[1].trim() });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Table: | col | col |
|
|
134
|
+
if (line.startsWith('|') && line.endsWith('|')) {
|
|
135
|
+
if (line.match(/^\|[\s-:|]+\|$/)) continue; // separator row
|
|
136
|
+
const cells = line.split('|').filter(c => c.trim()).map(c => c.trim());
|
|
137
|
+
currentSlide.table.push(cells);
|
|
138
|
+
currentSection = 'table';
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Chart: - label: value
|
|
143
|
+
if (currentSection === 'chart' && line.startsWith('-')) {
|
|
144
|
+
const m = line.match(/^-\s*(.+?):\s*(\d+)/);
|
|
145
|
+
if (m) currentSlide.chart.push({ label: m[1].trim(), value: parseInt(m[2]) });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Timeline: N. Title — description
|
|
150
|
+
if (currentSection === 'timeline' && line.match(/^\d+\./)) {
|
|
151
|
+
const m = line.match(/^(\d+)\.\s*\*?\*?(.+?)\*?\*?\s*[—–-]\s*(.+)/);
|
|
152
|
+
if (m) currentSlide.timeline.push({ year: m[1], title: m[2].trim(), desc: m[3].trim() });
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Quote
|
|
157
|
+
if (line.startsWith('>') || currentSection === 'quote') {
|
|
158
|
+
const q = line.replace(/^>\s*/, '').replace(/\*\*/g, '');
|
|
159
|
+
if (q) currentSlide.quote = (currentSlide.quote || '') + q + ' ';
|
|
160
|
+
currentSection = 'quote';
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Bullets
|
|
165
|
+
if (line.startsWith('-') || line.startsWith('•') || line.startsWith('*')) {
|
|
166
|
+
const bullet = line.replace(/^[-•*]\s*/, '').replace(/\*\*/g, '');
|
|
167
|
+
currentSlide.bullets.push(bullet);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// H3 = subsection title, add as body
|
|
172
|
+
if (line.startsWith('### ')) {
|
|
173
|
+
currentSlide.body.push({ type: 'heading', text: line.replace(/^###\s*/, '') });
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Plain text
|
|
178
|
+
currentSlide.body.push({ type: 'text', text: line.replace(/\*\*/g, '') });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (currentSlide) slides.push(currentSlide);
|
|
182
|
+
|
|
183
|
+
// Auto-detect best slide type
|
|
184
|
+
for (const slide of slides) {
|
|
185
|
+
if (slide.stats.length >= 3) slide.type = 'stats';
|
|
186
|
+
else if (slide.table.length >= 2) slide.type = 'table';
|
|
187
|
+
else if (slide.chart.length >= 2) slide.type = 'chart';
|
|
188
|
+
else if (slide.timeline.length >= 2) slide.type = 'timeline';
|
|
189
|
+
else if (slide.quote) slide.type = 'quote';
|
|
190
|
+
else if (slide.bullets.length >= 3 && slide.stats.length >= 2) slide.type = 'stats-bullets';
|
|
191
|
+
else if (slide.bullets.length >= 2) slide.type = 'bullets';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Split oversized slides
|
|
195
|
+
const final = [];
|
|
196
|
+
for (const slide of slides) {
|
|
197
|
+
if (slide.stats.length > 6) {
|
|
198
|
+
// Split stats across slides
|
|
199
|
+
for (let j = 0; j < slide.stats.length; j += 4) {
|
|
200
|
+
final.push({ ...slide, stats: slide.stats.slice(j, j + 4), type: 'stats', title: j === 0 ? slide.title : slide.title + ' (cont.)' });
|
|
201
|
+
}
|
|
202
|
+
} else if (slide.type === 'content' && slide.body.length > 8) {
|
|
203
|
+
// Split long content
|
|
204
|
+
const mid = Math.ceil(slide.body.length / 2);
|
|
205
|
+
final.push({ ...slide, body: slide.body.slice(0, mid) });
|
|
206
|
+
final.push({ ...slide, body: slide.body.slice(mid), title: slide.title + ' (cont.)' });
|
|
207
|
+
} else {
|
|
208
|
+
final.push(slide);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Also break out table/chart as separate slides if they exist alongside stats
|
|
212
|
+
if (slide.table.length >= 2 && slide.type !== 'table') {
|
|
213
|
+
final.push({ title: slide.title + ' — Data', type: 'table', table: slide.table, body: [], stats: [], chart: [], bullets: [], timeline: [], comparisons: [], quote: null });
|
|
214
|
+
}
|
|
215
|
+
if (slide.chart.length >= 2 && slide.type !== 'chart') {
|
|
216
|
+
final.push({ title: slide.title + ' — Chart', type: 'chart', chart: slide.chart, body: [], stats: [], table: [], bullets: [], timeline: [], comparisons: [], quote: null });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return final;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── Slide Renderers ──
|
|
224
|
+
|
|
225
|
+
function addTitleSlide(pptx, title, subtitle, theme) {
|
|
226
|
+
const slide = pptx.addSlide();
|
|
227
|
+
const t = THEMES[theme] || THEMES.executive;
|
|
228
|
+
|
|
229
|
+
slide.background = { fill: t.gradStart };
|
|
230
|
+
|
|
231
|
+
// Accent bar top
|
|
232
|
+
slide.addShape(pptx.ShapeType.rect, { x: 0, y: 0, w: '100%', h: 0.06, fill: { color: t.accent } });
|
|
233
|
+
|
|
234
|
+
// Decorative circle
|
|
235
|
+
slide.addShape(pptx.ShapeType.ellipse, {
|
|
236
|
+
x: 7.5, y: 2, w: 3.5, h: 3.5,
|
|
237
|
+
fill: { color: t.accent, transparency: 90 },
|
|
238
|
+
line: { color: t.accent, width: 2, transparency: 50 },
|
|
239
|
+
});
|
|
240
|
+
slide.addShape(pptx.ShapeType.ellipse, {
|
|
241
|
+
x: 8, y: 2.5, w: 2.5, h: 2.5,
|
|
242
|
+
fill: { color: t.accent2, transparency: 92 },
|
|
243
|
+
line: { color: t.accent2, width: 1, transparency: 60 },
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
slide.addText(title, {
|
|
247
|
+
x: 0.8, y: 1.5, w: 7, h: 2,
|
|
248
|
+
fontSize: 40, fontFace: 'Arial', color: t.text, bold: true,
|
|
249
|
+
lineSpacingMultiple: 1.1,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
if (subtitle) {
|
|
253
|
+
slide.addText(subtitle, {
|
|
254
|
+
x: 0.8, y: 3.5, w: 6, h: 0.6,
|
|
255
|
+
fontSize: 16, fontFace: 'Arial', color: t.subtle,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Bottom accent line
|
|
260
|
+
slide.addShape(pptx.ShapeType.rect, { x: 0.8, y: 3.2, w: 2, h: 0.05, fill: { color: t.accent } });
|
|
261
|
+
|
|
262
|
+
// Footer
|
|
263
|
+
addFooter(slide, pptx, t, null, null);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function addStatsSlide(pptx, slideData, theme, slideNum, total) {
|
|
267
|
+
const slide = pptx.addSlide();
|
|
268
|
+
const t = THEMES[theme] || THEMES.executive;
|
|
269
|
+
|
|
270
|
+
slide.background = { fill: t.bg };
|
|
271
|
+
addSlideHeader(slide, pptx, slideData.title, t);
|
|
272
|
+
|
|
273
|
+
const stats = slideData.stats.slice(0, 6);
|
|
274
|
+
const cols = Math.min(stats.length, 3);
|
|
275
|
+
const rows = Math.ceil(stats.length / cols);
|
|
276
|
+
const cardW = 2.6;
|
|
277
|
+
const cardH = 1.6;
|
|
278
|
+
const gap = 0.3;
|
|
279
|
+
const startX = (10 - (cols * cardW + (cols - 1) * gap)) / 2;
|
|
280
|
+
const startY = rows > 1 ? 1.8 : 2.2;
|
|
281
|
+
|
|
282
|
+
stats.forEach((stat, i) => {
|
|
283
|
+
const col = i % cols;
|
|
284
|
+
const row = Math.floor(i / cols);
|
|
285
|
+
const x = startX + col * (cardW + gap);
|
|
286
|
+
const y = startY + row * (cardH + gap);
|
|
287
|
+
|
|
288
|
+
// Card background
|
|
289
|
+
slide.addShape(pptx.ShapeType.roundRect, {
|
|
290
|
+
x, y, w: cardW, h: cardH, rectRadius: 0.15,
|
|
291
|
+
fill: { color: t.cardBg },
|
|
292
|
+
line: { color: t.border, width: 1 },
|
|
293
|
+
shadow: { type: 'outer', blur: 6, offset: 2, color: '000000', opacity: 0.15 },
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Icon
|
|
297
|
+
slide.addText(stat.icon || '📊', {
|
|
298
|
+
x, y: y + 0.15, w: cardW, h: 0.45,
|
|
299
|
+
fontSize: 24, align: 'center',
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Value
|
|
303
|
+
slide.addText(stat.value, {
|
|
304
|
+
x, y: y + 0.55, w: cardW, h: 0.5,
|
|
305
|
+
fontSize: 26, fontFace: 'Arial', color: t.accent, bold: true, align: 'center',
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Label
|
|
309
|
+
slide.addText(stat.label, {
|
|
310
|
+
x, y: y + 1.05, w: cardW, h: 0.35,
|
|
311
|
+
fontSize: 11, fontFace: 'Arial', color: t.subtle, align: 'center',
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Add bullets below if they exist
|
|
316
|
+
if (slideData.bullets.length > 0) {
|
|
317
|
+
const bulletY = startY + rows * (cardH + gap) + 0.2;
|
|
318
|
+
const bulletText = slideData.bullets.map(b => ({ text: ' • ' + b + '\n', options: { fontSize: 12, color: t.text } }));
|
|
319
|
+
slide.addText(bulletText, {
|
|
320
|
+
x: 0.8, y: bulletY, w: 8.4, h: 5 - bulletY,
|
|
321
|
+
fontFace: 'Arial', valign: 'top', lineSpacingMultiple: 1.4,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
addFooter(slide, pptx, t, slideNum, total);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function addTableSlide(pptx, slideData, theme, slideNum, total) {
|
|
329
|
+
const slide = pptx.addSlide();
|
|
330
|
+
const t = THEMES[theme] || THEMES.executive;
|
|
331
|
+
|
|
332
|
+
slide.background = { fill: t.bg };
|
|
333
|
+
addSlideHeader(slide, pptx, slideData.title, t);
|
|
334
|
+
|
|
335
|
+
if (slideData.table.length < 2) return;
|
|
336
|
+
|
|
337
|
+
const headers = slideData.table[0];
|
|
338
|
+
const rows = slideData.table.slice(1);
|
|
339
|
+
const tableData = [headers, ...rows];
|
|
340
|
+
|
|
341
|
+
const colW = Math.min(8.4 / headers.length, 3);
|
|
342
|
+
|
|
343
|
+
slide.addTable(tableData.map((row, ri) =>
|
|
344
|
+
row.map(cell => ({
|
|
345
|
+
text: cell,
|
|
346
|
+
options: {
|
|
347
|
+
fontSize: ri === 0 ? 12 : 11,
|
|
348
|
+
fontFace: 'Arial',
|
|
349
|
+
color: ri === 0 ? 'FFFFFF' : t.text,
|
|
350
|
+
bold: ri === 0,
|
|
351
|
+
fill: { color: ri === 0 ? t.accent : (ri % 2 === 0 ? t.cardBg : t.bg) },
|
|
352
|
+
border: { type: 'solid', color: t.border, pt: 0.5 },
|
|
353
|
+
valign: 'middle',
|
|
354
|
+
align: 'center',
|
|
355
|
+
margin: [4, 6, 4, 6],
|
|
356
|
+
}
|
|
357
|
+
}))
|
|
358
|
+
), {
|
|
359
|
+
x: 0.8, y: 1.8, w: 8.4,
|
|
360
|
+
autoPage: true,
|
|
361
|
+
autoPageRepeatHeader: true,
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
addFooter(slide, pptx, t, slideNum, total);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function addChartSlide(pptx, slideData, theme, slideNum, total) {
|
|
368
|
+
const slide = pptx.addSlide();
|
|
369
|
+
const t = THEMES[theme] || THEMES.executive;
|
|
370
|
+
|
|
371
|
+
slide.background = { fill: t.bg };
|
|
372
|
+
addSlideHeader(slide, pptx, slideData.title, t);
|
|
373
|
+
|
|
374
|
+
const items = slideData.chart;
|
|
375
|
+
if (items.length === 0) return;
|
|
376
|
+
|
|
377
|
+
const chartColors = [t.accent, t.accent2, 'D29922', 'F85149', 'BC8CFF', 'F778BA', 'A5D6FF', '7EE787'];
|
|
378
|
+
|
|
379
|
+
// Determine chart type
|
|
380
|
+
const useDonut = items.length <= 6;
|
|
381
|
+
|
|
382
|
+
if (useDonut) {
|
|
383
|
+
slide.addChart(pptx.charts.DOUGHNUT, [{
|
|
384
|
+
name: slideData.title,
|
|
385
|
+
labels: items.map(i => i.label),
|
|
386
|
+
values: items.map(i => i.value),
|
|
387
|
+
}], {
|
|
388
|
+
x: 0.8, y: 1.6, w: 5, h: 3.5,
|
|
389
|
+
showLegend: true, legendPos: 'r', legendFontSize: 11, legendColor: t.text,
|
|
390
|
+
chartColors: chartColors.slice(0, items.length),
|
|
391
|
+
dataLabelColor: 'FFFFFF', showPercent: true, dataLabelFontSize: 10,
|
|
392
|
+
holeSize: 55,
|
|
393
|
+
});
|
|
394
|
+
} else {
|
|
395
|
+
slide.addChart(pptx.charts.BAR, [{
|
|
396
|
+
name: slideData.title,
|
|
397
|
+
labels: items.map(i => i.label),
|
|
398
|
+
values: items.map(i => i.value),
|
|
399
|
+
}], {
|
|
400
|
+
x: 0.8, y: 1.6, w: 8.4, h: 3.5,
|
|
401
|
+
showLegend: false,
|
|
402
|
+
chartColors: [t.accent],
|
|
403
|
+
catAxisLabelColor: t.text, valAxisLabelColor: t.subtle,
|
|
404
|
+
catAxisLabelFontSize: 10, valAxisLabelFontSize: 9,
|
|
405
|
+
gridLineColor: t.border,
|
|
406
|
+
dataLabelColor: t.text, showValue: true, dataLabelFontSize: 9,
|
|
407
|
+
barDir: 'bar',
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
addFooter(slide, pptx, t, slideNum, total);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function addBulletsSlide(pptx, slideData, theme, slideNum, total) {
|
|
415
|
+
const slide = pptx.addSlide();
|
|
416
|
+
const t = THEMES[theme] || THEMES.executive;
|
|
417
|
+
|
|
418
|
+
slide.background = { fill: t.bg };
|
|
419
|
+
addSlideHeader(slide, pptx, slideData.title, t);
|
|
420
|
+
|
|
421
|
+
const bullets = slideData.bullets;
|
|
422
|
+
|
|
423
|
+
// Two-column layout if > 6 bullets
|
|
424
|
+
if (bullets.length > 6) {
|
|
425
|
+
const mid = Math.ceil(bullets.length / 2);
|
|
426
|
+
const left = bullets.slice(0, mid);
|
|
427
|
+
const right = bullets.slice(mid);
|
|
428
|
+
|
|
429
|
+
[left, right].forEach((col, ci) => {
|
|
430
|
+
const x = ci === 0 ? 0.8 : 5.2;
|
|
431
|
+
col.forEach((b, bi) => {
|
|
432
|
+
const y = 1.8 + bi * 0.55;
|
|
433
|
+
slide.addShape(pptx.ShapeType.roundRect, {
|
|
434
|
+
x, y, w: 0.3, h: 0.3, rectRadius: 0.05,
|
|
435
|
+
fill: { color: t.accent, transparency: 80 },
|
|
436
|
+
});
|
|
437
|
+
slide.addText('✦', { x, y, w: 0.3, h: 0.3, fontSize: 10, align: 'center', color: t.accent });
|
|
438
|
+
slide.addText(b, { x: x + 0.4, y, w: 3.8, h: 0.5, fontSize: 12, fontFace: 'Arial', color: t.text, valign: 'middle' });
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
} else {
|
|
442
|
+
bullets.forEach((b, i) => {
|
|
443
|
+
const y = 1.8 + i * 0.65;
|
|
444
|
+
|
|
445
|
+
// Accent dot
|
|
446
|
+
slide.addShape(pptx.ShapeType.ellipse, {
|
|
447
|
+
x: 1, y: y + 0.12, w: 0.16, h: 0.16,
|
|
448
|
+
fill: { color: t.accent },
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
slide.addText(b, {
|
|
452
|
+
x: 1.4, y, w: 7.5, h: 0.5,
|
|
453
|
+
fontSize: 14, fontFace: 'Arial', color: t.text, valign: 'middle',
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
addFooter(slide, pptx, t, slideNum, total);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function addQuoteSlide(pptx, slideData, theme, slideNum, total) {
|
|
462
|
+
const slide = pptx.addSlide();
|
|
463
|
+
const t = THEMES[theme] || THEMES.executive;
|
|
464
|
+
|
|
465
|
+
slide.background = { fill: t.gradStart };
|
|
466
|
+
|
|
467
|
+
// Big quote mark
|
|
468
|
+
slide.addText('"', {
|
|
469
|
+
x: 1, y: 1, w: 1.5, h: 1.5,
|
|
470
|
+
fontSize: 120, fontFace: 'Georgia', color: t.accent, transparency: 50,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
slide.addText(slideData.quote.trim(), {
|
|
474
|
+
x: 1.5, y: 2, w: 7, h: 2.5,
|
|
475
|
+
fontSize: 22, fontFace: 'Georgia', color: t.text, italic: true,
|
|
476
|
+
lineSpacingMultiple: 1.5, valign: 'middle',
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// Attribution line
|
|
480
|
+
slide.addShape(pptx.ShapeType.rect, { x: 1.5, y: 4.5, w: 1.5, h: 0.04, fill: { color: t.accent } });
|
|
481
|
+
|
|
482
|
+
addFooter(slide, pptx, t, slideNum, total);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function addContentSlide(pptx, slideData, theme, slideNum, total) {
|
|
486
|
+
const slide = pptx.addSlide();
|
|
487
|
+
const t = THEMES[theme] || THEMES.executive;
|
|
488
|
+
|
|
489
|
+
slide.background = { fill: t.bg };
|
|
490
|
+
addSlideHeader(slide, pptx, slideData.title, t);
|
|
491
|
+
|
|
492
|
+
let y = 1.8;
|
|
493
|
+
for (const item of slideData.body) {
|
|
494
|
+
if (item.type === 'heading') {
|
|
495
|
+
slide.addText(item.text, {
|
|
496
|
+
x: 0.8, y, w: 8.4, h: 0.5,
|
|
497
|
+
fontSize: 18, fontFace: 'Arial', color: t.accent, bold: true,
|
|
498
|
+
});
|
|
499
|
+
y += 0.6;
|
|
500
|
+
} else {
|
|
501
|
+
slide.addText(item.text, {
|
|
502
|
+
x: 0.8, y, w: 8.4, h: 0.45,
|
|
503
|
+
fontSize: 12, fontFace: 'Arial', color: t.text, lineSpacingMultiple: 1.4,
|
|
504
|
+
});
|
|
505
|
+
y += 0.5;
|
|
506
|
+
}
|
|
507
|
+
if (y > 4.8) break;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
addFooter(slide, pptx, t, slideNum, total);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function addTimelineSlide(pptx, slideData, theme, slideNum, total) {
|
|
514
|
+
const slide = pptx.addSlide();
|
|
515
|
+
const t = THEMES[theme] || THEMES.executive;
|
|
516
|
+
|
|
517
|
+
slide.background = { fill: t.bg };
|
|
518
|
+
addSlideHeader(slide, pptx, slideData.title, t);
|
|
519
|
+
|
|
520
|
+
const items = slideData.timeline.slice(0, 5);
|
|
521
|
+
const stepW = 8 / items.length;
|
|
522
|
+
|
|
523
|
+
// Horizontal line
|
|
524
|
+
slide.addShape(pptx.ShapeType.rect, {
|
|
525
|
+
x: 1, y: 2.8, w: 8, h: 0.04, fill: { color: t.accent },
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
items.forEach((item, i) => {
|
|
529
|
+
const x = 1 + i * stepW + stepW / 2 - 0.5;
|
|
530
|
+
|
|
531
|
+
// Circle node
|
|
532
|
+
slide.addShape(pptx.ShapeType.ellipse, {
|
|
533
|
+
x: x + 0.25, y: 2.6, w: 0.5, h: 0.5,
|
|
534
|
+
fill: { color: t.accent },
|
|
535
|
+
shadow: { type: 'outer', blur: 4, offset: 1, color: t.accent, opacity: 0.3 },
|
|
536
|
+
});
|
|
537
|
+
slide.addText(item.year, {
|
|
538
|
+
x: x + 0.25, y: 2.6, w: 0.5, h: 0.5,
|
|
539
|
+
fontSize: 10, fontFace: 'Arial', color: 'FFFFFF', bold: true, align: 'center', valign: 'middle',
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// Title + desc below
|
|
543
|
+
slide.addText(item.title, {
|
|
544
|
+
x: x - 0.2, y: 3.3, w: 1.4, h: 0.4,
|
|
545
|
+
fontSize: 11, fontFace: 'Arial', color: t.accent, bold: true, align: 'center',
|
|
546
|
+
});
|
|
547
|
+
slide.addText(item.desc, {
|
|
548
|
+
x: x - 0.3, y: 3.7, w: 1.6, h: 0.8,
|
|
549
|
+
fontSize: 9, fontFace: 'Arial', color: t.subtle, align: 'center', lineSpacingMultiple: 1.2,
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
addFooter(slide, pptx, t, slideNum, total);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function addThankYouSlide(pptx, theme, brandName) {
|
|
557
|
+
const slide = pptx.addSlide();
|
|
558
|
+
const t = THEMES[theme] || THEMES.executive;
|
|
559
|
+
|
|
560
|
+
slide.background = { fill: t.gradStart };
|
|
561
|
+
|
|
562
|
+
// Decorative circles
|
|
563
|
+
slide.addShape(pptx.ShapeType.ellipse, {
|
|
564
|
+
x: 6.5, y: 0.5, w: 4, h: 4,
|
|
565
|
+
fill: { color: t.accent, transparency: 92 },
|
|
566
|
+
line: { color: t.accent, width: 2, transparency: 60 },
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
slide.addText('Thank You', {
|
|
570
|
+
x: 0, y: 1.8, w: '100%', h: 1.2,
|
|
571
|
+
fontSize: 48, fontFace: 'Arial', color: t.text, bold: true, align: 'center',
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
slide.addShape(pptx.ShapeType.rect, { x: 4.2, y: 3.1, w: 1.6, h: 0.05, fill: { color: t.accent } });
|
|
575
|
+
|
|
576
|
+
slide.addText('Created with ' + (brandName || 'Squidclaw AI 🦑'), {
|
|
577
|
+
x: 0, y: 3.5, w: '100%', h: 0.5,
|
|
578
|
+
fontSize: 14, fontFace: 'Arial', color: t.subtle, align: 'center',
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ── Helpers ──
|
|
583
|
+
|
|
584
|
+
function addSlideHeader(slide, pptx, title, t) {
|
|
585
|
+
// Top accent bar
|
|
586
|
+
slide.addShape(pptx.ShapeType.rect, { x: 0, y: 0, w: '100%', h: 0.05, fill: { color: t.accent } });
|
|
587
|
+
|
|
588
|
+
// Title
|
|
589
|
+
slide.addText(title || '', {
|
|
590
|
+
x: 0.8, y: 0.3, w: 8.4, h: 0.8,
|
|
591
|
+
fontSize: 24, fontFace: 'Arial', color: t.text, bold: true,
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// Title underline
|
|
595
|
+
slide.addShape(pptx.ShapeType.rect, { x: 0.8, y: 1.15, w: 1.5, h: 0.04, fill: { color: t.accent } });
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function addFooter(slide, pptx, t, num, total) {
|
|
599
|
+
const footerY = 5.15;
|
|
600
|
+
|
|
601
|
+
// Footer line
|
|
602
|
+
slide.addShape(pptx.ShapeType.rect, { x: 0.5, y: footerY, w: 9, h: 0.01, fill: { color: t.border } });
|
|
603
|
+
|
|
604
|
+
// Brand
|
|
605
|
+
slide.addText('Squidclaw AI 🦑', {
|
|
606
|
+
x: 0.5, y: footerY + 0.05, w: 3, h: 0.35,
|
|
607
|
+
fontSize: 9, fontFace: 'Arial', color: t.subtle,
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// Page number
|
|
611
|
+
if (num && total) {
|
|
612
|
+
slide.addText(num + ' / ' + total, {
|
|
613
|
+
x: 7.5, y: footerY + 0.05, w: 2, h: 0.35,
|
|
614
|
+
fontSize: 9, fontFace: 'Arial', color: t.subtle, align: 'right',
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// ── Main Export ──
|
|
620
|
+
|
|
621
|
+
export async function generatePresentation(input) {
|
|
622
|
+
mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
623
|
+
|
|
624
|
+
const {
|
|
625
|
+
title = 'Presentation',
|
|
626
|
+
subtitle = '',
|
|
627
|
+
content = '',
|
|
628
|
+
slides: rawSlides = null,
|
|
629
|
+
theme = 'executive',
|
|
630
|
+
brand = 'Squidclaw AI 🦑',
|
|
631
|
+
} = typeof input === 'string' ? { content: input, title: 'Presentation' } : input;
|
|
632
|
+
|
|
633
|
+
const pptx = new PptxGenJS();
|
|
634
|
+
pptx.layout = 'LAYOUT_WIDE'; // 16:9
|
|
635
|
+
pptx.author = brand;
|
|
636
|
+
pptx.title = title;
|
|
637
|
+
|
|
638
|
+
// Parse content into slides
|
|
639
|
+
const parsedSlides = rawSlides || parseMarkdownToSlides(content);
|
|
640
|
+
const totalSlides = parsedSlides.length + 2; // +title +thankyou
|
|
641
|
+
|
|
642
|
+
// Title slide
|
|
643
|
+
addTitleSlide(pptx, title, subtitle || new Date().toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }), theme);
|
|
644
|
+
|
|
645
|
+
// Content slides
|
|
646
|
+
parsedSlides.forEach((slideData, i) => {
|
|
647
|
+
const num = i + 2;
|
|
648
|
+
switch (slideData.type) {
|
|
649
|
+
case 'stats':
|
|
650
|
+
case 'stats-bullets':
|
|
651
|
+
addStatsSlide(pptx, slideData, theme, num, totalSlides); break;
|
|
652
|
+
case 'table':
|
|
653
|
+
addTableSlide(pptx, slideData, theme, num, totalSlides); break;
|
|
654
|
+
case 'chart':
|
|
655
|
+
addChartSlide(pptx, slideData, theme, num, totalSlides); break;
|
|
656
|
+
case 'bullets':
|
|
657
|
+
addBulletsSlide(pptx, slideData, theme, num, totalSlides); break;
|
|
658
|
+
case 'quote':
|
|
659
|
+
addQuoteSlide(pptx, slideData, theme, num, totalSlides); break;
|
|
660
|
+
case 'timeline':
|
|
661
|
+
addTimelineSlide(pptx, slideData, theme, num, totalSlides); break;
|
|
662
|
+
default:
|
|
663
|
+
addContentSlide(pptx, slideData, theme, num, totalSlides); break;
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// Thank you slide
|
|
668
|
+
addThankYouSlide(pptx, theme, brand);
|
|
669
|
+
|
|
670
|
+
const filename = title.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '_').slice(0, 50) + '.pptx';
|
|
671
|
+
const filepath = join(OUTPUT_DIR, filename);
|
|
672
|
+
|
|
673
|
+
await pptx.writeFile({ fileName: filepath });
|
|
674
|
+
|
|
675
|
+
logger.info('pptx-pro', `Generated: ${filepath} (${parsedSlides.length + 2} slides, theme: ${theme})`);
|
|
676
|
+
|
|
677
|
+
return {
|
|
678
|
+
filepath,
|
|
679
|
+
filename,
|
|
680
|
+
slides: parsedSlides.length + 2,
|
|
681
|
+
theme,
|
|
682
|
+
types: [...new Set(parsedSlides.map(s => s.type))],
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
export { parseMarkdownToSlides, THEMES };
|
package/lib/tools/router.js
CHANGED
|
@@ -77,7 +77,16 @@ export class ToolRouter {
|
|
|
77
77
|
'- Section: ## New Section [section]',
|
|
78
78
|
'',
|
|
79
79
|
'Example:',
|
|
80
|
-
'
|
|
80
|
+
'Write content in markdown. Engine auto-detects slide types from content:',
|
|
81
|
+
'- ## Heading = new slide',
|
|
82
|
+
'- [stats] + lines like "- 💰 $500B — Market size" = stats cards slide',
|
|
83
|
+
'- | col | col | table rows = table slide',
|
|
84
|
+
'- [chart] + "- label: value" lines = chart slide (pie/bar/doughnut)',
|
|
85
|
+
'- [timeline] + "1. Title — desc" = timeline slide',
|
|
86
|
+
'- > quoted text = quote slide',
|
|
87
|
+
'- Bullet lists = bullets slide',
|
|
88
|
+
'',
|
|
89
|
+
'EXAMPLE: ---TOOL:pptx_slides:AI Market Report|executive|## Key Metrics\n[stats]\n- 🌍 195 — Countries\n- 💰 $500B — Market Size\n- 🚀 40% — Annual Growth\n- 👥 2.5M — AI Engineers\n\n## Revenue by Sector\n[chart]\n- Healthcare: 85\n- Finance: 72\n- Retail: 45\n- Energy: 38\n\n## Growth Timeline\n[timeline]\n1. 2020 — Early adoption phase\n2. 2022 — Rapid enterprise rollout\n3. 2024 — AI-first companies emerge\n4. 2026 — Full integration era\n\n## Market Data\n| Region | Revenue | Growth |\n| North America | $180B | 35% |\n| Europe | $120B | 28% |\n| Asia | $150B | 42% |\n\n> The companies that embrace AI today will define the industries of tomorrow.---');
|
|
81
90
|
|
|
82
91
|
tools.push('', '### Spawn Sub-Agent Session',
|
|
83
92
|
'---TOOL:session_spawn:task description---',
|
|
@@ -169,10 +178,11 @@ export class ToolRouter {
|
|
|
169
178
|
'---TOOL:handoff:reason---',
|
|
170
179
|
'Transfer the conversation to a human agent. Use when you cannot help further.');
|
|
171
180
|
|
|
172
|
-
tools.push('', '### Render Dashboard (sends as image!)',
|
|
173
|
-
'---TOOL:
|
|
174
|
-
'Render a
|
|
175
|
-
'
|
|
181
|
+
tools.push('', '### Render Dashboard PRO (sends as image!)',
|
|
182
|
+
'---TOOL:dashboard:Title|theme|markdown content---',
|
|
183
|
+
'Render a beautiful dashboard as image. Themes: dark, light, saudi, ocean, neon.',
|
|
184
|
+
'Use same markdown format as presentations. Engine auto-detects: [stats], [chart], [table], [progress], [donut].',
|
|
185
|
+
'EXAMPLE: ---TOOL:dashboard:Sales Dashboard|dark|## KPIs\n[stats]\n- 💰 $2.4M — Revenue (+12%)\n- 👥 1,234 — Customers (+8%)\n- 📈 78% — Conversion\n\n## Revenue by Product\n[chart]\n- Enterprise: 850\n- Pro: 420\n- Starter: 180\n\n## Targets\n[progress]\n- Q1 Revenue: 85%\n- New Customers: 72%\n- NPS Score: 91%---',
|
|
176
186
|
'', '### Render Chart (sends as image!)',
|
|
177
187
|
'---TOOL:canvas_chart:title|type|label:value,label:value---',
|
|
178
188
|
'Render a chart (bar, pie, doughnut) as image. Separate items with commas.',
|
|
@@ -370,45 +380,32 @@ export class ToolRouter {
|
|
|
370
380
|
}
|
|
371
381
|
case 'pptx':
|
|
372
382
|
case 'pptx_slides':
|
|
383
|
+
case 'pptx_pro':
|
|
373
384
|
case 'powerpoint':
|
|
374
385
|
case 'presentation': {
|
|
375
386
|
try {
|
|
376
|
-
const {
|
|
377
|
-
const gen = new PptxGenerator();
|
|
378
|
-
|
|
379
|
-
// Parse: title|theme|content or just content
|
|
387
|
+
const { generatePresentation } = await import('./pptx-pro.js');
|
|
380
388
|
const parts = toolArg.split('|');
|
|
381
|
-
let title, theme,
|
|
382
|
-
|
|
389
|
+
let title, theme, content;
|
|
383
390
|
if (parts.length >= 3) {
|
|
384
391
|
title = parts[0].trim();
|
|
385
392
|
theme = parts[1].trim();
|
|
386
|
-
|
|
393
|
+
content = parts.slice(2).join('|');
|
|
387
394
|
} else if (parts.length === 2) {
|
|
388
395
|
title = parts[0].trim();
|
|
389
|
-
|
|
390
|
-
theme = '
|
|
396
|
+
content = parts[1];
|
|
397
|
+
theme = 'executive';
|
|
391
398
|
} else {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
theme = 'corporate';
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const slides = PptxGenerator.parseContent(slideContent);
|
|
400
|
-
if (slides.length === 0) {
|
|
401
|
-
toolResult = 'No slides found. Use ## headings and - bullet points.';
|
|
402
|
-
break;
|
|
399
|
+
const firstH = toolArg.match(/^#\s+(.+)/m);
|
|
400
|
+
title = firstH ? firstH[1] : 'Presentation';
|
|
401
|
+
content = toolArg;
|
|
402
|
+
theme = 'executive';
|
|
403
403
|
}
|
|
404
|
-
|
|
405
|
-
const result = await gen.create(title, slides, { theme });
|
|
406
|
-
|
|
407
|
-
// Return file path for sending
|
|
404
|
+
const result = await generatePresentation({ title, theme, content });
|
|
408
405
|
return {
|
|
409
406
|
toolUsed: true,
|
|
410
407
|
toolName: 'pptx',
|
|
411
|
-
toolResult: 'PowerPoint created: ' + result.filename + ' (' + result.
|
|
408
|
+
toolResult: 'PowerPoint created: ' + result.filename + ' (' + result.slides + ' slides, types: ' + result.types.join(', ') + ')',
|
|
412
409
|
filePath: result.filepath,
|
|
413
410
|
fileName: result.filename,
|
|
414
411
|
cleanResponse
|
|
@@ -708,33 +705,17 @@ export class ToolRouter {
|
|
|
708
705
|
}
|
|
709
706
|
break;
|
|
710
707
|
}
|
|
711
|
-
case 'canvas_dashboard':
|
|
712
|
-
|
|
708
|
+
case 'canvas_dashboard':
|
|
709
|
+
case 'dashboard': {
|
|
713
710
|
try {
|
|
714
|
-
const
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
parts[1].split(',').forEach(s => {
|
|
721
|
-
const [icon, value, label] = s.trim().split(':');
|
|
722
|
-
if (value) stats.push({ icon: icon || '', value: value || '', label: label || '' });
|
|
723
|
-
});
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// Parse chart: label:value,label:value
|
|
727
|
-
const chart = [];
|
|
728
|
-
if (parts[2]) {
|
|
729
|
-
parts[2].split(',').forEach(c => {
|
|
730
|
-
const [label, value] = c.trim().split(':');
|
|
731
|
-
if (value) chart.push({ label: label || '', value: parseInt(value) || 0 });
|
|
732
|
-
});
|
|
711
|
+
const { renderDashboard } = await import('./dashboard-pro.js');
|
|
712
|
+
const result = await renderDashboard(toolArg);
|
|
713
|
+
if (result.buffer) {
|
|
714
|
+
return { toolUsed: true, toolName: 'dashboard', toolResult: 'Dashboard rendered', imageBase64: result.buffer.toString('base64'), mimeType: 'image/png', cleanResponse };
|
|
715
|
+
} else {
|
|
716
|
+
return { toolUsed: true, toolName: 'dashboard', toolResult: 'Dashboard created', filePath: result.filepath, fileName: result.filename, cleanResponse };
|
|
733
717
|
}
|
|
734
|
-
|
|
735
|
-
const result = await this._engine.canvas.renderDashboard({ title, stats, chart });
|
|
736
|
-
return { toolUsed: true, toolName: 'canvas', toolResult: 'Dashboard rendered', imageBase64: result.buffer.toString('base64'), mimeType: 'image/png', cleanResponse };
|
|
737
|
-
} catch (err) { toolResult = 'Canvas failed: ' + err.message; }
|
|
718
|
+
} catch (err) { toolResult = 'Dashboard failed: ' + err.message; }
|
|
738
719
|
break;
|
|
739
720
|
}
|
|
740
721
|
case 'canvas_chart': {
|