diff --git a/src/App.tsx b/src/App.tsx index b9b2cf7..234e985 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import Editor from './pages/Editor'; import Home from './pages/Home'; import Dashboard from './pages/Dashboard'; import Admin from './pages/Admin'; +import SparkPlug from './pages/SparkPlug'; import { AuthModal } from './components/AuthModal'; export default function App() { @@ -35,6 +36,7 @@ export default function App() { {/* Protected Routes (User) */} }> } /> + } /> } /> diff --git a/src/components/ApiSettingsModal.tsx b/src/components/DashboardSettings.tsx similarity index 75% rename from src/components/ApiSettingsModal.tsx rename to src/components/DashboardSettings.tsx index 6c12bfd..6b80481 100644 --- a/src/components/ApiSettingsModal.tsx +++ b/src/components/DashboardSettings.tsx @@ -3,17 +3,19 @@ import { motion, AnimatePresence } from 'framer-motion'; import { X, Settings, Key, Sparkles, Image as ImageIcon, Lightbulb, Box } from 'lucide-react'; import { cn } from '../lib/utils'; -interface ApiSettingsModalProps { +interface DashboardSettingsProps { isOpen: boolean; onClose: () => void; } -export const ApiSettingsModal: React.FC = ({ isOpen, onClose }) => { +export const DashboardSettings: React.FC = ({ isOpen, onClose }) => { const [falApiKey, setFalApiKey] = useState(''); const [tripoApiKey, setTripoApiKey] = useState(''); const [meshyApiKey, setMeshyApiKey] = useState(''); const [stabilityApiKey, setStabilityApiKey] = useState(''); const [openAiKey, setOpenAiKey] = useState(''); + const [geminiApiKey, setGeminiApiKey] = useState(''); + const [nanoBananaApiKey, setNanoBananaApiKey] = useState(''); useEffect(() => { if (isOpen) { @@ -22,6 +24,8 @@ export const ApiSettingsModal: React.FC = ({ isOpen, onCl setMeshyApiKey(localStorage.getItem('MESHY_API_KEY') || ''); setStabilityApiKey(localStorage.getItem('stability_api_key') || ''); setOpenAiKey(localStorage.getItem('openai_api_key') || ''); + setGeminiApiKey(localStorage.getItem('GEMINI_API_KEY') || ''); + setNanoBananaApiKey(localStorage.getItem('NANO_BANANA_API_KEY') || ''); } }, [isOpen]); @@ -31,6 +35,8 @@ export const ApiSettingsModal: React.FC = ({ isOpen, onCl localStorage.setItem('MESHY_API_KEY', meshyApiKey); localStorage.setItem('stability_api_key', stabilityApiKey); localStorage.setItem('openai_api_key', openAiKey); + localStorage.setItem('GEMINI_API_KEY', geminiApiKey); + localStorage.setItem('NANO_BANANA_API_KEY', nanoBananaApiKey); onClose(); }; @@ -65,6 +71,45 @@ export const ApiSettingsModal: React.FC = ({ isOpen, onCl {/* Content */}
+ {/* Spark Plug APIs */} +
+

+ Spark Plug (PDF to 3D) +

+ +
+
+
+ + For PDF Idea Extraction +
+ setGeminiApiKey(e.target.value)} + className="bg-zinc-950 border border-zinc-800 rounded-lg px-4 py-2 text-sm text-white focus:outline-none focus:border-amber-500/50" + /> +
+ +
+
+ + For Concept Image Generation +
+ setNanoBananaApiKey(e.target.value)} + className="bg-zinc-950 border border-zinc-800 rounded-lg px-4 py-2 text-sm text-white focus:outline-none focus:border-yellow-500/50" + /> +
+
+
+ +
+ {/* 3D Generation */}

diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index a773f53..f7742ea 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -1,7 +1,8 @@ import { useEffect, useState } from 'react'; import { useAuthStore } from '../store/authStore'; import { useNavigate, Link } from 'react-router-dom'; -import { LogOut, Plus, Folder, Loader2, Trash2 } from 'lucide-react'; +import { LogOut, Plus, Folder, Loader2, Trash2, Settings, Sparkles } from 'lucide-react'; +import { DashboardSettings } from '../components/DashboardSettings'; import { api } from '../lib/api'; interface Project { @@ -15,6 +16,7 @@ export default function Dashboard() { const navigate = useNavigate(); const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(true); + const [isSettingsOpen, setIsSettingsOpen] = useState(false); useEffect(() => { const fetchProjects = async () => { @@ -79,6 +81,13 @@ export default function Dashboard() { Admin Panel )}
{user?.email}
+

+ +
navigate('/spark-plug')}> +
+ +
+ Spark Plug (PDF to 3D) +
{projects.map(project => (
navigate(`/editor/${project.id}`)} className="aspect-video bg-zinc-900 border border-zinc-800 rounded-2xl p-4 flex flex-col cursor-pointer hover:border-zinc-700 transition-colors group relative overflow-hidden"> @@ -150,6 +166,8 @@ export default function Dashboard() {
)} + + setIsSettingsOpen(false)} />
); } diff --git a/src/pages/Editor.tsx b/src/pages/Editor.tsx index 39069b3..c784b63 100644 --- a/src/pages/Editor.tsx +++ b/src/pages/Editor.tsx @@ -52,7 +52,7 @@ import { UnifiedMappingMaterial } from '../components/UnifiedMappingMaterial'; import { ImageTo3DModal } from "../components/ImageTo3DModal"; import { UVEditor } from "../components/UVEditor"; import { VizAiModal } from "../components/VizAiModal"; -import { ApiSettingsModal } from "../components/ApiSettingsModal"; + import { EffectComposer, DepthOfField, Bloom, Vignette, BrightnessContrast, HueSaturation } from "@react-three/postprocessing"; import { cn } from "../lib/utils"; import * as THREE from "three"; @@ -519,7 +519,7 @@ export default function Editor() { const [showGrid, setShowGrid] = useState(true); const [showRenderModal, setShowRenderModal] = useState(false); const [showVizAi, setShowVizAi] = useState(false); - const [showApiSettings, setShowApiSettings] = useState(false); + const [vizAiBaseImage, setVizAiBaseImage] = useState(null); const [renderRequest, setRenderRequest] = useState(null); const [exportRequest, setExportRequest] = useState(false); @@ -2087,28 +2087,7 @@ export default function Editor() { Camera Image Render -
- setOpenMenu(openMenu === 'tools' ? null : 'tools')} - > - Tools - - {openMenu === 'tools' && ( -
- -
- )} -
+
- setShowApiSettings(false)} - /> - {/* Export / Save Dialog */} {exportDialog && (
(null); + + // Step 2: Extracted Text + const [extractedPrompt, setExtractedPrompt] = useState(''); + + // Step 3: Generated Image + const [generatedImage, setGeneratedImage] = useState(null); + + // Step 4: 3D Model + const [modelProgress, setModelProgress] = useState(0); + + 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; + setIsLoading(true); + + try { + // Simulate reading PDF with Gemini + const geminiKey = localStorage.getItem('GEMINI_API_KEY'); + if (!geminiKey) { + alert("Please configure your Gemini API Key in the Dashboard Settings first."); + setIsLoading(false); + return; + } + + // We would normally send the PDF to the backend or use Gemini SDK directly here. + // Mocking the extraction for now based on the Indica Shampoo PDF context: + await new Promise(r => setTimeout(r, 2000)); + + setExtractedPrompt("Product: Indica Easy Herbal Shield Colour Shampoo\nType: Sachet/Packet\nColor Palette: Green, White\nKey Elements: Woman with black hair, Amla & Aloe illustration, 10 Minutes Herbal Care text.\n\nDescription: A glossy green rectangular sachet with vivid branding and product imagery for an herbal hair colour shampoo."); + + setCurrentStep(2); + } catch (err) { + console.error(err); + alert("Failed to process PDF."); + } finally { + setIsLoading(false); + } + }; + + const processStep2 = async () => { + if (!extractedPrompt) return; + setIsLoading(true); + + try { + const nanoBananaKey = localStorage.getItem('NANO_BANANA_API_KEY'); + if (!nanoBananaKey) { + alert("Please configure your Nano Banana API Key in the Dashboard Settings first."); + setIsLoading(false); + return; + } + + // Mocking the image generation with Nano Banana + await new Promise(r => setTimeout(r, 3000)); + + // Placeholder image for the shampoo sachet + setGeneratedImage("https://images.unsplash.com/photo-1629198688000-71f23e745b6e?w=800&q=80"); // Generic beauty product placeholder + + setCurrentStep(3); + } catch (err) { + console.error(err); + alert("Failed to generate image."); + } finally { + setIsLoading(false); + } + }; + + const processStep3 = async () => { + if (!generatedImage) return; + setIsLoading(true); + + try { + const meshyKey = localStorage.getItem('MESHY_API_KEY'); + if (!meshyKey) { + alert("Please configure your Meshy API Key in the Dashboard Settings first."); + setIsLoading(false); + return; + } + + // Simulate Meshy generation + for (let i = 1; i <= 10; i++) { + await new Promise(r => setTimeout(r, 500)); + setModelProgress(i * 10); + } + + setCurrentStep(4); + } catch (err) { + console.error(err); + alert("Failed to generate 3D model."); + } finally { + setIsLoading(false); + } + }; + + const processStep4 = async () => { + setIsLoading(true); + try { + // Create project and save + const res = await api.saveProject({ + name: "Spark Plug Output - Sachet", + scene_data: {} // We would normally attach the generated GLB URL here + }); + 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 */} +
+ + {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. +

+ +