mirror of https://github.com/kortix-ai/suna.git
Merge 92cc53bf08
into a189e25a82
This commit is contained in:
commit
0391674387
File diff suppressed because it is too large
Load Diff
|
@ -8,3 +8,4 @@ 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
|
|
@ -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")
|
||||||
|
|
|
@ -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()):
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue