squidclaw 2.1.0 โ 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/channels/telegram/bot.js +12 -0
- package/lib/core/agent-tools-mixin.js +10 -0
- package/lib/middleware/ai-process.js +2 -0
- package/lib/middleware/commands.js +28 -15
- package/lib/middleware/response-sender.js +12 -0
- package/lib/tools/pptx.js +744 -0
- package/lib/tools/router.js +71 -1
- package/package.json +2 -1
|
@@ -202,6 +202,18 @@ export class TelegramManager {
|
|
|
202
202
|
/**
|
|
203
203
|
* Send voice note
|
|
204
204
|
*/
|
|
205
|
+
async sendDocument(agentId, contactId, filePath, fileName, caption, metadata = {}) {
|
|
206
|
+
const botInfo = this.bots.get(agentId);
|
|
207
|
+
if (!botInfo?.bot) throw new Error('Bot not running');
|
|
208
|
+
const chatId = contactId.replace('tg_', '');
|
|
209
|
+
const { readFileSync } = await import('fs');
|
|
210
|
+
const buffer = readFileSync(filePath);
|
|
211
|
+
await botInfo.bot.api.sendDocument(chatId, new InputFile(buffer, fileName || 'file'), {
|
|
212
|
+
caption: caption || '',
|
|
213
|
+
});
|
|
214
|
+
logger.info('telegram', 'Sent document: ' + fileName);
|
|
215
|
+
}
|
|
216
|
+
|
|
205
217
|
async sendVoiceNote(agentId, contactId, audioBuffer, metadata = {}) {
|
|
206
218
|
const botInfo = this.bots.get(agentId);
|
|
207
219
|
if (!botInfo?.bot) return;
|
|
@@ -58,6 +58,8 @@ export function addToolSupport(agent, toolRouter, knowledgeBase) {
|
|
|
58
58
|
const fullResponse = result.messages.join('\n');
|
|
59
59
|
toolRouter._currentContactId = contactId;
|
|
60
60
|
toolRouter.storage = agent.storage;
|
|
61
|
+
toolRouter._currentContactId = contactId;
|
|
62
|
+
toolRouter._currentPlatform = metadata?.platform;
|
|
61
63
|
const toolResult = await toolRouter.processResponse(fullResponse, agent.id);
|
|
62
64
|
|
|
63
65
|
if (toolResult.toolUsed && toolResult.toolName === 'remind' && toolResult.reminderTime) {
|
|
@@ -71,6 +73,14 @@ export function addToolSupport(agent, toolRouter, knowledgeBase) {
|
|
|
71
73
|
return result;
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
// File attachment (pptx, etc)
|
|
77
|
+
if (toolResult.toolUsed && toolResult.filePath) {
|
|
78
|
+
result.filePath = toolResult.filePath;
|
|
79
|
+
result.fileName = toolResult.fileName;
|
|
80
|
+
result.messages = [toolResult.toolResult || 'Here\'s your file! ๐'];
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
74
84
|
if (toolResult.toolUsed && (toolResult.imageBase64 || toolResult.imageUrl)) {
|
|
75
85
|
// Image generated โ pass through directly
|
|
76
86
|
result.image = { base64: toolResult.imageBase64, url: toolResult.imageUrl, mimeType: toolResult.mimeType };
|
|
@@ -12,6 +12,8 @@ export async function aiProcessMiddleware(ctx, next) {
|
|
|
12
12
|
messages: result.messages || [],
|
|
13
13
|
reaction: result.reaction || null,
|
|
14
14
|
image: result.image || null,
|
|
15
|
+
filePath: result.filePath || null,
|
|
16
|
+
fileName: result.fileName || null,
|
|
15
17
|
_reminder: result._reminder || null,
|
|
16
18
|
};
|
|
17
19
|
ctx.metadata.originalType = ctx.metadata.originalType || null;
|
|
@@ -39,27 +39,32 @@ export async function commandsMiddleware(ctx, next) {
|
|
|
39
39
|
|
|
40
40
|
if (cmd === '/status' || textCmd === 'status') {
|
|
41
41
|
const uptime = process.uptime();
|
|
42
|
-
const
|
|
42
|
+
const d = Math.floor(uptime / 86400);
|
|
43
|
+
const h = Math.floor((uptime % 86400) / 3600);
|
|
43
44
|
const m = Math.floor((uptime % 3600) / 60);
|
|
45
|
+
const uptimeStr = d > 0 ? d + 'd ' + h + 'h' : h + 'h ' + m + 'm';
|
|
44
46
|
const usage = await ctx.storage.getUsage(ctx.agentId) || {};
|
|
45
47
|
const tokens = (usage.input_tokens || 0) + (usage.output_tokens || 0);
|
|
46
|
-
const fmtT =
|
|
48
|
+
const fmtT = (n) => n >= 1e6 ? (n/1e6).toFixed(1)+'M' : n >= 1e3 ? (n/1e3).toFixed(1)+'K' : String(n || 0);
|
|
47
49
|
const waOn = Object.values(ctx.engine.whatsappManager?.getStatuses() || {}).some(s => s.connected);
|
|
50
|
+
const memCount = ctx.storage.db.prepare('SELECT COUNT(*) as c FROM memories WHERE agent_id = ?').get(ctx.agentId)?.c || 0;
|
|
51
|
+
const convCount = ctx.storage.db.prepare('SELECT COUNT(*) as c FROM conversations WHERE agent_id = ?').get(ctx.agentId)?.c || 0;
|
|
52
|
+
const reminderCount = ctx.storage.db.prepare("SELECT COUNT(*) as c FROM reminders WHERE agent_id = ? AND fired = 0").get(ctx.agentId)?.c || 0;
|
|
53
|
+
const ram = (process.memoryUsage().rss / 1024 / 1024).toFixed(0);
|
|
48
54
|
|
|
49
55
|
await ctx.reply([
|
|
50
|
-
|
|
51
|
-
'
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
'
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
'
|
|
61
|
-
|
|
62
|
-
`๐ฃ๏ธ Language: ${ctx.agent?.language || 'bilingual'}`,
|
|
56
|
+
'๐ฆ *Squidclaw v2.2.0*',
|
|
57
|
+
'*' + (ctx.agent?.name || 'Agent') + '* โ Status',
|
|
58
|
+
'๐ง Model: ' + (ctx.agent?.model || '?'),
|
|
59
|
+
'โก Pipeline: 14 middleware ยท 40+ skills',
|
|
60
|
+
'๐ฃ๏ธ Language: Bilingual (AR/EN)',
|
|
61
|
+
'๐ฌ Messages: ' + convCount,
|
|
62
|
+
'๐ช Tokens: ' + fmtT(tokens) + ' (โ' + fmtT(usage.input_tokens || 0) + ' โ' + fmtT(usage.output_tokens || 0) + ')',
|
|
63
|
+
'๐ฐ Cost: $' + (usage.cost_usd || 0).toFixed(4),
|
|
64
|
+
'โ๏ธ Telegram: ' + (ctx.engine.telegramManager ? '๐ข' : '๐ด') + ' ยท WhatsApp: ' + (waOn ? '๐ข' : '๐ด'),
|
|
65
|
+
'๐พ Memories: ' + memCount + ' ยท Reminders: ' + reminderCount,
|
|
66
|
+
'โฑ๏ธ Uptime: ' + uptimeStr + ' ยท RAM: ' + ram + 'MB',
|
|
67
|
+
'๐ Heartbeat: active',
|
|
63
68
|
].join('\n'));
|
|
64
69
|
return;
|
|
65
70
|
}
|
|
@@ -132,6 +137,14 @@ export async function commandsMiddleware(ctx, next) {
|
|
|
132
137
|
return;
|
|
133
138
|
}
|
|
134
139
|
|
|
140
|
+
if (cmd === '/reset' || cmd === '/restart') {
|
|
141
|
+
await ctx.reply('๐ Restarting Squidclaw...');
|
|
142
|
+
setTimeout(() => {
|
|
143
|
+
process.exit(0); // Process manager (pm2/systemd) will restart it
|
|
144
|
+
}, 1000);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
135
148
|
if (cmd === '/allow') {
|
|
136
149
|
const args = msg.slice(7).trim();
|
|
137
150
|
if (!args) { await ctx.reply('Usage: /allow <user_id or phone>'); return; }
|
|
@@ -29,6 +29,18 @@ export async function responseSenderMiddleware(ctx, next) {
|
|
|
29
29
|
|
|
30
30
|
// Send via appropriate channel
|
|
31
31
|
if (ctx.platform === 'telegram' && tm) {
|
|
32
|
+
// Send file attachment (pptx, etc)
|
|
33
|
+
if (response.filePath) {
|
|
34
|
+
try {
|
|
35
|
+
await tm.sendDocument(agentId, contactId, response.filePath, response.fileName, response.messages?.[0] || '');
|
|
36
|
+
await next();
|
|
37
|
+
return;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
logger.error('sender', 'File send failed: ' + err.message);
|
|
40
|
+
// Fall through to text
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
if (response.image) {
|
|
33
45
|
const photoData = response.image.url ? { url: response.image.url } : { base64: response.image.base64 };
|
|
34
46
|
await tm.sendPhoto(agentId, contactId, photoData, response.messages?.[0] || '', metadata);
|
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ๐ฆ PowerPoint Generator PRO
|
|
3
|
+
* Full-featured .pptx with charts, images, tables, layouts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { logger } from '../core/logger.js';
|
|
7
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
|
|
10
|
+
export class PptxGenerator {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.outputDir = '/tmp/squidclaw-pptx';
|
|
13
|
+
mkdirSync(this.outputDir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// โโ THEMES โโ
|
|
17
|
+
static THEMES = {
|
|
18
|
+
dark: { bg: '0d1117', bg2: '161b22', title: 'e6edf3', text: 'b1bac4', accent: '58a6ff', accent2: '3fb950', accent3: 'bc8cff', border: '30363d' },
|
|
19
|
+
light: { bg: 'ffffff', bg2: 'f6f8fa', title: '1f2328', text: '656d76', accent: '0969da', accent2: '1a7f37', accent3: '8250df', border: 'd0d7de' },
|
|
20
|
+
blue: { bg: '0a1628', bg2: '0f2440', title: 'ffffff', text: 'a3c4e8', accent: '4da6ff', accent2: '00d4aa', accent3: 'ff6b9d', border: '1e3a5f' },
|
|
21
|
+
green: { bg: '0d1f0d', bg2: '132a13', title: 'ffffff', text: 'a7d7a7', accent: '4ade80', accent2: 'facc15', accent3: 'fb923c', border: '1a3d1a' },
|
|
22
|
+
corporate: { bg: 'ffffff', bg2: 'f1f5f9', title: '0f172a', text: '475569', accent: '2563eb', accent2: '059669', accent3: 'dc2626', border: 'e2e8f0' },
|
|
23
|
+
red: { bg: '1a0000', bg2: '2d0a0a', title: 'ffffff', text: 'fca5a5', accent: 'ef4444', accent2: 'f59e0b', accent3: '8b5cf6', border: '450a0a' },
|
|
24
|
+
gradient: { bg: '0c0c1d', bg2: '1a1a3e', title: 'ffffff', text: 'c4b5fd', accent: '818cf8', accent2: 'f472b6', accent3: '34d399', border: '312e81' },
|
|
25
|
+
minimal: { bg: 'fafafa', bg2: 'f0f0f0', title: '111111', text: '555555', accent: '111111', accent2: '888888', accent3: 'bbbbbb', border: 'dddddd' },
|
|
26
|
+
saudi: { bg: '003c1f', bg2: '004d28', title: 'ffffff', text: 'c8e6c9', accent: '4caf50', accent2: 'ffd54f', accent3: 'ffffff', border: '1b5e20' },
|
|
27
|
+
ocean: { bg: '0a192f', bg2: '112240', title: 'ccd6f6', text: '8892b0', accent: '64ffda', accent2: 'ffd700', accent3: 'ff6b6b', border: '233554' },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create a full presentation
|
|
32
|
+
*/
|
|
33
|
+
async create(title, slides, options = {}) {
|
|
34
|
+
const pptxgen = (await import('pptxgenjs')).default;
|
|
35
|
+
const pres = new pptxgen();
|
|
36
|
+
|
|
37
|
+
pres.author = options.author || 'Squidclaw AI ๐ฆ';
|
|
38
|
+
pres.title = title;
|
|
39
|
+
pres.subject = options.subtitle || title;
|
|
40
|
+
pres.layout = 'LAYOUT_WIDE'; // 16:9
|
|
41
|
+
|
|
42
|
+
const theme = PptxGenerator.THEMES[options.theme] || PptxGenerator.THEMES.corporate;
|
|
43
|
+
|
|
44
|
+
// โโ TITLE SLIDE โโ
|
|
45
|
+
this._addTitleSlide(pres, title, options, theme);
|
|
46
|
+
|
|
47
|
+
// โโ CONTENT SLIDES โโ
|
|
48
|
+
for (let i = 0; i < slides.length; i++) {
|
|
49
|
+
const slide = slides[i];
|
|
50
|
+
const slideNum = i + 2;
|
|
51
|
+
const totalSlides = slides.length + 1;
|
|
52
|
+
|
|
53
|
+
switch (slide.type) {
|
|
54
|
+
case 'chart':
|
|
55
|
+
this._addChartSlide(pres, slide, theme, slideNum, totalSlides);
|
|
56
|
+
break;
|
|
57
|
+
case 'table':
|
|
58
|
+
this._addTableSlide(pres, slide, theme, slideNum, totalSlides);
|
|
59
|
+
break;
|
|
60
|
+
case 'comparison':
|
|
61
|
+
this._addComparisonSlide(pres, slide, theme, slideNum, totalSlides);
|
|
62
|
+
break;
|
|
63
|
+
case 'timeline':
|
|
64
|
+
this._addTimelineSlide(pres, slide, theme, slideNum, totalSlides);
|
|
65
|
+
break;
|
|
66
|
+
case 'stats':
|
|
67
|
+
this._addStatsSlide(pres, slide, theme, slideNum, totalSlides);
|
|
68
|
+
break;
|
|
69
|
+
case 'quote':
|
|
70
|
+
this._addQuoteSlide(pres, slide, theme, slideNum, totalSlides);
|
|
71
|
+
break;
|
|
72
|
+
case 'image':
|
|
73
|
+
this._addImageSlide(pres, slide, theme, slideNum, totalSlides);
|
|
74
|
+
break;
|
|
75
|
+
case 'section':
|
|
76
|
+
this._addSectionSlide(pres, slide, theme, slideNum, totalSlides);
|
|
77
|
+
break;
|
|
78
|
+
default:
|
|
79
|
+
this._addContentSlide(pres, slide, theme, slideNum, totalSlides);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// โโ THANK YOU SLIDE โโ
|
|
84
|
+
if (options.thankYou !== false) {
|
|
85
|
+
this._addThankYouSlide(pres, options, theme);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Save
|
|
89
|
+
const filename = title.replace(/[^a-zA-Z0-9\u0600-\u06FF ]/g, '').replace(/\s+/g, '_').slice(0, 50) + '.pptx';
|
|
90
|
+
const filepath = join(this.outputDir, filename);
|
|
91
|
+
const data = await pres.write({ outputType: 'nodebuffer' });
|
|
92
|
+
writeFileSync(filepath, data);
|
|
93
|
+
|
|
94
|
+
logger.info('pptx', `Created PRO: ${filepath} (${slides.length + 2} slides)`);
|
|
95
|
+
return { filepath, filename, slideCount: slides.length + 2 };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// โโ SLIDE BUILDERS โโ
|
|
99
|
+
|
|
100
|
+
_addBase(pres, theme, slideNum, totalSlides) {
|
|
101
|
+
const s = pres.addSlide();
|
|
102
|
+
s.background = { color: theme.bg };
|
|
103
|
+
|
|
104
|
+
// Top accent bar
|
|
105
|
+
s.addShape(pres.ShapeType.rect, {
|
|
106
|
+
x: 0, y: 0, w: '100%', h: 0.06, fill: { color: theme.accent },
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Footer
|
|
110
|
+
if (slideNum && totalSlides) {
|
|
111
|
+
s.addText(slideNum + ' / ' + totalSlides, {
|
|
112
|
+
x: 11.5, y: 7.1, w: 1.5, h: 0.3,
|
|
113
|
+
fontSize: 9, color: theme.text, align: 'right', italic: true,
|
|
114
|
+
});
|
|
115
|
+
s.addText('Squidclaw AI ๐ฆ', {
|
|
116
|
+
x: 0.3, y: 7.1, w: 2, h: 0.3,
|
|
117
|
+
fontSize: 9, color: theme.border,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return s;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
_addTitleSlide(pres, title, options, theme) {
|
|
124
|
+
const s = pres.addSlide();
|
|
125
|
+
s.background = { color: theme.bg };
|
|
126
|
+
|
|
127
|
+
// Large accent shape
|
|
128
|
+
s.addShape(pres.ShapeType.rect, {
|
|
129
|
+
x: 0, y: 0, w: '100%', h: 0.08, fill: { color: theme.accent },
|
|
130
|
+
});
|
|
131
|
+
s.addShape(pres.ShapeType.rect, {
|
|
132
|
+
x: 0.5, y: 5.5, w: 3, h: 0.05, fill: { color: theme.accent },
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Title
|
|
136
|
+
s.addText(title, {
|
|
137
|
+
x: 0.5, y: 1.8, w: 12, h: 1.5,
|
|
138
|
+
fontSize: 42, bold: true, color: theme.title,
|
|
139
|
+
fontFace: 'Arial',
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Subtitle
|
|
143
|
+
if (options.subtitle) {
|
|
144
|
+
s.addText(options.subtitle, {
|
|
145
|
+
x: 0.5, y: 3.4, w: 10, h: 0.8,
|
|
146
|
+
fontSize: 20, color: theme.text, fontFace: 'Arial',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Date + author
|
|
151
|
+
const date = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
|
152
|
+
s.addText((options.author || '') + (options.author ? ' โข ' : '') + date, {
|
|
153
|
+
x: 0.5, y: 5.8, w: 8, h: 0.5,
|
|
154
|
+
fontSize: 14, color: theme.accent, fontFace: 'Arial',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
_addContentSlide(pres, slide, theme, slideNum, total) {
|
|
159
|
+
const s = this._addBase(pres, theme, slideNum, total);
|
|
160
|
+
|
|
161
|
+
// Title
|
|
162
|
+
s.addText(slide.title || '', {
|
|
163
|
+
x: 0.5, y: 0.3, w: 12, h: 0.8,
|
|
164
|
+
fontSize: 28, bold: true, color: theme.title, fontFace: 'Arial',
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Accent line under title
|
|
168
|
+
s.addShape(pres.ShapeType.rect, {
|
|
169
|
+
x: 0.5, y: 1.15, w: 2.5, h: 0.04, fill: { color: theme.accent },
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (slide.bullets && slide.bullets.length > 0) {
|
|
173
|
+
const rows = slide.bullets.map(b => ({
|
|
174
|
+
text: b,
|
|
175
|
+
options: {
|
|
176
|
+
fontSize: 18, color: theme.text, fontFace: 'Arial',
|
|
177
|
+
bullet: { type: 'bullet', style: 'โ', color: theme.accent },
|
|
178
|
+
paraSpaceAfter: 10, paraSpaceBefore: 4,
|
|
179
|
+
indentLevel: 0,
|
|
180
|
+
},
|
|
181
|
+
}));
|
|
182
|
+
|
|
183
|
+
s.addText(rows, { x: 0.7, y: 1.5, w: 11.5, h: 5 });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (slide.content) {
|
|
187
|
+
s.addText(slide.content, {
|
|
188
|
+
x: 0.5, y: 1.5, w: 12, h: 5,
|
|
189
|
+
fontSize: 18, color: theme.text, fontFace: 'Arial',
|
|
190
|
+
valign: 'top', wrap: true, lineSpacing: 28,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
_addChartSlide(pres, slide, theme, slideNum, total) {
|
|
196
|
+
const s = this._addBase(pres, theme, slideNum, total);
|
|
197
|
+
|
|
198
|
+
s.addText(slide.title || 'Chart', {
|
|
199
|
+
x: 0.5, y: 0.3, w: 12, h: 0.7,
|
|
200
|
+
fontSize: 26, bold: true, color: theme.title, fontFace: 'Arial',
|
|
201
|
+
});
|
|
202
|
+
s.addShape(pres.ShapeType.rect, {
|
|
203
|
+
x: 0.5, y: 1.05, w: 2, h: 0.04, fill: { color: theme.accent },
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const chartType = slide.chartType || 'bar';
|
|
207
|
+
const chartTypes = {
|
|
208
|
+
bar: pres.ChartType.bar,
|
|
209
|
+
line: pres.ChartType.line,
|
|
210
|
+
pie: pres.ChartType.pie,
|
|
211
|
+
doughnut: pres.ChartType.doughnut,
|
|
212
|
+
area: pres.ChartType.area,
|
|
213
|
+
bar3d: pres.ChartType.bar3D,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const type = chartTypes[chartType] || pres.ChartType.bar;
|
|
217
|
+
const colors = [theme.accent, theme.accent2, theme.accent3, theme.text, theme.border];
|
|
218
|
+
|
|
219
|
+
const chartData = slide.chartData || [
|
|
220
|
+
{ name: 'Series 1', labels: ['A', 'B', 'C', 'D'], values: [10, 20, 30, 40] }
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
s.addChart(type, chartData, {
|
|
224
|
+
x: 0.8, y: 1.4, w: 11.5, h: 5.2,
|
|
225
|
+
showTitle: false,
|
|
226
|
+
showValue: slide.showValues !== false,
|
|
227
|
+
catAxisLabelColor: theme.text,
|
|
228
|
+
valAxisLabelColor: theme.text,
|
|
229
|
+
chartColors: colors,
|
|
230
|
+
legendColor: theme.text,
|
|
231
|
+
showLegend: chartData.length > 1,
|
|
232
|
+
legendPos: 'b',
|
|
233
|
+
dataLabelColor: theme.title,
|
|
234
|
+
dataLabelFontSize: 10,
|
|
235
|
+
valGridLine: { color: theme.border, size: 0.5 },
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
_addTableSlide(pres, slide, theme, slideNum, total) {
|
|
240
|
+
const s = this._addBase(pres, theme, slideNum, total);
|
|
241
|
+
|
|
242
|
+
s.addText(slide.title || 'Table', {
|
|
243
|
+
x: 0.5, y: 0.3, w: 12, h: 0.7,
|
|
244
|
+
fontSize: 26, bold: true, color: theme.title, fontFace: 'Arial',
|
|
245
|
+
});
|
|
246
|
+
s.addShape(pres.ShapeType.rect, {
|
|
247
|
+
x: 0.5, y: 1.05, w: 2, h: 0.04, fill: { color: theme.accent },
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const rows = slide.tableData || [['Header 1', 'Header 2'], ['Data 1', 'Data 2']];
|
|
251
|
+
const tableRows = rows.map((row, ri) =>
|
|
252
|
+
row.map(cell => ({
|
|
253
|
+
text: String(cell),
|
|
254
|
+
options: {
|
|
255
|
+
fontSize: ri === 0 ? 14 : 13,
|
|
256
|
+
bold: ri === 0,
|
|
257
|
+
color: ri === 0 ? 'ffffff' : theme.text,
|
|
258
|
+
fill: { color: ri === 0 ? theme.accent : (ri % 2 === 0 ? theme.bg2 : theme.bg) },
|
|
259
|
+
border: [{ color: theme.border, pt: 0.5 }],
|
|
260
|
+
align: 'left',
|
|
261
|
+
valign: 'middle',
|
|
262
|
+
fontFace: 'Arial',
|
|
263
|
+
},
|
|
264
|
+
}))
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const colW = 11.5 / (rows[0]?.length || 2);
|
|
268
|
+
s.addTable(tableRows, {
|
|
269
|
+
x: 0.5, y: 1.4, w: 12, h: 0.5,
|
|
270
|
+
colW: Array(rows[0]?.length || 2).fill(colW),
|
|
271
|
+
rowH: rows.map((_, i) => i === 0 ? 0.5 : 0.45),
|
|
272
|
+
margin: [5, 8, 5, 8],
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
_addComparisonSlide(pres, slide, theme, slideNum, total) {
|
|
277
|
+
const s = this._addBase(pres, theme, slideNum, total);
|
|
278
|
+
|
|
279
|
+
s.addText(slide.title || 'Comparison', {
|
|
280
|
+
x: 0.5, y: 0.3, w: 12, h: 0.7,
|
|
281
|
+
fontSize: 26, bold: true, color: theme.title, fontFace: 'Arial',
|
|
282
|
+
});
|
|
283
|
+
s.addShape(pres.ShapeType.rect, {
|
|
284
|
+
x: 0.5, y: 1.05, w: 2, h: 0.04, fill: { color: theme.accent },
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const left = slide.left || { title: 'Option A', points: ['Point 1'] };
|
|
288
|
+
const right = slide.right || { title: 'Option B', points: ['Point 1'] };
|
|
289
|
+
|
|
290
|
+
// Left column
|
|
291
|
+
s.addShape(pres.ShapeType.roundRect, {
|
|
292
|
+
x: 0.5, y: 1.4, w: 5.8, h: 5.2,
|
|
293
|
+
fill: { color: theme.bg2 }, rectRadius: 0.15,
|
|
294
|
+
line: { color: theme.accent, width: 1.5 },
|
|
295
|
+
});
|
|
296
|
+
s.addText(left.title, {
|
|
297
|
+
x: 0.8, y: 1.6, w: 5.2, h: 0.6,
|
|
298
|
+
fontSize: 20, bold: true, color: theme.accent, fontFace: 'Arial',
|
|
299
|
+
});
|
|
300
|
+
if (left.points) {
|
|
301
|
+
const lpts = left.points.map(p => ({
|
|
302
|
+
text: p,
|
|
303
|
+
options: { fontSize: 15, color: theme.text, bullet: { code: '2022', color: theme.accent }, paraSpaceAfter: 8 },
|
|
304
|
+
}));
|
|
305
|
+
s.addText(lpts, { x: 1, y: 2.3, w: 4.8, h: 4 });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// VS
|
|
309
|
+
s.addText('VS', {
|
|
310
|
+
x: 5.8, y: 3.5, w: 1.4, h: 0.6,
|
|
311
|
+
fontSize: 18, bold: true, color: theme.accent, align: 'center',
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Right column
|
|
315
|
+
s.addShape(pres.ShapeType.roundRect, {
|
|
316
|
+
x: 6.7, y: 1.4, w: 5.8, h: 5.2,
|
|
317
|
+
fill: { color: theme.bg2 }, rectRadius: 0.15,
|
|
318
|
+
line: { color: theme.accent2, width: 1.5 },
|
|
319
|
+
});
|
|
320
|
+
s.addText(right.title, {
|
|
321
|
+
x: 7, y: 1.6, w: 5.2, h: 0.6,
|
|
322
|
+
fontSize: 20, bold: true, color: theme.accent2, fontFace: 'Arial',
|
|
323
|
+
});
|
|
324
|
+
if (right.points) {
|
|
325
|
+
const rpts = right.points.map(p => ({
|
|
326
|
+
text: p,
|
|
327
|
+
options: { fontSize: 15, color: theme.text, bullet: { code: '2022', color: theme.accent2 }, paraSpaceAfter: 8 },
|
|
328
|
+
}));
|
|
329
|
+
s.addText(rpts, { x: 7.2, y: 2.3, w: 4.8, h: 4 });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
_addTimelineSlide(pres, slide, theme, slideNum, total) {
|
|
334
|
+
const s = this._addBase(pres, theme, slideNum, total);
|
|
335
|
+
|
|
336
|
+
s.addText(slide.title || 'Timeline', {
|
|
337
|
+
x: 0.5, y: 0.3, w: 12, h: 0.7,
|
|
338
|
+
fontSize: 26, bold: true, color: theme.title, fontFace: 'Arial',
|
|
339
|
+
});
|
|
340
|
+
s.addShape(pres.ShapeType.rect, {
|
|
341
|
+
x: 0.5, y: 1.05, w: 2, h: 0.04, fill: { color: theme.accent },
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const events = slide.events || [{ date: '2024', text: 'Event 1' }];
|
|
345
|
+
const count = Math.min(events.length, 6);
|
|
346
|
+
const totalW = 11.5;
|
|
347
|
+
const spacing = totalW / count;
|
|
348
|
+
|
|
349
|
+
// Timeline line
|
|
350
|
+
s.addShape(pres.ShapeType.rect, {
|
|
351
|
+
x: 0.5, y: 3.5, w: totalW, h: 0.04, fill: { color: theme.accent },
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
events.slice(0, 6).forEach((ev, i) => {
|
|
355
|
+
const x = 0.5 + (i * spacing) + (spacing / 2) - 0.8;
|
|
356
|
+
const above = i % 2 === 0;
|
|
357
|
+
|
|
358
|
+
// Circle
|
|
359
|
+
s.addShape(pres.ShapeType.ellipse, {
|
|
360
|
+
x: x + 0.55, y: 3.3, w: 0.4, h: 0.4,
|
|
361
|
+
fill: { color: theme.accent },
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Date
|
|
365
|
+
s.addText(ev.date || '', {
|
|
366
|
+
x: x, y: above ? 2.2 : 3.9, w: 1.6, h: 0.4,
|
|
367
|
+
fontSize: 13, bold: true, color: theme.accent, align: 'center', fontFace: 'Arial',
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Text
|
|
371
|
+
s.addText(ev.text || '', {
|
|
372
|
+
x: x - 0.2, y: above ? 2.6 : 4.3, w: 2, h: 0.8,
|
|
373
|
+
fontSize: 11, color: theme.text, align: 'center', fontFace: 'Arial', wrap: true,
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
_addStatsSlide(pres, slide, theme, slideNum, total) {
|
|
379
|
+
const s = this._addBase(pres, theme, slideNum, total);
|
|
380
|
+
|
|
381
|
+
s.addText(slide.title || 'Key Metrics', {
|
|
382
|
+
x: 0.5, y: 0.3, w: 12, h: 0.7,
|
|
383
|
+
fontSize: 26, bold: true, color: theme.title, fontFace: 'Arial',
|
|
384
|
+
});
|
|
385
|
+
s.addShape(pres.ShapeType.rect, {
|
|
386
|
+
x: 0.5, y: 1.05, w: 2, h: 0.04, fill: { color: theme.accent },
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const stats = slide.stats || [{ value: '100', label: 'Stat' }];
|
|
390
|
+
const count = Math.min(stats.length, 4);
|
|
391
|
+
const cardW = 2.5;
|
|
392
|
+
const gap = (12 - (count * cardW)) / (count + 1);
|
|
393
|
+
const colors = [theme.accent, theme.accent2, theme.accent3, theme.text];
|
|
394
|
+
|
|
395
|
+
stats.slice(0, 4).forEach((stat, i) => {
|
|
396
|
+
const x = gap + i * (cardW + gap);
|
|
397
|
+
|
|
398
|
+
// Card bg
|
|
399
|
+
s.addShape(pres.ShapeType.roundRect, {
|
|
400
|
+
x, y: 2, w: cardW, h: 3.5,
|
|
401
|
+
fill: { color: theme.bg2 }, rectRadius: 0.15,
|
|
402
|
+
line: { color: colors[i], width: 1.5 },
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// Icon/emoji
|
|
406
|
+
if (stat.icon) {
|
|
407
|
+
s.addText(stat.icon, {
|
|
408
|
+
x, y: 2.3, w: cardW, h: 0.8,
|
|
409
|
+
fontSize: 32, align: 'center',
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Big number
|
|
414
|
+
s.addText(stat.value || '0', {
|
|
415
|
+
x, y: stat.icon ? 3 : 2.5, w: cardW, h: 1.2,
|
|
416
|
+
fontSize: 40, bold: true, color: colors[i], align: 'center', fontFace: 'Arial',
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Label
|
|
420
|
+
s.addText(stat.label || '', {
|
|
421
|
+
x, y: stat.icon ? 4.1 : 3.7, w: cardW, h: 0.8,
|
|
422
|
+
fontSize: 14, color: theme.text, align: 'center', fontFace: 'Arial', wrap: true,
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Sub text
|
|
426
|
+
if (stat.sub) {
|
|
427
|
+
s.addText(stat.sub, {
|
|
428
|
+
x, y: stat.icon ? 4.7 : 4.3, w: cardW, h: 0.5,
|
|
429
|
+
fontSize: 11, color: theme.border, align: 'center', fontFace: 'Arial', italic: true,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
_addQuoteSlide(pres, slide, theme, slideNum, total) {
|
|
436
|
+
const s = this._addBase(pres, theme, slideNum, total);
|
|
437
|
+
|
|
438
|
+
// Large quote mark
|
|
439
|
+
s.addText('"', {
|
|
440
|
+
x: 1, y: 1.2, w: 2, h: 2,
|
|
441
|
+
fontSize: 120, color: theme.accent, fontFace: 'Georgia', bold: true,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// Quote text
|
|
445
|
+
s.addText(slide.quote || slide.content || '', {
|
|
446
|
+
x: 1.5, y: 2.5, w: 10, h: 2.5,
|
|
447
|
+
fontSize: 28, color: theme.title, fontFace: 'Georgia', italic: true,
|
|
448
|
+
lineSpacing: 36, wrap: true,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// Attribution
|
|
452
|
+
if (slide.author || slide.attribution) {
|
|
453
|
+
s.addShape(pres.ShapeType.rect, {
|
|
454
|
+
x: 1.5, y: 5.2, w: 1.5, h: 0.03, fill: { color: theme.accent },
|
|
455
|
+
});
|
|
456
|
+
s.addText('โ ' + (slide.author || slide.attribution), {
|
|
457
|
+
x: 1.5, y: 5.4, w: 8, h: 0.5,
|
|
458
|
+
fontSize: 16, color: theme.accent, fontFace: 'Arial',
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
_addImageSlide(pres, slide, theme, slideNum, total) {
|
|
464
|
+
const s = this._addBase(pres, theme, slideNum, total);
|
|
465
|
+
|
|
466
|
+
if (slide.title) {
|
|
467
|
+
s.addText(slide.title, {
|
|
468
|
+
x: 0.5, y: 0.3, w: 12, h: 0.7,
|
|
469
|
+
fontSize: 26, bold: true, color: theme.title, fontFace: 'Arial',
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (slide.imageUrl) {
|
|
474
|
+
try {
|
|
475
|
+
s.addImage({
|
|
476
|
+
path: slide.imageUrl,
|
|
477
|
+
x: 1, y: 1.5, w: 11, h: 5,
|
|
478
|
+
sizing: { type: 'contain', w: 11, h: 5 },
|
|
479
|
+
});
|
|
480
|
+
} catch {}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (slide.caption) {
|
|
484
|
+
s.addText(slide.caption, {
|
|
485
|
+
x: 0.5, y: 6.5, w: 12, h: 0.5,
|
|
486
|
+
fontSize: 12, color: theme.text, align: 'center', italic: true,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
_addSectionSlide(pres, slide, theme, slideNum, total) {
|
|
492
|
+
const s = pres.addSlide();
|
|
493
|
+
s.background = { color: theme.bg2 };
|
|
494
|
+
|
|
495
|
+
s.addShape(pres.ShapeType.rect, {
|
|
496
|
+
x: 0, y: 0, w: '100%', h: 0.08, fill: { color: theme.accent },
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
s.addText(slide.title || 'Section', {
|
|
500
|
+
x: 0.5, y: 2.5, w: 12, h: 1.5,
|
|
501
|
+
fontSize: 38, bold: true, color: theme.title, fontFace: 'Arial',
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
if (slide.subtitle) {
|
|
505
|
+
s.addText(slide.subtitle, {
|
|
506
|
+
x: 0.5, y: 4, w: 10, h: 0.8,
|
|
507
|
+
fontSize: 18, color: theme.text, fontFace: 'Arial',
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
s.addShape(pres.ShapeType.rect, {
|
|
512
|
+
x: 0.5, y: 4.9, w: 2.5, h: 0.04, fill: { color: theme.accent },
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
_addThankYouSlide(pres, options, theme) {
|
|
517
|
+
const s = pres.addSlide();
|
|
518
|
+
s.background = { color: theme.bg };
|
|
519
|
+
|
|
520
|
+
s.addShape(pres.ShapeType.rect, {
|
|
521
|
+
x: 0, y: 0, w: '100%', h: 0.08, fill: { color: theme.accent },
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
s.addText('Thank You', {
|
|
525
|
+
x: 0, y: 2, w: 13, h: 1.5,
|
|
526
|
+
fontSize: 48, bold: true, color: theme.title, align: 'center', fontFace: 'Arial',
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
s.addShape(pres.ShapeType.rect, {
|
|
530
|
+
x: 5.5, y: 3.6, w: 2, h: 0.04, fill: { color: theme.accent },
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
if (options.contact) {
|
|
534
|
+
s.addText(options.contact, {
|
|
535
|
+
x: 0, y: 4, w: 13, h: 0.8,
|
|
536
|
+
fontSize: 16, color: theme.text, align: 'center', fontFace: 'Arial',
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
s.addText('Created with Squidclaw AI ๐ฆ', {
|
|
541
|
+
x: 0, y: 6.5, w: 13, h: 0.5,
|
|
542
|
+
fontSize: 11, color: theme.border, align: 'center', italic: true,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// โโ PARSER โโ
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Parse AI content into structured slides
|
|
550
|
+
* Supports: ## Title, - bullets, [chart:bar], [table], [stats], [timeline], [quote], [compare], [section]
|
|
551
|
+
*/
|
|
552
|
+
static parseContent(text) {
|
|
553
|
+
const slides = [];
|
|
554
|
+
const sections = text.split(/^## /gm).filter(s => s.trim());
|
|
555
|
+
|
|
556
|
+
for (const section of sections) {
|
|
557
|
+
const lines = section.trim().split('\n');
|
|
558
|
+
const titleLine = lines[0].trim();
|
|
559
|
+
|
|
560
|
+
// Detect slide type from markers
|
|
561
|
+
let slide = { title: titleLine, type: 'content', bullets: [], content: '' };
|
|
562
|
+
|
|
563
|
+
// [chart:bar] or [chart:pie] etc
|
|
564
|
+
const chartMatch = titleLine.match(/\[chart:(\w+)\]/i) || lines.some(l => l.match(/\[chart:(\w+)\]/i));
|
|
565
|
+
if (chartMatch || lines.some(l => /\[chart/i.test(l))) {
|
|
566
|
+
slide.type = 'chart';
|
|
567
|
+
slide.title = titleLine.replace(/\[chart:\w+\]/i, '').trim();
|
|
568
|
+
const typeMatch = section.match(/\[chart:(\w+)\]/i);
|
|
569
|
+
slide.chartType = typeMatch ? typeMatch[1] : 'bar';
|
|
570
|
+
slide.chartData = this._parseChartData(lines.slice(1));
|
|
571
|
+
slides.push(slide);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// [table]
|
|
576
|
+
if (lines.some(l => /\[table\]|^\|/.test(l))) {
|
|
577
|
+
slide.type = 'table';
|
|
578
|
+
slide.title = titleLine.replace(/\[table\]/i, '').trim();
|
|
579
|
+
slide.tableData = this._parseTableData(lines.slice(1));
|
|
580
|
+
slides.push(slide);
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// [stats]
|
|
585
|
+
if (lines.some(l => /\[stats?\]/i.test(l))) {
|
|
586
|
+
slide.type = 'stats';
|
|
587
|
+
slide.title = titleLine.replace(/\[stats?\]/i, '').trim();
|
|
588
|
+
slide.stats = this._parseStats(lines.slice(1));
|
|
589
|
+
slides.push(slide);
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// [timeline]
|
|
594
|
+
if (lines.some(l => /\[timeline\]/i.test(l))) {
|
|
595
|
+
slide.type = 'timeline';
|
|
596
|
+
slide.title = titleLine.replace(/\[timeline\]/i, '').trim();
|
|
597
|
+
slide.events = this._parseTimeline(lines.slice(1));
|
|
598
|
+
slides.push(slide);
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// [quote]
|
|
603
|
+
if (lines.some(l => /\[quote\]/i.test(l)) || titleLine.includes('[quote]')) {
|
|
604
|
+
slide.type = 'quote';
|
|
605
|
+
slide.title = titleLine.replace(/\[quote\]/i, '').trim();
|
|
606
|
+
const quoteLines = lines.slice(1).filter(l => !l.includes('[quote]'));
|
|
607
|
+
const authorLine = quoteLines.find(l => l.startsWith('โ') || l.startsWith('-โ') || l.startsWith('- Author:'));
|
|
608
|
+
slide.quote = quoteLines.filter(l => l !== authorLine).map(l => l.replace(/^[-โข*]\s*/, '').trim()).filter(Boolean).join(' ');
|
|
609
|
+
slide.author = authorLine ? authorLine.replace(/^[-โโข*\s]*Author:\s*|^[-โ]\s*/i, '').trim() : '';
|
|
610
|
+
slides.push(slide);
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// [compare] or [vs]
|
|
615
|
+
if (lines.some(l => /\[compare\]|\[vs\]/i.test(l))) {
|
|
616
|
+
slide.type = 'comparison';
|
|
617
|
+
slide.title = titleLine.replace(/\[compare\]|\[vs\]/i, '').trim();
|
|
618
|
+
const parsed = this._parseComparison(lines.slice(1));
|
|
619
|
+
slide.left = parsed.left;
|
|
620
|
+
slide.right = parsed.right;
|
|
621
|
+
slides.push(slide);
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// [section]
|
|
626
|
+
if (titleLine.includes('[section]') || lines.some(l => /\[section\]/i.test(l))) {
|
|
627
|
+
slide.type = 'section';
|
|
628
|
+
slide.title = titleLine.replace(/\[section\]/i, '').trim();
|
|
629
|
+
slide.subtitle = lines.slice(1).filter(l => !l.includes('[section]')).map(l => l.trim()).filter(Boolean).join(' ');
|
|
630
|
+
slides.push(slide);
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Default: bullets
|
|
635
|
+
for (let i = 1; i < lines.length; i++) {
|
|
636
|
+
const line = lines[i].trim();
|
|
637
|
+
if (line.startsWith('- ') || line.startsWith('โข ') || line.startsWith('* ')) {
|
|
638
|
+
slide.bullets.push(line.replace(/^[-โข*]\s*/, ''));
|
|
639
|
+
} else if (line) {
|
|
640
|
+
slide.content += (slide.content ? '\n' : '') + line;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (!slide.bullets.length && slide.content) slide.bullets = null;
|
|
644
|
+
slides.push(slide);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return slides;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
static _parseChartData(lines) {
|
|
651
|
+
const data = [];
|
|
652
|
+
let current = null;
|
|
653
|
+
const labels = [];
|
|
654
|
+
const values = [];
|
|
655
|
+
|
|
656
|
+
for (const line of lines) {
|
|
657
|
+
const trimmed = line.trim();
|
|
658
|
+
if (!trimmed || trimmed.includes('[chart')) continue;
|
|
659
|
+
|
|
660
|
+
// Format: "Label: value" or "Label | value"
|
|
661
|
+
const match = trimmed.match(/^[-โข*]?\s*(.+?)[\s:|\-โ]+(\d+[\d,.]*%?)\s*$/);
|
|
662
|
+
if (match) {
|
|
663
|
+
labels.push(match[1].trim());
|
|
664
|
+
values.push(parseFloat(match[2].replace(/[,%]/g, '')));
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (labels.length > 0) {
|
|
669
|
+
data.push({ name: 'Data', labels, values });
|
|
670
|
+
}
|
|
671
|
+
return data.length > 0 ? data : [{ name: 'Sample', labels: ['A', 'B', 'C'], values: [30, 50, 20] }];
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
static _parseTableData(lines) {
|
|
675
|
+
const rows = [];
|
|
676
|
+
for (const line of lines) {
|
|
677
|
+
const trimmed = line.trim();
|
|
678
|
+
if (!trimmed || trimmed.includes('[table]') || /^[-|:]+$/.test(trimmed)) continue;
|
|
679
|
+
if (trimmed.startsWith('|')) {
|
|
680
|
+
const cells = trimmed.split('|').filter(c => c.trim()).map(c => c.trim());
|
|
681
|
+
if (cells.length > 0) rows.push(cells);
|
|
682
|
+
} else if (trimmed.includes(',') || trimmed.includes('\t')) {
|
|
683
|
+
rows.push(trimmed.split(/[,\t]+/).map(c => c.trim()));
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return rows.length > 0 ? rows : [['Col 1', 'Col 2'], ['Data', 'Data']];
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
static _parseStats(lines) {
|
|
690
|
+
const stats = [];
|
|
691
|
+
for (const line of lines) {
|
|
692
|
+
const trimmed = line.trim();
|
|
693
|
+
if (!trimmed || trimmed.includes('[stat')) continue;
|
|
694
|
+
// Format: "๐ 100M+ โ Active users" or "100M: Active users"
|
|
695
|
+
const match = trimmed.match(/^[-โข*]?\s*([^\w]*?)?\s*([\d,.]+[%KMBkm+]*)\s*[-:โ|]+\s*(.+)/);
|
|
696
|
+
if (match) {
|
|
697
|
+
stats.push({
|
|
698
|
+
icon: match[1]?.trim() || undefined,
|
|
699
|
+
value: match[2].trim(),
|
|
700
|
+
label: match[3].trim(),
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return stats.length > 0 ? stats : [{ value: '?', label: 'No data' }];
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
static _parseTimeline(lines) {
|
|
708
|
+
const events = [];
|
|
709
|
+
for (const line of lines) {
|
|
710
|
+
const trimmed = line.trim();
|
|
711
|
+
if (!trimmed || trimmed.includes('[timeline]')) continue;
|
|
712
|
+
const match = trimmed.match(/^[-โข*]?\s*(\d{4}[s]?|Phase \d+|Q\d|Step \d+)\s*[-:โ|]+\s*(.+)/i);
|
|
713
|
+
if (match) {
|
|
714
|
+
events.push({ date: match[1].trim(), text: match[2].trim() });
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return events.length > 0 ? events : [{ date: '???', text: 'No events' }];
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
static _parseComparison(lines) {
|
|
721
|
+
const left = { title: 'Option A', points: [] };
|
|
722
|
+
const right = { title: 'Option B', points: [] };
|
|
723
|
+
let side = 'left';
|
|
724
|
+
|
|
725
|
+
for (const line of lines) {
|
|
726
|
+
const trimmed = line.trim();
|
|
727
|
+
if (!trimmed || trimmed.includes('[compare]') || trimmed.includes('[vs]')) continue;
|
|
728
|
+
|
|
729
|
+
if (trimmed.toLowerCase().includes('vs') || trimmed === '---') {
|
|
730
|
+
side = 'right';
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const target = side === 'left' ? left : right;
|
|
735
|
+
if (trimmed.startsWith('###') || trimmed.startsWith('**')) {
|
|
736
|
+
target.title = trimmed.replace(/^[#*\s]+|[*]+$/g, '');
|
|
737
|
+
} else if (trimmed.startsWith('- ') || trimmed.startsWith('โข ')) {
|
|
738
|
+
target.points.push(trimmed.replace(/^[-โข]\s*/, ''));
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
return { left, right };
|
|
743
|
+
}
|
|
744
|
+
}
|
package/lib/tools/router.js
CHANGED
|
@@ -23,7 +23,9 @@ export class ToolRouter {
|
|
|
23
23
|
getToolDescriptions() {
|
|
24
24
|
const tools = [
|
|
25
25
|
'## Available Tools',
|
|
26
|
-
'You can use tools by including special tags in your response
|
|
26
|
+
'You can use tools by including special tags in your response.',
|
|
27
|
+
'IMPORTANT: You CAN send files, images, voice notes, and documents in this chat. The system handles delivery automatically.',
|
|
28
|
+
'When asked to create files (PowerPoint, etc), USE THE TOOL โ do NOT say you cannot send files.',
|
|
27
29
|
'',
|
|
28
30
|
'### Web Search',
|
|
29
31
|
'---TOOL:search:your search query---',
|
|
@@ -59,6 +61,24 @@ export class ToolRouter {
|
|
|
59
61
|
'Send an email.');
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
tools.push('', '### Create PowerPoint (SENDS AS FILE IN CHAT!)',
|
|
65
|
+
'---TOOL:pptx_slides:Title|theme|slides content---',
|
|
66
|
+
'Creates a .pptx file and SENDS IT directly. You MUST use this when asked for PPT/presentation.',
|
|
67
|
+
'Format: title|theme|slides. Themes: dark, light, blue, green, corporate, red, gradient, minimal, saudi, ocean.',
|
|
68
|
+
'',
|
|
69
|
+
'SLIDE TYPES (use tags in ## title):',
|
|
70
|
+
'- Normal: ## Title then - bullet points',
|
|
71
|
+
'- Chart: ## Revenue [chart:bar] then - Label: value (types: bar, pie, line, doughnut, area)',
|
|
72
|
+
'- Table: ## Data [table] then | Col1 | Col2 | rows',
|
|
73
|
+
'- Stats: ## Metrics [stats] then - ๐ 100M โ Active users',
|
|
74
|
+
'- Timeline: ## History [timeline] then - 2020: Event happened',
|
|
75
|
+
'- Quote: ## [quote] then quote text then โ Author Name',
|
|
76
|
+
'- Compare: ## Comparison [compare] then left items, ---, right items',
|
|
77
|
+
'- Section: ## New Section [section]',
|
|
78
|
+
'',
|
|
79
|
+
'Example:',
|
|
80
|
+
'---TOOL:pptx_slides:AI Report|dark|## Introduction\n- AI is transforming industries\n- Revenue growing 40% YoY\n\n## Growth [chart:bar]\n- 2020: 50\n- 2021: 80\n- 2022: 120\n- 2023: 200\n\n## Key Stats [stats]\n- ๐ 195 โ Countries using AI\n- ๐ฐ $500B โ Market size\n- ๐ 40% โ Annual growth---');
|
|
81
|
+
|
|
62
82
|
tools.push('', '### Allow User',
|
|
63
83
|
'---TOOL:allow:user_id_or_phone---',
|
|
64
84
|
'Add someone to the allowlist so they can message you.',
|
|
@@ -221,6 +241,56 @@ export class ToolRouter {
|
|
|
221
241
|
}
|
|
222
242
|
break;
|
|
223
243
|
}
|
|
244
|
+
case 'pptx':
|
|
245
|
+
case 'pptx_slides':
|
|
246
|
+
case 'powerpoint':
|
|
247
|
+
case 'presentation': {
|
|
248
|
+
try {
|
|
249
|
+
const { PptxGenerator } = await import('./pptx.js');
|
|
250
|
+
const gen = new PptxGenerator();
|
|
251
|
+
|
|
252
|
+
// Parse: title|theme|content or just content
|
|
253
|
+
const parts = toolArg.split('|');
|
|
254
|
+
let title, theme, slideContent;
|
|
255
|
+
|
|
256
|
+
if (parts.length >= 3) {
|
|
257
|
+
title = parts[0].trim();
|
|
258
|
+
theme = parts[1].trim();
|
|
259
|
+
slideContent = parts.slice(2).join('|');
|
|
260
|
+
} else if (parts.length === 2) {
|
|
261
|
+
title = parts[0].trim();
|
|
262
|
+
slideContent = parts[1];
|
|
263
|
+
theme = 'corporate';
|
|
264
|
+
} else {
|
|
265
|
+
// Try to extract title from first ## heading
|
|
266
|
+
const firstH2 = toolArg.match(/^##\s+(.+)/m);
|
|
267
|
+
title = firstH2 ? firstH2[1] : 'Presentation';
|
|
268
|
+
slideContent = toolArg;
|
|
269
|
+
theme = 'corporate';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const slides = PptxGenerator.parseContent(slideContent);
|
|
273
|
+
if (slides.length === 0) {
|
|
274
|
+
toolResult = 'No slides found. Use ## headings and - bullet points.';
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const result = await gen.create(title, slides, { theme });
|
|
279
|
+
|
|
280
|
+
// Return file path for sending
|
|
281
|
+
return {
|
|
282
|
+
toolUsed: true,
|
|
283
|
+
toolName: 'pptx',
|
|
284
|
+
toolResult: 'PowerPoint created: ' + result.filename + ' (' + result.slideCount + ' slides)',
|
|
285
|
+
filePath: result.filepath,
|
|
286
|
+
fileName: result.filename,
|
|
287
|
+
cleanResponse
|
|
288
|
+
};
|
|
289
|
+
} catch (err) {
|
|
290
|
+
toolResult = 'PowerPoint failed: ' + err.message;
|
|
291
|
+
}
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
224
294
|
case 'allow': {
|
|
225
295
|
try {
|
|
226
296
|
const { AllowlistManager } = await import('../features/allowlist-manager.js');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squidclaw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "๐ฆ AI agent platform โ human-like agents for WhatsApp, Telegram & more",
|
|
5
5
|
"main": "lib/engine.js",
|
|
6
6
|
"bin": {
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"node-edge-tts": "^1.2.10",
|
|
50
50
|
"pdfjs-dist": "^5.4.624",
|
|
51
51
|
"pino": "^10.3.1",
|
|
52
|
+
"pptxgenjs": "^4.0.1",
|
|
52
53
|
"puppeteer-core": "^24.37.5",
|
|
53
54
|
"qrcode-terminal": "^0.12.0",
|
|
54
55
|
"sharp": "^0.34.5",
|