import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Settings, ShieldCheck, LogOut, Loader2, RefreshCw, Trash2, BrainCircuit, X } from 'lucide-react'; import { CreditButton } from './components/CreditButton'; import { SEO } from './components/SEO'; import { ApiKeyModal } from './components/ApiKeyModal'; import axios from 'axios'; import { ProductType, CreativityLevel, AspectRatio, ImageSize } from './types'; import Dashboard from './Dashboard'; import Login from './pages/LoginPage'; import { useAuth } from './AuthContext'; import { useTranslation } from 'react-i18next'; import { Tooltip } from './components/Tooltip'; import { ZoomableImage } from './components/ZoomableImage'; import { ProcessGuideGenerator, ProcessGuideRef } from './components/ProcessGuideGenerator'; import { VideoGenerator } from './components/VideoGenerator'; import { LegalModal } from './components/LegalModal'; import { Layout } from './components/Layout'; import { USER_AGREEMENT_TEXT, KVKK_TEXT, DISCLAIMER_TEXT } from './legal_texts'; import { Header } from './components/Header'; import NeuroScorecard from './components/NeuroScorecard'; // Imported NeuroScorecard // AXIOS INSTANCE REMOVED - Using global axios for AuthContext compatibility const Loader = ({ message }: { message: string }) => (

{message}

); const Home: React.FC = () => { // ---------------------------------------------------------------------- // MOCKUP STUDIO CONSTANTS // ---------------------------------------------------------------------- const CLIENT_MOCKUP_SCENARIOS: Record> = { "Wall Art": { // Classic "living_room": "Living Room (Modern)", "bedroom": "Bedroom (Cozy)", "office": "Home Office", "kitchen": "Kitchen & Dining", "bathroom": "Luxury Bathroom", "nursery": "Nursery (Baby)", // Commercial "cafe_wall": "Trendy Cafe Wall", "restaurant_booth": "Restaurant Booth", "bar_lounge": "Bar & Lounge (Mood)", "asian_restaurant": "Asian Restaurant (Sushi)", "hotel_lobby": "Hotel Lobby", "boutique_store": "Boutique Store", "yoga_studio": "Yoga Studio (Zen)", "gym_wall": "Gym / Fitness", // Specific Frames "frame_black": "Black Frame (Clean)", "frame_white": "White Frame (Scandi)", "frame_gold": "Gold Frame (Luxury)", "frame_wood": "Oak Wood Frame (Boho)", "frame_poster": "Poster Hanger (Casual)", // Creative "gamer_room": "Gamer Room (Neon)", "streamer_studio": "Streamer Studio", "leaning_floor": "Leaning on Floor (Loft)", "fairy_lights": "Fairy Lights (Cozy)", "shelf_decor": "Shelf Decor", "minimal_entryway": "Minimal Entryway", "industrial_loft": "Industrial Loft", // Detail "gallery": "Art Gallery Spotlight", "studio": "Artist Easel", "frame_close": "Frame Detail (Macro)", "macro_texture": "Paper Texture (Extreme Zoom)", "macro_canvas": "Canvas Corner (Side View)", "hand_held": "Hand Holding Print (Context)", "ink_detail": "Ink Quality (Sharpness)" }, "Sticker": { // Tech "laptop_lid": "Laptop Lid (Tech)", "tablet_case": "Tablet Case (iPad)", "phone_case": "Phone Case (Clear)", "gamer_setup": "Gamer PC Case", // Lifestyle "sticker_bottle": "Water Bottle (Hydro)", "travel_mug": "Travel Mug (Cozy)", "sticker_notebook": "Notebook Cover", "planner_spread": "Planner Page Decoration", "clipboard": "Clipboard (Office)", "luggage": "Travel Luggage", "guitar_case": "Guitar Case", // Urban "skateboard": "Skateboard Deck", "street_pole": "Street Pole (Urban)", "car_bumper": "Car Bumper", // Special "flatlay_desk": "Desk Flatlay", "hand_held": "Hand Held (POV)" }, "Planner": { "tablet": "iPad Display (Digital)", "notebook": "Physical Planner", "desk_flatlay": "Desk Setup", "coffee_shop": "Coffee Shop" }, "Bookmark": { "in_book": "Inside Vintage Book", "flatlay_book": "Cozy Reading Nook" }, "Phone Wallpaper": { "phone_lock": "Lock Screen (Hand Held)", "phone_table": "On Coffee Table" }, "Label": { "jar_candle": "Candle Jar Label", "cosmetic_bottle": "Cosmetic Bottle", "wine_bottle": "Wine Bottle", "shopping_bag": "Shopping Bag" }, "Custom": { "custom": "✨ Custom Scenario (Write your own)" } }; const CLIENT_ATMOSPHERES: Record = { "neutral": "Neutral / Day", "warm": "Warm / Golden Hour", "cool": "Cool / Morning", "dark": "Dark / Moody", "bright": "Bright / Airy", "luxury": "Luxury / High End", "minimalist": "Minimalist / Clean" }; const CLIENT_ASPECT_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4", "2:3", "3:2", "21:9", "32:9", "2.35:1", "9:21"]; const { t } = useTranslation(); const { user, logout, refreshUser } = useAuth(); // VIEW MODE: 'dashboard' | 'editor' const [viewMode, setViewMode] = useState<'dashboard' | 'editor'>('dashboard'); const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false); // Neuro-Scorecard State const [showNeuroModal, setShowNeuroModal] = useState(false); const [isAnalyzing, setIsAnalyzing] = useState(false); const [neuroAnalysis, setNeuroAnalysis] = useState(null); // Axios Interceptor for BYOK useEffect(() => { const interceptor = axios.interceptors.request.use((config) => { const token = localStorage.getItem('token'); const apiKey = localStorage.getItem('gemini_api_key'); if (token) config.headers.Authorization = `Bearer ${token}`; if (apiKey) config.headers['X-Gemini-API-Key'] = apiKey; return config; }); return () => axios.interceptors.request.eject(interceptor); }, []); const [niche, setNiche] = useState(''); const [aspectRatio, setAspectRatio] = useState('3:4'); const [productType, setProductType] = useState("Wall Art"); const [creativity, setCreativity] = useState("Balanced"); const [isLoading, setIsLoading] = useState(false); const [loadingMsg, setLoadingMsg] = useState(''); const [useExactReference, setUseExactReference] = useState(false); const [isStickerSet, setIsStickerSet] = useState(false); const [setSize, setSetSize] = useState("6"); const [projectData, setProjectData] = useState(null); // DNA Vault const [dnaProfiles, setDnaProfiles] = useState<{ id: string, name: string }[]>([]); const [activeDnaProfile, setActiveDnaProfile] = useState<{ id: string, name: string } | null>(null); useEffect(() => { axios.get('/api/profiles').then(r => setDnaProfiles(r.data.profiles)).catch(console.error); refreshUser(); }, [viewMode]); // Refresh when switching views // RESTORE ACTIVE DNA PROFILE ON MOUNT // RESTORE ACTIVE DNA PROFILE ON MOUNT useEffect(() => { const savedProfileId = localStorage.getItem('activeDnaProfileId'); if (savedProfileId) { loadDnaProfile(savedProfileId); } }, []); // Run ONCE on mount // URL SYNC: Load project from URL query param useEffect(() => { const params = new URLSearchParams(window.location.search); const projectId = params.get('project'); if (projectId && viewMode === 'dashboard') { handleSelectProject(projectId); } }, []); const [referenceImages, setReferenceImages] = useState([]); const [selectedRatio, setSelectedRatio] = useState("3:4"); const [selectedSize, setSelectedSize] = useState("4K"); const [error, setError] = useState(null); // NEW: Refine & Variants State const [revisionBrief, setRevisionBrief] = useState(''); const [isRefining, setIsRefining] = useState(false); const [variants, setVariants] = useState([]); const [variantMetadata, setVariantMetadata] = useState>({}); // Cache for hover inspection const [isGeneratingVariants, setIsGeneratingVariants] = useState(false); // NEW: Mockups State const [mockups, setMockups] = useState<{ id?: string; scenario: string; aspectRatio?: string; path: string }[]>([]); const [isGeneratingMockups, setIsGeneratingMockups] = useState(false); const [mockupScenario, setMockupScenario] = useState('living_room'); const [customMockupPrompt, setCustomMockupPrompt] = useState(""); const [mockupRatio, setMockupRatio] = useState('16:9'); const processGuideRef = useRef(null); const [mockupAtmosphere, setMockupAtmosphere] = useState('neutral'); const [useWatermark, setUseWatermark] = useState(false); // New Watermark State const [regeneratingMockupId, setRegeneratingMockupId] = useState(null); // NEW: Draft/Master Logic const [activeAssetId, setActiveAssetId] = useState(null); const [isUpscaling, setIsUpscaling] = useState(false); const [upscaleRatioOverride, setUpscaleRatioOverride] = useState('auto'); const [variantFillType, setVariantFillType] = useState('auto'); // 'auto', 'white', 'black' // NEW: Videos State const [videos, setVideos] = useState<{ id?: string; path: string; simulated?: boolean; presetId?: string }[]>([]); // FORCE 1:1 ASPECT RATIO FOR STICKERS useEffect(() => { if (productType === "Sticker") { setAspectRatio("1:1"); } }, [productType]); // NEW: Upscale State (DUPLICATE REMOVED) const [upscaledAsset, setUpscaledAsset] = useState<{ id: string; path: string; width: number; height: number; printSize: string } | null>(null); // NEW: Revision History (Last 5 versions) const [revisionHistory, setRevisionHistory] = useState<{ path: string; brief: string; timestamp: string }[]>([]); // NEW: Collection/Set State const [selectedHistoryItems, setSelectedHistoryItems] = useState([]); const [isCreatingCollection, setIsCreatingCollection] = useState(false); const [isPublishingEtsy, setIsPublishingEtsy] = useState(false); const [isRegeneratingStrategy, setIsRegeneratingStrategy] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); // Prevents ZoomableImage click during delete confirm // PASTE-TO-UPLOAD HANDLER const handlePaste = useCallback((e: ClipboardEvent) => { const items = e.clipboardData?.items; if (!items) return; for (let i = 0; i < items.length; i++) { if (items[i].type.indexOf("image") !== -1) { const blob = items[i].getAsFile(); if (!blob) continue; const reader = new FileReader(); reader.onload = (event) => { const base64 = event.target?.result as string; setReferenceImages(prev => [...prev, base64]); }; reader.readAsDataURL(blob); } } }, []); useEffect(() => { console.log("Home Component Mounted"); window.addEventListener('paste', handlePaste); return () => window.removeEventListener('paste', handlePaste); }, [handlePaste]); const loadDnaProfile = async (profileId: string) => { try { const res = await axios.get(`/api/profiles/${profileId}`); if (res.data.profile?.images) { setReferenceImages(res.data.profile.images); const profile = { id: res.data.profile.id, name: res.data.profile.name }; setActiveDnaProfile(profile); localStorage.setItem('activeDnaProfileId', profile.id); } } catch (e) { console.error("Failed to load profile:", e); } }; const handleNewProject = () => { // Reset Project Data & States setProjectData(null); setNiche(''); setReferenceImages([]); setActiveDnaProfile(null); // Reset DNA localStorage.removeItem('activeDnaProfileId'); // Clear Persistence setActiveAssetId(null); setVariants([]); setMockups([]); setVideos([]); setUpscaledAsset(null); setRevisionHistory([]); setSelectedHistoryItems([]); setError(null); setLoadingMsg(''); setIsLoading(false); // Reset Settings to Defaults setProductType("Wall Art"); setAspectRatio("3:4"); setCreativity("Balanced"); setIsStickerSet(false); setSetSize("6"); // Clear URL Params const newUrl = window.location.pathname; window.history.pushState({ path: newUrl }, '', newUrl); // Switch to Editor View setViewMode('editor'); }; const handleDeleteDnaProfile = async (id: string) => { try { await axios.delete(`/api/profiles/${id}`); alert("DNA Profile Deleted"); // Refresh list const r = await axios.get('/api/profiles'); setDnaProfiles(r.data.profiles); // If it was the active one, clear it if (activeDnaProfile?.id === id) { setActiveDnaProfile(null); setReferenceImages([]); // Clear broken thumbnails localStorage.removeItem('activeDnaProfileId'); } } catch (e) { console.error(e); alert("Failed to delete DNA profile"); } }; // DASHBOARD HANDLERS const handleSelectProject = async (id: string, updateUrl = true) => { setIsLoading(true); setLoadingMsg("Resurrecting Project..."); try { const res = await axios.get(`/api/projects/${id}`); // CRITICAL FIX: The UI expects 'assets' at the top level of projectData // But the API returns { project: { assets: [] }, strategy: {} } // We must HOIST the assets up and ensure structure consistency const project = res.data.project || {}; const rawAssets = project.assets || []; // Sort assets by date desc const sortedAssets = [...rawAssets].sort((a: any, b: any) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ); // Construct the State Object exactly how the UI components demand it // CRITICAL FIX: Fallback to seoData if strategy is missing (Recovered Projects Scenario) const resolvedStrategy = res.data.strategy || res.data.project?.seoData || {}; // Parse Attributes and Map Category Field if (typeof resolvedStrategy.attributes === 'string') { try { resolvedStrategy.attributes = JSON.parse(resolvedStrategy.attributes); } catch (e) { } } if (!resolvedStrategy.categorySuggestion && resolvedStrategy.categoryPath) { resolvedStrategy.categorySuggestion = resolvedStrategy.categoryPath; } const normalizedData = { project: project, strategy: resolvedStrategy, assets: sortedAssets // <--- HOISTED for UI visibility }; setProjectData(normalizedData); setViewMode('editor'); // URL UPDATE const newUrl = `${window.location.pathname}?project=${id}`; if (updateUrl) { window.history.pushState({ path: newUrl }, '', newUrl); } else { window.history.replaceState({ path: newUrl }, '', newUrl); } // Auto-select latest master/variant/upscaled asset // FORCE PRIORITY: Upscaled > Revision > Master (Because Upscaled is the final truth) let latestMaster = sortedAssets.find((a: any) => a.type === 'upscaled'); if (!latestMaster) { latestMaster = sortedAssets.find((a: any) => ['master', 'revision'].includes(a.type)); } if (latestMaster) setActiveAssetId(latestMaster.id); // Restore Sub-States setMockups(sortedAssets.filter((a: any) => a.type === 'mockup').map((m: any) => ({ id: m.id, scenario: m.path.includes('mockup_') ? m.path.split('mockup_')[1].split('.png')[0] : 'custom', path: m.path, aspectRatio: m.meta ? JSON.parse(m.meta).aspectRatio : (project.aspectRatio || '16:9') })) || []); setVideos(sortedAssets.filter((a: any) => a.type === 'video').map((v: any) => ({ id: v.id, path: v.path, simulated: v.path.includes('sim_'), presetId: v.meta ? JSON.parse(v.meta).presetId : undefined })) || []); // Restore Revision History setRevisionHistory(sortedAssets.filter((a: any) => a.type === 'revision').map((r: any) => ({ path: r.path, brief: r.meta ? JSON.parse(r.meta).brief : 'Revision', timestamp: new Date(r.createdAt).toLocaleTimeString() }))); // RESTORE VARIANTS STATE (Latest 5 only to match user expectation) setVariants(sortedAssets.filter((a: any) => a.type === 'variant') .map((v: any) => ({ ratio: v.meta ? JSON.parse(v.meta).ratio : '1:1', label: v.meta ? JSON.parse(v.meta).label : 'Variant', path: v.path, id: v.id })) || []); // RESTORE UPSCALED ASSET STATE const upscaled = sortedAssets.find((a: any) => a.type === 'upscaled'); if (upscaled) { setUpscaledAsset({ id: upscaled.id, path: upscaled.path, width: upscaled.meta ? JSON.parse(upscaled.meta).width : 6000, height: upscaled.meta ? JSON.parse(upscaled.meta).height : 8000, printSize: upscaled.meta ? JSON.parse(upscaled.meta).printSize : '20"x30"' }); } else { setUpscaledAsset(null); } // PRE-POPULATE CREATION PANEL (So user sees original settings) setNiche(project.niche || ''); setProductType((project.productType as ProductType) || 'Wall Art'); setCreativity((project.creativity as CreativityLevel) || 'Balanced'); setSelectedRatio((project.aspectRatio as AspectRatio) || '3:4'); setAspectRatio(project.detectedRatio || project.aspectRatio || '3:4'); // SYNC MOCKUP RATIO WITH PROJECT RATIO setMockupRatio(project.aspectRatio || '16:9'); setUseExactReference(project.useExactReference || false); // RESTORE REFERENCE IMAGES (Visual DNA) const refAssets = sortedAssets.filter((a: any) => a.type === 'reference' || a.type === 'dna'); // 1. First, load Project Defaults (Safety Net) if (refAssets.length > 0) { const refPaths = refAssets.map((r: any) => `/storage/${r.path}`); setReferenceImages(refPaths); } else { setReferenceImages([]); } // 2. Then, Check Session Override (User Preference) // If the user has an active DNA profile in this session, try to load it. const sessionProfileId = localStorage.getItem('activeDnaProfileId'); if (sessionProfileId) { loadDnaProfile(sessionProfileId); } } catch (e) { console.error(e); if ((e as any).response?.status !== 403) { console.warn("Project load non-fatal error", e); } } finally { setIsLoading(false); } }; const handleDeleteProject = async (id: string) => { if (!confirm("Delete project?")) return; try { await axios.delete(`/api/projects/${id}`); alert("Project deleted"); window.location.reload(); // Simple reload to refresh dashboard } catch (e) { alert("Failed to delete"); } }; const handleCreateProject = async () => { console.log("handleCreateProject called"); // DEBUG // Validation for Credit Check handled by backend 402 if (!process.env.NODE_ENV && !user?.apiKey) { // In dev mode we might skip, but in prod we need key } setIsLoading(true); setLoadingMsg("Initializing Creative Strategy..."); // Step 1 setError(null); try { console.log("Sending POST to /api/projects..."); // DEBUG const res = await axios.post('/api/projects', { niche, productType, creativity, referenceImages, aspectRatio, useExactReference, isStickerSet, setSize }); console.log("Create Response:", res.data); // DEBUG // If successful, load project - NORMALIZE STRUCTURE TO MATCH handleSelectProject const newProject = res.data.project; setProjectData({ project: newProject, strategy: newProject.strategy || {}, assets: newProject.assets || [] }); await refreshUser(); // Update credits setViewMode('editor'); } catch (err: any) { console.error("Create Project Error:", err); // DEBUG const msg = err.response?.data?.error || err.message; if (err.response?.status === 402) { alert(`⚠️ ${msg}`); } alert(`Creation Failed: ${msg}`); // FORCE ALERT setError(msg); } finally { setIsLoading(false); } }; const handleRefine = async () => { const projectId = projectData?.project?.id || projectData?.id; // alert(`DEBUG: Refine Clicked. ProjectID: ${projectId}`); // REMOVED console.log("[Client] handleRefine triggered. ID:", projectId); if (!projectId) return; setIsRefining(true); setError(null); try { // Optimistic Update? No, wait for result. const response = await axios.post(`/api/projects/${projectId}/refine`, { revisionBrief: revisionBrief, sourceAssetId: activeAssetId // <--- Explicitly tell backend WHICH image to refine }); // Update UI with the full project data returned from backend if (response.data.project) { const project = response.data.project; const sortedAssets = [...(project.assets || [])].sort((a: any, b: any) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ); setProjectData({ project: project, strategy: response.data.strategy || projectData.strategy, assets: sortedAssets }); // Auto Select New Master (it might have a new ID if we replaced it, or same ID but updated path) const latestMaster = sortedAssets.find((a: any) => ['master', 'revision', 'variant'].includes(a.type)); if (latestMaster) setActiveAssetId(latestMaster.id); } setRevisionBrief(''); await refreshUser(); } catch (err: any) { console.error("[Client] Refine Error:", err); const msg = err.response?.data?.error || err.message; alert(`REFINE FAILED: ${msg}`); // FORCE ERROR VISIBILITY if (err.response?.status === 402) alert(`⚠️ ${msg}`); setError(`Refine Error: ${msg}`); } finally { setIsRefining(false); } }; const handleGenerateVariants = async () => { const projectId = projectData?.project?.id || projectData?.id; if (!projectId) return; setIsGeneratingVariants(true); setError(null); try { const response = await axios.post(`/api/projects/${projectId}/variants`, { variantFillType: variantFillType // Pass user preference }); setVariants(response.data.variants || []); await refreshUser(); } catch (err: any) { const msg = err.response?.data?.error || err.message; if (err.response?.status === 402) alert(`⚠️ ${msg}`); setError(`Variants Error: ${msg}`); } finally { setIsGeneratingVariants(false); } }; const handleVariantHover = useCallback(async (id: string) => { // If already cached, don't refetch if (variantMetadata[id]) return; try { const res = await axios.get(`/api/assets/${id}/metadata`); setVariantMetadata(prev => ({ ...prev, [id]: res.data })); } catch (e) { console.error("Failed to inspect variant", e); } }, [variantMetadata]); const handleRegenerateVariants = async () => { if (!confirm("This will DELETE all current variants and regenerate them based on the SELECTED image. Continue?")) return; setIsGeneratingVariants(true); try { // Determine Source ID (Strip prefix if any) const sourceAssetId = activeAssetId ? activeAssetId.replace('lightbox_', '') : null; // 1. Delete All await axios.delete(`/api/projects/${projectData.project.id}/variants`); setVariants([]); // Clear UI immediately // 2. Regenerate with SOURCE const response = await axios.post(`/api/projects/${projectData.project.id}/variants`, { regenerate: true, sourceAssetId: sourceAssetId, variantFillType: variantFillType // Pass user preference }); if (response.data.success && response.data.variants) { setVariants(response.data.variants.map((v: any) => ({ ratio: v.ratio, label: v.label, path: v.path, id: v.id }))); } // 3. Update State (Force refresh to get correct meta) handleSelectProject(projectData.project.id, false); } catch (e: any) { alert("Regeneration failed: " + e.message); } finally { setIsGeneratingVariants(false); } }; const handleDownloadCricut = async (assetId: string) => { try { const res = await axios.get(`/api/assets/${assetId}/cricut-package`, { responseType: 'blob' }); const url = window.URL.createObjectURL(new Blob([res.data])); const link = document.createElement('a'); link.href = url; link.setAttribute('download', `cricut_pack_${assetId.substring(0, 8)}.zip`); document.body.appendChild(link); link.click(); link.remove(); } catch (e) { console.error("Cricut download failed", e); alert("Failed to download Cricut package."); } }; const handleGenerateStickerSheet = async () => { if (!projectData?.project?.id) return; setIsLoading(true); setLoadingMsg("Compositing Sticker Sheet..."); try { // Default to A4 for now, or use the project's default if it matches paper size? // Let's use A4 as safe default. const response = await axios.post(`/api/projects/${projectData.project.id}/sheet`, { paperSize: 'A4' }); // Reload assets to show the new sheet setLoadingMsg("Reloading Assets..."); await handleSelectProject(projectData.project.id, false); alert("Sticker Sheet Generated! Check the 'Sheets' section (or look for the new asset)."); } catch (e: any) { console.error(e); alert("Failed to generate sheet: " + (e.response?.data?.error || e.message)); } finally { setIsLoading(false); } }; // MOCKUPS HANDLER const handleUpscale = async () => { const projectId = projectData?.project?.id || projectData?.id; // alert(`DEBUG: Upscale Clicked. ProjectID: ${projectId}`); // REMOVED console.log("handleUpscale called. ActiveAssetId:", activeAssetId); if (!activeAssetId) { console.warn("handleUpscale aborted: No activeAssetId"); return; } setIsUpscaling(true); try { console.log("Sending upscale request for:", activeAssetId); const res = await axios.post(`/api/projects/${projectId}/upscale`, { assetId: activeAssetId }); // Refresh data handleSelectProject(projectId); // Auto-select the new master asset logic will be handled in useEffect } catch (e: any) { const msg = e.response?.data?.error || "Upscale failed"; alert(`UPSCALE FAILED: ${msg}`); // FORCE ERROR VISIBILITY setError(msg); } }; const handleGenerateMockups = async () => { const projectId = projectData?.project?.id || projectData?.id; if (!projectId) return; setIsGeneratingMockups(true); setError(null); // SPECIAL HANDLING: Process Guide if (mockupScenario === 'process_guide') { if (processGuideRef.current) { try { await processGuideRef.current.generate(); } catch (e) { console.error("Guide generation failed"); } } setIsGeneratingMockups(false); return; } try { // Batch generation const response = await axios.post(`/api/projects/${projectId}/mockups`, { watermarkOptions: { enabled: useWatermark } }); // Add new mockups to list if (response.data.mockups) { setMockups(prev => [...prev, ...response.data.mockups]); } await refreshUser(); } catch (err: any) { const msg = err.response?.data?.error || err.message; if (err.response?.status === 402) alert(`⚠️ ${msg}`); setError(`Mockup Error: ${msg}`); } finally { setIsGeneratingMockups(false); } }; // REGENERATE SINGLE MOCKUP const handleRegenerateMockup = async (id: string, scenario: string, ratio: string) => { setRegeneratingMockupId(id); try { await handleGenerateMockups(); } catch (e) { alert("Regeneration failed"); } finally { setRegeneratingMockupId(null); } } // GENERATE SINGLE MOCKUP (New) const handleGenerateSingleMockup = async () => { const projectId = projectData?.project?.id || projectData?.id; // alert(`DEBUG: Mockup Clicked. ProjectID: ${projectId}`); // REMOVED console.log("[Client] handleGenerateSingleMockup triggered. ID:", projectId); if (!projectId) return; setIsGeneratingMockups(true); setError(null); // Find MASTER ASSET to use let targetAssetId = activeAssetId; if (!targetAssetId) { // Fallback to searching in the hoisted assets array const assets = projectData.assets || projectData.project.assets || []; const master = assets.find((a: any) => a.type === 'master' || a.type === 'MASTER'); if (master) targetAssetId = master.id; } if (!targetAssetId) { alert("Please select a Master Asset or Generate one first!"); setIsGeneratingMockups(false); return; } try { const response = await axios.post(`/api/projects/${projectId}/mockup`, { selectedScenario: mockupScenario, selectedRatio: mockupRatio, atmosphere: mockupAtmosphere, customPrompt: mockupScenario === 'custom' ? customMockupPrompt : undefined, masterAssetId: targetAssetId, watermarkOptions: { enabled: useWatermark, opacity: 30, // Default opacity position: 'center' } }); if (response.data.mockup) { setMockups(prev => [response.data.mockup, ...prev]); } await refreshUser(); } catch (err: any) { const msg = err.response?.data?.error || err.message; if (err.response?.status === 402) alert(`⚠️ ${msg}`); setError(`Mockup Error: ${msg}`); } finally { setIsGeneratingMockups(false); } }; const handleDeleteAsset = async (assetId: string, type: string) => { // Confirmation is handled by the calling button - no duplicate confirm here console.log(`[DELETE] Starting delete for asset: ${assetId} (type: ${type})`); try { const response = await axios.delete(`/api/assets/${assetId}`); console.log(`[DELETE] Server response:`, response.data); // Remove from UI based on type if (type === 'mockup') { setMockups(prev => prev.filter((m: any) => m.id !== assetId)); } else if (type === 'video') { setVideos(prev => prev.filter((v: any) => v.id !== assetId)); } else if (type === 'variant') { setVariants(prev => prev.filter((v: any) => v.id !== assetId)); } else if (type === 'upscaled') { setUpscaledAsset(null); } // CRITICAL: Update projectData assets for ALL types (master, revision, upscaled, etc.) setProjectData((prev: any) => { const newAssets = prev.assets ? prev.assets.filter((a: any) => a.id !== assetId) : []; console.log(`[DELETE] Updated projectData: ${prev.assets?.length || 0} -> ${newAssets.length} assets`); return { ...prev, assets: newAssets }; }); // Reset active asset if we deleted the currently active one if (activeAssetId === assetId) { setActiveAssetId(null); } console.log(`[DELETE] Successfully deleted asset: ${assetId}`); } catch (e: any) { console.error(`[DELETE] Error deleting asset:`, e); const errorMsg = e.response?.data?.error || e.message || "Unknown error"; alert(`Delete failed: ${errorMsg}`); } } if (!user) { return ; } if (viewMode === 'dashboard') { return ( {/* Use Layout wrapper for Dashboard too to keep Header */} ); } // ---------------------------------------------------------------------- // NEURO-SCORECARD HANDLER // ---------------------------------------------------------------------- const handleNeuroAnalyze = async (assetId: string) => { const asset = projectData.assets.find((prev: any) => prev.id === assetId); if (!asset) return; setShowNeuroModal(true); // Ensure modal opens setIsAnalyzing(true); setNeuroAnalysis(null); try { // Fetch the image data (blind fetch for now, assuming server handles path) const cleanPath = asset.path.startsWith('/') ? asset.path.substring(1) : asset.path; // We need to convert the image to base64 to send it to the API // Or if the API supports path, send path. // The existing /api/neuro-score expects imageBase64. // Let's fetch the image client-side to get blob, then base64. const response = await fetch(`/storage/${cleanPath}`); const blob = await response.blob(); const reader = new FileReader(); reader.readAsDataURL(blob); reader.onloadend = async () => { const base64data = reader.result as string; try { const res = await axios.post('/api/neuro-score', { imageBase64: base64data }); setNeuroAnalysis(res.data.data); } catch (err: any) { console.error("Analysis Failed:", err); // Don't close modal on error, show alert alert("Analysis Failed: " + (err.response?.data?.error || err.message)); } finally { setIsAnalyzing(false); } }; } catch (error) { console.error("Failed to load image for analysis", error); setIsAnalyzing(false); setShowNeuroModal(false); } }; const handleApplyNeuroFix = (suggestion: string) => { setRevisionBrief(prev => { const separator = prev.length > 0 ? " " : ""; return `${prev}${separator}${suggestion}`; }); // Scroll to refine input const refineSection = document.getElementById('refine-section'); if (refineSection) { refineSection.scrollIntoView({ behavior: 'smooth', block: 'center' }); const input = refineSection.querySelector('input'); if (input) input.focus(); } }; return ( setIsApiKeyModalOpen(false)} /> {/* Hidden Process Guide Generator */}
{/* maybe reload assets? */ }} />
{/* MAIN EDITOR UI */}
{/* TOOLBAR */}

{projectData ? (projectData.strategy?.seoTitle || projectData.project?.niche || "Untitled Project").substring(0, 30) + (projectData.strategy?.seoTitle ? '...' : '') : 'New Project'}

{projectData ? 'Editing Mode' : 'Setup Phase'}
Project Budget ${projectData?.project?.totalCost?.toFixed(4) || '0.000'}
{ }} disabled className="bg-stone-100 text-stone-400 cursor-default px-4"> {isLoading ? 'Processing...' : 'Ready'}
{!projectData ? ( // ---------------------------------------------------------------------- // SETUP WIZARD (New Project) // ----------------------------------------------------------------------
{/* Background Decoration */}

New Creation

Define your vision. The AI will handle the strategy.

{/* Row 1: Niche */}
setNiche(e.target.value)} placeholder="e.g., 'Boho Nursery Wall Art', 'Minimalist Planner'..." className="w-full text-base font-bold bg-stone-50 border border-stone-200 rounded-xl px-4 py-3 focus:border-stone-900 focus:outline-none transition-colors placeholder:text-stone-300" />
{/* Row 2: Parameters Grid */}
{/* Product Type */}
{["Wall Art", "Sticker", "Planner", "Bookmark", "Label"].map((type) => ( ))}
{/* Sticker Set Configuration */} {productType === "Sticker" && (
setIsStickerSet(!isStickerSet)}>
{isStickerSet && (
)}
)} {/* Aspect Ratio */}
{(() => { // Handle Paper Sizes if (aspectRatio === "A4" || aspectRatio === "A5") return 📄; if (aspectRatio === "Letter") return 📝; const parts = aspectRatio.split(':'); const w = parseInt(parts[0]); const h = parseInt(parts[1]); const icon = w > h ? '🖼️' : w < h ? '📱' : '⬜'; return {icon} })()}
{/* Creativity Level */}
setCreativity(e.target.value === "0" ? "Conservative" : e.target.value === "1" ? "Balanced" : "Wild")} className="w-full accent-stone-900 cursor-pointer h-2 bg-stone-200 rounded-full appearance-none" />
Strict Balanced Wild
{/* Row 3: Quality & DNA */}
{/* Quality Selection */}
{[ { id: "SD", l: "Standard", desc: "Fast & Stable" }, { id: "HD", l: "High Def", desc: "Detailed" }, { id: "4K", l: "Ultra", desc: "Best Finish" } ].map((q) => ( ))}

Drafts are generated at these scales. Upscaling to 6000px+ is available in the next step.

{/* Visual DNA */}
{/* STYLE vs STRUCTURE TOGGLE */}
{/* DNA DROPDOWN */} {/* DNA ACTIONS (Visible if images exist) */} {referenceImages.length > 0 && (
{activeDnaProfile && ( <> )}
)}
{referenceImages.map((src, i) => (
))}
+ { if (!e.target.files) return; Array.from(e.target.files).forEach((f: any) => { const r = new FileReader(); r.onload = ev => setReferenceImages(p => [...p, ev.target?.result as string]); r.readAsDataURL(f); }); }} className="absolute inset-0 opacity-0 cursor-pointer" />
{isLoading ? : `Start Creation`}
) : ( // ---------------------------------------------------------------------- // PROJECT EDITOR // ----------------------------------------------------------------------
{isLoading && (
)} {/* CREATION SUMMARY PANEL (Blueprint) */}
📋

Project Blueprint

{/* Prompt Niche */}