feat: add Spark Plug module for PDF to 3D generation and move API settings to Dashboard
Build and Deploy / build-and-push (push) Successful in 45s
Build and Deploy / build-and-push (push) Successful in 45s
This commit is contained in:
@@ -8,6 +8,7 @@ import Editor from './pages/Editor';
|
|||||||
import Home from './pages/Home';
|
import Home from './pages/Home';
|
||||||
import Dashboard from './pages/Dashboard';
|
import Dashboard from './pages/Dashboard';
|
||||||
import Admin from './pages/Admin';
|
import Admin from './pages/Admin';
|
||||||
|
import SparkPlug from './pages/SparkPlug';
|
||||||
import { AuthModal } from './components/AuthModal';
|
import { AuthModal } from './components/AuthModal';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
@@ -35,6 +36,7 @@ export default function App() {
|
|||||||
{/* Protected Routes (User) */}
|
{/* Protected Routes (User) */}
|
||||||
<Route element={<ProtectedRoute allowedRoles={['user', 'admin']} />}>
|
<Route element={<ProtectedRoute allowedRoles={['user', 'admin']} />}>
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
|
<Route path="/spark-plug" element={<SparkPlug />} />
|
||||||
<Route path="/editor/:projectId?" element={<Editor />} />
|
<Route path="/editor/:projectId?" element={<Editor />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,19 @@ import { motion, AnimatePresence } from 'framer-motion';
|
|||||||
import { X, Settings, Key, Sparkles, Image as ImageIcon, Lightbulb, Box } from 'lucide-react';
|
import { X, Settings, Key, Sparkles, Image as ImageIcon, Lightbulb, Box } from 'lucide-react';
|
||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
|
|
||||||
interface ApiSettingsModalProps {
|
interface DashboardSettingsProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ApiSettingsModal: React.FC<ApiSettingsModalProps> = ({ isOpen, onClose }) => {
|
export const DashboardSettings: React.FC<DashboardSettingsProps> = ({ isOpen, onClose }) => {
|
||||||
const [falApiKey, setFalApiKey] = useState('');
|
const [falApiKey, setFalApiKey] = useState('');
|
||||||
const [tripoApiKey, setTripoApiKey] = useState('');
|
const [tripoApiKey, setTripoApiKey] = useState('');
|
||||||
const [meshyApiKey, setMeshyApiKey] = useState('');
|
const [meshyApiKey, setMeshyApiKey] = useState('');
|
||||||
const [stabilityApiKey, setStabilityApiKey] = useState('');
|
const [stabilityApiKey, setStabilityApiKey] = useState('');
|
||||||
const [openAiKey, setOpenAiKey] = useState('');
|
const [openAiKey, setOpenAiKey] = useState('');
|
||||||
|
const [geminiApiKey, setGeminiApiKey] = useState('');
|
||||||
|
const [nanoBananaApiKey, setNanoBananaApiKey] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
@@ -22,6 +24,8 @@ export const ApiSettingsModal: React.FC<ApiSettingsModalProps> = ({ isOpen, onCl
|
|||||||
setMeshyApiKey(localStorage.getItem('MESHY_API_KEY') || '');
|
setMeshyApiKey(localStorage.getItem('MESHY_API_KEY') || '');
|
||||||
setStabilityApiKey(localStorage.getItem('stability_api_key') || '');
|
setStabilityApiKey(localStorage.getItem('stability_api_key') || '');
|
||||||
setOpenAiKey(localStorage.getItem('openai_api_key') || '');
|
setOpenAiKey(localStorage.getItem('openai_api_key') || '');
|
||||||
|
setGeminiApiKey(localStorage.getItem('GEMINI_API_KEY') || '');
|
||||||
|
setNanoBananaApiKey(localStorage.getItem('NANO_BANANA_API_KEY') || '');
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
@@ -31,6 +35,8 @@ export const ApiSettingsModal: React.FC<ApiSettingsModalProps> = ({ isOpen, onCl
|
|||||||
localStorage.setItem('MESHY_API_KEY', meshyApiKey);
|
localStorage.setItem('MESHY_API_KEY', meshyApiKey);
|
||||||
localStorage.setItem('stability_api_key', stabilityApiKey);
|
localStorage.setItem('stability_api_key', stabilityApiKey);
|
||||||
localStorage.setItem('openai_api_key', openAiKey);
|
localStorage.setItem('openai_api_key', openAiKey);
|
||||||
|
localStorage.setItem('GEMINI_API_KEY', geminiApiKey);
|
||||||
|
localStorage.setItem('NANO_BANANA_API_KEY', nanoBananaApiKey);
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -65,6 +71,45 @@ export const ApiSettingsModal: React.FC<ApiSettingsModalProps> = ({ isOpen, onCl
|
|||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1 overflow-y-auto p-6 bg-zinc-900/50 flex flex-col gap-6 custom-scrollbar">
|
<div className="flex-1 overflow-y-auto p-6 bg-zinc-900/50 flex flex-col gap-6 custom-scrollbar">
|
||||||
|
|
||||||
|
{/* Spark Plug APIs */}
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<h3 className="text-xs font-bold text-zinc-500 uppercase tracking-widest flex items-center gap-2">
|
||||||
|
<Lightbulb size={14} /> Spark Plug (PDF to 3D)
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 pl-2 border-l-2 border-zinc-800">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-sm font-bold text-amber-400">Gemini AI Key</label>
|
||||||
|
<span className="text-[10px] text-zinc-500">For PDF Idea Extraction</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter Google Gemini API Key..."
|
||||||
|
value={geminiApiKey}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-sm font-bold text-yellow-400">Nano Banana Key</label>
|
||||||
|
<span className="text-[10px] text-zinc-500">For Concept Image Generation</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter Nano Banana API Key..."
|
||||||
|
value={nanoBananaApiKey}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-px bg-zinc-800/50 w-full" />
|
||||||
|
|
||||||
{/* 3D Generation */}
|
{/* 3D Generation */}
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<h3 className="text-xs font-bold text-zinc-500 uppercase tracking-widest flex items-center gap-2">
|
<h3 className="text-xs font-bold text-zinc-500 uppercase tracking-widest flex items-center gap-2">
|
||||||
+19
-1
@@ -1,7 +1,8 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '../store/authStore';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
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';
|
import { api } from '../lib/api';
|
||||||
|
|
||||||
interface Project {
|
interface Project {
|
||||||
@@ -15,6 +16,7 @@ export default function Dashboard() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [projects, setProjects] = useState<Project[]>([]);
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchProjects = async () => {
|
const fetchProjects = async () => {
|
||||||
@@ -79,6 +81,13 @@ export default function Dashboard() {
|
|||||||
<Link to="/admin" className="text-sm text-primary hover:underline font-medium">Admin Panel</Link>
|
<Link to="/admin" className="text-sm text-primary hover:underline font-medium">Admin Panel</Link>
|
||||||
)}
|
)}
|
||||||
<div className="text-sm text-zinc-400">{user?.email}</div>
|
<div className="text-sm text-zinc-400">{user?.email}</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsSettingsOpen(true)}
|
||||||
|
className="p-2 text-zinc-400 hover:text-white hover:bg-zinc-800 rounded-lg transition-colors"
|
||||||
|
title="Global Settings"
|
||||||
|
>
|
||||||
|
<Settings size={18} />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleSignOut}
|
onClick={handleSignOut}
|
||||||
className="p-2 text-zinc-400 hover:text-white hover:bg-zinc-800 rounded-lg transition-colors"
|
className="p-2 text-zinc-400 hover:text-white hover:bg-zinc-800 rounded-lg transition-colors"
|
||||||
@@ -127,6 +136,13 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium">Create Blank Scene</span>
|
<span className="text-sm font-medium">Create Blank Scene</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="aspect-video bg-gradient-to-br from-amber-500/20 to-purple-500/20 border border-zinc-800 border-dashed rounded-2xl flex flex-col items-center justify-center text-zinc-400 hover:text-white hover:border-amber-500/50 transition-colors cursor-pointer group" onClick={() => navigate('/spark-plug')}>
|
||||||
|
<div className="w-10 h-10 rounded-full bg-zinc-800 group-hover:bg-amber-500/20 flex items-center justify-center mb-2 transition-colors text-amber-500">
|
||||||
|
<Sparkles size={20} />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium text-amber-500/80 group-hover:text-amber-400 transition-colors">Spark Plug (PDF to 3D)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{projects.map(project => (
|
{projects.map(project => (
|
||||||
<div key={project.id} onClick={() => 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">
|
<div key={project.id} onClick={() => 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() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<DashboardSettings isOpen={isSettingsOpen} onClose={() => setIsSettingsOpen(false)} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-29
@@ -52,7 +52,7 @@ import { UnifiedMappingMaterial } from '../components/UnifiedMappingMaterial';
|
|||||||
import { ImageTo3DModal } from "../components/ImageTo3DModal";
|
import { ImageTo3DModal } from "../components/ImageTo3DModal";
|
||||||
import { UVEditor } from "../components/UVEditor";
|
import { UVEditor } from "../components/UVEditor";
|
||||||
import { VizAiModal } from "../components/VizAiModal";
|
import { VizAiModal } from "../components/VizAiModal";
|
||||||
import { ApiSettingsModal } from "../components/ApiSettingsModal";
|
|
||||||
import { EffectComposer, DepthOfField, Bloom, Vignette, BrightnessContrast, HueSaturation } from "@react-three/postprocessing";
|
import { EffectComposer, DepthOfField, Bloom, Vignette, BrightnessContrast, HueSaturation } from "@react-three/postprocessing";
|
||||||
import { cn } from "../lib/utils";
|
import { cn } from "../lib/utils";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
@@ -519,7 +519,7 @@ export default function Editor() {
|
|||||||
const [showGrid, setShowGrid] = useState(true);
|
const [showGrid, setShowGrid] = useState(true);
|
||||||
const [showRenderModal, setShowRenderModal] = useState(false);
|
const [showRenderModal, setShowRenderModal] = useState(false);
|
||||||
const [showVizAi, setShowVizAi] = useState(false);
|
const [showVizAi, setShowVizAi] = useState(false);
|
||||||
const [showApiSettings, setShowApiSettings] = useState(false);
|
|
||||||
const [vizAiBaseImage, setVizAiBaseImage] = useState<string | null>(null);
|
const [vizAiBaseImage, setVizAiBaseImage] = useState<string | null>(null);
|
||||||
const [renderRequest, setRenderRequest] = useState<RenderSettings | null>(null);
|
const [renderRequest, setRenderRequest] = useState<RenderSettings | null>(null);
|
||||||
const [exportRequest, setExportRequest] = useState(false);
|
const [exportRequest, setExportRequest] = useState(false);
|
||||||
@@ -2087,28 +2087,7 @@ export default function Editor() {
|
|||||||
<span className="hover:text-white cursor-pointer">Camera</span>
|
<span className="hover:text-white cursor-pointer">Camera</span>
|
||||||
<span className="hover:text-white cursor-pointer">Image</span>
|
<span className="hover:text-white cursor-pointer">Image</span>
|
||||||
<span className="hover:text-white cursor-pointer">Render</span>
|
<span className="hover:text-white cursor-pointer">Render</span>
|
||||||
<div className="relative">
|
|
||||||
<span
|
|
||||||
className="hover:text-primary cursor-pointer px-1 py-0.5 rounded hover:bg-white/40 dark:hover:bg-white/5 transition-colors"
|
|
||||||
onClick={() => setOpenMenu(openMenu === 'tools' ? null : 'tools')}
|
|
||||||
>
|
|
||||||
Tools
|
|
||||||
</span>
|
|
||||||
{openMenu === 'tools' && (
|
|
||||||
<div className="absolute top-full left-0 mt-1 w-48 bg-white dark:bg-[#1a1a1a] border border-zinc-200 dark:border-zinc-800 rounded-xl shadow-[0_10px_40px_rgba(0,0,0,0.15)] dark:shadow-[0_10px_40px_rgba(0,0,0,0.5)] z-50 py-1 overflow-hidden">
|
|
||||||
<button
|
|
||||||
className="w-full flex items-center gap-2 px-4 py-2 text-xs text-zinc-300 hover:bg-zinc-800 hover:text-white"
|
|
||||||
onClick={() => {
|
|
||||||
setShowApiSettings(true);
|
|
||||||
setOpenMenu(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Settings size={14} />
|
|
||||||
<span>API Settings</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<span
|
<span
|
||||||
className="hover:text-primary cursor-pointer px-1 py-0.5 rounded hover:bg-white/40 dark:hover:bg-white/5 transition-colors"
|
className="hover:text-primary cursor-pointer px-1 py-0.5 rounded hover:bg-white/40 dark:hover:bg-white/5 transition-colors"
|
||||||
@@ -2782,11 +2761,6 @@ export default function Editor() {
|
|||||||
baseImage={vizAiBaseImage}
|
baseImage={vizAiBaseImage}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ApiSettingsModal
|
|
||||||
isOpen={showApiSettings}
|
|
||||||
onClose={() => setShowApiSettings(false)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Export / Save Dialog */}
|
{/* Export / Save Dialog */}
|
||||||
{exportDialog && (
|
{exportDialog && (
|
||||||
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
||||||
|
|||||||
@@ -0,0 +1,328 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
// Step 1: PDF File
|
||||||
|
const [pdfFile, setPdfFile] = useState<File | null>(null);
|
||||||
|
|
||||||
|
// Step 2: Extracted Text
|
||||||
|
const [extractedPrompt, setExtractedPrompt] = useState<string>('');
|
||||||
|
|
||||||
|
// Step 3: Generated Image
|
||||||
|
const [generatedImage, setGeneratedImage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Step 4: 3D Model
|
||||||
|
const [modelProgress, setModelProgress] = useState(0);
|
||||||
|
|
||||||
|
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
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 (
|
||||||
|
<div className="min-h-screen bg-zinc-950 text-white flex flex-col">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="h-16 border-b border-zinc-800 bg-zinc-900 flex items-center px-8 shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/dashboard')}
|
||||||
|
className="flex items-center gap-2 text-zinc-400 hover:text-white transition-colors mr-6"
|
||||||
|
>
|
||||||
|
<ChevronLeft size={18} />
|
||||||
|
<span className="text-sm font-medium">Dashboard</span>
|
||||||
|
</button>
|
||||||
|
<div className="flex items-center gap-2 font-bold text-lg tracking-tight">
|
||||||
|
<Sparkles className="text-amber-500" size={20} />
|
||||||
|
Spark Plug <span className="text-zinc-500 font-normal text-sm ml-2">| Idea to 3D Generation</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="flex-1 flex flex-col items-center max-w-5xl mx-auto w-full p-8">
|
||||||
|
|
||||||
|
{/* Stepper */}
|
||||||
|
<div className="w-full flex items-center justify-between mb-12 relative">
|
||||||
|
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-full h-0.5 bg-zinc-800 -z-10" />
|
||||||
|
{STEPS.map((step) => {
|
||||||
|
const Icon = step.icon;
|
||||||
|
const isCompleted = currentStep > step.id;
|
||||||
|
const isCurrent = currentStep === step.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={step.id} className="flex flex-col items-center gap-2 bg-zinc-950 px-4">
|
||||||
|
<div className={`w-10 h-10 rounded-full flex items-center justify-center border-2 transition-colors ${
|
||||||
|
isCompleted ? 'bg-amber-500 border-amber-500 text-white' :
|
||||||
|
isCurrent ? 'bg-zinc-900 border-amber-500 text-amber-500' :
|
||||||
|
'bg-zinc-900 border-zinc-800 text-zinc-600'
|
||||||
|
}`}>
|
||||||
|
{isCompleted ? <Check size={18} /> : <Icon size={18} />}
|
||||||
|
</div>
|
||||||
|
<span className={`text-xs font-medium ${isCurrent ? 'text-amber-500' : isCompleted ? 'text-zinc-300' : 'text-zinc-600'}`}>
|
||||||
|
{step.title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Area */}
|
||||||
|
<div className="w-full max-w-3xl bg-zinc-900 border border-zinc-800 rounded-2xl p-8 shadow-2xl min-h-[400px] flex flex-col">
|
||||||
|
|
||||||
|
{currentStep === 1 && (
|
||||||
|
<div className="flex-1 flex flex-col items-center justify-center gap-6 animate-in fade-in zoom-in duration-300">
|
||||||
|
<div className="w-20 h-20 bg-zinc-800 rounded-full flex items-center justify-center">
|
||||||
|
<Upload size={32} className="text-zinc-400" />
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-xl font-bold mb-2">Upload Mintel Spark PDF</h2>
|
||||||
|
<p className="text-sm text-zinc-400 max-w-md mx-auto">
|
||||||
|
Upload a product concept PDF. Spark Plug will extract the product details using Gemini AI to prepare for 3D generation.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative group w-full max-w-md mt-4">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".pdf"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10"
|
||||||
|
/>
|
||||||
|
<div className={`w-full p-6 border-2 border-dashed rounded-xl flex flex-col items-center gap-3 transition-colors ${pdfFile ? 'border-amber-500 bg-amber-500/10' : 'border-zinc-700 bg-zinc-950 group-hover:border-zinc-500'}`}>
|
||||||
|
<FileText size={24} className={pdfFile ? 'text-amber-500' : 'text-zinc-500'} />
|
||||||
|
<span className="text-sm font-medium text-zinc-300">
|
||||||
|
{pdfFile ? pdfFile.name : 'Click or drag PDF here'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{pdfFile && (
|
||||||
|
<button
|
||||||
|
onClick={processStep1}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="mt-4 flex items-center gap-2 bg-amber-500 hover:bg-amber-600 text-black px-6 py-3 rounded-xl font-bold transition-all disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isLoading ? <Loader2 className="animate-spin" size={18} /> : <Sparkles size={18} />}
|
||||||
|
Extract with Gemini
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentStep === 2 && (
|
||||||
|
<div className="flex-1 flex flex-col gap-4 animate-in fade-in slide-in-from-right-8 duration-300">
|
||||||
|
<h2 className="text-xl font-bold">Review Extracted Concept</h2>
|
||||||
|
<p className="text-sm text-zinc-400">
|
||||||
|
Gemini AI has analyzed the PDF. Review and modify the prompt below before generating images.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
value={extractedPrompt}
|
||||||
|
onChange={(e) => setExtractedPrompt(e.target.value)}
|
||||||
|
className="w-full flex-1 min-h-[200px] bg-zinc-950 border border-zinc-800 rounded-xl p-4 text-sm text-zinc-200 focus:outline-none focus:border-amber-500 resize-none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between mt-4">
|
||||||
|
<button
|
||||||
|
onClick={() => processStep1()} // re-run extraction
|
||||||
|
className="flex items-center gap-2 text-zinc-400 hover:text-white px-4 py-2 rounded-lg hover:bg-zinc-800 transition-colors"
|
||||||
|
>
|
||||||
|
<RefreshCw size={16} /> Regenerate Extraction
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={processStep2}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="flex items-center gap-2 bg-amber-500 hover:bg-amber-600 text-black px-6 py-3 rounded-xl font-bold transition-all disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isLoading ? <Loader2 className="animate-spin" size={18} /> : <ArrowRight size={18} />}
|
||||||
|
Generate Images (Nano Banana)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentStep === 3 && (
|
||||||
|
<div className="flex-1 flex flex-col gap-4 animate-in fade-in slide-in-from-right-8 duration-300">
|
||||||
|
<h2 className="text-xl font-bold">Concept Art generated by Nano Banana</h2>
|
||||||
|
<p className="text-sm text-zinc-400">
|
||||||
|
Review the generated product imagery. These images will be used by Meshy to construct the final 3D model.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex-1 bg-zinc-950 border border-zinc-800 rounded-xl flex items-center justify-center overflow-hidden p-4 relative">
|
||||||
|
{generatedImage ? (
|
||||||
|
<img src={generatedImage} alt="Generated Concept" className="max-w-full max-h-[300px] object-contain rounded-lg shadow-xl" />
|
||||||
|
) : (
|
||||||
|
<ImageIcon size={48} className="text-zinc-800" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between mt-4">
|
||||||
|
<button
|
||||||
|
onClick={() => processStep2()} // re-run generation
|
||||||
|
className="flex items-center gap-2 text-zinc-400 hover:text-white px-4 py-2 rounded-lg hover:bg-zinc-800 transition-colors"
|
||||||
|
>
|
||||||
|
<RefreshCw size={16} /> Regenerate Images
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={processStep3}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="flex items-center gap-2 bg-amber-500 hover:bg-amber-600 text-black px-6 py-3 rounded-xl font-bold transition-all disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isLoading ? <Loader2 className="animate-spin" size={18} /> : <ArrowRight size={18} />}
|
||||||
|
Construct 3D Model (Meshy)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentStep === 4 && (
|
||||||
|
<div className="flex-1 flex flex-col items-center justify-center gap-6 animate-in fade-in slide-in-from-right-8 duration-300">
|
||||||
|
<div className="w-24 h-24 rounded-full border-4 border-zinc-800 border-t-amber-500 animate-spin flex items-center justify-center">
|
||||||
|
<Box size={32} className="text-zinc-500 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center w-full max-w-sm">
|
||||||
|
<h2 className="text-xl font-bold mb-2">Generating 3D Model...</h2>
|
||||||
|
<p className="text-sm text-zinc-400 mb-6">
|
||||||
|
Meshy is constructing the model based on the Nano Banana concept images.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="w-full bg-zinc-950 rounded-full h-3 overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-amber-500 transition-all duration-300"
|
||||||
|
style={{ width: `${modelProgress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs font-bold text-amber-500 mt-2">{modelProgress}% Complete</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{modelProgress >= 100 && (
|
||||||
|
<button
|
||||||
|
onClick={processStep4}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="mt-6 flex items-center gap-2 bg-amber-500 hover:bg-amber-600 text-black px-8 py-3 rounded-xl font-bold transition-all disabled:opacity-50 animate-in fade-in slide-in-from-bottom-4"
|
||||||
|
>
|
||||||
|
{isLoading ? <Loader2 className="animate-spin" size={18} /> : <Check size={18} />}
|
||||||
|
Open in Editor
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user