From cf6c6142c50389621eb41a1a75628e9768325d27 Mon Sep 17 00:00:00 2001 From: Krishav Raj Singh Date: Thu, 9 Oct 2025 07:13:07 +0530 Subject: [PATCH 1/2] validate ppt --- .../samples/presentation_agent_image.md | 22 +-- backend/core/tools/sb_presentation_tool.py | 178 ++++++++++++++++++ backend/core/utils/tool_groups.py | 6 + 3 files changed, 193 insertions(+), 13 deletions(-) diff --git a/backend/core/prompts/samples/presentation_agent_image.md b/backend/core/prompts/samples/presentation_agent_image.md index e46e5535..996071e7 100644 --- a/backend/core/prompts/samples/presentation_agent_image.md +++ b/backend/core/prompts/samples/presentation_agent_image.md @@ -5,7 +5,7 @@ You are a **Creative Presentation Virtuoso**, an elite visual storyteller and de ## 🚨 **Core Directives** 1. **Theme Consistency is Paramount**: You MUST maintain a single, consistent visual theme throughout the entire presentation. This includes colors, fonts, and layout patterns. No exceptions. -2. **Content Density is Strictly Controlled**: You MUST ensure that the content on each slide is concise and fits comfortably within the 1080px slide height. You will use a **Content Density Score** to validate this before creating each slide. +2. **Content Density is Strictly Controlled**: You MUST ensure that the content on each slide is concise and fits comfortably within the 1080px slide height. You will use the `validate_slide` tool after creating each slide to ensure proper dimensions. ## 🎨 **Mandatory Workflow** @@ -48,20 +48,16 @@ Follow this simplified, four-step workflow for every presentation. **DO NOT SKIP For each slide in your outline, you will perform the following steps: -1. **Calculate Content Density Score**: Before creating the slide, you MUST calculate a **Content Density Score**. This score is the sum of the following: - * **Title**: Number of characters / 10 - * **Paragraphs**: Total number of characters in all paragraphs / 20 - * **Bullet Points**: Number of bullet points * 5 - * **Images**: Number of images * 15 +1. **Create the Slide**: Create the slide using the `create_slide` tool. All styling MUST be derived from the **Theme Object** defined in Phase 2. Use relative path like `../images/[name]` to link images. -2. **Validate Content Density**: The **Content Density Score MUST NOT exceed 100**. If it does, you must revise the content to be more concise. +2. **Validate Slide Dimensions**: After creating each slide, you MUST use the `validate_slide` tool to verify that the slide height does not exceed 1080px. This tool will: + * Check for explicit height values exceeding 1080px + * Analyze content density and layout + * Provide warnings and recommendations if issues are detected + + If validation fails or warnings are raised, revise the slide to reduce content or adjust spacing before proceeding to the next slide. -3. **Declare and Create**: Once the score is validated, announce the score and then create the slide using the `create_slide` tool. All styling MUST be derived from the **Theme Object** defined in Phase 2. Use relative path like `../images/[name]` to link images. - - > **Example Slide Creation Announcement:** - > "The Content Density Score for this slide is 85, which is within the acceptable limit. I will now create the slide." - -4. **Enforce Theme Consistency**: Ensure that every slide uses the *exact same* colors and fonts from the **Theme Object**. Do not introduce new styles or deviate from the established theme. +3. **Enforce Theme Consistency**: Ensure that every slide uses the *exact same* colors and fonts from the **Theme Object**. Do not introduce new styles or deviate from the established theme. ### **Phase 4: Final Presentation** 🎯 diff --git a/backend/core/tools/sb_presentation_tool.py b/backend/core/tools/sb_presentation_tool.py index 4c67a8c6..7386fc88 100644 --- a/backend/core/tools/sb_presentation_tool.py +++ b/backend/core/tools/sb_presentation_tool.py @@ -6,6 +6,7 @@ import json import os from datetime import datetime import re +import asyncio class SandboxPresentationTool(SandboxToolsBase): """ @@ -427,6 +428,183 @@ class SandboxPresentationTool(SandboxToolsBase): return self.fail_response(f"Failed to delete presentation: {str(e)}") + @openapi_schema({ + "type": "function", + "function": { + "name": "validate_slide", + "description": "Validate a slide by reading its HTML code and checking if the content height exceeds 1080px. Use this tool to ensure slides fit within the standard presentation dimensions before finalizing them. This helps maintain proper slide formatting and prevents content overflow issues.", + "parameters": { + "type": "object", + "properties": { + "presentation_name": { + "type": "string", + "description": "Name of the presentation containing the slide to validate" + }, + "slide_number": { + "type": "integer", + "description": "Slide number to validate (1-based)" + } + }, + "required": ["presentation_name", "slide_number"] + } + } + }) + async def validate_slide(self, presentation_name: str, slide_number: int) -> ToolResult: + """Validate a slide by rendering it in a browser and measuring actual content height""" + try: + await self._ensure_sandbox() + + if not presentation_name: + return self.fail_response("Presentation name is required.") + + if slide_number < 1: + return self.fail_response("Slide number must be 1 or greater.") + + safe_name = self._sanitize_filename(presentation_name) + presentation_path = f"{self.workspace_path}/{self.presentations_dir}/{safe_name}" + + # Load metadata to verify slide exists + metadata = await self._load_presentation_metadata(presentation_path) + + if not metadata.get("slides") or str(slide_number) not in metadata["slides"]: + return self.fail_response(f"Slide {slide_number} not found in presentation '{presentation_name}'") + + # Get slide info + slide_info = metadata["slides"][str(slide_number)] + slide_filename = slide_info["filename"] + + # Create a Python script to measure the actual rendered height using Playwright + measurement_script = f''' +import asyncio +import json +from playwright.async_api import async_playwright + +async def measure_slide_height(): + async with async_playwright() as p: + browser = await p.chromium.launch( + headless=True, + args=['--no-sandbox', '--disable-setuid-sandbox'] + ) + page = await browser.new_page(viewport={{"width": 1920, "height": 1080}}) + + # Load the HTML file + await page.goto('file:///workspace/{self.presentations_dir}/{safe_name}/{slide_filename}') + + # Wait for page to load + await page.wait_for_load_state('networkidle') + + # Measure the actual content height + dimensions = await page.evaluate(""" + () => {{ + const body = document.body; + const html = document.documentElement; + + // Get the actual scroll height (total content height) + const scrollHeight = Math.max( + body.scrollHeight, body.offsetHeight, + html.clientHeight, html.scrollHeight, html.offsetHeight + ); + + // Get viewport height + const viewportHeight = window.innerHeight; + + // Check if content overflows + const overflows = scrollHeight > 1080; + + return {{ + scrollHeight: scrollHeight, + viewportHeight: viewportHeight, + overflows: overflows, + excessHeight: scrollHeight - 1080 + }}; + }} + """) + + await browser.close() + return dimensions + +result = asyncio.run(measure_slide_height()) +print(json.dumps(result)) +''' + + # Write the script to a temporary file in the sandbox + script_path = f"{self.workspace_path}/.validate_slide_temp.py" + await self.sandbox.fs.upload_file(measurement_script.encode(), script_path) + + # Execute the script + try: + result = await self.sandbox.process.exec( + f"/bin/sh -c 'cd /workspace && python3 .validate_slide_temp.py'", + timeout=30 + ) + + # Parse the result + output = (getattr(result, "result", None) or getattr(result, "output", "") or "").strip() + if not output: + raise Exception("No output from validation script") + + dimensions = json.loads(output) + + # Clean up the temporary script + try: + await self.sandbox.fs.delete_file(script_path) + except: + pass + + except Exception as e: + # Clean up on error + try: + await self.sandbox.fs.delete_file(script_path) + except: + pass + return self.fail_response(f"Failed to measure slide dimensions: {str(e)}") + + # Analyze results + validation_results = { + "slide_number": slide_number, + "slide_title": slide_info["title"], + "actual_content_height": dimensions["scrollHeight"], + "target_height": 1080, + "excess_height": dimensions["excessHeight"], + "validation_passed": not dimensions["overflows"], + "warnings": [], + "errors": [], + "recommendations": [] + } + + # Add errors or success messages based on actual measurements + if dimensions["overflows"]: + validation_results["validation_passed"] = False + validation_results["errors"].append( + f"Content height ({dimensions['scrollHeight']}px) exceeds the 1080px limit by {dimensions['excessHeight']}px" + ) + validation_results["recommendations"].append( + f"Reduce content or spacing by at least {dimensions['excessHeight']}px to fit within 1080px height" + ) + + # Provide warnings for slides close to the limit + elif dimensions["scrollHeight"] > 1000: + margin = 1080 - dimensions["scrollHeight"] + validation_results["warnings"].append( + f"Content height ({dimensions['scrollHeight']}px) is close to the 1080px limit (only {margin}px margin remaining)" + ) + validation_results["recommendations"].append( + "Consider reducing content slightly to ensure comfortable fit across different browsers" + ) + + # Summary message + if validation_results["validation_passed"] and not validation_results["warnings"]: + validation_results["message"] = f"✓ Slide {slide_number} '{slide_info['title']}' validation passed. Content height: {dimensions['scrollHeight']}px (within 1080px limit)" + elif validation_results["validation_passed"] and validation_results["warnings"]: + validation_results["message"] = f"⚠ Slide {slide_number} '{slide_info['title']}' validation passed with {len(validation_results['warnings'])} warning(s)" + else: + validation_results["message"] = f"✗ Slide {slide_number} '{slide_info['title']}' validation failed. Content height: {dimensions['scrollHeight']}px exceeds 1080px by {dimensions['excessHeight']}px" + + return self.success_response(validation_results) + + except Exception as e: + return self.fail_response(f"Failed to validate slide: {str(e)}") + @openapi_schema({ "type": "function", "function": { diff --git a/backend/core/utils/tool_groups.py b/backend/core/utils/tool_groups.py index 31283e9d..4d6b86bb 100644 --- a/backend/core/utils/tool_groups.py +++ b/backend/core/utils/tool_groups.py @@ -218,6 +218,12 @@ TOOL_GROUPS: Dict[str, ToolGroup] = { description="Delete entire presentations", enabled=True ), + ToolMethod( + name="validate_slide", + display_name="Validate Slide", + description="Validate slide dimensions and content height", + enabled=True + ), ToolMethod( name="present_presentation", display_name="Present Presentation", From 5ea69c198075b215fff4decac340ef60c709dd2a Mon Sep 17 00:00:00 2001 From: Krishav Raj Singh Date: Thu, 9 Oct 2025 16:26:31 +0530 Subject: [PATCH 2/2] ui: validate slide --- .../samples/presentation_agent_image.md | 18 +++-- backend/core/tools/sb_presentation_tool.py | 65 +++++-------------- .../agents/tools/tool-groups-comprehensive.ts | 6 ++ .../tool-views/wrapper/ToolViewRegistry.tsx | 2 + 4 files changed, 37 insertions(+), 54 deletions(-) diff --git a/backend/core/prompts/samples/presentation_agent_image.md b/backend/core/prompts/samples/presentation_agent_image.md index 996071e7..68651eaa 100644 --- a/backend/core/prompts/samples/presentation_agent_image.md +++ b/backend/core/prompts/samples/presentation_agent_image.md @@ -16,7 +16,7 @@ Follow this simplified, four-step workflow for every presentation. **DO NOT SKIP 1. **Understand the User’s Needs**: Ask the user about the presentation’s **audience, context, and goals**. 2. **Gather Information**: Use `web_search` and `web_scape` to research the topic thoroughly. 3. **Create a Content Outline**: Develop a structured outline that maps out the content for each slide. Focus on one main idea per slide. Also decide if a slide need any images or not, if yes what all. images will it need based on content. -4. **Search Images**: Use `image_search` to batch search all images that are required, set num_results based on the number of images needed. +4. **Search Images**: Use `image_search` to batch search all images that are required. **IMPORTANT**: Search for only 2 images per topic (set `num_results=2`) to avoid confusion and keep results focused. 5. **Download Images**: Use `wget` command to batch download all images in `presentations/images` folder. ### **Phase 2: Theme Definition** 🎨 @@ -50,14 +50,18 @@ For each slide in your outline, you will perform the following steps: 1. **Create the Slide**: Create the slide using the `create_slide` tool. All styling MUST be derived from the **Theme Object** defined in Phase 2. Use relative path like `../images/[name]` to link images. -2. **Validate Slide Dimensions**: After creating each slide, you MUST use the `validate_slide` tool to verify that the slide height does not exceed 1080px. This tool will: - * Check for explicit height values exceeding 1080px - * Analyze content density and layout - * Provide warnings and recommendations if issues are detected +2. **Validate Slide Dimensions**: After creating each slide, you MUST use the `validate_slide` tool to verify that the slide height does not exceed 1080px. The validation is simple pass/fail: + * **Pass**: Content height ≤ 1080px + * **Fail**: Content height > 1080px - If validation fails or warnings are raised, revise the slide to reduce content or adjust spacing before proceeding to the next slide. + If validation fails, you must edit the slide to reduce content or adjust spacing before proceeding to the next slide. -3. **Enforce Theme Consistency**: Ensure that every slide uses the *exact same* colors and fonts from the **Theme Object**. Do not introduce new styles or deviate from the established theme. +3. **Edit Slides When Needed**: When you need to modify existing slides (e.g., after validation failures or to update content), use the appropriate file editing tools: + * **`edit_file`** (PREFERRED): AI-powered editing tool for quick, precise edits. Specify only the lines you want to change using `// ... existing code ...` for unchanged sections. This is the fastest and most efficient option. + * **`str_replace`**: For simple exact text replacements (titles, colors, specific strings). Only use when `edit_file` is not suitable and the string appears exactly once. + * **`full_file_rewrite`**: Complete file rewrite. Use as last resort when other tools fail or when replacing entire slide content. + +4. **Enforce Theme Consistency**: Ensure that every slide uses the *exact same* colors and fonts from the **Theme Object**. Do not introduce new styles or deviate from the established theme. ### **Phase 4: Final Presentation** 🎯 diff --git a/backend/core/tools/sb_presentation_tool.py b/backend/core/tools/sb_presentation_tool.py index 7386fc88..651cc0a1 100644 --- a/backend/core/tools/sb_presentation_tool.py +++ b/backend/core/tools/sb_presentation_tool.py @@ -493,29 +493,21 @@ async def measure_slide_height(): # Wait for page to load await page.wait_for_load_state('networkidle') - # Measure the actual content height + # Measure the actual rendered height of the content dimensions = await page.evaluate(""" () => {{ - const body = document.body; - const html = document.documentElement; + // Get the actual bounding box height of the body content + const bodyRect = document.body.getBoundingClientRect(); + const actualHeight = Math.ceil(bodyRect.height); - // Get the actual scroll height (total content height) - const scrollHeight = Math.max( - body.scrollHeight, body.offsetHeight, - html.clientHeight, html.scrollHeight, html.offsetHeight - ); - - // Get viewport height const viewportHeight = window.innerHeight; - - // Check if content overflows - const overflows = scrollHeight > 1080; + const overflows = actualHeight > 1080; return {{ - scrollHeight: scrollHeight, + scrollHeight: actualHeight, viewportHeight: viewportHeight, overflows: overflows, - excessHeight: scrollHeight - 1080 + excessHeight: Math.max(0, actualHeight - 1080) }}; }} """) @@ -559,46 +551,25 @@ print(json.dumps(result)) pass return self.fail_response(f"Failed to measure slide dimensions: {str(e)}") - # Analyze results + # Analyze results - simple pass/fail + validation_passed = not dimensions["overflows"] + validation_results = { + "presentation_name": presentation_name, + "presentation_path": presentation_path, "slide_number": slide_number, "slide_title": slide_info["title"], "actual_content_height": dimensions["scrollHeight"], "target_height": 1080, - "excess_height": dimensions["excessHeight"], - "validation_passed": not dimensions["overflows"], - "warnings": [], - "errors": [], - "recommendations": [] + "validation_passed": validation_passed } - # Add errors or success messages based on actual measurements - if dimensions["overflows"]: - validation_results["validation_passed"] = False - validation_results["errors"].append( - f"Content height ({dimensions['scrollHeight']}px) exceeds the 1080px limit by {dimensions['excessHeight']}px" - ) - validation_results["recommendations"].append( - f"Reduce content or spacing by at least {dimensions['excessHeight']}px to fit within 1080px height" - ) - - # Provide warnings for slides close to the limit - elif dimensions["scrollHeight"] > 1000: - margin = 1080 - dimensions["scrollHeight"] - validation_results["warnings"].append( - f"Content height ({dimensions['scrollHeight']}px) is close to the 1080px limit (only {margin}px margin remaining)" - ) - validation_results["recommendations"].append( - "Consider reducing content slightly to ensure comfortable fit across different browsers" - ) - - # Summary message - if validation_results["validation_passed"] and not validation_results["warnings"]: - validation_results["message"] = f"✓ Slide {slide_number} '{slide_info['title']}' validation passed. Content height: {dimensions['scrollHeight']}px (within 1080px limit)" - elif validation_results["validation_passed"] and validation_results["warnings"]: - validation_results["message"] = f"⚠ Slide {slide_number} '{slide_info['title']}' validation passed with {len(validation_results['warnings'])} warning(s)" + # Add pass/fail message + if validation_passed: + validation_results["message"] = f"✓ Slide {slide_number} '{slide_info['title']}' validation passed. Content height: {dimensions['scrollHeight']}px" else: - validation_results["message"] = f"✗ Slide {slide_number} '{slide_info['title']}' validation failed. Content height: {dimensions['scrollHeight']}px exceeds 1080px by {dimensions['excessHeight']}px" + validation_results["message"] = f"✗ Slide {slide_number} '{slide_info['title']}' validation failed. Content height: {dimensions['scrollHeight']}px exceeds 1080px limit by {dimensions['excessHeight']}px" + validation_results["excess_height"] = dimensions["excessHeight"] return self.success_response(validation_results) diff --git a/frontend/src/components/agents/tools/tool-groups-comprehensive.ts b/frontend/src/components/agents/tools/tool-groups-comprehensive.ts index 9cad1167..a68e5acc 100644 --- a/frontend/src/components/agents/tools/tool-groups-comprehensive.ts +++ b/frontend/src/components/agents/tools/tool-groups-comprehensive.ts @@ -484,6 +484,12 @@ export const TOOL_GROUPS: Record = { description: 'Delete entire presentations', enabled: true, }, + { + name: 'validate_slide', + displayName: 'Validate Slide', + description: 'Validate slide dimensions and content height', + enabled: true, + } ], }, diff --git a/frontend/src/components/thread/tool-views/wrapper/ToolViewRegistry.tsx b/frontend/src/components/thread/tool-views/wrapper/ToolViewRegistry.tsx index a988c8bb..0bb233f2 100644 --- a/frontend/src/components/thread/tool-views/wrapper/ToolViewRegistry.tsx +++ b/frontend/src/components/thread/tool-views/wrapper/ToolViewRegistry.tsx @@ -136,6 +136,7 @@ const defaultRegistry: ToolViewRegistryType = { 'list-presentations': ListPresentationsToolView, 'delete-slide': DeleteSlideToolView, 'delete-presentation': DeletePresentationToolView, + 'validate-slide': PresentationViewer, // 'presentation-styles': PresentationStylesToolView, 'present-presentation': PresentPresentationToolView, @@ -263,6 +264,7 @@ export function ToolView({ name = 'default', assistantContent, toolContent, ...p 'list-slides', 'delete-slide', 'delete-presentation', + 'validate-slide', // 'presentation-styles', 'present-presentation', ]