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

View File

@ -239,26 +239,6 @@ class PromptManager:
else: else:
system_content = default_system_content 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: 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 = "\n\n--- MCP Tools Available ---\n"
mcp_info += "You have access to external MCP (Model Context Protocol) server tools.\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", "type": "function",
"function": { "function": {
"name": "create_presentation_outline", "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": { "parameters": {
"type": "object", "type": "object",
"properties": { "properties": {

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -46,7 +46,7 @@ export const AgentLoader = () => {
style={{ position: "absolute" }} style={{ position: "absolute" }}
className='ml-7' className='ml-7'
> >
<AnimatedShinyText>{items[index].content}</AnimatedShinyText> <AnimatedShinyText className='text-xs'>{items[index].content}</AnimatedShinyText>
</motion.div> </motion.div>
</AnimatePresence> </AnimatePresence>
</div> </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 { constructHtmlPreviewUrl } from '@/lib/utils/url';
import { CodeBlockCode } from '@/components/ui/code-block'; import { CodeBlockCode } from '@/components/ui/code-block';
import { LoadingState } from './shared/LoadingState'; import { LoadingState } from './shared/LoadingState';
import { useMutation } from '@tanstack/react-query';
import { backendApi } from '@/lib/api-client';
import { useParams } from 'next/navigation';
interface SlideInfo { interface SlideInfo {
slide_number: number; slide_number: number;
@ -46,6 +49,23 @@ interface PresentationData {
total_slides?: number; 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({ export function PresentationToolView({
assistantContent, assistantContent,
toolContent, toolContent,
@ -59,8 +79,60 @@ export function PresentationToolView({
const [currentSlideIndex, setCurrentSlideIndex] = useState(0); const [currentSlideIndex, setCurrentSlideIndex] = useState(0);
const [slideContents, setSlideContents] = useState<Record<string, string>>({}); const [slideContents, setSlideContents] = useState<Record<string, string>>({});
const [loadingSlides, setLoadingSlides] = useState<Set<string>>(new Set()); 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); const { toolResult } = extractToolData(toolContent);
let presentationData: PresentationData | null = null; let presentationData: PresentationData | null = null;
@ -89,30 +161,20 @@ export function PresentationToolView({
if (slideContents[slidePath] || loadingSlides.has(slidePath)) { if (slideContents[slidePath] || loadingSlides.has(slidePath)) {
return; return;
} }
setLoadingSlides(prev => new Set(prev).add(slidePath));
setLoadingSlides(prev => new Set([...prev, slidePath]));
try { try {
const slideUrl = constructHtmlPreviewUrl(project?.sandbox?.sandbox_url, slidePath); const fullUrl = constructHtmlPreviewUrl(project?.sandbox?.sandbox_url, slidePath);
if (!slideUrl) { const response = await fetch(fullUrl);
throw new Error('Unable to construct slide URL - sandbox not available');
}
const response = await fetch(slideUrl);
if (!response.ok) { 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) { } catch (error) {
console.error('Failed to load slide content:', 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 { } finally {
setLoadingSlides(prev => { setLoadingSlides(prev => {
const newSet = new Set(prev); const newSet = new Set(prev);
@ -123,9 +185,8 @@ export function PresentationToolView({
}; };
const currentSlide = presentationData?.slides[currentSlideIndex]; 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)) { if (currentSlide && !slideContent && !loadingSlides.has(currentSlide.file)) {
loadSlideContent(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 = () => { const renderSlidePreview = () => {
if (!currentSlide) return null; if (!currentSlide) return null;
@ -151,7 +223,8 @@ export function PresentationToolView({
} }
return ( 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 <iframe
srcDoc={slideContent} srcDoc={slideContent}
className="w-full h-full border-0" className="w-full h-full border-0"
@ -159,6 +232,7 @@ export function PresentationToolView({
sandbox="allow-same-origin" sandbox="allow-same-origin"
/> />
</div> </div>
</div>
); );
}; };
@ -276,10 +350,16 @@ export function PresentationToolView({
variant="outline" variant="outline"
size="sm" size="sm"
className="gap-2 h-7 text-xs" className="gap-2 h-7 text-xs"
disabled onClick={handleExportPPTX}
title="Export functionality coming soon" 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" /> <Download className="h-3 w-3" />
)}
{exportMutation.isPending ? "Exporting..." : "PPTX"}
</Button> </Button>
<TabsList className="h-8 bg-muted/50 border border-border/50 p-0.5 gap-1"> <TabsList className="h-8 bg-muted/50 border border-border/50 p-0.5 gap-1">
<TabsTrigger <TabsTrigger
@ -326,8 +406,8 @@ export function PresentationToolView({
</div> </div>
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
<TabsContent value="preview" className="h-full mt-0 p-4"> <TabsContent value="preview" className="h-full mt-0">
<div className="h-full rounded-lg border bg-muted/20 p-4"> <div className="h-full rounded-lg border bg-muted/20">
{renderSlidePreview()} {renderSlidePreview()}
</div> </div>
</TabsContent> </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"> <div className="h-full flex items-center gap-2 text-sm text-zinc-500 dark:text-zinc-400">
{!isStreaming && presentationData && ( {!isStreaming && presentationData && (
<Badge variant="outline" className="h-6 py-0.5 bg-zinc-50 dark:bg-zinc-900"> <Badge variant="outline" className="h-6 py-0.5 bg-zinc-50 dark:bg-zinc-900">
<Presentation className="h-3 w-3 mr-1" /> {presentationData.slides.length} slides
Presentation
</Badge> </Badge>
)} )}
</div> </div>
<div className="h-full flex items-center gap-2 text-xs text-zinc-400 dark:text-zinc-500">
<div className="text-xs text-zinc-500 dark:text-zinc-400 flex items-center gap-2"> <Clock className="h-3 w-3" />
<Clock className="h-3.5 w-3.5" /> <span>
{toolTimestamp && !isStreaming {formatTimestamp(toolTimestamp)}
? formatTimestamp(toolTimestamp) </span>
: assistantTimestamp
? formatTimestamp(assistantTimestamp)
: ''}
</div> </div>
</div> </div>
</Card> </Card>

View File

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

View File

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