mirror of https://github.com/kortix-ai/suna.git
feat: upload to drive
This commit is contained in:
parent
67705e6345
commit
045f27eaa1
|
@ -48,6 +48,7 @@ import { useAgentSelection } from '@/lib/stores/agent-selection-store';
|
|||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { threadKeys } from '@/hooks/react-query/threads/keys';
|
||||
import { useProjectRealtime } from '@/hooks/useProjectRealtime';
|
||||
import { handleGoogleSlidesUpload } from './tool-views/utils/presentation-utils';
|
||||
|
||||
interface ThreadComponentProps {
|
||||
projectId: string;
|
||||
|
@ -172,6 +173,60 @@ export function ThreadComponent({ projectId, threadId, compact = false, configur
|
|||
queryClient.invalidateQueries({ queryKey: threadKeys.messages(threadId) });
|
||||
}, [threadId, queryClient]);
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
if (urlParams.get('google_auth') === 'success') {
|
||||
// Clean up the URL parameters first
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
|
||||
// Check if there was an intent to upload to Google Slides
|
||||
const uploadIntent = sessionStorage.getItem('google_slides_upload_intent');
|
||||
if (uploadIntent) {
|
||||
sessionStorage.removeItem('google_slides_upload_intent');
|
||||
|
||||
try {
|
||||
const uploadData = JSON.parse(uploadIntent);
|
||||
const { presentation_path, sandbox_url } = uploadData;
|
||||
|
||||
if (presentation_path && sandbox_url) {
|
||||
// Handle upload in async function
|
||||
(async () => {
|
||||
const uploadPromise = handleGoogleSlidesUpload(
|
||||
sandbox_url,
|
||||
presentation_path
|
||||
);
|
||||
|
||||
// Show loading toast and handle upload
|
||||
const loadingToast = toast.loading('Google authentication successful! Uploading presentation...');
|
||||
|
||||
try {
|
||||
await uploadPromise;
|
||||
// Success toast is now handled universally by handleGoogleSlidesUpload
|
||||
} catch (error) {
|
||||
console.error('Upload failed:', error);
|
||||
// Error toast is also handled universally by handleGoogleSlidesUpload
|
||||
} finally {
|
||||
// Always dismiss loading toast
|
||||
toast.dismiss(loadingToast);
|
||||
}
|
||||
})();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing Google Slides upload from session:', error);
|
||||
// Error toast is handled universally by handleGoogleSlidesUpload, no need to duplicate
|
||||
}
|
||||
} else {
|
||||
toast.success('Google authentication successful!');
|
||||
}
|
||||
} else if (urlParams.get('google_auth') === 'error') {
|
||||
const error = urlParams.get('error');
|
||||
sessionStorage.removeItem('google_slides_upload_intent');
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
toast.error(`Google authentication failed: ${error || 'Unknown error'}`);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (agents.length > 0) {
|
||||
// If configuredAgentId is provided, use it as the forced selection
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { constructHtmlPreviewUrl } from '@/lib/utils/url';
|
||||
import { downloadPresentation, DownloadFormat } from '../utils/presentation-utils';
|
||||
import { downloadPresentation, DownloadFormat, handleGoogleSlidesUpload } from '../utils/presentation-utils';
|
||||
|
||||
interface SlideMetadata {
|
||||
title: string;
|
||||
|
@ -65,6 +65,7 @@ export function FullScreenPresentationViewer({
|
|||
const [showEditor, setShowEditor] = useState(false);
|
||||
const [isDownloadingPDF, setIsDownloadingPDF] = useState(false);
|
||||
const [isDownloadingPPTX, setIsDownloadingPPTX] = useState(false);
|
||||
const [isDownloadingGoogleSlides, setIsDownloadingGoogleSlides] = useState(false);
|
||||
|
||||
// Create a stable refresh timestamp when metadata changes (like PresentationViewer)
|
||||
const refreshTimestamp = useMemo(() => Date.now(), [metadata]);
|
||||
|
@ -262,6 +263,32 @@ export function FullScreenPresentationViewer({
|
|||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Download handlers
|
||||
const handleDownload = async (format: DownloadFormat) => {
|
||||
if (!sandboxUrl || !presentationName) return;
|
||||
|
||||
const setDownloadState = format === DownloadFormat.PDF ? setIsDownloadingPDF :
|
||||
format === DownloadFormat.PPTX ? setIsDownloadingPPTX :
|
||||
setIsDownloadingGoogleSlides;
|
||||
|
||||
setDownloadState(true);
|
||||
try {
|
||||
if (format === DownloadFormat.GOOGLE_SLIDES) {
|
||||
const result = await handleGoogleSlidesUpload(sandboxUrl, `/workspace/presentations/${presentationName}`);
|
||||
// If redirected to auth, don't show error
|
||||
if (result?.redirected_to_auth) {
|
||||
return; // Don't set loading false, user is being redirected
|
||||
}
|
||||
} else {
|
||||
await downloadPresentation(format, sandboxUrl, `/workspace/presentations/${presentationName}`, presentationName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error downloading ${format}:`, error);
|
||||
} finally {
|
||||
setDownloadState(false);
|
||||
}
|
||||
};
|
||||
|
||||
const currentSlideData = slides.find(slide => slide.number === currentSlide);
|
||||
|
||||
// Memoized slide iframe component with proper scaling (matching PresentationViewer)
|
||||
|
@ -417,9 +444,9 @@ export function FullScreenPresentationViewer({
|
|||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
title="Export presentation"
|
||||
disabled={isDownloadingPDF || isDownloadingPPTX}
|
||||
disabled={isDownloadingPDF || isDownloadingPPTX || isDownloadingGoogleSlides}
|
||||
>
|
||||
{(isDownloadingPDF || isDownloadingPPTX) ? (
|
||||
{(isDownloadingPDF || isDownloadingPPTX || isDownloadingGoogleSlides) ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
|
@ -427,15 +454,27 @@ export function FullScreenPresentationViewer({
|
|||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-32">
|
||||
<DropdownMenuItem className="cursor-pointer" onClick={() => downloadPresentation(DownloadFormat.PDF, sandboxUrl, `/workspace/presentations/${presentationName}`, presentationName)} disabled={isDownloadingPDF}>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDownload(DownloadFormat.PDF)}
|
||||
disabled={isDownloadingPDF}
|
||||
>
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
PDF
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="cursor-pointer" onClick={() => downloadPresentation(DownloadFormat.PPTX, sandboxUrl, `/workspace/presentations/${presentationName}`, presentationName)} disabled={isDownloadingPPTX}>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDownload(DownloadFormat.PPTX)}
|
||||
disabled={isDownloadingPPTX}
|
||||
>
|
||||
<Presentation className="h-4 w-4 mr-2" />
|
||||
PPTX
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="cursor-pointer">
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDownload(DownloadFormat.GOOGLE_SLIDES)}
|
||||
disabled={isDownloadingGoogleSlides}
|
||||
>
|
||||
<ExternalLink className="h-4 w-4 mr-2" />
|
||||
Google Slides
|
||||
</DropdownMenuItem>
|
||||
|
|
|
@ -10,13 +10,14 @@ import {
|
|||
AlertTriangle,
|
||||
Loader2,
|
||||
PresentationIcon,
|
||||
ExternalLink,
|
||||
} from 'lucide-react';
|
||||
import { ToolViewProps } from '../types';
|
||||
import {
|
||||
getToolTitle,
|
||||
extractToolData,
|
||||
} from '../utils';
|
||||
import { downloadPresentation, DownloadFormat } from '../utils/presentation-utils';
|
||||
import { downloadPresentation, DownloadFormat, handleGoogleSlidesUpload } from '../utils/presentation-utils';
|
||||
import { toast } from 'sonner';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
@ -83,15 +84,28 @@ export function PresentPresentationToolView({
|
|||
|
||||
setIsDownloading(true);
|
||||
try {
|
||||
await downloadPresentation(format,
|
||||
project.sandbox.sandbox_url,
|
||||
`/workspace/${presentationPath}`,
|
||||
presentationName
|
||||
);
|
||||
toast.success(`${format} downloaded successfully`);
|
||||
if (format === DownloadFormat.GOOGLE_SLIDES) {
|
||||
const result = await handleGoogleSlidesUpload(
|
||||
project.sandbox.sandbox_url,
|
||||
`/workspace/${presentationPath}`
|
||||
);
|
||||
// If redirected to auth, don't show error
|
||||
if (result?.redirected_to_auth) {
|
||||
return; // Don't set loading false, user is being redirected
|
||||
}
|
||||
} else {
|
||||
await downloadPresentation(format,
|
||||
project.sandbox.sandbox_url,
|
||||
`/workspace/${presentationPath}`,
|
||||
presentationName
|
||||
);
|
||||
toast.success(`${format} downloaded successfully`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error downloading ${format}:`, error);
|
||||
toast.error(`Failed to download ${format}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
if (format !== DownloadFormat.GOOGLE_SLIDES) {
|
||||
toast.error(`Failed to download ${format}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
|
@ -224,6 +238,14 @@ export function PresentPresentationToolView({
|
|||
<Presentation className="h-4 w-4 mr-2" />
|
||||
PPTX
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDownload(DownloadFormat.GOOGLE_SLIDES)}
|
||||
className="cursor-pointer"
|
||||
disabled={isDownloading}
|
||||
>
|
||||
<ExternalLink className="h-4 w-4 mr-2" />
|
||||
Google Slides
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
|
|
@ -30,12 +30,11 @@ import {
|
|||
} from 'lucide-react';
|
||||
import { ToolViewProps } from '../types';
|
||||
import { formatTimestamp, extractToolData, getToolTitle } from '../utils';
|
||||
import { downloadPresentation } from '../utils/presentation-utils';
|
||||
import { downloadPresentation, handleGoogleSlidesUpload } from '../utils/presentation-utils';
|
||||
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';
|
||||
import { DownloadFormat } from '../utils/presentation-utils';
|
||||
|
||||
interface SlideMetadata {
|
||||
|
@ -476,15 +475,18 @@ export function PresentationViewer({
|
|||
return <SlideIframe slide={slide} />;
|
||||
}, [SlideIframe]);
|
||||
|
||||
const handleDownload = useCallback(async (setIsDownloading: (isDownloading: boolean) => void, format: DownloadFormat) => {
|
||||
const handleDownload = async (setIsDownloading: (isDownloading: boolean) => void, format: DownloadFormat) => {
|
||||
|
||||
if (!project?.sandbox?.sandbox_url || !extractedPresentationName) return;
|
||||
|
||||
setIsDownloading(true);
|
||||
try{
|
||||
if (format === DownloadFormat.GOOGLE_SLIDES){
|
||||
// TODO: Implement Google Slides download
|
||||
console.log('Downloading Google Slides');
|
||||
const result = await handleGoogleSlidesUpload(project!.sandbox!.sandbox_url, `/workspace/presentations/${extractedPresentationName}`);
|
||||
// If redirected to auth, don't show error
|
||||
if (result?.redirected_to_auth) {
|
||||
return; // Don't set loading false, user is being redirected
|
||||
}
|
||||
} else{
|
||||
await downloadPresentation(format, project.sandbox.sandbox_url, `/workspace/presentations/${extractedPresentationName}`, extractedPresentationName);
|
||||
}
|
||||
|
@ -493,7 +495,8 @@ export function PresentationViewer({
|
|||
} finally {
|
||||
setIsDownloading(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">
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
import { backendApi } from "@/lib/api-client";
|
||||
import { createClient } from "@/lib/supabase/client";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export enum DownloadFormat {
|
||||
PDF = 'pdf',
|
||||
PPTX = 'pptx',
|
||||
|
@ -63,10 +67,11 @@ export function createPresentationViewerToolContent(
|
|||
}
|
||||
|
||||
/**
|
||||
* Downloads a presentation as PDF
|
||||
* Downloads a presentation as PDF or PPTX
|
||||
* @param sandboxUrl - The sandbox URL for the API endpoint
|
||||
* @param presentationPath - The path to the presentation in the workspace
|
||||
* @param presentationName - The name of the presentation for the downloaded file
|
||||
* @param format - The format to download the presentation as
|
||||
* @returns Promise that resolves when download is complete
|
||||
*/
|
||||
export async function downloadPresentation(
|
||||
|
@ -103,3 +108,108 @@ export async function downloadPresentation(
|
|||
throw error; // Re-throw to allow calling code to handle
|
||||
}
|
||||
}
|
||||
|
||||
export const handleGoogleAuth = async (presentationPath: string, sandboxUrl: string) => {
|
||||
try {
|
||||
// Store intent to upload to Google Slides after OAuth
|
||||
sessionStorage.setItem('google_slides_upload_intent', JSON.stringify({
|
||||
presentation_path: presentationPath,
|
||||
sandbox_url: sandboxUrl
|
||||
}));
|
||||
|
||||
// Pass the current URL to the backend so it can be included in the OAuth state
|
||||
const currentUrl = encodeURIComponent(window.location.href);
|
||||
const response = await backendApi.get(`/google/auth-url?return_url=${currentUrl}`);
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(response.error?.message || 'Failed to get auth URL');
|
||||
}
|
||||
|
||||
const { auth_url } = response.data;
|
||||
|
||||
if (auth_url) {
|
||||
window.location.href = auth_url;
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error initiating Google auth:', error);
|
||||
toast.error('Failed to initiate Google authentication');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const handleGoogleSlidesUpload = async (sandboxUrl: string, presentationPath: string) => {
|
||||
if (!sandboxUrl || !presentationPath) {
|
||||
throw new Error('Missing required parameters');
|
||||
}
|
||||
|
||||
try {
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
// Use proper backend API client with authentication and extended timeout for PPTX generation
|
||||
const response = await backendApi.post('/presentation-tools/convert-and-upload-to-slides', {
|
||||
presentation_path: presentationPath,
|
||||
sandbox_url: sandboxUrl,
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${session.access_token}`,
|
||||
},
|
||||
timeout: 180000, // 3 minutes timeout for PPTX generation (longer than backend's 2 minute timeout)
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error('Failed to upload to Google Slides');
|
||||
}
|
||||
|
||||
const result = response.data;
|
||||
|
||||
if (!result.success && !result.is_api_enabled) {
|
||||
toast.info('Redirecting to Google authentication...', {
|
||||
duration: 3000,
|
||||
});
|
||||
handleGoogleAuth(presentationPath, sandboxUrl);
|
||||
return {
|
||||
success: false,
|
||||
redirected_to_auth: true,
|
||||
message: 'Redirecting to Google authentication'
|
||||
};
|
||||
}
|
||||
|
||||
if (result.google_slides_url) {
|
||||
// Always show rich success toast - this is universal
|
||||
toast.success('🎉 Presentation uploaded successfully!', {
|
||||
action: {
|
||||
label: 'Open in Google Slides',
|
||||
onClick: () => window.open(result.google_slides_url, '_blank'),
|
||||
},
|
||||
duration: 20000,
|
||||
});
|
||||
|
||||
// Extract presentation name from path for display
|
||||
const presentationName = presentationPath.split('/').pop() || 'presentation';
|
||||
|
||||
return {
|
||||
success: true,
|
||||
google_slides_url: result.google_slides_url,
|
||||
message: `"${presentationName}" uploaded successfully`
|
||||
};
|
||||
}
|
||||
|
||||
// Only throw error if no Google Slides URL was returned
|
||||
throw new Error(result.message || 'No Google Slides URL returned');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error uploading to Google Slides:', error);
|
||||
|
||||
// Show error toasts - this is also universal
|
||||
if (error instanceof Error && error.message.includes('not authenticated')) {
|
||||
toast.error('Please authenticate with Google first');
|
||||
} else {
|
||||
toast.error('Failed to upload to Google Slides');
|
||||
}
|
||||
|
||||
// Re-throw for any calling code that needs to handle it
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue