mirror of https://github.com/kortix-ai/suna.git
Merge pull request #1435 from KrishavRajSingh/feat/editor
HTML editor and export PDF
This commit is contained in:
commit
8a55491775
|
@ -20,7 +20,7 @@ You can modify the sandbox environment for development or to add new capabilitie
|
|||
```
|
||||
cd backend/sandbox/docker
|
||||
docker compose build
|
||||
docker push kortix/suna:0.1.3.9
|
||||
docker push kortix/suna:0.1.3.11
|
||||
```
|
||||
3. Test your changes locally using docker-compose
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ services:
|
|||
dockerfile: ${DOCKERFILE:-Dockerfile}
|
||||
args:
|
||||
TARGETPLATFORM: ${TARGETPLATFORM:-linux/amd64}
|
||||
image: kortix/suna:0.1.3.9
|
||||
image: kortix/suna:0.1.3.11
|
||||
ports:
|
||||
- "6080:6080" # noVNC web interface
|
||||
- "5901:5901" # VNC port
|
||||
|
|
|
@ -74,11 +74,11 @@ class PresentationToPDFAPI:
|
|||
|
||||
for slide_num, slide_data in slides.items():
|
||||
filename = slide_data.get('filename')
|
||||
file_path = slide_data.get('file_path')
|
||||
title = slide_data.get('title', f'Slide {slide_num}')
|
||||
|
||||
if filename:
|
||||
# Treat filename as absolute path only
|
||||
html_path = Path(filename)
|
||||
if file_path:
|
||||
html_path = Path(f"/workspace/{file_path}")
|
||||
print(f"Using path: {html_path}")
|
||||
|
||||
# Verify the path exists
|
||||
|
|
|
@ -6,4 +6,5 @@ pydantic==2.6.1
|
|||
pytesseract==0.3.13
|
||||
pandas==2.3.0
|
||||
playwright>=1.40.0
|
||||
PyPDF2>=3.0.0
|
||||
PyPDF2>=3.0.0
|
||||
bs4==0.0.2
|
|
@ -535,6 +535,7 @@ def inject_editor_functionality(html_content: str, file_path: str) -> str:
|
|||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.save-btn, .cancel-btn {
|
||||
|
|
|
@ -306,8 +306,8 @@ class Configuration:
|
|||
STRIPE_PRODUCT_ID_STAGING: str = 'prod_SCgIj3G7yPOAWY'
|
||||
|
||||
# Sandbox configuration
|
||||
SANDBOX_IMAGE_NAME = "kortix/suna:0.1.3.9"
|
||||
SANDBOX_SNAPSHOT_NAME = "kortix/suna:0.1.3.9"
|
||||
SANDBOX_IMAGE_NAME = "kortix/suna:0.1.3.11"
|
||||
SANDBOX_SNAPSHOT_NAME = "kortix/suna:0.1.3.11"
|
||||
SANDBOX_ENTRYPOINT = "/usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf"
|
||||
|
||||
# LangFuse configuration
|
||||
|
|
|
@ -127,8 +127,8 @@ As part of the setup, you'll need to:
|
|||
1. Create a Daytona account
|
||||
2. Generate an API key
|
||||
3. Create a Snapshot:
|
||||
- Name: `kortix/suna:0.1.3.9`
|
||||
- Image name: `kortix/suna:0.1.3.9`
|
||||
- Name: `kortix/suna:0.1.3.11`
|
||||
- Image name: `kortix/suna:0.1.3.11`
|
||||
- Entrypoint: `/usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf`
|
||||
|
||||
## Manual Configuration
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
SkipBack,
|
||||
SkipForward,
|
||||
Edit,
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
@ -44,6 +45,7 @@ interface FullScreenPresentationViewerProps {
|
|||
presentationName?: string;
|
||||
sandboxUrl?: string;
|
||||
initialSlide?: number;
|
||||
onPDFDownload?: (setIsDownloadingPDF: (isDownloading: boolean) => void) => void;
|
||||
}
|
||||
|
||||
export function FullScreenPresentationViewer({
|
||||
|
@ -52,12 +54,15 @@ export function FullScreenPresentationViewer({
|
|||
presentationName,
|
||||
sandboxUrl,
|
||||
initialSlide = 1,
|
||||
onPDFDownload,
|
||||
}: FullScreenPresentationViewerProps) {
|
||||
const [metadata, setMetadata] = useState<PresentationMetadata | null>(null);
|
||||
const [currentSlide, setCurrentSlide] = useState(initialSlide);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showControls, setShowControls] = useState(true);
|
||||
const [showEditor, setShowEditor] = useState(false);
|
||||
const [isDownloadingPDF, setIsDownloadingPDF] = useState(false);
|
||||
|
||||
// Create a stable refresh timestamp when metadata changes (like PresentationViewer)
|
||||
const refreshTimestamp = useMemo(() => Date.now(), [metadata]);
|
||||
|
@ -115,6 +120,24 @@ export function FullScreenPresentationViewer({
|
|||
}
|
||||
}, [isOpen, loadMetadata, initialSlide]);
|
||||
|
||||
// Reload metadata when exiting editor mode to refresh with latest changes
|
||||
useEffect(() => {
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
if (!showEditor) {
|
||||
// Add a small delay to allow the editor to save changes
|
||||
timeoutId = setTimeout(() => {
|
||||
loadMetadata();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
}, [showEditor, loadMetadata]);
|
||||
|
||||
// Navigation functions
|
||||
const goToNextSlide = useCallback(() => {
|
||||
if (currentSlide < totalSlides) {
|
||||
|
@ -133,6 +156,7 @@ export function FullScreenPresentationViewer({
|
|||
if (!isOpen) return;
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
|
||||
// Prevent default for all our handled keys
|
||||
const handledKeys = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', ' ', 'Home', 'End', 'Escape'];
|
||||
if (handledKeys.includes(e.key)) {
|
||||
|
@ -157,7 +181,11 @@ export function FullScreenPresentationViewer({
|
|||
setCurrentSlide(totalSlides);
|
||||
break;
|
||||
case 'Escape':
|
||||
onClose();
|
||||
if (showEditor) {
|
||||
setShowEditor(false);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -165,7 +193,7 @@ export function FullScreenPresentationViewer({
|
|||
// Add event listener to document with capture to ensure we get the events first
|
||||
document.addEventListener('keydown', handleKeyDown, true);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown, true);
|
||||
}, [isOpen, goToNextSlide, goToPreviousSlide, totalSlides, onClose]);
|
||||
}, [isOpen, goToNextSlide, goToPreviousSlide, totalSlides, onClose, showEditor]);
|
||||
|
||||
|
||||
|
||||
|
@ -247,11 +275,11 @@ export function FullScreenPresentationViewer({
|
|||
}}
|
||||
>
|
||||
<iframe
|
||||
key={`slide-${slide.number}-${refreshTimestamp}`} // Key with stable timestamp ensures iframe refreshes when metadata changes
|
||||
src={slideUrlWithCacheBust}
|
||||
key={`slide-${slide.number}-${refreshTimestamp}-${showEditor}`} // Key with stable timestamp ensures iframe refreshes when metadata changes
|
||||
src={showEditor ? `${sandboxUrl}/api/html/${slide.file_path}/editor` : slideUrlWithCacheBust}
|
||||
title={`Slide ${slide.number}: ${slide.title}`}
|
||||
className="border-0 rounded-xl"
|
||||
sandbox="allow-same-origin allow-scripts"
|
||||
sandbox="allow-same-origin allow-scripts allow-modals"
|
||||
style={{
|
||||
width: '1920px',
|
||||
height: '1080px',
|
||||
|
@ -261,7 +289,7 @@ export function FullScreenPresentationViewer({
|
|||
transformOrigin: '0 0',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
left: `calc((100% - ${1920 * scale}px) / 2)`,
|
||||
willChange: 'transform',
|
||||
backfaceVisibility: 'hidden',
|
||||
WebkitBackfaceVisibility: 'hidden'
|
||||
|
@ -278,7 +306,7 @@ export function FullScreenPresentationViewer({
|
|||
|
||||
SlideIframeComponent.displayName = 'SlideIframeComponent';
|
||||
return SlideIframeComponent;
|
||||
}, [sandboxUrl, refreshTimestamp]);
|
||||
}, [sandboxUrl, refreshTimestamp, showEditor]);
|
||||
|
||||
// Render slide iframe with proper scaling
|
||||
const renderSlide = useMemo(() => {
|
||||
|
@ -317,9 +345,10 @@ export function FullScreenPresentationViewer({
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
title="Edit presentation"
|
||||
title={showEditor ? "Close editor" : "Edit presentation"}
|
||||
onClick={() => setShowEditor(!showEditor)}
|
||||
>
|
||||
<Edit className="h-3.5 w-3.5" />
|
||||
{showEditor ? <Presentation className="h-3.5 w-3.5" /> : <Edit className='h-3.5 w-3.5'/>}
|
||||
</Button>
|
||||
|
||||
{/* Export dropdown */}
|
||||
|
@ -331,11 +360,15 @@ export function FullScreenPresentationViewer({
|
|||
className="h-8 w-8 p-0"
|
||||
title="Export presentation"
|
||||
>
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
{isDownloadingPDF ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-32">
|
||||
<DropdownMenuItem className="cursor-pointer">
|
||||
<DropdownMenuItem className="cursor-pointer" onClick={() => onPDFDownload(setIsDownloadingPDF)}>
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
PDF
|
||||
</DropdownMenuItem>
|
||||
|
|
|
@ -34,6 +34,7 @@ import { constructHtmlPreviewUrl } from '@/lib/utils/url';
|
|||
import { CodeBlockCode } from '@/components/ui/code-block';
|
||||
import { LoadingState } from '../shared/LoadingState';
|
||||
import { FullScreenPresentationViewer } from './FullScreenPresentationViewer';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface SlideMetadata {
|
||||
title: string;
|
||||
|
@ -75,6 +76,7 @@ export function PresentationViewer({
|
|||
const [visibleSlide, setVisibleSlide] = useState<number | null>(null);
|
||||
const [isFullScreenOpen, setIsFullScreenOpen] = useState(false);
|
||||
const [fullScreenInitialSlide, setFullScreenInitialSlide] = useState<number | null>(null);
|
||||
const [isDownloadingPDF, setIsDownloadingPDF] = useState(false);
|
||||
|
||||
// Extract presentation info from tool data
|
||||
const { toolResult } = extractToolData(toolContent);
|
||||
|
@ -423,8 +425,52 @@ export function PresentationViewer({
|
|||
return <SlideIframe slide={slide} />;
|
||||
}, [SlideIframe]);
|
||||
|
||||
const downloadPresentationAsPDF = async (sandboxUrl: string, presentationName: string): Promise<void> => {
|
||||
try {
|
||||
const response = await fetch(`${sandboxUrl}/presentation/convert-to-pdf`, {
|
||||
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 PDF');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${presentationName}.pdf`;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Error downloading PDF:', error);
|
||||
// You could show a toast notification here
|
||||
toast.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handlePDFDownload = useCallback(async (setIsDownloadingPDF: (isDownloading: boolean) => void) => {
|
||||
|
||||
if (!project?.sandbox?.sandbox_url || !extractedPresentationName) return;
|
||||
|
||||
setIsDownloadingPDF(true);
|
||||
try{
|
||||
await downloadPresentationAsPDF(project.sandbox.sandbox_url, extractedPresentationName);
|
||||
} catch (error) {
|
||||
console.error('Error downloading PDF:', error);
|
||||
} finally {
|
||||
setIsDownloadingPDF(false);
|
||||
}
|
||||
}, [project?.sandbox?.sandbox_url, extractedPresentationName]);
|
||||
|
||||
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">
|
||||
<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">
|
||||
|
@ -465,15 +511,16 @@ export function PresentationViewer({
|
|||
className="h-8 w-8 p-0"
|
||||
title="Export presentation"
|
||||
>
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
{isDownloadingPDF ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-32">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
// TODO: Implement PDF export
|
||||
console.log('Export as PDF');
|
||||
}}
|
||||
onClick={() => handlePDFDownload(setIsDownloadingPDF)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
|
@ -647,10 +694,15 @@ export function PresentationViewer({
|
|||
onClose={() => {
|
||||
setIsFullScreenOpen(false);
|
||||
setFullScreenInitialSlide(null);
|
||||
// Reload metadata after closing full screen viewer in case edits were made
|
||||
setTimeout(() => {
|
||||
loadMetadata();
|
||||
}, 300);
|
||||
}}
|
||||
presentationName={extractedPresentationName}
|
||||
sandboxUrl={project?.sandbox?.sandbox_url}
|
||||
initialSlide={fullScreenInitialSlide || visibleSlide || currentSlideNumber || slides[0]?.number || 1}
|
||||
onPDFDownload={handlePDFDownload}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
|
4
setup.py
4
setup.py
|
@ -669,8 +669,8 @@ class SetupWizard:
|
|||
f"Visit {Colors.GREEN}https://app.daytona.io/dashboard/snapshots{Colors.ENDC}{Colors.CYAN} to create a snapshot."
|
||||
)
|
||||
print_info("Create a snapshot with these exact settings:")
|
||||
print_info(f" - Name:\t\t{Colors.GREEN}kortix/suna:0.1.3.9{Colors.ENDC}")
|
||||
print_info(f" - Snapshot name:\t{Colors.GREEN}kortix/suna:0.1.3.9{Colors.ENDC}")
|
||||
print_info(f" - Name:\t\t{Colors.GREEN}kortix/suna:0.1.3.11{Colors.ENDC}")
|
||||
print_info(f" - Snapshot name:\t{Colors.GREEN}kortix/suna:0.1.3.11{Colors.ENDC}")
|
||||
print_info(
|
||||
f" - Entrypoint:\t{Colors.GREEN}/usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf{Colors.ENDC}"
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue