improved presentation skill

This commit is contained in:
Arjun 2026-01-30 22:57:20 +05:30
parent 9cd7d11969
commit 7133dbe1d9
5 changed files with 72 additions and 816 deletions

View file

@ -150,15 +150,9 @@ When a user asks for ANY task that might require external capabilities (web sear
- NEVER ask what OS the user is on - they are on macOS.
- Load the \`organize-files\` skill for guidance on file organization tasks.
**Command Approval:**
- Approved shell commands are listed in \`~/.rowboat/config/security.json\`. Read this file to see what commands are allowed.
- Only use commands from the approved list. Commands not in the list will be blocked.
- If you cannot accomplish a task with the approved commands, tell the user which command you need and ask them to add it to \`security.json\`.
- Always confirm with the user before executing commands that modify files outside \`~/.rowboat/\` (e.g., "I'll move 12 screenshots to ~/Desktop/Screenshots. Proceed?").
## Builtin Tools vs Shell Commands
**IMPORTANT**: Rowboat provides builtin tools that are internal and do NOT require security allowlist entries:
**IMPORTANT**: Rowboat provides builtin tools that are internal and do NOT require any user approval:
- \`workspace-readFile\`, \`workspace-writeFile\`, \`workspace-edit\`, \`workspace-remove\` - File operations
- \`workspace-readdir\`, \`workspace-exists\`, \`workspace-stat\`, \`workspace-glob\`, \`workspace-grep\` - Directory exploration and file search
- \`workspace-mkdir\`, \`workspace-rename\`, \`workspace-copy\` - File/directory management
@ -166,13 +160,20 @@ When a user asks for ANY task that might require external capabilities (web sear
- \`addMcpServer\`, \`listMcpServers\`, \`listMcpTools\`, \`executeMcpTool\` - MCP server management and execution
- \`loadSkill\` - Skill loading
These tools work directly and are NOT filtered by \`.rowboat/config/security.json\`.
**Prefer these tools whenever possible** they work instantly with zero friction. For file operations inside \`~/.rowboat/\`, always use these instead of \`executeCommand\`.
**Shell commands via \`executeCommand\`:**
- You can run ANY shell command via \`executeCommand\`. Some commands are pre-approved in \`~/.rowboat/config/security.json\` and run immediately.
- Commands not on the pre-approved list will trigger a one-time approval prompt for the user this is fine and expected, just a minor friction. Do NOT let this stop you from running commands you need.
- **Never say "I can't run this command"** or ask the user to run something manually. Just call \`executeCommand\` and let the approval flow handle it.
- When calling \`executeCommand\`, do NOT provide the \`cwd\` parameter unless absolutely necessary. The default working directory is already set to the workspace root.
- Always confirm with the user before executing commands that modify files outside \`~/.rowboat/\` (e.g., "I'll move 12 screenshots to ~/Desktop/Screenshots. Proceed?").
**CRITICAL: MCP Server Configuration**
- ALWAYS use the \`addMcpServer\` builtin tool to add or update MCP servers—it validates the configuration before saving
- NEVER manually edit \`config/mcp.json\` using \`workspace-writeFile\` for MCP servers
- Invalid MCP configs will prevent the agent from starting with validation errors
**Only \`executeCommand\` (shell/bash commands) is filtered** by the security allowlist. If you need to delete a file, use the \`workspace-remove\` builtin tool, not \`executeCommand\` with \`rm\`. If you need to create a file, use \`workspace-writeFile\`, not \`executeCommand\` with \`touch\` or \`echo >\`.
**Only \`executeCommand\` (shell/bash commands) goes through the approval flow.** If you need to delete a file, use the \`workspace-remove\` builtin tool, not \`executeCommand\` with \`rm\`. If you need to create a file, use \`workspace-writeFile\`, not \`executeCommand\` with \`touch\` or \`echo >\`.
The security allowlist in \`security.json\` only applies to shell commands executed via \`executeCommand\`, not to Rowboat's internal builtin tools.`;
Rowboat's internal builtin tools never require approval only shell commands via \`executeCommand\` do.`;

View file

@ -1,446 +0,0 @@
import React from 'react';
import {
Document,
Page,
Text,
View,
Image,
StyleSheet,
renderToFile,
} from '@react-pdf/renderer';
import type { Slide, Theme, PresentationData, TitleSlide, ContentSlide, SectionSlide, StatsSlide, TwoColumnSlide, QuoteSlide, ImageSlide, TeamSlide, CTASlide } from './types.js';
const defaultTheme: Theme = {
primaryColor: '#6366f1',
secondaryColor: '#8b5cf6',
accentColor: '#f59e0b',
textColor: '#1f2937',
textLight: '#6b7280',
background: '#ffffff',
backgroundAlt: '#f9fafb',
fontFamily: 'Helvetica',
};
const SLIDE_WIDTH = 1280;
const SLIDE_HEIGHT = 720;
const createStyles = (theme: Theme) =>
StyleSheet.create({
slide: {
width: SLIDE_WIDTH,
height: SLIDE_HEIGHT,
padding: 60,
backgroundColor: theme.background,
position: 'relative',
},
slideAlt: {
backgroundColor: theme.backgroundAlt,
},
slideGradient: {
backgroundColor: theme.primaryColor,
},
pageNumber: {
position: 'absolute',
bottom: 30,
right: 40,
fontSize: 14,
color: theme.textLight,
},
slideTitle: {
fontSize: 42,
fontWeight: 'bold',
color: theme.textColor,
marginBottom: 30,
},
slideBody: {
fontSize: 24,
color: theme.textColor,
lineHeight: 1.6,
},
titleSlide: {
justifyContent: 'center' as const,
alignItems: 'center' as const,
},
mainTitle: {
fontSize: 64,
fontWeight: 'bold',
color: '#ffffff',
textAlign: 'center' as const,
marginBottom: 20,
},
mainSubtitle: {
fontSize: 28,
color: 'rgba(255, 255, 255, 0.9)',
textAlign: 'center' as const,
marginBottom: 30,
},
presenter: {
fontSize: 20,
color: 'rgba(255, 255, 255, 0.8)',
textAlign: 'center' as const,
},
titleDecoration: {
position: 'absolute' as const,
bottom: 0,
left: 0,
right: 0,
height: 8,
backgroundColor: theme.accentColor,
},
sectionNumber: {
fontSize: 80,
fontWeight: 'bold',
color: theme.primaryColor,
opacity: 0.2,
marginBottom: -20,
},
sectionTitle: {
fontSize: 56,
fontWeight: 'bold',
color: theme.textColor,
},
sectionSubtitle: {
fontSize: 24,
color: theme.textLight,
marginTop: 15,
},
contentList: {
marginTop: 10,
},
listItem: {
flexDirection: 'row' as const,
marginBottom: 16,
alignItems: 'flex-start' as const,
},
listBullet: {
width: 12,
height: 12,
borderRadius: 6,
backgroundColor: theme.primaryColor,
marginRight: 20,
marginTop: 8,
},
listText: {
flex: 1,
fontSize: 24,
color: theme.textColor,
lineHeight: 1.5,
},
columnsContainer: {
flexDirection: 'row' as const,
flex: 1,
gap: 60,
},
column: {
flex: 1,
},
columnTitle: {
fontSize: 24,
fontWeight: 'bold',
color: theme.primaryColor,
marginBottom: 15,
},
statsGrid: {
flexDirection: 'row' as const,
justifyContent: 'space-around' as const,
alignItems: 'center' as const,
flex: 1,
},
statItem: {
alignItems: 'center' as const,
padding: 30,
},
statValue: {
fontSize: 72,
fontWeight: 'bold',
color: theme.primaryColor,
marginBottom: 10,
},
statLabel: {
fontSize: 20,
color: theme.textLight,
textTransform: 'uppercase' as const,
letterSpacing: 1,
},
statsNote: {
textAlign: 'center' as const,
fontSize: 18,
color: theme.textLight,
marginTop: 20,
},
quoteSlide: {
justifyContent: 'center' as const,
alignItems: 'center' as const,
},
quoteText: {
fontSize: 36,
fontStyle: 'italic',
color: theme.textColor,
textAlign: 'center' as const,
maxWidth: 900,
lineHeight: 1.5,
},
quoteAttribution: {
fontSize: 20,
color: theme.textLight,
marginTop: 30,
textAlign: 'center' as const,
},
imageContainer: {
flex: 1,
justifyContent: 'center' as const,
alignItems: 'center' as const,
marginVertical: 20,
},
slideImage: {
maxWidth: '100%',
maxHeight: 450,
objectFit: 'contain' as const,
},
imageCaption: {
textAlign: 'center' as const,
fontSize: 18,
color: theme.textLight,
},
teamGrid: {
flexDirection: 'row' as const,
justifyContent: 'center' as const,
gap: 50,
flex: 1,
alignItems: 'center' as const,
},
teamMember: {
alignItems: 'center' as const,
maxWidth: 200,
},
memberPhotoPlaceholder: {
width: 120,
height: 120,
borderRadius: 60,
backgroundColor: theme.primaryColor,
marginBottom: 15,
},
memberPhoto: {
width: 120,
height: 120,
borderRadius: 60,
marginBottom: 15,
},
memberName: {
fontSize: 20,
fontWeight: 'bold',
color: theme.textColor,
textAlign: 'center' as const,
},
memberRole: {
fontSize: 16,
color: theme.primaryColor,
marginTop: 5,
textAlign: 'center' as const,
},
memberBio: {
fontSize: 14,
color: theme.textLight,
marginTop: 10,
textAlign: 'center' as const,
lineHeight: 1.4,
},
ctaSlide: {
justifyContent: 'center' as const,
alignItems: 'center' as const,
},
ctaTitle: {
fontSize: 56,
fontWeight: 'bold',
color: '#ffffff',
textAlign: 'center' as const,
marginBottom: 20,
},
ctaSubtitle: {
fontSize: 24,
color: 'rgba(255, 255, 255, 0.9)',
textAlign: 'center' as const,
marginBottom: 40,
},
ctaContact: {
fontSize: 20,
color: 'rgba(255, 255, 255, 0.8)',
textAlign: 'center' as const,
},
});
type Styles = ReturnType<typeof createStyles>;
const TitleSlideComponent: React.FC<{ slide: TitleSlide; styles: Styles }> = ({ slide, styles }) => (
<Page size={[SLIDE_WIDTH, SLIDE_HEIGHT]} style={[styles.slide, styles.slideGradient, styles.titleSlide]}>
<Text style={styles.mainTitle}>{slide.title}</Text>
{slide.subtitle && <Text style={styles.mainSubtitle}>{slide.subtitle}</Text>}
{slide.presenter && <Text style={styles.presenter}>{slide.presenter}</Text>}
<View style={styles.titleDecoration} />
</Page>
);
const SectionSlideComponent: React.FC<{ slide: SectionSlide; pageNum: number; styles: Styles }> = ({ slide, pageNum, styles }) => (
<Page size={[SLIDE_WIDTH, SLIDE_HEIGHT]} style={[styles.slide, styles.slideAlt]}>
<View style={{ flex: 1, justifyContent: 'center' }}>
<Text style={styles.sectionNumber}>{String(pageNum).padStart(2, '0')}</Text>
<Text style={styles.sectionTitle}>{slide.title}</Text>
{slide.subtitle && <Text style={styles.sectionSubtitle}>{slide.subtitle}</Text>}
</View>
<Text style={styles.pageNumber}>{pageNum}</Text>
</Page>
);
const ContentSlideComponent: React.FC<{ slide: ContentSlide; pageNum: number; styles: Styles }> = ({ slide, pageNum, styles }) => (
<Page size={[SLIDE_WIDTH, SLIDE_HEIGHT]} style={styles.slide}>
<Text style={styles.slideTitle}>{slide.title}</Text>
{slide.content && <Text style={styles.slideBody}>{slide.content}</Text>}
{slide.items && (
<View style={styles.contentList}>
{slide.items.map((item, i) => (
<View key={i} style={styles.listItem}>
<View style={styles.listBullet} />
<Text style={styles.listText}>{item}</Text>
</View>
))}
</View>
)}
<Text style={styles.pageNumber}>{pageNum}</Text>
</Page>
);
const TwoColumnSlideComponent: React.FC<{ slide: TwoColumnSlide; pageNum: number; styles: Styles }> = ({ slide, pageNum, styles }) => (
<Page size={[SLIDE_WIDTH, SLIDE_HEIGHT]} style={styles.slide}>
<Text style={styles.slideTitle}>{slide.title}</Text>
<View style={styles.columnsContainer}>
{slide.columns.map((col, i) => (
<View key={i} style={styles.column}>
{col.title && <Text style={styles.columnTitle}>{col.title}</Text>}
{col.content && <Text style={styles.slideBody}>{col.content}</Text>}
{col.items && (
<View style={styles.contentList}>
{col.items.map((item, j) => (
<View key={j} style={styles.listItem}>
<View style={styles.listBullet} />
<Text style={styles.listText}>{item}</Text>
</View>
))}
</View>
)}
</View>
))}
</View>
<Text style={styles.pageNumber}>{pageNum}</Text>
</Page>
);
const StatsSlideComponent: React.FC<{ slide: StatsSlide; pageNum: number; styles: Styles }> = ({ slide, pageNum, styles }) => (
<Page size={[SLIDE_WIDTH, SLIDE_HEIGHT]} style={styles.slide}>
<Text style={styles.slideTitle}>{slide.title}</Text>
<View style={styles.statsGrid}>
{slide.stats.map((stat, i) => (
<View key={i} style={styles.statItem}>
<Text style={styles.statValue}>{stat.value}</Text>
<Text style={styles.statLabel}>{stat.label}</Text>
</View>
))}
</View>
{slide.note && <Text style={styles.statsNote}>{slide.note}</Text>}
<Text style={styles.pageNumber}>{pageNum}</Text>
</Page>
);
const QuoteSlideComponent: React.FC<{ slide: QuoteSlide; pageNum: number; styles: Styles }> = ({ slide, pageNum, styles }) => (
<Page size={[SLIDE_WIDTH, SLIDE_HEIGHT]} style={[styles.slide, styles.slideAlt, styles.quoteSlide]}>
<Text style={styles.quoteText}>"{slide.quote}"</Text>
{slide.attribution && <Text style={styles.quoteAttribution}> {slide.attribution}</Text>}
<Text style={styles.pageNumber}>{pageNum}</Text>
</Page>
);
const ImageSlideComponent: React.FC<{ slide: ImageSlide; pageNum: number; styles: Styles }> = ({ slide, pageNum, styles }) => (
<Page size={[SLIDE_WIDTH, SLIDE_HEIGHT]} style={styles.slide}>
<Text style={styles.slideTitle}>{slide.title}</Text>
<View style={styles.imageContainer}>
<Image src={slide.imagePath} style={styles.slideImage} />
</View>
{slide.caption && <Text style={styles.imageCaption}>{slide.caption}</Text>}
<Text style={styles.pageNumber}>{pageNum}</Text>
</Page>
);
const TeamSlideComponent: React.FC<{ slide: TeamSlide; pageNum: number; styles: Styles }> = ({ slide, pageNum, styles }) => (
<Page size={[SLIDE_WIDTH, SLIDE_HEIGHT]} style={styles.slide}>
<Text style={styles.slideTitle}>{slide.title}</Text>
<View style={styles.teamGrid}>
{slide.members.map((member, i) => (
<View key={i} style={styles.teamMember}>
{member.photoPath ? (
<Image src={member.photoPath} style={styles.memberPhoto} />
) : (
<View style={styles.memberPhotoPlaceholder} />
)}
<Text style={styles.memberName}>{member.name}</Text>
<Text style={styles.memberRole}>{member.role}</Text>
{member.bio && <Text style={styles.memberBio}>{member.bio}</Text>}
</View>
))}
</View>
<Text style={styles.pageNumber}>{pageNum}</Text>
</Page>
);
const CTASlideComponent: React.FC<{ slide: CTASlide; pageNum: number; styles: Styles }> = ({ slide, pageNum, styles }) => (
<Page size={[SLIDE_WIDTH, SLIDE_HEIGHT]} style={[styles.slide, styles.slideGradient, styles.ctaSlide]}>
<Text style={styles.ctaTitle}>{slide.title}</Text>
{slide.subtitle && <Text style={styles.ctaSubtitle}>{slide.subtitle}</Text>}
{slide.contact && <Text style={styles.ctaContact}>{slide.contact}</Text>}
<Text style={[styles.pageNumber, { color: 'rgba(255,255,255,0.6)' }]}>{pageNum}</Text>
</Page>
);
const renderSlide = (
slide: Slide,
index: number,
styles: Styles
): React.ReactElement => {
const pageNum = index + 1;
switch (slide.type) {
case 'title':
return <TitleSlideComponent key={index} slide={slide} styles={styles} />;
case 'section':
return <SectionSlideComponent key={index} slide={slide} pageNum={pageNum} styles={styles} />;
case 'content':
return <ContentSlideComponent key={index} slide={slide} pageNum={pageNum} styles={styles} />;
case 'two-column':
return <TwoColumnSlideComponent key={index} slide={slide} pageNum={pageNum} styles={styles} />;
case 'stats':
return <StatsSlideComponent key={index} slide={slide} pageNum={pageNum} styles={styles} />;
case 'quote':
return <QuoteSlideComponent key={index} slide={slide} pageNum={pageNum} styles={styles} />;
case 'image':
return <ImageSlideComponent key={index} slide={slide} pageNum={pageNum} styles={styles} />;
case 'team':
return <TeamSlideComponent key={index} slide={slide} pageNum={pageNum} styles={styles} />;
case 'cta':
return <CTASlideComponent key={index} slide={slide} pageNum={pageNum} styles={styles} />;
default:
return <ContentSlideComponent key={index} slide={slide as ContentSlide} pageNum={pageNum} styles={styles} />;
}
};
const Presentation: React.FC<PresentationData> = ({ slides, theme }) => {
const mergedTheme = { ...defaultTheme, ...theme };
const styles = createStyles(mergedTheme);
return <Document>{slides.map((slide, i) => renderSlide(slide, i, styles))}</Document>;
};
export async function generatePresentation(
data: PresentationData,
outputPath: string
): Promise<string> {
await renderToFile(<Presentation {...data} />, outputPath);
return outputPath;
}

View file

@ -1,216 +1,83 @@
export const skill = String.raw`
# PDF Presentation Generator Skill
## Overview
## When to Use
This skill enables Rowboat to create visually compelling PDF presentations from natural language requests. You have full freedom to write and execute your own code to generate presentations install any npm packages you need, generate charts, use custom layouts, and make the output look polished and professional.
A minimal reference implementation using @react-pdf/renderer exists in the codebase at:
- **Types:** src/application/assistant/skills/create-presentations/types.ts
- **Generator:** src/application/assistant/skills/create-presentations/presentation-generator.tsx
**This code is just a starting point.** It shows one basic approach to PDF generation. You are NOT limited to it. Feel free to:
- Write your own code from scratch
- Use different libraries (e.g., pdfkit, puppeteer with HTML/CSS, jsPDF, or anything else)
- Install any npm packages you need via executeCommand
- Generate charts and visualizations (e.g., chartjs-node-canvas, d3-node, vega-lite, mermaid)
- Render charts as PNG images and embed them in slides
- Create custom layouts, gradients, decorative elements whatever makes the presentation look great
## When to Use This Skill
Activate this skill when the user requests:
- Creating presentations, slide decks, or pitch decks
- Making PDF slides for meetings, talks, or pitches
- Generating visual summaries or reports in presentation format
- Keywords: "presentation", "slides", "deck", "pitch deck", "slide deck", "PDF presentation"
## Knowledge Sources
Before creating any presentation, gather context from the user's knowledge base:
~~~
~/.rowboat/knowledge/
company/
about.md # Company description, mission, vision
team.md # Founder bios, team members
metrics.md # KPIs, growth numbers, financials
product.md # Product description, features, roadmap
branding.md # Colors, fonts, logo paths, style guide
fundraising/
previous-rounds.md # Past funding history
investors.md # Current investors, target investors
use-of-funds.md # How funds will be allocated
projections.md # Financial projections
market/
problem.md # Problem statement
solution.md # How product solves it
competitors.md # Competitive landscape
tam-sam-som.md # Market size analysis
traction.md # Customer testimonials, case studies
assets/
logo.png # Company logo
product-screenshots/
team-photos/
~~~
**Important:** Always check for and read relevant files from ~/.rowboat/knowledge/ before generating content. If files don't exist, ask the user for the information and offer to save it for future use.
Activate when the user wants to create presentations, slide decks, or pitch decks.
## Workflow
### Step 1: Understand the Request & Gather Preferences
1. Check ~/.rowboat/knowledge/ for relevant context about the company, product, team, etc.
2. Ensure Playwright is installed: 'npm install playwright && npx playwright install chromium'
3. Create an HTML file (e.g., /tmp/presentation.html) with slides (1280x720px each)
4. Create a Node.js script to convert HTML to PDF:
Before doing anything else, ask the user about their preferences:
~~~javascript
// save as /tmp/convert.js
const { chromium } = require('playwright');
const path = require('path');
1. **Content density**: Should the slides be text-heavy with detailed explanations, or minimal with just key points and big numbers?
2. **Color / theme**: Do they have brand colors or a color preference? (e.g., "use our brand blue #2563eb", "dark theme", "warm tones", "professional and clean")
3. **Presentation type**: pitch deck, product demo, team intro, investor update, etc.
4. **Audience**: investors, customers, internal team, conference
5. **Tone**: formal, casual, technical, inspirational
6. **Length**: number of slides (default: 10-12 for pitch decks)
Ask these as a concise set of questions in a single message. Use any answers the user already provided in their initial request and only ask about what's missing.
### Step 2: Gather Knowledge
~~~bash
# Check what knowledge exists
ls -la ~/.rowboat/knowledge/ 2>/dev/null || echo "No knowledge directory found"
# Read relevant files based on presentation type
# For a pitch deck, prioritize:
cat ~/.rowboat/knowledge/company/about.md 2>/dev/null
cat ~/.rowboat/knowledge/market/problem.md 2>/dev/null
cat ~/.rowboat/knowledge/company/metrics.md 2>/dev/null
cat ~/.rowboat/knowledge/company/branding.md 2>/dev/null
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('file:///tmp/presentation.html', { waitUntil: 'networkidle' });
await page.pdf({
path: path.join(process.env.HOME, 'Desktop', 'presentation.pdf'),
width: '1280px',
height: '720px',
printBackground: true,
});
await browser.close();
console.log('Done: ~/Desktop/presentation.pdf');
})();
~~~
### Step 3: Present the Outline for Approval
5. Run it: 'node /tmp/convert.js'
6. Tell the user: "Your presentation is ready at ~/Desktop/presentation.pdf"
Before generating slides, present a structured outline to the user:
Do NOT show HTML code to the user. Do NOT explain how to export. Just create the PDF and deliver it.
~~~
## Proposed Presentation Outline
## PDF Export Rules
**Title:** [Presentation Title]
**Slides:** [N] slides
**Style:** [Color scheme / theme description]
**These rules prevent rendering issues in PDF. Violating them causes overlapping rectangles and broken layouts.**
### Flow:
1. **No layered elements** - Never create separate elements for backgrounds or shadows. Style content elements directly.
2. **No box-shadow** - Use borders instead: \`border: 1px solid #e5e7eb\`
3. **Bullets via CSS only** - Use \`li::before\` pseudo-elements, not separate DOM elements
4. **Content must fit** - Slides are 1280x720px with 60px padding. Safe area is 1160x600px. Use \`overflow: hidden\`.
1. **Title Slide**
- Company name, tagline, presenter name
## Required CSS
2. **Problem**
- [One sentence summary of the problem]
3. **Solution**
- [One sentence summary of your solution]
...
---
Does this look good? I can adjust the outline, then I'll go ahead and generate the PDF for you.
- Add/remove slides
- Reorder sections
- Adjust emphasis on any area
~~~css
@page { size: 1280px 720px; margin: 0; }
html { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
.slide {
width: 1280px;
height: 720px;
padding: 60px;
overflow: hidden;
page-break-after: always;
page-break-inside: avoid;
}
.slide:last-child { page-break-after: auto; }
~~~
After the user approves (or after incorporating their feedback), immediately ask: **"I'll generate the PDF now — where should I save it?"** If the user has already indicated a path or preference, skip asking and generate directly.
## Playwright Export
**IMPORTANT:** Always generate the PDF. Never suggest the user copy content into Keynote, Google Slides, or any other tool. The whole point of this skill is to produce a finished PDF.
~~~typescript
import { chromium } from 'playwright';
### Step 4: Generate the Presentation
Write code to generate the presentation. You have complete freedom here:
1. **Install any packages you need** via executeCommand (e.g., npm install @react-pdf/renderer chartjs-node-canvas)
2. **Write a script** that generates the PDF you can use the reference code as inspiration or write something entirely different
3. **Generate charts** for any data that would benefit from visualization (revenue growth, market size, traction metrics, competitive positioning, etc.) use chartjs-node-canvas, d3, vega, or any charting library
4. **Execute the script** to produce the final PDF
## Visual Quality Guidelines
**Do NOT produce plain, boring slides.** Make them look professional and visually engaging:
- **Use color intentionally** gradient backgrounds on title/CTA slides, accent colors for bullets and highlights, colored stat numbers
- **Apply the user's brand colors** throughout not just on the title slide, but as accents, backgrounds, and highlights across all slides
- **Charts and visualizations** whenever there are numbers (revenue, growth, market size, user counts), generate a chart instead of just listing numbers. Bar charts, line charts, pie charts, and simple diagrams make slides far more impactful
- **Visual hierarchy** large bold headings, generous whitespace, clear separation between sections
- **Consistent theming** every slide should feel like part of the same deck, with consistent colors, fonts, and spacing
- **Decorative elements** subtle accent bars, colored bullets, gradient sections, and background tints add polish
## Slide Types (Reference)
These are common slide patterns. You can implement these or create your own:
| Type | Description | When to Use |
|------|-------------|-------------|
| Title | Bold opening with gradient/colored background | First slide |
| Section | Section divider between topics | Between major sections |
| Content | Text with bullet points | Explaining concepts, lists |
| Two-column | Side-by-side comparison | Us vs. them, before/after |
| Stats | Big bold numbers | Key metrics, traction, market size |
| Chart | Data visualization | Revenue growth, market breakdown, trends |
| Quote | Testimonial or notable quote | Customer feedback, press quotes |
| Image | Full or partial image with caption | Product screenshots, team photos |
| Team | Grid of team member cards | Team introduction |
| CTA | Call to action / closing | Final slide |
## Content Limits Per Slide
Each slide is a fixed page. Content that exceeds the available space will overflow. Follow these limits:
| Slide Type | Max Items / Content |
|------------|-------------------|
| Content | 5 bullet points max (~80 chars each). Paragraph text: max ~4 lines. |
| Two-column | 4 bullet points per column max (~60 chars each). |
| Stats | 3-4 stats max. Keep labels short. |
| Team | 4 members max per slide. Split into multiple slides if needed. |
| Quote | Keep quotes under ~200 characters. |
**If the user's content needs more space**, split it across multiple slides rather than cramming it into one.
## Pitch Deck Templates
### Series A Pitch Deck (12 slides)
1. **Title** - Company name, tagline, presenter
2. **Problem** - What pain point you solve
3. **Solution** - Your product/service
4. **Product** - Demo/screenshots
5. **Market** - TAM/SAM/SOM (use a chart!)
6. **Business Model** - How you make money
7. **Traction** - Metrics and growth (use charts!)
8. **Competition** - Positioning (two-column or matrix chart)
9. **Team** - Key team members
10. **Financials** - Projections (use a chart!)
11. **The Ask** - Funding amount and use (pie chart for allocation)
12. **Contact** - CTA with contact info
### Product Demo Deck (8 slides)
1. **Title** - Product name and tagline
2. **Problem** - User pain points
3. **Solution** - High-level approach
4. **Features** - Key capabilities (two-column)
5. **Demo** - Screenshots
6. **Pricing** - Plans and pricing
7. **Testimonials** - Customer quotes
8. **Get Started** - CTA
## Best Practices
1. **Keep slides simple** - One idea per slide
2. **Use charts for numbers** - Never just list numbers when a chart would be more impactful
3. **Limit bullet points** - 3-5 max per slide, keep them short
4. **Use two-column for comparisons** - Us vs. them, before/after
5. **End with clear CTA** - What do you want them to do?
6. **Gather knowledge first** - Check ~/.rowboat/knowledge/ before generating
7. **Use absolute paths** for images (PNG, JPG supported)
8. **Never overflow** - If content doesn't fit, split across multiple slides
9. **Make it visually rich** - Colors, charts, gradients not just text on white backgrounds
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('file://' + htmlPath, { waitUntil: 'networkidle' });
await page.pdf({
path: '~/Desktop/presentation.pdf',
width: '1280px',
height: '720px',
printBackground: true,
});
await browser.close();
~~~
`;
export default skill;
export default skill;

View file

@ -1,100 +0,0 @@
export interface SlideBase {
type: string;
title?: string;
subtitle?: string;
content?: string;
}
export interface TitleSlide extends SlideBase {
type: 'title';
title: string;
subtitle?: string;
presenter?: string;
}
export interface ContentSlide extends SlideBase {
type: 'content';
title: string;
content?: string;
items?: string[];
}
export interface SectionSlide extends SlideBase {
type: 'section';
title: string;
subtitle?: string;
}
export interface StatsSlide extends SlideBase {
type: 'stats';
title: string;
stats: Array<{ value: string; label: string }>;
note?: string;
}
export interface TwoColumnSlide extends SlideBase {
type: 'two-column';
title: string;
columns: [
{ title?: string; content?: string; items?: string[] },
{ title?: string; content?: string; items?: string[] }
];
}
export interface QuoteSlide extends SlideBase {
type: 'quote';
quote: string;
attribution?: string;
}
export interface ImageSlide extends SlideBase {
type: 'image';
title: string;
imagePath: string;
caption?: string;
}
export interface TeamSlide extends SlideBase {
type: 'team';
title: string;
members: Array<{
name: string;
role: string;
bio?: string;
photoPath?: string;
}>;
}
export interface CTASlide extends SlideBase {
type: 'cta';
title: string;
subtitle?: string;
contact?: string;
}
export type Slide =
| TitleSlide
| ContentSlide
| SectionSlide
| StatsSlide
| TwoColumnSlide
| QuoteSlide
| ImageSlide
| TeamSlide
| CTASlide;
export interface Theme {
primaryColor: string;
secondaryColor: string;
accentColor: string;
textColor: string;
textLight: string;
background: string;
backgroundAlt: string;
fontFamily: string;
}
export interface PresentationData {
slides: Slide[];
theme?: Partial<Theme>;
}

View file

@ -12,7 +12,6 @@ import * as workspace from "../../workspace/workspace.js";
import { IAgentsRepo } from "../../agents/repo.js";
import { WorkDir } from "../../config/config.js";
import type { ToolContext } from "./exec-tool.js";
import { generatePresentation } from "../assistant/skills/create-presentations/presentation-generator.js";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const BuiltinToolsSchema = z.record(z.string(), z.object({
@ -607,76 +606,11 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
},
},
generatePresentation: {
description: 'Generate a PDF presentation from slide data. Creates a 16:9 PDF with styled slides.',
inputSchema: z.object({
slides: z.array(z.object({
type: z.enum(['title', 'content', 'section', 'stats', 'two-column', 'quote', 'image', 'team', 'cta']),
title: z.string().optional(),
subtitle: z.string().optional(),
content: z.string().optional(),
presenter: z.string().optional(),
items: z.array(z.string()).optional(),
stats: z.array(z.object({ value: z.string(), label: z.string() })).optional(),
note: z.string().optional(),
columns: z.array(z.object({
title: z.string().optional(),
content: z.string().optional(),
items: z.array(z.string()).optional(),
})).optional(),
quote: z.string().optional(),
attribution: z.string().optional(),
imagePath: z.string().optional(),
caption: z.string().optional(),
members: z.array(z.object({
name: z.string(),
role: z.string(),
bio: z.string().optional(),
photoPath: z.string().optional(),
})).optional(),
contact: z.string().optional(),
})).describe('Array of slide objects'),
theme: z.object({
primaryColor: z.string().optional(),
secondaryColor: z.string().optional(),
accentColor: z.string().optional(),
textColor: z.string().optional(),
textLight: z.string().optional(),
background: z.string().optional(),
backgroundAlt: z.string().optional(),
fontFamily: z.string().optional(),
}).optional().describe('Optional theme customization'),
outputPath: z.string().describe('Absolute path for the output PDF file'),
}),
execute: async ({ slides, theme, outputPath }: {
slides: Array<Record<string, unknown>>;
theme?: Record<string, string>;
outputPath: string;
}) => {
try {
const result = await generatePresentation(
{ slides: slides as never, theme },
outputPath,
);
return {
success: true,
outputPath: result,
slideCount: slides.length,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
},
},
executeCommand: {
description: 'Execute a shell command and return the output. Use this to run bash/shell commands.',
inputSchema: z.object({
command: z.string().describe('The shell command to execute (e.g., "ls -la", "cat file.txt")'),
cwd: z.string().optional().describe('Working directory to execute the command in (defaults to workspace root)'),
cwd: z.string().optional().describe('Working directory to execute the command in (defaults to workspace root). You do not need to set this unless absolutely necessary.'),
}),
execute: async ({ command, cwd }: { command: string, cwd?: string }, ctx?: ToolContext) => {
try {