mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-25 00:16:29 +02:00
improved presentation skill
This commit is contained in:
parent
9cd7d11969
commit
7133dbe1d9
5 changed files with 72 additions and 816 deletions
|
|
@ -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.
|
- NEVER ask what OS the user is on - they are on macOS.
|
||||||
- Load the \`organize-files\` skill for guidance on file organization tasks.
|
- 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
|
## 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-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-readdir\`, \`workspace-exists\`, \`workspace-stat\`, \`workspace-glob\`, \`workspace-grep\` - Directory exploration and file search
|
||||||
- \`workspace-mkdir\`, \`workspace-rename\`, \`workspace-copy\` - File/directory management
|
- \`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
|
- \`addMcpServer\`, \`listMcpServers\`, \`listMcpTools\`, \`executeMcpTool\` - MCP server management and execution
|
||||||
- \`loadSkill\` - Skill loading
|
- \`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**
|
**CRITICAL: MCP Server Configuration**
|
||||||
- ALWAYS use the \`addMcpServer\` builtin tool to add or update MCP servers—it validates the configuration before saving
|
- 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
|
- NEVER manually edit \`config/mcp.json\` using \`workspace-writeFile\` for MCP servers
|
||||||
- Invalid MCP configs will prevent the agent from starting with validation errors
|
- 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.`;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,216 +1,83 @@
|
||||||
export const skill = String.raw`
|
export const skill = String.raw`
|
||||||
# PDF Presentation Generator Skill
|
# 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.
|
Activate when the user wants to create presentations, slide decks, or pitch decks.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Workflow
|
## 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?
|
(async () => {
|
||||||
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")
|
const browser = await chromium.launch();
|
||||||
3. **Presentation type**: pitch deck, product demo, team intro, investor update, etc.
|
const page = await browser.newPage();
|
||||||
4. **Audience**: investors, customers, internal team, conference
|
await page.goto('file:///tmp/presentation.html', { waitUntil: 'networkidle' });
|
||||||
5. **Tone**: formal, casual, technical, inspirational
|
await page.pdf({
|
||||||
6. **Length**: number of slides (default: 10-12 for pitch decks)
|
path: path.join(process.env.HOME, 'Desktop', 'presentation.pdf'),
|
||||||
|
width: '1280px',
|
||||||
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.
|
height: '720px',
|
||||||
|
printBackground: true,
|
||||||
### Step 2: Gather Knowledge
|
});
|
||||||
|
await browser.close();
|
||||||
~~~bash
|
console.log('Done: ~/Desktop/presentation.pdf');
|
||||||
# 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
|
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.
|
||||||
|
|
||||||
~~~
|
## PDF Export Rules
|
||||||
## Proposed Presentation Outline
|
|
||||||
|
|
||||||
**Title:** [Presentation Title]
|
**These rules prevent rendering issues in PDF. Violating them causes overlapping rectangles and broken layouts.**
|
||||||
**Slides:** [N] slides
|
|
||||||
**Style:** [Color scheme / theme description]
|
|
||||||
|
|
||||||
### 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**
|
## Required CSS
|
||||||
- Company name, tagline, presenter name
|
|
||||||
|
|
||||||
2. **Problem**
|
~~~css
|
||||||
- [One sentence summary of the problem]
|
@page { size: 1280px 720px; margin: 0; }
|
||||||
|
html { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
|
||||||
3. **Solution**
|
.slide {
|
||||||
- [One sentence summary of your solution]
|
width: 1280px;
|
||||||
|
height: 720px;
|
||||||
...
|
padding: 60px;
|
||||||
|
overflow: hidden;
|
||||||
---
|
page-break-after: always;
|
||||||
|
page-break-inside: avoid;
|
||||||
Does this look good? I can adjust the outline, then I'll go ahead and generate the PDF for you.
|
}
|
||||||
- Add/remove slides
|
.slide:last-child { page-break-after: auto; }
|
||||||
- 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.
|
## 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
|
const browser = await chromium.launch();
|
||||||
|
const page = await browser.newPage();
|
||||||
Write code to generate the presentation. You have complete freedom here:
|
await page.goto('file://' + htmlPath, { waitUntil: 'networkidle' });
|
||||||
|
await page.pdf({
|
||||||
1. **Install any packages you need** via executeCommand (e.g., npm install @react-pdf/renderer chartjs-node-canvas)
|
path: '~/Desktop/presentation.pdf',
|
||||||
2. **Write a script** that generates the PDF — you can use the reference code as inspiration or write something entirely different
|
width: '1280px',
|
||||||
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
|
height: '720px',
|
||||||
4. **Execute the script** to produce the final PDF
|
printBackground: true,
|
||||||
|
});
|
||||||
## Visual Quality Guidelines
|
await browser.close();
|
||||||
|
~~~
|
||||||
**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
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default skill;
|
export default skill;
|
||||||
|
|
@ -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>;
|
|
||||||
}
|
|
||||||
|
|
@ -12,7 +12,6 @@ import * as workspace from "../../workspace/workspace.js";
|
||||||
import { IAgentsRepo } from "../../agents/repo.js";
|
import { IAgentsRepo } from "../../agents/repo.js";
|
||||||
import { WorkDir } from "../../config/config.js";
|
import { WorkDir } from "../../config/config.js";
|
||||||
import type { ToolContext } from "./exec-tool.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
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const BuiltinToolsSchema = z.record(z.string(), z.object({
|
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: {
|
executeCommand: {
|
||||||
description: 'Execute a shell command and return the output. Use this to run bash/shell commands.',
|
description: 'Execute a shell command and return the output. Use this to run bash/shell commands.',
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
command: z.string().describe('The shell command to execute (e.g., "ls -la", "cat file.txt")'),
|
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) => {
|
execute: async ({ command, cwd }: { command: string, cwd?: string }, ctx?: ToolContext) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue