This commit is contained in:
Krishav 2025-08-25 13:18:08 +06:00 committed by GitHub
commit 0391674387
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 1227 additions and 10 deletions

File diff suppressed because it is too large Load Diff

View File

@ -7,4 +7,5 @@ pytesseract==0.3.13
pandas==2.3.0 pandas==2.3.0
playwright>=1.40.0 playwright>=1.40.0
PyPDF2>=3.0.0 PyPDF2>=3.0.0
bs4==0.0.2 bs4==0.0.2
python-pptx>=0.6.23

View File

@ -5,9 +5,10 @@ import uvicorn
import os import os
from pathlib import Path from pathlib import Path
# Import PDF router and Visual HTML Editor router # Import PDF router, PPTX router, and Visual HTML Editor router
from html_to_pdf_router import router as pdf_router from html_to_pdf_router import router as pdf_router
from visual_html_editor_router import router as editor_router from visual_html_editor_router import router as editor_router
from html_to_pptx_router import router as pptx_router
# Ensure we're serving from the /workspace directory # Ensure we're serving from the /workspace directory
workspace_dir = "/workspace" workspace_dir = "/workspace"
@ -26,6 +27,7 @@ app.add_middleware(WorkspaceDirMiddleware)
# Include routers # Include routers
app.include_router(pdf_router) app.include_router(pdf_router)
app.include_router(editor_router) app.include_router(editor_router)
app.include_router(pptx_router)
# Create output directory for generated PDFs (needed by PDF router) # Create output directory for generated PDFs (needed by PDF router)
output_dir = Path("generated_pdfs") output_dir = Path("generated_pdfs")

View File

@ -14,7 +14,7 @@ from fastapi import APIRouter, HTTPException
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel from pydantic import BaseModel
from bs4 import BeautifulSoup, NavigableString from bs4 import BeautifulSoup, NavigableString, Comment
# Create router # Create router
router = APIRouter(prefix="/api/html", tags=["visual-editor"]) router = APIRouter(prefix="/api/html", tags=["visual-editor"])
@ -78,6 +78,24 @@ async def get_editable_elements(file_path: str):
# Find all elements that could contain text # Find all elements that could contain text
all_elements = soup.find_all(TEXT_ELEMENTS + ['div']) all_elements = soup.find_all(TEXT_ELEMENTS + ['div'])
# Filter out elements that only contain comments
filtered_elements = []
for element in all_elements:
# Check if element only contains comments
only_comments = True
for child in element.children:
if isinstance(child, Comment):
continue
if isinstance(child, NavigableString) and not child.strip():
continue
only_comments = False
break
if not only_comments:
filtered_elements.append(element)
all_elements = filtered_elements
for element in all_elements: for element in all_elements:
# Strategy 1: Elements with ONLY text content (no child elements) # Strategy 1: Elements with ONLY text content (no child elements)
if element.string and element.string.strip(): if element.string and element.string.strip():
@ -99,6 +117,9 @@ async def get_editable_elements(file_path: str):
has_mixed_content = False has_mixed_content = False
# Process each child node # Process each child node
for child in list(element.contents): # Use list() to avoid modification during iteration for child in list(element.contents): # Use list() to avoid modification during iteration
# Skip comment nodes (Comments are a subclass of NavigableString)
if isinstance(child, Comment):
continue
# Check if it's a NavigableString (raw text) with actual content # Check if it's a NavigableString (raw text) with actual content
if (isinstance(child, NavigableString) and child.strip()): if (isinstance(child, NavigableString) and child.strip()):
@ -335,6 +356,24 @@ def inject_editor_functionality(html_content: str, file_path: str) -> str:
# Find all elements that could contain text # Find all elements that could contain text
all_elements = soup.find_all(TEXT_ELEMENTS + ['div']) all_elements = soup.find_all(TEXT_ELEMENTS + ['div'])
# Filter out elements that only contain comments
filtered_elements = []
for element in all_elements:
# Check if element only contains comments
only_comments = True
for child in element.children:
if isinstance(child, Comment):
continue
if isinstance(child, NavigableString) and not child.strip():
continue
only_comments = False
break
if not only_comments:
filtered_elements.append(element)
all_elements = filtered_elements
for element in all_elements: for element in all_elements:
# Strategy 1: Elements with ONLY text content (no child elements) # Strategy 1: Elements with ONLY text content (no child elements)
if element.string and element.string.strip(): if element.string and element.string.strip():
@ -347,6 +386,9 @@ def inject_editor_functionality(html_content: str, file_path: str) -> str:
has_mixed_content = False has_mixed_content = False
# Process each child node # Process each child node
for child in list(element.contents): # Use list() to avoid modification during iteration for child in list(element.contents): # Use list() to avoid modification during iteration
# Skip comment nodes (Comments are a subclass of NavigableString)
if isinstance(child, Comment):
continue
# Check if it's a NavigableString (raw text) with actual content # Check if it's a NavigableString (raw text) with actual content
if (isinstance(child, NavigableString) and child.strip()): if (isinstance(child, NavigableString) and child.strip()):

View File

@ -46,6 +46,7 @@ interface FullScreenPresentationViewerProps {
sandboxUrl?: string; sandboxUrl?: string;
initialSlide?: number; initialSlide?: number;
onPDFDownload?: (setIsDownloadingPDF: (isDownloading: boolean) => void) => void; onPDFDownload?: (setIsDownloadingPDF: (isDownloading: boolean) => void) => void;
onPPTXDownload?: (setIsDownloadingPPTX: (isDownloading: boolean) => void) => void;
} }
export function FullScreenPresentationViewer({ export function FullScreenPresentationViewer({
@ -55,6 +56,7 @@ export function FullScreenPresentationViewer({
sandboxUrl, sandboxUrl,
initialSlide = 1, initialSlide = 1,
onPDFDownload, onPDFDownload,
onPPTXDownload,
}: FullScreenPresentationViewerProps) { }: FullScreenPresentationViewerProps) {
const [metadata, setMetadata] = useState<PresentationMetadata | null>(null); const [metadata, setMetadata] = useState<PresentationMetadata | null>(null);
const [currentSlide, setCurrentSlide] = useState(initialSlide); const [currentSlide, setCurrentSlide] = useState(initialSlide);
@ -65,6 +67,7 @@ export function FullScreenPresentationViewer({
const [showControls, setShowControls] = useState(true); const [showControls, setShowControls] = useState(true);
const [showEditor, setShowEditor] = useState(false); const [showEditor, setShowEditor] = useState(false);
const [isDownloadingPDF, setIsDownloadingPDF] = useState(false); const [isDownloadingPDF, setIsDownloadingPDF] = useState(false);
const [isDownloadingPPTX, setIsDownloadingPPTX] = useState(false);
// Create a stable refresh timestamp when metadata changes (like PresentationViewer) // Create a stable refresh timestamp when metadata changes (like PresentationViewer)
const refreshTimestamp = useMemo(() => Date.now(), [metadata]); const refreshTimestamp = useMemo(() => Date.now(), [metadata]);
@ -418,7 +421,7 @@ export function FullScreenPresentationViewer({
className="h-8 w-8 p-0" className="h-8 w-8 p-0"
title="Export presentation" title="Export presentation"
> >
{isDownloadingPDF ? ( {(isDownloadingPDF || isDownloadingPPTX) ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" /> <Loader2 className="h-3.5 w-3.5 animate-spin" />
) : ( ) : (
<Download className="h-3.5 w-3.5" /> <Download className="h-3.5 w-3.5" />
@ -430,7 +433,7 @@ export function FullScreenPresentationViewer({
<FileText className="h-4 w-4 mr-2" /> <FileText className="h-4 w-4 mr-2" />
PDF PDF
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="cursor-pointer"> <DropdownMenuItem className="cursor-pointer" onClick={() => onPPTXDownload(setIsDownloadingPPTX)}>
<Presentation className="h-4 w-4 mr-2" /> <Presentation className="h-4 w-4 mr-2" />
PPTX PPTX
</DropdownMenuItem> </DropdownMenuItem>

View File

@ -79,6 +79,7 @@ export function PresentationViewer({
const [isFullScreenOpen, setIsFullScreenOpen] = useState(false); const [isFullScreenOpen, setIsFullScreenOpen] = useState(false);
const [fullScreenInitialSlide, setFullScreenInitialSlide] = useState<number | null>(null); const [fullScreenInitialSlide, setFullScreenInitialSlide] = useState<number | null>(null);
const [isDownloadingPDF, setIsDownloadingPDF] = useState(false); const [isDownloadingPDF, setIsDownloadingPDF] = useState(false);
const [isDownloadingPPTX, setIsDownloadingPPTX] = useState(false);
// Extract presentation info from tool data // Extract presentation info from tool data
const { toolResult } = extractToolData(toolContent); const { toolResult } = extractToolData(toolContent);
@ -505,6 +506,36 @@ export function PresentationViewer({
} }
}; };
const downloadPresentationAsPPTX = async (sandboxUrl: string, presentationName: string): Promise<void> => {
try {
const response = await fetch(`${sandboxUrl}/presentation/convert-to-pptx`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
presentation_path: `/workspace/presentations/${presentationName}`,
download: true
})
});
if (!response.ok) {
throw new Error('Failed to download PPTX');
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${presentationName}.pptx`;
a.click();
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('Error downloading PPTX:', error);
toast.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
const handlePDFDownload = useCallback(async (setIsDownloadingPDF: (isDownloading: boolean) => void) => { const handlePDFDownload = useCallback(async (setIsDownloadingPDF: (isDownloading: boolean) => void) => {
@ -520,6 +551,20 @@ export function PresentationViewer({
} }
}, [project?.sandbox?.sandbox_url, extractedPresentationName]); }, [project?.sandbox?.sandbox_url, extractedPresentationName]);
const handlePPTXDownload = useCallback(async (setIsDownloadingPPTX: (isDownloading: boolean) => void) => {
if (!project?.sandbox?.sandbox_url || !extractedPresentationName) return;
setIsDownloadingPPTX(true);
try{
await downloadPresentationAsPPTX(project.sandbox.sandbox_url, extractedPresentationName);
} catch (error) {
console.error('Error downloading PPTX:', error);
} finally {
setIsDownloadingPPTX(false);
}
}, [project?.sandbox?.sandbox_url, extractedPresentationName]);
return ( return (
<Card className="gap-0 flex border shadow-none border-t border-b-0 border-x-0 p-0 rounded-none flex-col h-full overflow-hidden bg-card"> <Card className="gap-0 flex border shadow-none border-t border-b-0 border-x-0 p-0 rounded-none flex-col h-full overflow-hidden bg-card">
<CardHeader className="h-14 bg-zinc-50/80 dark:bg-zinc-900/80 backdrop-blur-sm border-b p-2 px-4 space-y-2"> <CardHeader className="h-14 bg-zinc-50/80 dark:bg-zinc-900/80 backdrop-blur-sm border-b p-2 px-4 space-y-2">
@ -560,7 +605,7 @@ export function PresentationViewer({
className="h-8 w-8 p-0" className="h-8 w-8 p-0"
title="Export presentation" title="Export presentation"
> >
{isDownloadingPDF ? ( {(isDownloadingPDF || isDownloadingPPTX) ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" /> <Loader2 className="h-3.5 w-3.5 animate-spin" />
) : ( ) : (
<Download className="h-3.5 w-3.5" /> <Download className="h-3.5 w-3.5" />
@ -571,16 +616,15 @@ export function PresentationViewer({
<DropdownMenuItem <DropdownMenuItem
onClick={() => handlePDFDownload(setIsDownloadingPDF)} onClick={() => handlePDFDownload(setIsDownloadingPDF)}
className="cursor-pointer" className="cursor-pointer"
disabled={isDownloadingPPTX}
> >
<FileText className="h-4 w-4 mr-2" /> <FileText className="h-4 w-4 mr-2" />
PDF PDF
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => handlePPTXDownload(setIsDownloadingPPTX)}
// TODO: Implement PPTX export
console.log('Export as PPTX');
}}
className="cursor-pointer" className="cursor-pointer"
disabled={isDownloadingPDF}
> >
<Presentation className="h-4 w-4 mr-2" /> <Presentation className="h-4 w-4 mr-2" />
PPTX PPTX
@ -591,6 +635,7 @@ export function PresentationViewer({
console.log('Export to Google Slides'); console.log('Export to Google Slides');
}} }}
className="cursor-pointer" className="cursor-pointer"
disabled={isDownloadingPDF || isDownloadingPPTX}
> >
<ExternalLink className="h-4 w-4 mr-2" /> <ExternalLink className="h-4 w-4 mr-2" />
Google Slides Google Slides
@ -781,6 +826,7 @@ export function PresentationViewer({
sandboxUrl={project?.sandbox?.sandbox_url} sandboxUrl={project?.sandbox?.sandbox_url}
initialSlide={fullScreenInitialSlide || visibleSlide || currentSlideNumber || slides[0]?.number || 1} initialSlide={fullScreenInitialSlide || visibleSlide || currentSlideNumber || slides[0]?.number || 1}
onPDFDownload={handlePDFDownload} onPDFDownload={handlePDFDownload}
onPPTXDownload={handlePPTXDownload}
/> />
</Card> </Card>
); );