imprive presentation tool

This commit is contained in:
Saumya 2025-08-06 17:18:37 +05:30
parent bbd4b8f4d2
commit d9277ff63e
13 changed files with 3183 additions and 1926 deletions

View File

@ -3,6 +3,7 @@ from fastapi.responses import StreamingResponse
import asyncio
import json
import traceback
import base64
from datetime import datetime, timezone
import uuid
from typing import Optional, List, Dict, Any
@ -33,6 +34,7 @@ from .versioning.api import router as version_router, initialize as initialize_v
async def _get_version_service():
return await get_version_service()
from utils.suna_default_agent_service import SunaDefaultAgentService
from .tools.sb_presentation_tool import SandboxPresentationTool
router = APIRouter()
router.include_router(version_router)
@ -3329,3 +3331,98 @@ async def update_agent_custom_mcps(
except Exception as e:
logger.error(f"Error updating agent custom MCPs: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/tools/export-presentation")
async def export_presentation(
request: Dict[str, Any] = Body(...),
user_id: str = Depends(get_current_user_id_from_jwt)
):
try:
presentation_name = request.get("presentation_name")
export_format = request.get("format", "pptx")
project_id = request.get("project_id")
if not presentation_name:
raise HTTPException(status_code=400, detail="presentation_name is required")
if not project_id:
raise HTTPException(status_code=400, detail="project_id is required")
if db is None:
db_conn = DBConnection()
client = await db_conn.client
else:
client = await db.client
project_result = await client.table('projects').select('sandbox').eq('project_id', project_id).execute()
if not project_result.data:
raise HTTPException(status_code=404, detail="Project not found")
sandbox_data = project_result.data[0].get('sandbox', {})
sandbox_id = sandbox_data.get('id')
if not sandbox_id:
raise HTTPException(status_code=400, detail="No sandbox found for this project")
thread_manager = ThreadManager()
presentation_tool = SandboxPresentationTool(
project_id=project_id,
thread_manager=thread_manager
)
result = await presentation_tool.export_presentation(
presentation_name=presentation_name,
format=export_format
)
if result.success:
import json
import urllib.parse
data = json.loads(result.output)
export_file = data.get("export_file")
logger.info(f"Export file from tool: {export_file}")
logger.info(f"Sandbox ID: {sandbox_id}")
if export_file:
from fastapi.responses import Response
from sandbox.api import get_sandbox_by_id_safely, verify_sandbox_access
try:
file_path = export_file.replace("/workspace/", "").lstrip("/")
full_path = f"/workspace/{file_path}"
sandbox = await get_sandbox_by_id_safely(client, sandbox_id)
file_content = await sandbox.fs.download_file(full_path)
return {
"success": True,
"message": data.get("message"),
"file_content": base64.b64encode(file_content).decode('utf-8'),
"filename": export_file.split('/')[-1],
"export_file": data.get("export_file"),
"format": data.get("format"),
"file_size": data.get("file_size")
}
except Exception as e:
logger.error(f"Failed to read exported file: {str(e)}")
return {
"success": False,
"error": f"Failed to read exported file: {str(e)}"
}
else:
return {
"success": True,
"message": data.get("message"),
"download_url": data.get("download_url"),
"export_file": data.get("export_file"),
"format": data.get("format"),
"file_size": data.get("file_size")
}
else:
raise HTTPException(status_code=400, detail=result.output or "Export failed")
except Exception as e:
logger.error(f"Export presentation error: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to export presentation: {str(e)}")

View File

@ -129,7 +129,8 @@ You have the abilixwty to execute operations using both Python and CLI tools:
- If we have a data provider for a specific task, use that over web searching, crawling and scraping.
### 2.3.9 PRESENTATION TOOLS
- You have powerful presentation creation capabilities with two specialized tools:
- You have premium presentation creation capabilities with hardcoded professional templates ensuring uniformity and minimalism:
- **Presentation Outline Tool (`create_presentation_outline`):**
* Create structured presentation outlines with slide titles, descriptions, and speaker notes
* Plan the overall flow and content of presentations before creating actual slides
@ -141,59 +142,62 @@ You have the abilixwty to execute operations using both Python and CLI tools:
<parameter name="slides">[
{{
"title": "The Future of AI",
"description": "Title slide introducing the presentation on AI's future impact",
"notes": "Welcome everyone to this exploration of AI's transformative potential"
"description": "Hero slide with striking visuals introducing AI's transformative potential",
"notes": "Open with confidence using full-screen imagery"
}},
{{
"title": "Current State of AI",
"description": "Overview of today's AI capabilities and applications",
"notes": "Discuss recent breakthroughs in LLMs, computer vision, and robotics"
"title": "Current AI Landscape",
"description": "Content slide showcasing key AI capabilities with supporting data",
"notes": "Present statistics and real-world applications"
}},
{{
"title": "AI in Healthcare",
"description": "How AI is revolutionizing medical diagnosis and treatment",
"notes": "Examples: drug discovery, personalized medicine, diagnostic imaging"
"title": "What comes next?",
"description": "Minimal slide with thought-provoking question for audience engagement",
"notes": "Pause for emphasis, let the question resonate"
}}
]</parameter>
</invoke>
</function_calls>
- **Presentation Creation Tool (`create_presentation`):**
* Generate beautiful HTML-based presentations with Apple-inspired minimalist design language
* Create slides with various layouts optimized for visual impact and proper image positioning
* Support for structured content with bullet points, images, quotes, and hero sections
* Each slide is generated as an individual HTML file with modern styling and responsive design
* Includes slide navigation, preview capabilities, and an index page
* **Available Layouts:**
- `default`: Standard layout with title and content
- `centered`: Center-aligned content for emphasis
- `minimal`: Large, bold text for maximum impact (Apple keynote style)
- `hero`: Gradient background for dramatic effect
- `image-hero`: Full-screen background image with overlay text
- `image-right`: Content on left, image on right with proper positioning
- `image-left`: Image on left, content on right with proper positioning
- `two-column`: Equal columns for balanced content
- `split-content`: Side-by-side layout for comparisons
* **Image Handling:**
- Professional image positioning with `object-fit: cover` and `overflow: hidden`
- Automatic image optimization and cropping for slide dimensions
- Shadow effects and rounded corners for visual polish
- Hero images with gradient overlays for text readability
- Responsive image scaling for different screen sizes
* Example:
* Uses **premium hardcoded templates** ensuring professional design consistency
* **NO CUSTOM CSS** - templates guarantee uniformity and Apple Keynote-level polish
* **Template Options:**
- `minimal`: Clean Apple Keynote-inspired design (SF Pro Display, elegant spacing, gradient text effects)
- `corporate`: Professional business presentations (structured layouts, data visualization support)
- `creative`: Artistic magazine-style design (Playfair Display font, visual storytelling)
* **Color Schemes per Template:**
- **Minimal**: "Dark" (Apple black), "Light" (clean white), "Blue" (ocean theme)
- **Corporate**: "Professional" (charcoal green), "Navy" (deep blue), "Charcoal" (modern gray)
- **Creative**: "Sunset" (purple gradient), "Forest" (nature green), "Ocean" (teal blue)
* **Template-Specific Layouts:**
- **Minimal Template**: `hero`, `content`, `image-split`, `quote`, `minimal`
- **Corporate Template**: `title`, `agenda`, `content`, `data`
- **Creative Template**: `image-hero`, `gallery`, `story`, `quote`
* **Template Features:**
- **16:9 aspect ratio enforced** in all templates (1920x1080 standard)
- **Responsive design** with clamp() functions preventing text cutoff
- **Static design** optimized for PPTX export compatibility
- **Optimized typography** with proper font hierarchies
- **Image handling** with object-fit cover and professional shadows
* Example with template system:
<function_calls>
<invoke name="create_presentation">
<parameter name="presentation_name">apple_style_ai_presentation</parameter>
<parameter name="presentation_name">ai_future_keynote</parameter>
<parameter name="title">The Future of AI</parameter>
<parameter name="template">minimal</parameter>
<parameter name="color_scheme">Dark</parameter>
<parameter name="slides">[
{{
"title": "The Future of AI",
"content": {{
"hero_image": "https://images.unsplash.com/photo-1677442136019-21780ecad995?w=1600&h=900&fit=crop",
"title": "The Future of AI",
"subtitle": "Transforming how we work, create, and connect"
}},
"layout": "image-hero",
"background_color": "#1D1D1F"
"layout": "hero"
}},
{{
"title": "Revolutionary Technology",
@ -202,44 +206,52 @@ You have the abilixwty to execute operations using both Python and CLI tools:
"main_points": [
{{"emoji": "🧠", "text": "Advanced neural networks that learn and adapt"}},
{{"emoji": "🎯", "text": "Precision automation for complex tasks"}},
{{"emoji": "💡", "text": "Creative AI that generates art, music, and code"}},
{{"emoji": "🌐", "text": "Global connectivity through intelligent systems"}}
],
"image": "https://images.unsplash.com/photo-1555255707-c07966088b7b?w=800&h=600&fit=crop"
{{"emoji": "🎨", "text": "Creative tools for art and content generation"}}
]
}},
"layout": "image-right",
"background_color": "#1D1D1F"
"layout": "content"
}},
{{
"title": "Breakthrough",
"content": {{
"subtitle": "What makes this moment different?"
"title": "The Question",
"content": "What if we could augment human intelligence instead of replacing it?",
"layout": "minimal"
}},
"layout": "minimal",
"background_color": "#007AFF"
{{
"title": "Human-AI Partnership",
"content": {{
"subtitle": "The future is collaborative",
"main_points": [
{{"emoji": "👥", "text": "AI amplifies human creativity"}},
{{"emoji": "", "text": "Faster decision-making"}},
{{"emoji": "🌍", "text": "Global problem-solving at scale"}}
],
"image": "https://images.unsplash.com/photo-1485827404703-89b55fcc595e"
}},
"layout": "image-split"
}}
]</parameter>
</invoke>
</function_calls>
- **Apple Design Language Features:**
* Minimalist, clean layouts with ample white space
* SF Pro Display font family for Apple-like typography
* Sophisticated color schemes with Apple's signature dark grays and blues
* Precise image positioning with overflow hidden for professional cropping
* Smooth animations and transitions for polished interactions
* Responsive design that works on all devices
* High-quality shadows and visual depth for modern appearance
- **Template Selection Guide:**
* **Choose `minimal`** for: Keynote-style presentations, tech demos, startup pitches, creative showcases
* **Choose `corporate`** for: Business reports, quarterly reviews, strategy presentations, data-heavy content
* **Choose `creative`** for: Brand stories, portfolio showcases, artistic presentations, visual narratives
- **Best Practices for Presentations:**
* Always create an outline first to plan the presentation structure
* Use `image-hero` layout for impactful opening slides with full-screen visuals
* Apply `minimal` layout for key messages and transitions
* Utilize `image-right` or `image-left` for content with supporting visuals
* Include relevant high-quality images from Unsplash, Pexels, or other sources
* Add emojis to make bullet points more engaging and visual
* Use Apple's color palette: `#1D1D1F` (dark), `#007AFF` (blue), `#2D2D30` (medium gray)
* Ensure images are high-resolution and properly cropped with `&fit=crop` parameters
* **MANDATORY RESEARCH REQUIREMENT:** ALWAYS conduct extensive research using web search tools before creating any presentation. Research the topic thoroughly, gather current information, find high-quality real image URLs, and base your content on verified facts and data.
* Always create an outline first to plan the presentation structure based on your research findings
* Use ONLY real image URLs from your web search results - never use placeholder or example URLs
* **Template-Layout Matching:** Ensure your chosen layouts match your selected template (check template-specific layout options above)
* **Content Structure Guidelines:**
- Hero/Title slides: Use for impactful opening statements
- Content slides: Perfect for bullet points with emojis and supporting images
- Minimal slides: Great for single powerful statements or questions
- Image-split/Gallery: Ideal for visual storytelling with supporting text
- Data slides: Best for metrics, statistics, and quantified information
* Include relevant high-quality images from research for professional appearance
* Balance text-heavy slides with visual slides for dynamic flow
* Export functionality (PDF/PPTX) is available for future implementation
* **Export functionality** available - presentations can be exported to PPTX format maintaining design fidelity
# 3. TOOLKIT & METHODOLOGY

View File

@ -239,26 +239,6 @@ class PromptManager:
else:
system_content = default_system_content
if await is_enabled("knowledge_base"):
try:
from services.supabase import DBConnection
kb_db = DBConnection()
kb_client = await kb_db.client
current_agent_id = agent_config.get('agent_id') if agent_config else None
kb_result = await kb_client.rpc('get_combined_knowledge_base_context', {
'p_thread_id': thread_id,
'p_agent_id': current_agent_id,
'p_max_tokens': 4000
}).execute()
if kb_result.data and kb_result.data.strip():
system_content += "\n\n" + kb_result.data
except Exception as e:
logger.error(f"Error retrieving knowledge base context for thread {thread_id}: {e}")
if agent_config and (agent_config.get('configured_mcps') or agent_config.get('custom_mcps')) and mcp_wrapper_instance and mcp_wrapper_instance._initialized:
mcp_info = "\n\n--- MCP Tools Available ---\n"
mcp_info += "You have access to external MCP (Model Context Protocol) server tools.\n"

View File

@ -0,0 +1,811 @@
from typing import Dict, List, Any
from dataclasses import dataclass
@dataclass
class ColorScheme:
name: str
primary: str
secondary: str
accent: str
background: str
text: str
text_secondary: str
@dataclass
class PresentationTemplate:
name: str
description: str
color_schemes: List[ColorScheme]
layouts: List[str]
css_template: str
MINIMAL_SCHEMES = [
ColorScheme("Dark", "#000000", "#1D1D1F", "#007AFF", "#000000", "#FFFFFF", "#A1A1A6"),
ColorScheme("Light", "#FFFFFF", "#F2F2F7", "#007AFF", "#FFFFFF", "#000000", "#6D6D70"),
ColorScheme("Blue", "#001F3F", "#003366", "#00A8FF", "#001F3F", "#FFFFFF", "#B3D9FF"),
]
CORPORATE_SCHEMES = [
ColorScheme("Professional", "#1C1C1E", "#2C2C2E", "#00C896", "#1C1C1E", "#FFFFFF", "#98989D"),
ColorScheme("Navy", "#0A1628", "#1A2332", "#4A90E2", "#0A1628", "#FFFFFF", "#7FB3D3"),
ColorScheme("Charcoal", "#2D2D30", "#3E3E42", "#FF6B35", "#2D2D30", "#FFFFFF", "#CDCDCD"),
]
CREATIVE_SCHEMES = [
ColorScheme("Sunset", "#1A0B3D", "#2D1B69", "#FF6B9D", "#1A0B3D", "#FFFFFF", "#C9A9DD"),
ColorScheme("Forest", "#0F2027", "#203A43", "#2C5F41", "#0F2027", "#FFFFFF", "#A8D5BA"),
ColorScheme("Ocean", "#0C1B3D", "#1E3A5F", "#4ECDC4", "#0C1B3D", "#FFFFFF", "#87CEEB"),
]
MINIMAL_CSS = """
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue', Arial, sans-serif;
font-weight: 300;
line-height: 1.4;
height: 100vh;
width: 100vw;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: {background};
color: {text};
}}
.slide {{
width: 100vw;
height: 100vh;
max-width: 1920px;
max-height: 1080px;
aspect-ratio: 16/9;
display: flex;
flex-direction: column;
justify-content: center;
padding: clamp(40px, 5vw, 120px);
position: relative;
margin: 0 auto;
background: {background};
}}
/* Typography Hierarchy */
h1 {{
font-size: clamp(3rem, 8vw, 8rem);
font-weight: 700;
letter-spacing: -0.025em;
line-height: 0.9;
margin-bottom: clamp(20px, 3vw, 60px);
color: {text};
}}
h2 {{
font-size: clamp(1.5rem, 4vw, 4rem);
font-weight: 600;
letter-spacing: -0.015em;
line-height: 1.1;
margin-bottom: clamp(15px, 2vw, 40px);
color: {text};
}}
.subtitle {{
font-size: clamp(1.2rem, 2.5vw, 2.5rem);
font-weight: 300;
opacity: 0.85;
margin-bottom: clamp(30px, 4vw, 80px);
color: {text_secondary};
}}
/* Layout: Hero */
.slide.hero {{
text-align: center;
justify-content: center;
}}
.slide.hero h1 {{
font-size: clamp(4rem, 10vw, 12rem);
background: linear-gradient(135deg, {text}, {accent});
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}}
/* Layout: Content */
.slide.content {{
justify-content: flex-start;
padding-top: clamp(60px, 8vw, 160px);
}}
.content-section {{
max-width: 75%;
}}
ul {{
list-style: none;
margin: clamp(20px, 3vw, 60px) 0;
}}
li {{
margin: clamp(12px, 2vw, 32px) 0;
font-size: clamp(1.1rem, 2vw, 2.2rem);
display: flex;
align-items: center;
}}
.emoji {{
font-size: clamp(1.3rem, 2.5vw, 2.8rem);
margin-right: clamp(12px, 2vw, 24px);
min-width: clamp(32px, 4vw, 48px);
}}
/* Layout: Image Split */
.slide.image-split {{
flex-direction: row;
align-items: center;
gap: clamp(40px, 6vw, 120px);
}}
.content-half {{
flex: 1;
max-width: 50%;
}}
.image-half {{
flex: 1;
max-width: 50%;
height: 70vh;
display: flex;
align-items: center;
justify-content: center;
}}
.image-half img {{
width: 100%;
height: 100%;
object-fit: cover;
border-radius: clamp(12px, 2vw, 32px);
box-shadow: 0 clamp(20px, 4vw, 60px) clamp(40px, 8vw, 120px) rgba(0, 0, 0, 0.3);
}}
/* Layout: Quote */
.slide.quote {{
text-align: center;
justify-content: center;
}}
.quote-text {{
font-size: clamp(2rem, 5vw, 5rem);
font-weight: 300;
line-height: 1.2;
font-style: italic;
margin-bottom: clamp(30px, 4vw, 80px);
color: {text};
opacity: 0.95;
}}
.quote-author {{
font-size: clamp(1rem, 2vw, 2rem);
font-weight: 500;
color: {accent};
opacity: 0.8;
}}
/* Layout: Minimal Center */
.slide.minimal {{
text-align: center;
justify-content: center;
}}
.slide.minimal h1 {{
font-size: clamp(5rem, 12vw, 14rem);
font-weight: 200;
letter-spacing: -0.04em;
}}
/* Slide Number */
.slide-number {{
position: absolute;
bottom: clamp(20px, 3vw, 60px);
right: clamp(20px, 3vw, 60px);
font-size: clamp(0.8rem, 1.5vw, 1.5rem);
opacity: 0.4;
font-weight: 300;
}}
.slide {{
/* Static slide - no animations */
}}
/* Responsive */
@media (max-width: 768px) {{
.slide {{
padding: clamp(20px, 5vw, 40px);
}}
.slide.image-split {{
flex-direction: column;
gap: clamp(20px, 4vw, 40px);
}}
.content-half,
.image-half {{
max-width: 100%;
}}
}}
"""
CORPORATE_CSS = """
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-weight: 400;
height: 100vh;
width: 100vw;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: {background};
color: {text};
}}
.slide {{
width: 100vw;
height: 100vh;
max-width: 1920px;
max-height: 1080px;
aspect-ratio: 16/9;
display: flex;
flex-direction: column;
padding: clamp(60px, 7vw, 140px);
position: relative;
margin: 0 auto;
background: linear-gradient(135deg, {background} 0%, {secondary} 100%);
}}
/* Header Section */
.slide-header {{
border-bottom: 2px solid {accent};
padding-bottom: clamp(20px, 3vw, 40px);
margin-bottom: clamp(40px, 5vw, 80px);
}}
h1 {{
font-size: clamp(2.5rem, 6vw, 6rem);
font-weight: 700;
line-height: 1.1;
color: {text};
margin-bottom: clamp(10px, 2vw, 20px);
}}
.subtitle {{
font-size: clamp(1rem, 2vw, 2rem);
font-weight: 300;
color: {accent};
text-transform: uppercase;
letter-spacing: 0.1em;
}}
/* Layout: Title Slide */
.slide.title {{
justify-content: center;
text-align: center;
}}
.slide.title h1 {{
font-size: clamp(4rem, 9vw, 9rem);
font-weight: 800;
margin-bottom: clamp(30px, 4vw, 60px);
}}
.company-logo {{
width: clamp(60px, 10vw, 120px);
height: auto;
margin-top: clamp(40px, 6vw, 80px);
opacity: 0.8;
}}
/* Layout: Agenda */
.slide.agenda {{
justify-content: flex-start;
}}
.agenda-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: clamp(20px, 3vw, 40px);
margin-top: clamp(20px, 3vw, 40px);
}}
.agenda-item {{
background: rgba(255, 255, 255, 0.05);
padding: clamp(20px, 3vw, 40px);
border-radius: clamp(8px, 1.5vw, 16px);
border-left: 4px solid {accent};
backdrop-filter: blur(10px);
}}
.agenda-number {{
font-size: clamp(1.5rem, 3vw, 3rem);
font-weight: 700;
color: {accent};
margin-bottom: clamp(10px, 2vw, 20px);
}}
.agenda-title {{
font-size: clamp(1.1rem, 2vw, 2rem);
font-weight: 600;
margin-bottom: clamp(8px, 1.5vw, 16px);
}}
.agenda-desc {{
font-size: clamp(0.9rem, 1.5vw, 1.5rem);
opacity: 0.8;
line-height: 1.4;
}}
/* Layout: Content with Data */
.slide.data {{
justify-content: flex-start;
}}
.metrics-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: clamp(20px, 3vw, 40px);
margin-top: clamp(30px, 4vw, 60px);
}}
.metric-card {{
text-align: center;
background: rgba(255, 255, 255, 0.08);
padding: clamp(30px, 4vw, 60px) clamp(20px, 3vw, 40px);
border-radius: clamp(12px, 2vw, 24px);
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(20px);
}}
.metric-value {{
font-size: clamp(2.5rem, 5vw, 5rem);
font-weight: 800;
color: {accent};
line-height: 1;
margin-bottom: clamp(10px, 2vw, 20px);
}}
.metric-label {{
font-size: clamp(0.9rem, 1.5vw, 1.5rem);
font-weight: 500;
opacity: 0.9;
text-transform: uppercase;
letter-spacing: 0.05em;
}}
/* Content Lists */
ul {{
list-style: none;
margin: clamp(20px, 3vw, 40px) 0;
}}
li {{
margin: clamp(15px, 2.5vw, 30px) 0;
font-size: clamp(1.1rem, 2vw, 2rem);
padding-left: clamp(30px, 4vw, 60px);
position: relative;
}}
li::before {{
content: "";
position: absolute;
left: 0;
color: {accent};
font-size: 0.8em;
}}
/* Slide Number */
.slide-number {{
position: absolute;
bottom: clamp(30px, 4vw, 60px);
right: clamp(30px, 4vw, 60px);
font-size: clamp(1rem, 1.8vw, 1.8rem);
opacity: 0.6;
font-weight: 500;
}}
/* Responsive */
@media (max-width: 768px) {{
.slide {{
padding: clamp(30px, 6vw, 60px);
}}
.metrics-grid {{
grid-template-columns: 1fr;
}}
.agenda-grid {{
grid-template-columns: 1fr;
}}
}}
"""
CREATIVE_CSS = """
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: 'Playfair Display', 'Georgia', serif;
font-weight: 300;
height: 100vh;
width: 100vw;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: {background};
color: {text};
}}
.slide {{
width: 100vw;
height: 100vh;
max-width: 1920px;
max-height: 1080px;
aspect-ratio: 16/9;
display: flex;
flex-direction: column;
justify-content: center;
padding: clamp(40px, 5vw, 100px);
position: relative;
margin: 0 auto;
background: radial-gradient(ellipse at center, {secondary} 0%, {background} 100%);
}}
/* Typography with Artistic Flair */
h1 {{
font-size: clamp(3.5rem, 8vw, 8.5rem);
font-weight: 300;
line-height: 0.95;
letter-spacing: -0.02em;
margin-bottom: clamp(20px, 3vw, 50px);
color: {text};
text-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}}
h2 {{
font-size: clamp(1.8rem, 4vw, 4.5rem);
font-weight: 400;
line-height: 1.2;
margin-bottom: clamp(20px, 3vw, 40px);
color: {text};
}}
.subtitle {{
font-family: 'Inter', sans-serif;
font-size: clamp(1.1rem, 2.2vw, 2.2rem);
font-weight: 300;
opacity: 0.9;
margin-bottom: clamp(30px, 4vw, 60px);
color: {text_secondary};
font-style: italic;
}}
/* Layout: Image Hero */
.slide.image-hero {{
padding: 0;
position: relative;
overflow: hidden;
}}
.hero-background {{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}}
.hero-overlay {{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
135deg,
rgba(0, 0, 0, 0.6) 0%,
rgba(0, 0, 0, 0.3) 50%,
rgba(0, 0, 0, 0.7) 100%
);
z-index: 2;
}}
.hero-content {{
position: relative;
z-index: 3;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
padding: clamp(60px, 8vw, 160px);
text-shadow: 0 8px 32px rgba(0, 0, 0, 0.8);
}}
.hero-content h1 {{
font-size: clamp(4rem, 10vw, 11rem);
font-weight: 200;
margin-bottom: clamp(30px, 4vw, 60px);
max-width: 70%;
}}
/* Layout: Gallery */
.slide.gallery {{
flex-direction: row;
align-items: stretch;
gap: clamp(30px, 4vw, 80px);
padding: clamp(60px, 7vw, 140px);
}}
.gallery-content {{
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}}
.gallery-images {{
flex: 1;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: clamp(15px, 2vw, 30px);
height: 70vh;
}}
.gallery-image {{
border-radius: clamp(12px, 2vw, 24px);
overflow: hidden;
box-shadow: 0 clamp(10px, 2vw, 30px) clamp(30px, 5vw, 80px) rgba(0, 0, 0, 0.4);
}}
.gallery-image img {{
width: 100%;
height: 100%;
object-fit: cover;
}}
.gallery-image:first-child {{
grid-row: 1 / 3;
}}
/* Layout: Story (Full Image with Text Overlay) */
.slide.story {{
padding: 0;
position: relative;
color: white;
}}
.story-background {{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}}
.story-gradient {{
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 60%;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
z-index: 2;
}}
.story-text {{
position: absolute;
bottom: clamp(60px, 8vw, 160px);
left: clamp(60px, 8vw, 160px);
right: clamp(60px, 8vw, 160px);
z-index: 3;
}}
.story-text h1 {{
font-size: clamp(3rem, 7vw, 7rem);
margin-bottom: clamp(20px, 3vw, 40px);
}}
.story-text p {{
font-size: clamp(1.2rem, 2.5vw, 2.5rem);
line-height: 1.4;
opacity: 0.95;
}}
/* Layout: Quote with Artistic Elements */
.slide.quote {{
text-align: center;
justify-content: center;
position: relative;
}}
.quote::before {{
content: "\\"";
position: absolute;
top: clamp(20px, 3vw, 60px);
left: clamp(40px, 5vw, 100px);
font-size: clamp(8rem, 15vw, 20rem);
color: {accent};
opacity: 0.2;
font-family: serif;
line-height: 1;
}}
.quote-text {{
font-size: clamp(2.2rem, 5vw, 5.5rem);
font-weight: 300;
line-height: 1.3;
font-style: italic;
margin-bottom: clamp(40px, 5vw, 80px);
max-width: 85%;
margin-left: auto;
margin-right: auto;
position: relative;
z-index: 2;
}}
.quote-author {{
font-family: 'Inter', sans-serif;
font-size: clamp(1.1rem, 2.2vw, 2.2rem);
font-weight: 500;
color: {accent};
opacity: 0.9;
position: relative;
z-index: 2;
}}
.quote-author::before {{
content: "";
}}
/* Artistic Elements */
.slide::after {{
content: "";
position: absolute;
top: clamp(40px, 5vw, 80px);
right: clamp(40px, 5vw, 80px);
width: clamp(60px, 8vw, 120px);
height: clamp(60px, 8vw, 120px);
background: linear-gradient(45deg, {accent}, transparent);
border-radius: 50%;
opacity: 0.1;
z-index: 1;
}}
/* Slide Number */
.slide-number {{
position: absolute;
bottom: clamp(30px, 4vw, 60px);
right: clamp(30px, 4vw, 60px);
font-family: 'Inter', sans-serif;
font-size: clamp(0.9rem, 1.6vw, 1.6rem);
opacity: 0.5;
font-weight: 300;
z-index: 10;
}}
/* Static slides - no animations */
.slide {{
/* No animations for better PPTX compatibility */
}}
/* Responsive */
@media (max-width: 768px) {{
.slide.gallery {{
flex-direction: column;
}}
.gallery-images {{
grid-template-columns: 1fr;
grid-template-rows: repeat(4, 1fr);
height: 50vh;
}}
.gallery-image:first-child {{
grid-row: auto;
}}
.hero-content,
.story-text {{
padding: clamp(30px, 6vw, 60px);
}}
}}
"""
TEMPLATES = {
"minimal": PresentationTemplate(
name="Minimal",
description="Clean, Apple Keynote-inspired design with focus on typography and white space",
color_schemes=MINIMAL_SCHEMES,
layouts=["hero", "content", "image-split", "quote", "minimal"],
css_template=MINIMAL_CSS
),
"corporate": PresentationTemplate(
name="Corporate",
description="Professional business presentation with data visualization support",
color_schemes=CORPORATE_SCHEMES,
layouts=["title", "agenda", "content", "data"],
css_template=CORPORATE_CSS
),
"creative": PresentationTemplate(
name="Creative",
description="Artistic, magazine-style design for visual storytelling",
color_schemes=CREATIVE_SCHEMES,
layouts=["image-hero", "gallery", "story", "quote"],
css_template=CREATIVE_CSS
)
}
def get_template(template_name: str) -> PresentationTemplate:
return TEMPLATES.get(template_name.lower(), TEMPLATES["minimal"])
def get_template_css(template_name: str, color_scheme_name: str = None) -> str:
template = get_template(template_name)
if not color_scheme_name:
color_scheme = template.color_schemes[0]
else:
color_scheme = next(
(cs for cs in template.color_schemes if cs.name.lower() == color_scheme_name.lower()),
template.color_schemes[0]
)
css = template.css_template.format(
primary=color_scheme.primary,
secondary=color_scheme.secondary,
accent=color_scheme.accent,
background=color_scheme.background,
text=color_scheme.text,
text_secondary=color_scheme.text_secondary
)
return css
def list_templates() -> Dict[str, Any]:
return {
name: {
"name": template.name,
"description": template.description,
"layouts": template.layouts,
"color_schemes": [
{
"name": cs.name,
"colors": {
"primary": cs.primary,
"accent": cs.accent,
"background": cs.background
}
} for cs in template.color_schemes
]
}
for name, template in TEMPLATES.items()
}

View File

@ -13,7 +13,7 @@ class SandboxPresentationOutlineTool(SandboxToolsBase):
"type": "function",
"function": {
"name": "create_presentation_outline",
"description": "Create a structured outline for a presentation with slide titles and descriptions. This tool helps plan the overall structure and flow of a presentation before creating the actual slides.",
"description": "Create a structured outline for a presentation with slide titles and descriptions. This tool helps plan the overall structure and flow of a presentation before creating the actual slides with standard PowerPoint dimensions (1920x1080, 16:9 aspect ratio).",
"parameters": {
"type": "object",
"properties": {

File diff suppressed because it is too large Load Diff

View File

@ -66,6 +66,7 @@ dependencies = [
"chardet==5.2.0",
"PyYAML==6.0.1",
"composio>=0.8.0",
"aspose-slides>=25.7.0",
]
[project.urls]

File diff suppressed because it is too large Load Diff

View File

@ -46,7 +46,7 @@ export const AgentLoader = () => {
style={{ position: "absolute" }}
className='ml-7'
>
<AnimatedShinyText>{items[index].content}</AnimatedShinyText>
<AnimatedShinyText className='text-xs'>{items[index].content}</AnimatedShinyText>
</motion.div>
</AnimatePresence>
</div>

View File

@ -0,0 +1,188 @@
import React from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Palette, Layout, Sparkles, Building2, Brush } from 'lucide-react';
import { ToolViewProps } from './types';
import { extractToolData } from './utils';
interface ColorScheme {
name: string;
colors: {
primary: string;
accent: string;
background: string;
};
}
interface Template {
name: string;
description: string;
layouts: string[];
color_schemes: ColorScheme[];
}
interface TemplatesData {
templates: Record<string, Template>;
}
const templateIcons = {
minimal: Sparkles,
corporate: Building2,
creative: Brush,
};
const templateColors = {
minimal: 'bg-blue-50 border-blue-200 dark:bg-blue-950/30 dark:border-blue-800',
corporate: 'bg-emerald-50 border-emerald-200 dark:bg-emerald-950/30 dark:border-emerald-800',
creative: 'bg-purple-50 border-purple-200 dark:bg-purple-950/30 dark:border-purple-800',
};
const templateAccents = {
minimal: 'text-blue-600 dark:text-blue-400',
corporate: 'text-emerald-600 dark:text-emerald-400',
creative: 'text-purple-600 dark:text-purple-400',
};
export function ListPresentationTemplatesToolView({ toolContent }: ToolViewProps) {
const { toolResult } = extractToolData(toolContent);
let templatesData: TemplatesData | null = null;
let error: string | null = null;
try {
if (toolResult && toolResult.toolOutput) {
const output = toolResult.toolOutput;
if (typeof output === 'string') {
try {
templatesData = JSON.parse(output);
} catch (e) {
console.error('Failed to parse tool output:', e);
error = 'Failed to parse templates data';
}
} else {
templatesData = output as unknown as TemplatesData;
}
}
} catch (e) {
console.error('Error processing tool result:', e);
error = 'Error processing templates data';
}
if (!templatesData || !templatesData.templates || error) {
return (
<div className="text-center p-8">
<Palette className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<p className="text-muted-foreground">
{error || 'No template data available'}
</p>
</div>
);
}
const templates = templatesData.templates;
return (
<div className="space-y-6">
<div className="text-center space-y-2">
<div className="flex items-center justify-center gap-2 text-2xl font-bold">
<Palette className="h-8 w-8 text-primary" />
Premium Presentation Templates
</div>
<p className="text-muted-foreground">
Professional hardcoded templates ensuring uniformity and minimalism
</p>
</div>
<div className="grid gap-6 md:grid-cols-1 lg:grid-cols-3">
{Object.entries(templates).map(([key, template]) => {
const IconComponent = templateIcons[key as keyof typeof templateIcons] || Sparkles;
const cardClass = templateColors[key as keyof typeof templateColors] || templateColors.minimal;
const accentClass = templateAccents[key as keyof typeof templateAccents] || templateAccents.minimal;
return (
<Card key={key} className={`transition-all duration-300 hover:shadow-lg ${cardClass}`}>
<CardHeader className="pb-3">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg bg-white/60 dark:bg-gray-800/60`}>
<IconComponent className={`h-6 w-6 ${accentClass}`} />
</div>
<div>
<CardTitle className="text-xl capitalize">{template.name}</CardTitle>
<CardDescription className="text-sm mt-1">
{template.description}
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Layouts */}
<div>
<div className="flex items-center gap-2 mb-2">
<Layout className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">Available Layouts</span>
</div>
<div className="flex flex-wrap gap-1">
{template.layouts.map((layout) => (
<Badge
key={layout}
variant="secondary"
className="text-xs px-2 py-1"
>
{layout}
</Badge>
))}
</div>
</div>
{/* Color Schemes */}
<div>
<div className="flex items-center gap-2 mb-3">
<Palette className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">Color Schemes</span>
</div>
<div className="space-y-2">
{template.color_schemes.map((scheme) => (
<div
key={scheme.name}
className="flex items-center justify-between p-2 rounded-lg bg-white/40 dark:bg-gray-800/40"
>
<span className="text-sm font-medium">{scheme.name}</span>
<div className="flex gap-1">
<div
className="w-4 h-4 rounded-full border border-gray-300 dark:border-gray-600"
style={{ backgroundColor: scheme.colors.primary }}
title={`Primary: ${scheme.colors.primary}`}
/>
<div
className="w-4 h-4 rounded-full border border-gray-300 dark:border-gray-600"
style={{ backgroundColor: scheme.colors.accent }}
title={`Accent: ${scheme.colors.accent}`}
/>
<div
className="w-4 h-4 rounded-full border border-gray-300 dark:border-gray-600"
style={{ backgroundColor: scheme.colors.background }}
title={`Background: ${scheme.colors.background}`}
/>
</div>
</div>
))}
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
<div className="mt-8 p-4 bg-muted/50 rounded-lg">
<h3 className="font-semibold mb-2">Template Selection Guide:</h3>
<div className="text-sm text-muted-foreground space-y-1">
<div><strong className="text-blue-600 dark:text-blue-400">Minimal:</strong> Keynote-style presentations, tech demos, startup pitches</div>
<div><strong className="text-emerald-600 dark:text-emerald-400">Corporate:</strong> Business reports, quarterly reviews, data-heavy content</div>
<div><strong className="text-purple-600 dark:text-purple-400">Creative:</strong> Brand stories, portfolio showcases, artistic presentations</div>
</div>
</div>
</div>
);
}

View File

@ -28,6 +28,9 @@ import { cn } from '@/lib/utils';
import { constructHtmlPreviewUrl } from '@/lib/utils/url';
import { CodeBlockCode } from '@/components/ui/code-block';
import { LoadingState } from './shared/LoadingState';
import { useMutation } from '@tanstack/react-query';
import { backendApi } from '@/lib/api-client';
import { useParams } from 'next/navigation';
interface SlideInfo {
slide_number: number;
@ -46,6 +49,23 @@ interface PresentationData {
total_slides?: number;
}
interface ExportPresentationRequest {
presentation_name: string;
format: string;
project_id: string;
}
interface ExportPresentationResponse {
success: boolean;
message: string;
download_url?: string;
file_content?: string;
filename?: string;
export_file: string;
format: string;
file_size: number;
}
export function PresentationToolView({
assistantContent,
toolContent,
@ -59,8 +79,60 @@ export function PresentationToolView({
const [currentSlideIndex, setCurrentSlideIndex] = useState(0);
const [slideContents, setSlideContents] = useState<Record<string, string>>({});
const [loadingSlides, setLoadingSlides] = useState<Set<string>>(new Set());
const params = useParams();
const projectId = params.projectId as string;
console.log('Project ID:', projectId);
const exportMutation = useMutation<ExportPresentationResponse, Error, ExportPresentationRequest>({
mutationFn: async (request) => {
const response = await backendApi.post('/tools/export-presentation', request);
if (!response.success) {
throw new Error(response.error?.message || 'Export failed');
}
return response.data;
},
onSuccess: async (data) => {
try {
let blob: Blob;
let filename: string;
if (data.file_content) {
const binaryString = atob(data.file_content);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
blob = new Blob([bytes], { type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' });
filename = data.filename || `${data.export_file.split('/').pop()}`;
} else if (data.download_url) {
const response = await fetch(data.download_url);
if (!response.ok) {
throw new Error(`Download failed: ${response.statusText}`);
}
blob = await response.blob();
filename = `${data.export_file.split('/').pop()}`;
} else {
throw new Error('No download method available');
}
const blobUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = blobUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(blobUrl);
} catch (error) {
console.error('Download error:', error);
}
},
onError: (error) => {
console.error('Export error:', error);
},
});
// Parse the tool output using existing helper
const { toolResult } = extractToolData(toolContent);
let presentationData: PresentationData | null = null;
@ -89,30 +161,20 @@ export function PresentationToolView({
if (slideContents[slidePath] || loadingSlides.has(slidePath)) {
return;
}
setLoadingSlides(prev => new Set(prev).add(slidePath));
setLoadingSlides(prev => new Set([...prev, slidePath]));
try {
const slideUrl = constructHtmlPreviewUrl(project?.sandbox?.sandbox_url, slidePath);
if (!slideUrl) {
throw new Error('Unable to construct slide URL - sandbox not available');
}
const response = await fetch(slideUrl);
const fullUrl = constructHtmlPreviewUrl(project?.sandbox?.sandbox_url, slidePath);
const response = await fetch(fullUrl);
if (!response.ok) {
throw new Error(`Failed to fetch slide: ${response.statusText}`);
throw new Error(`Failed to load slide: ${response.statusText}`);
}
const htmlContent = await response.text();
setSlideContents(prev => ({ ...prev, [slidePath]: htmlContent }));
const content = await response.text();
setSlideContents(prev => ({ ...prev, [slidePath]: content }));
} catch (error) {
console.error('Failed to load slide content:', error);
const errorContent = `
<div style="display: flex; align-items: center; justify-content: center; height: 100vh; font-family: system-ui;">
<div style="text-align: center; color: #ef4444;">
<h2>Failed to load slide</h2>
<p>Could not fetch content from: ${slidePath}</p>
<p style="font-size: 0.875rem; color: #666;">Error: ${error instanceof Error ? error.message : 'Unknown error'}</p>
</div>
</div>
`;
setSlideContents(prev => ({ ...prev, [slidePath]: errorContent }));
} finally {
setLoadingSlides(prev => {
const newSet = new Set(prev);
@ -123,9 +185,8 @@ export function PresentationToolView({
};
const currentSlide = presentationData?.slides[currentSlideIndex];
const slideContent = slideContents[currentSlide?.file || ''];
const slideContent = currentSlide ? slideContents[currentSlide.file] : null;
// Load current slide content if not loaded
if (currentSlide && !slideContent && !loadingSlides.has(currentSlide.file)) {
loadSlideContent(currentSlide.file);
}
@ -139,6 +200,17 @@ export function PresentationToolView({
}
};
const handleExportPPTX = () => {
if(!presentationData?.presentation_name || !projectId) {
return;
}
exportMutation.mutate({
presentation_name: presentationData.presentation_name,
format: 'pptx',
project_id: projectId
});
};
const renderSlidePreview = () => {
if (!currentSlide) return null;
@ -151,7 +223,8 @@ export function PresentationToolView({
}
return (
<div className="relative w-full h-full bg-white dark:bg-zinc-900 rounded-lg overflow-hidden">
<div className="w-full h-full aspect-[16/9] flex items-center justify-center bg-zinc-100 dark:bg-zinc-800 rounded-lg">
<div className="w-full h-full bg-white dark:bg-zinc-900 rounded-lg overflow-hidden shadow-lg">
<iframe
srcDoc={slideContent}
className="w-full h-full border-0"
@ -159,6 +232,7 @@ export function PresentationToolView({
sandbox="allow-same-origin"
/>
</div>
</div>
);
};
@ -276,10 +350,16 @@ export function PresentationToolView({
variant="outline"
size="sm"
className="gap-2 h-7 text-xs"
disabled
title="Export functionality coming soon"
onClick={handleExportPPTX}
disabled={exportMutation.isPending || !presentationData?.presentation_name}
title={exportMutation.isPending ? "Exporting..." : "Download as PPTX"}
>
{exportMutation.isPending ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<Download className="h-3 w-3" />
)}
{exportMutation.isPending ? "Exporting..." : "PPTX"}
</Button>
<TabsList className="h-8 bg-muted/50 border border-border/50 p-0.5 gap-1">
<TabsTrigger
@ -326,8 +406,8 @@ export function PresentationToolView({
</div>
<div className="flex-1 overflow-hidden">
<TabsContent value="preview" className="h-full mt-0 p-4">
<div className="h-full rounded-lg border bg-muted/20 p-4">
<TabsContent value="preview" className="h-full mt-0">
<div className="h-full rounded-lg border bg-muted/20">
{renderSlidePreview()}
</div>
</TabsContent>
@ -348,19 +428,15 @@ export function PresentationToolView({
<div className="h-full flex items-center gap-2 text-sm text-zinc-500 dark:text-zinc-400">
{!isStreaming && presentationData && (
<Badge variant="outline" className="h-6 py-0.5 bg-zinc-50 dark:bg-zinc-900">
<Presentation className="h-3 w-3 mr-1" />
Presentation
{presentationData.slides.length} slides
</Badge>
)}
</div>
<div className="text-xs text-zinc-500 dark:text-zinc-400 flex items-center gap-2">
<Clock className="h-3.5 w-3.5" />
{toolTimestamp && !isStreaming
? formatTimestamp(toolTimestamp)
: assistantTimestamp
? formatTimestamp(assistantTimestamp)
: ''}
<div className="h-full flex items-center gap-2 text-xs text-zinc-400 dark:text-zinc-500">
<Clock className="h-3 w-3" />
<span>
{formatTimestamp(toolTimestamp)}
</span>
</div>
</div>
</Card>

View File

@ -29,6 +29,7 @@ import { GetCurrentAgentConfigToolView } from '../get-current-agent-config/get-c
import { TaskListToolView } from '../task-list/TaskListToolView';
import { PresentationOutlineToolView } from '../PresentationOutlineToolView';
import { PresentationToolView } from '../PresentationToolView';
import { ListPresentationTemplatesToolView } from '../ListPresentationTemplatesToolView';
export type ToolViewComponent = React.ComponentType<ToolViewProps>;
@ -99,6 +100,7 @@ const defaultRegistry: ToolViewRegistryType = {
'create-presentation-outline': PresentationOutlineToolView,
'create-presentation': PresentationToolView,
'export-presentation': PresentationToolView,
'list-presentation-templates': ListPresentationTemplatesToolView,
'default': GenericToolView,
};

View File

@ -323,12 +323,16 @@ const TOOL_DISPLAY_NAMES = new Map([
['deploy', 'Deploying'],
['ask', 'Ask'],
['create-tasks', 'Creating Tasks'],
['update-tasks', 'Updating Tasks'],
['complete', 'Completing Task'],
['crawl-webpage', 'Crawling Website'],
['expose-port', 'Exposing Port'],
['scrape-webpage', 'Scraping Website'],
['web-search', 'Searching Web'],
['see-image', 'Viewing Image'],
['create-presentation-outline', 'Creating Presentation Outline'],
['create-presentation', 'Creating Presentation'],
['update-agent', 'Updating Agent'],