import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Sparkles, Upload, FileText, Image as ImageIcon, Box, Check, Loader2, ArrowRight, RefreshCw, ChevronLeft } from 'lucide-react'; import { api } from '../lib/api'; const STEPS = [ { id: 1, title: 'Upload PDF', icon: Upload }, { id: 2, title: 'AI Review', icon: FileText }, { id: 3, title: 'Concept Art', icon: ImageIcon }, { id: 4, title: '3D Generation', icon: Box }, ]; export default function SparkPlug() { const navigate = useNavigate(); const [currentStep, setCurrentStep] = useState(1); const [isLoading, setIsLoading] = useState(false); const [loadingMessage, setLoadingMessage] = useState(''); // Step 1: PDF File const [pdfFile, setPdfFile] = useState(null); // Step 2: Extracted Text const [extractedPrompt, setExtractedPrompt] = useState(''); // Step 3: Generated Images (Multi-view) const [generatedImages, setGeneratedImages] = useState([]); // Step 4: 3D Model const [modelProgress, setModelProgress] = useState(0); const [generatedGlbUrl, setGeneratedGlbUrl] = useState(null); const handleFileUpload = async (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { setPdfFile(e.target.files[0]); } }; const processStep1 = async () => { if (!pdfFile) return; setLoadingMessage('Extracting details with Gemini AI...'); setIsLoading(true); try { // Validate keys const keys = await api.getUserKeys(); const geminiKey = keys.GEMINI_API_KEY; if (!geminiKey) { alert("Please configure your Gemini API Key in the Dashboard Settings first."); setIsLoading(false); return; } // Call our backend endpoint to parse PDF and query Gemini const prompt = await api.extractPDF(pdfFile); setExtractedPrompt(prompt); setCurrentStep(2); } catch (err: any) { console.error(err); alert(`Failed to process PDF: ${err.message || 'Unknown error'}`); } finally { setIsLoading(false); } }; const processStep2 = async () => { if (!extractedPrompt) return; setLoadingMessage('Generating multi-view concepts with Nano Banana...'); setIsLoading(true); try { // Validate keys const keys = await api.getUserKeys(); const nanoBananaKey = keys.NANO_BANANA_API_KEY; if (!nanoBananaKey) { alert("Please configure your Nano Banana API Key in the Dashboard Settings first."); setIsLoading(false); return; } // Call our backend endpoint which queries Gemini Imagen const images = await api.generateImages(extractedPrompt); if (images && images.length > 0) { setGeneratedImages(images); setCurrentStep(3); } else { throw new Error("Failed to generate images."); } } catch (err: any) { console.error(err); alert(`Failed to generate images: ${err.message || 'Unknown error'}`); } finally { setIsLoading(false); } }; const processStep3 = async () => { if (generatedImages.length === 0) return; setLoadingMessage('Constructing 3D Model with Meshy API...'); setIsLoading(true); try { // Validate keys const keys = await api.getUserKeys(); const meshyKey = keys.MESHY_API_KEY; if (!meshyKey) { alert("Please configure your Meshy API Key in the Dashboard Settings first."); setIsLoading(false); return; } // Call backend to start Meshy task const taskId = await api.generate3D(generatedImages); // Immediately move to step 4 to show the progress bar setCurrentStep(4); // Poll Meshy status let isDone = false; let attempts = 0; while (!isDone && attempts < 60) { // Max 5 mins (60 * 5s) attempts++; await new Promise(resolve => setTimeout(resolve, 5000)); const statusData = await api.get3DStatus(taskId); const { status, progress, model_urls, task_error } = statusData; if (progress) { setModelProgress(progress); } if (status === 'SUCCEEDED') { setGeneratedGlbUrl(model_urls.glb); isDone = true; // The step is already 4, user just clicks "Open in Editor" when progress >= 100 } else if (status === 'FAILED') { throw new Error(`Meshy generation failed: ${task_error?.message || 'Unknown error'}`); } } if (!isDone) { throw new Error("Meshy task timed out after 5 minutes."); } } catch (err: any) { console.error(err); alert(`Failed to construct 3D model: ${err.message || 'Unknown error'}`); // If it fails, revert back to step 3 so they can try again setCurrentStep(3); } finally { setIsLoading(false); } }; const processStep4 = async () => { setLoadingMessage('Saving project to your workspace...'); setIsLoading(true); try { if (!generatedGlbUrl) { throw new Error("No GLB URL available to save."); } // Create project and save with model injected into scene_data const res = await api.saveProject({ name: "Spark Plug Output", scene_data: { objects: [ { id: "model_" + Date.now(), type: "model", name: "Generated Product Model", visible: true, position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1], url: generatedGlbUrl } ] } }); if (res.id) { navigate(`/editor/${res.id}`); } } catch (err) { console.error('Failed to create project', err); alert('Failed to save project'); } finally { setIsLoading(false); } }; return (
{/* Header */}
Spark Plug | Idea to 3D Generation
{/* Stepper */}
{STEPS.map((step) => { const Icon = step.icon; const isCompleted = currentStep > step.id; const isCurrent = currentStep === step.id; return (
{isCompleted ? : }
{step.title}
); })}
{/* Content Area */}
{isLoading && (

{loadingMessage}

This might take a few moments...

)} {currentStep === 1 && (

Upload Mintel Spark PDF

Upload a product concept PDF. Spark Plug will extract the product details using Gemini AI to prepare for 3D generation.

{pdfFile ? pdfFile.name : 'Click or drag PDF here'}
{pdfFile && ( )}
)} {currentStep === 2 && (

Review Extracted Concept

Gemini AI has analyzed the PDF. Review and modify the prompt below before generating images.