mirror of https://github.com/kortix-ai/suna.git
imprive presentation tool
This commit is contained in:
parent
bbd4b8f4d2
commit
d9277ff63e
|
@ -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)}")
|
||||
|
|
|
@ -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",
|
||||
"title": "The Question",
|
||||
"content": "What if we could augment human intelligence instead of replacing it?",
|
||||
"layout": "minimal"
|
||||
}},
|
||||
{{
|
||||
"title": "Human-AI Partnership",
|
||||
"content": {{
|
||||
"subtitle": "What makes this moment different?"
|
||||
"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": "minimal",
|
||||
"background_color": "#007AFF"
|
||||
"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
|
||||
|
||||
|
|
|
@ -238,26 +238,6 @@ class PromptManager:
|
|||
system_content = render_prompt_template(agent_config['system_prompt'].strip())
|
||||
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"
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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
|
@ -66,6 +66,7 @@ dependencies = [
|
|||
"chardet==5.2.0",
|
||||
"PyYAML==6.0.1",
|
||||
"composio>=0.8.0",
|
||||
"aspose-slides>=25.7.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
|
2734
backend/uv.lock
2734
backend/uv.lock
File diff suppressed because it is too large
Load Diff
|
@ -36,19 +36,19 @@ export const AgentLoader = () => {
|
|||
return (
|
||||
<div className="flex py-2 items-center w-full">
|
||||
<div>✨</div>
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
key={items[index].id}
|
||||
initial={{ y: 20, opacity: 0, filter: "blur(8px)" }}
|
||||
animate={{ y: 0, opacity: 1, filter: "blur(0px)" }}
|
||||
exit={{ y: -20, opacity: 0, filter: "blur(8px)" }}
|
||||
transition={{ ease: "easeInOut" }}
|
||||
style={{ position: "absolute" }}
|
||||
className='ml-7'
|
||||
>
|
||||
<AnimatedShinyText>{items[index].content}</AnimatedShinyText>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
key={items[index].id}
|
||||
initial={{ y: 20, opacity: 0, filter: "blur(8px)" }}
|
||||
animate={{ y: 0, opacity: 1, filter: "blur(0px)" }}
|
||||
exit={{ y: -20, opacity: 0, filter: "blur(8px)" }}
|
||||
transition={{ ease: "easeInOut" }}
|
||||
style={{ position: "absolute" }}
|
||||
className='ml-7'
|
||||
>
|
||||
<AnimatedShinyText className='text-xs'>{items[index].content}</AnimatedShinyText>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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,13 +223,15 @@ export function PresentationToolView({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full bg-white dark:bg-zinc-900 rounded-lg overflow-hidden">
|
||||
<iframe
|
||||
srcDoc={slideContent}
|
||||
className="w-full h-full border-0"
|
||||
title={`Slide ${currentSlide.slide_number}: ${currentSlide.title}`}
|
||||
sandbox="allow-same-origin"
|
||||
/>
|
||||
<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"
|
||||
title={`Slide ${currentSlide.slide_number}: ${currentSlide.title}`}
|
||||
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"}
|
||||
>
|
||||
<Download className="h-3 w-3" />
|
||||
{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>
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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'],
|
||||
|
|
Loading…
Reference in New Issue