mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-27 17:36:25 +02:00
feat: extract presentation generator into executable code with builtin tool
Move the presentation implementation out of the skill string into real TypeScript files (types.ts, presentation-generator.tsx) and add a generatePresentation builtin tool so the agent calls it directly instead of writing code. Rewrite the skill to guidance-only with content limits, preference gathering, and JSON examples for each slide type. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a3e681a7c4
commit
e6c6571b07
9 changed files with 1392 additions and 1 deletions
|
|
@ -26,6 +26,8 @@ Rowboat is an agentic assistant for everyday work - emails, meetings, projects,
|
|||
|
||||
**Meeting Prep:** When users ask you to prepare for a meeting, prep for a call, or brief them on attendees, load the \`meeting-prep\` skill first. It provides structured guidance for gathering context about attendees from the knowledge base and creating useful meeting briefs.
|
||||
|
||||
**Create Presentations:** When users ask you to create a presentation, slide deck, pitch deck, or PDF slides, load the \`create-presentations\` skill first. It provides structured guidance for generating PDF presentations using context from the knowledge base.
|
||||
|
||||
**Document Collaboration:** When users ask you to work on a document, collaborate on writing, create a new document, edit/refine existing notes, or say things like "let's work on [X]", "help me write [X]", "create a doc for [X]", or "let's draft [X]", you MUST load the \`doc-collab\` skill first. This is required for any document creation or editing task. The skill provides structured guidance for creating, editing, and refining documents in the knowledge base.
|
||||
|
||||
## Memory That Compounds
|
||||
|
|
|
|||
|
|
@ -0,0 +1,446 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,367 @@
|
|||
export const skill = String.raw`
|
||||
# PDF Presentation Generator Skill
|
||||
|
||||
## Overview
|
||||
|
||||
This skill enables Rowboat to create stunning PDF presentations from natural language requests. Use the built-in **generatePresentation** tool to render slides to PDF.
|
||||
|
||||
## 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.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Understand the Request & Gather Preferences
|
||||
|
||||
Before doing anything else, ask the user about their preferences:
|
||||
|
||||
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" or "dark theme" or "keep it default")
|
||||
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
|
||||
~~~
|
||||
|
||||
### Step 3: Present the Outline for Approval
|
||||
|
||||
Before generating slides, present a structured outline to the user:
|
||||
|
||||
~~~
|
||||
## Proposed Presentation Outline
|
||||
|
||||
**Title:** [Presentation Title]
|
||||
**Slides:** [N] slides
|
||||
**Estimated read time:** [X] minutes
|
||||
|
||||
### Flow:
|
||||
|
||||
1. **Title Slide**
|
||||
- Company name, tagline, presenter name
|
||||
|
||||
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
|
||||
~~~
|
||||
|
||||
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.
|
||||
|
||||
**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.
|
||||
|
||||
### Step 4: Generate the Presentation
|
||||
|
||||
Once approved, call the **generatePresentation** tool with the slides JSON and output path. Apply the user's theme/color preferences from Step 1.
|
||||
|
||||
## Slide Types Reference
|
||||
|
||||
| Type | Description | Required Fields |
|
||||
|------|-------------|-----------------|
|
||||
| title | Opening slide with gradient background | title |
|
||||
| section | Section divider with large number | title |
|
||||
| content | Standard content slide | title, content or items |
|
||||
| two-column | Two column layout | title, columns (array of 2) |
|
||||
| stats | Big numbers display | title, stats (array of {value, label}) |
|
||||
| quote | Testimonial/quote | quote |
|
||||
| image | Image with caption | title, imagePath |
|
||||
| team | Team member grid | title, members (array) |
|
||||
| cta | Call to action / closing | title |
|
||||
|
||||
## Slide Type Details
|
||||
|
||||
### title
|
||||
~~~json
|
||||
{
|
||||
"type": "title",
|
||||
"title": "Company Name",
|
||||
"subtitle": "Tagline or description",
|
||||
"presenter": "Name • Context • Date"
|
||||
}
|
||||
~~~
|
||||
|
||||
### content
|
||||
~~~json
|
||||
{
|
||||
"type": "content",
|
||||
"title": "Slide Title",
|
||||
"content": "Optional paragraph text",
|
||||
"items": ["Bullet point 1", "Bullet point 2", "Bullet point 3"]
|
||||
}
|
||||
~~~
|
||||
|
||||
### section
|
||||
~~~json
|
||||
{
|
||||
"type": "section",
|
||||
"title": "Section Title",
|
||||
"subtitle": "Optional subtitle"
|
||||
}
|
||||
~~~
|
||||
|
||||
### stats
|
||||
~~~json
|
||||
{
|
||||
"type": "stats",
|
||||
"title": "Key Metrics",
|
||||
"stats": [
|
||||
{ "value": "$5M", "label": "Revenue" },
|
||||
{ "value": "150%", "label": "YoY Growth" },
|
||||
{ "value": "10K+", "label": "Users" }
|
||||
],
|
||||
"note": "Optional footnote"
|
||||
}
|
||||
~~~
|
||||
|
||||
### two-column
|
||||
~~~json
|
||||
{
|
||||
"type": "two-column",
|
||||
"title": "Comparison",
|
||||
"columns": [
|
||||
{
|
||||
"title": "Column A",
|
||||
"content": "Optional text",
|
||||
"items": ["Item 1", "Item 2"]
|
||||
},
|
||||
{
|
||||
"title": "Column B",
|
||||
"content": "Optional text",
|
||||
"items": ["Item 1", "Item 2"]
|
||||
}
|
||||
]
|
||||
}
|
||||
~~~
|
||||
|
||||
### quote
|
||||
~~~json
|
||||
{
|
||||
"type": "quote",
|
||||
"quote": "The quote text goes here.",
|
||||
"attribution": "Person Name, Title"
|
||||
}
|
||||
~~~
|
||||
|
||||
### image
|
||||
~~~json
|
||||
{
|
||||
"type": "image",
|
||||
"title": "Product Screenshot",
|
||||
"imagePath": "/absolute/path/to/image.png",
|
||||
"caption": "Optional caption"
|
||||
}
|
||||
~~~
|
||||
|
||||
### team
|
||||
~~~json
|
||||
{
|
||||
"type": "team",
|
||||
"title": "Our Team",
|
||||
"members": [
|
||||
{
|
||||
"name": "Jane Doe",
|
||||
"role": "CEO",
|
||||
"bio": "Optional short bio",
|
||||
"photoPath": "/absolute/path/to/photo.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
~~~
|
||||
|
||||
### cta
|
||||
~~~json
|
||||
{
|
||||
"type": "cta",
|
||||
"title": "Let's Build Together",
|
||||
"subtitle": "email@company.com",
|
||||
"contact": "website.com • github.com/org"
|
||||
}
|
||||
~~~
|
||||
|
||||
## Theme Customization
|
||||
|
||||
Pass an optional theme object to customize colors:
|
||||
|
||||
~~~json
|
||||
{
|
||||
"primaryColor": "#2563eb",
|
||||
"secondaryColor": "#7c3aed",
|
||||
"accentColor": "#f59e0b",
|
||||
"textColor": "#1f2937",
|
||||
"textLight": "#6b7280",
|
||||
"background": "#ffffff",
|
||||
"backgroundAlt": "#f9fafb",
|
||||
"fontFamily": "Helvetica"
|
||||
}
|
||||
~~~
|
||||
|
||||
All theme fields are optional — defaults are used for any omitted fields.
|
||||
|
||||
## Example: Calling generatePresentation
|
||||
|
||||
~~~json
|
||||
{
|
||||
"slides": [
|
||||
{
|
||||
"type": "title",
|
||||
"title": "Acme Corp",
|
||||
"subtitle": "Revolutionizing Widget Manufacturing",
|
||||
"presenter": "Jane Doe • Series A • 2025"
|
||||
},
|
||||
{
|
||||
"type": "content",
|
||||
"title": "The Problem",
|
||||
"items": [
|
||||
"Widget production is slow and expensive",
|
||||
"Legacy systems can't keep up with demand",
|
||||
"Quality control remains manual"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "stats",
|
||||
"title": "Traction",
|
||||
"stats": [
|
||||
{ "value": "500+", "label": "Customers" },
|
||||
{ "value": "$2M", "label": "ARR" },
|
||||
{ "value": "3x", "label": "YoY Growth" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "cta",
|
||||
"title": "Let's Talk",
|
||||
"subtitle": "jane@acme.com",
|
||||
"contact": "acme.com"
|
||||
}
|
||||
],
|
||||
"theme": {
|
||||
"primaryColor": "#2563eb"
|
||||
},
|
||||
"outputPath": "/Users/user/Desktop/acme_pitch.pdf"
|
||||
}
|
||||
~~~
|
||||
|
||||
## Pitch Deck Templates
|
||||
|
||||
### Series A Pitch Deck (12 slides)
|
||||
|
||||
Standard flow for investor presentations:
|
||||
|
||||
1. **Title** (type: title) - Company name, tagline, presenter
|
||||
2. **Problem** (type: content) - What pain point you solve
|
||||
3. **Solution** (type: content) - Your product/service
|
||||
4. **Product** (type: image) - Demo/screenshots
|
||||
5. **Market** (type: stats) - TAM/SAM/SOM
|
||||
6. **Business Model** (type: content) - How you make money
|
||||
7. **Traction** (type: stats) - Metrics and growth
|
||||
8. **Competition** (type: two-column) - Your differentiation
|
||||
9. **Team** (type: team) - Key team members
|
||||
10. **Financials** (type: content or stats) - Projections
|
||||
11. **The Ask** (type: content) - Funding amount and use
|
||||
12. **Contact** (type: cta) - 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 (image)
|
||||
6. **Pricing** - Plans and pricing
|
||||
7. **Testimonials** - Customer quotes (quote)
|
||||
8. **Get Started** - CTA
|
||||
|
||||
## Content Limits Per Slide (IMPORTANT)
|
||||
|
||||
Each slide is a fixed 1280x720 page. Content that exceeds the available space will be clipped. Follow these limits strictly:
|
||||
|
||||
| Slide Type | Max Items / Content |
|
||||
|------------|-------------------|
|
||||
| content | 5 bullet points max (keep each bullet to 1 line, ~80 chars). If using paragraph text instead, max ~4 lines. |
|
||||
| two-column | 4 bullet points per column max. Keep bullets short (~60 chars). |
|
||||
| stats | 3-4 stats max. Keep labels short (1-2 words). |
|
||||
| team | 4 members max per slide. Split into multiple team slides if needed. |
|
||||
| quote | Keep quotes under ~200 characters. |
|
||||
| image | Caption should be 1 line. |
|
||||
|
||||
**If the user's content needs more space**, split it across multiple slides of the same type rather than cramming it into one. For example, if there are 8 bullet points, use two content slides (4 each) with titles like "Key Benefits (1/2)" and "Key Benefits (2/2)".
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Keep slides simple** - One idea per slide
|
||||
2. **Use stats slides for numbers** - Big, bold metrics
|
||||
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
|
||||
|
||||
## Output
|
||||
|
||||
The generatePresentation tool produces:
|
||||
- **PDF file** at the specified outputPath
|
||||
- **16:9 aspect ratio** (1280x720px per slide)
|
||||
- **Print-ready** quality
|
||||
- **Embedded fonts** for portability
|
||||
`;
|
||||
|
||||
export default skill;
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
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>;
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import mcpIntegrationSkill from "./mcp-integration/skill.js";
|
|||
import meetingPrepSkill from "./meeting-prep/skill.js";
|
||||
import organizeFilesSkill from "./organize-files/skill.js";
|
||||
import workflowAuthoringSkill from "./workflow-authoring/skill.js";
|
||||
import createPresentationsSkill from "./create-presentations/skill.js";
|
||||
import workflowRunOpsSkill from "./workflow-run-ops/skill.js";
|
||||
|
||||
const CURRENT_FILE = fileURLToPath(import.meta.url);
|
||||
|
|
@ -29,6 +30,13 @@ type ResolvedSkill = {
|
|||
};
|
||||
|
||||
const definitions: SkillDefinition[] = [
|
||||
{
|
||||
id: "create-presentations",
|
||||
title: "Create Presentations",
|
||||
folder: "create-presentations",
|
||||
summary: "Create PDF presentations and slide decks from natural language requests using knowledge base context.",
|
||||
content: createPresentationsSkill,
|
||||
},
|
||||
{
|
||||
id: "doc-collab",
|
||||
title: "Document Collaboration",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ 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({
|
||||
|
|
@ -606,6 +607,71 @@ 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({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue