feat: upload to drive

This commit is contained in:
Krishav Raj Singh 2025-08-29 05:55:02 +05:30
parent 67705e6345
commit 045f27eaa1
5 changed files with 250 additions and 21 deletions

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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;
}
};