Files
digicraft-fe/Home.tsx
Fahri Can Seçer 6e3bee17ef
Some checks failed
Deploy Frontend / deploy (push) Has been cancelled
main
2026-02-05 01:34:13 +03:00

2544 lines
171 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 }) => (
<div className="flex flex-col items-center justify-center p-12 space-y-4">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-stone-900"></div>
<p className="text-stone-600 font-bold animate-pulse text-xs tracking-[0.2em] uppercase text-center max-w-sm leading-loose">{message}</p>
</div>
);
const Home: React.FC = () => {
// ----------------------------------------------------------------------
// MOCKUP STUDIO CONSTANTS
// ----------------------------------------------------------------------
const CLIENT_MOCKUP_SCENARIOS: Record<string, Record<string, string>> = {
"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<string, string> = {
"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<any>(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<ProductType>("Wall Art");
const [creativity, setCreativity] = useState<CreativityLevel>("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<any | null>(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<string[]>([]);
const [selectedRatio, setSelectedRatio] = useState<AspectRatio>("3:4");
const [selectedSize, setSelectedSize] = useState<ImageSize>("4K");
const [error, setError] = useState<string | null>(null);
// NEW: Refine & Variants State
const [revisionBrief, setRevisionBrief] = useState('');
const [isRefining, setIsRefining] = useState(false);
const [variants, setVariants] = useState<any[]>([]);
const [variantMetadata, setVariantMetadata] = useState<Record<string, any>>({}); // 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<ProcessGuideRef>(null);
const [mockupAtmosphere, setMockupAtmosphere] = useState('neutral');
const [useWatermark, setUseWatermark] = useState(false); // New Watermark State
const [regeneratingMockupId, setRegeneratingMockupId] = useState<string | null>(null);
// NEW: Draft/Master Logic
const [activeAssetId, setActiveAssetId] = useState<string | null>(null);
const [isUpscaling, setIsUpscaling] = useState(false);
const [upscaleRatioOverride, setUpscaleRatioOverride] = useState<string>('auto');
const [variantFillType, setVariantFillType] = useState<string>('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<string[]>([]);
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 <Login />;
}
if (viewMode === 'dashboard') {
return (
<Layout> {/* Use Layout wrapper for Dashboard too to keep Header */}
<Dashboard
onNewProject={handleNewProject}
onSelectProject={handleSelectProject}
/>
</Layout>
);
}
// ----------------------------------------------------------------------
// 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 (
<Layout>
<SEO
title={projectData ? `${projectData.strategy?.seoTitle} | Editor` : "New Project"}
description="AI-Powered Etsy Asset Generator"
/>
<ApiKeyModal isOpen={isApiKeyModalOpen} onClose={() => setIsApiKeyModalOpen(false)} />
{/* Hidden Process Guide Generator */}
<div className="hidden">
<ProcessGuideGenerator
ref={processGuideRef}
project={projectData?.project}
onGenerate={() => {/* maybe reload assets? */ }}
/>
</div>
{/* MAIN EDITOR UI */}
<main className="min-h-screen bg-stone-50 pb-20">
{/* TOOLBAR */}
<div className="z-30 bg-white/80 backdrop-blur-md border-b border-stone-200 px-6 py-4 flex justify-between items-center shadow-sm">
<div className="flex items-center gap-4">
<button onClick={handleNewProject} className="p-2 hover:bg-stone-100 rounded-full transition-colors" title="Gallery Vault">
<span className="text-xl"></span>
</button>
<div>
<h2 className="font-black text-lg text-stone-900 leading-none">
{projectData ? (projectData.strategy?.seoTitle || projectData.project?.niche || "Untitled Project").substring(0, 30) + (projectData.strategy?.seoTitle ? '...' : '') : 'New Project'}
</h2>
<span className="text-[10px] font-bold uppercase tracking-widest text-stone-400">{projectData ? 'Editing Mode' : 'Setup Phase'}</span>
</div>
</div>
<div className="flex items-center gap-3">
<div className="hidden md:flex flex-col items-end mr-4">
<span className="text-[10px] uppercase font-black tracking-widest text-stone-400">Project Budget</span>
<span className="text-xs font-bold font-mono text-stone-600">${projectData?.project?.totalCost?.toFixed(4) || '0.000'}</span>
</div>
<CreditButton cost={0} onClick={() => { }} disabled className="bg-stone-100 text-stone-400 cursor-default px-4">
{isLoading ? 'Processing...' : 'Ready'}
</CreditButton>
</div>
</div>
{!projectData ? (
// ----------------------------------------------------------------------
// SETUP WIZARD (New Project)
// ----------------------------------------------------------------------
<div className="max-w-4xl mx-auto mt-12 px-4">
<div className="bg-white rounded-[2rem] p-8 shadow-xl border border-stone-100 relative overflow-hidden">
{/* Background Decoration */}
<div className="absolute top-0 right-0 w-64 h-64 bg-gradient-to-br from-purple-50 to-blue-50 rounded-bl-[10rem] -z-10 opacity-50" />
<div className="mb-8 text-center">
<h1 className="text-3xl font-black text-stone-900 tracking-tight mb-2">New Creation</h1>
<p className="text-stone-500 text-sm font-medium">Define your vision. The AI will handle the strategy.</p>
</div>
<div className="space-y-6">
{/* Row 1: Niche */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-stone-400 mb-2 block">Product Niche</label>
<input
type="text"
value={niche}
onChange={(e) => 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"
/>
</div>
{/* Row 2: Parameters Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Product Type */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-stone-400 mb-2 block">Product Type</label>
<div className="flex flex-wrap gap-2">
{["Wall Art", "Sticker", "Planner", "Bookmark", "Label"].map((type) => (
<button
key={type}
onClick={() => setProductType(type as ProductType)}
className={`px-3 py-1.5 rounded-lg font-bold text-[10px] transition-all ${productType === type ? 'bg-stone-900 text-white shadow-md' : 'bg-stone-100 text-stone-500 hover:bg-stone-200'}`}
>
{type}
</button>
))}
</div>
</div>
{/* Sticker Set Configuration */}
{productType === "Sticker" && (
<div className="col-span-full bg-purple-50 p-4 rounded-xl border border-purple-100 flex items-center gap-6 animate-fade-in">
<div className="flex items-center gap-3">
<div className="w-10 h-6 bg-purple-200 rounded-full p-1 cursor-pointer transition-colors relative" onClick={() => setIsStickerSet(!isStickerSet)}>
<div className={`w-4 h-4 bg-purple-600 rounded-full shadow-sm transition-transform ${isStickerSet ? 'translate-x-4' : 'translate-x-0'}`} />
</div>
<label className="text-xs font-black uppercase tracking-widest text-purple-900 cursor-pointer" onClick={() => setIsStickerSet(!isStickerSet)}>
Generate Sticker Set
</label>
</div>
{isStickerSet && (
<div className="flex items-center gap-3">
<label className="text-[10px] font-black uppercase tracking-widest text-purple-400">Set Size</label>
<select
value={setSize}
onChange={(e) => setSetSize(e.target.value)}
className="bg-white border border-purple-200 text-purple-900 text-xs font-bold rounded-lg px-3 py-1.5 outline-none focus:ring-2 focus:ring-purple-400 cursor-pointer"
>
{[6, 9, 12, 16, 20, 25].map(n => <option key={n} value={n}>{n} Stickers</option>)}
</select>
</div>
)}
</div>
)}
{/* Aspect Ratio */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-stone-400 mb-2 block flex justify-between">
<span>Orientation / Ratio</span>
<span className="text-stone-900">{aspectRatio}</span>
</label>
<div className="flex items-center gap-2">
<select
value={selectedRatio}
onChange={(e) => {
const r = e.target.value as AspectRatio;
setSelectedRatio(r);
setAspectRatio(r);
}}
className="w-full bg-white border border-stone-200 text-stone-700 px-3 py-2 rounded-lg text-xs font-bold focus:ring-1 focus:ring-stone-300 cursor-pointer outline-none appearance-none"
>
{productType === "Sticker" ? (
// STICKER MODE: Only Paper Sizes
["A4", "A5", "Letter"].map(r => (
<option key={r} value={r}>{r} (Paper)</option>
))
) : (
// STANDARD MODE: All Ratios
["1:1", "3:4", "4:3", "2:3", "3:2", "9:16", "16:9", "4:5", "5:4", "A4", "Letter"].map(r => (
<option key={r} value={r}>{r} {['A4', 'Letter', 'A5'].includes(r) ? '(Paper)' : r === '1:1' ? '(Square)' : (parseInt(r.split(':')[0]) > parseInt(r.split(':')[1]) ? '(Landscape)' : '(Portrait)')}</option>
))
)}
</select>
{(() => {
// Handle Paper Sizes
if (aspectRatio === "A4" || aspectRatio === "A5") return <span className="text-xl grayscale opacity-50 px-1">📄</span>;
if (aspectRatio === "Letter") return <span className="text-xl grayscale opacity-50 px-1">📝</span>;
const parts = aspectRatio.split(':');
const w = parseInt(parts[0]);
const h = parseInt(parts[1]);
const icon = w > h ? '🖼️' : w < h ? '📱' : '⬜';
return <span className="text-xl grayscale opacity-50 px-1">{icon}</span>
})()}
</div>
</div>
{/* Creativity Level */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-stone-400 mb-2 block flex justify-between">
<span>Creative Freedom</span>
<span className="text-stone-900">{creativity}</span>
</label>
<input
type="range"
min="0" max="2" step="1"
value={creativity === "Conservative" ? 0 : creativity === "Balanced" ? 1 : 2}
onChange={(e) => 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"
/>
<div className="flex justify-between text-[8px] font-bold uppercase tracking-widest text-stone-300 mt-1">
<span>Strict</span>
<span>Balanced</span>
<span>Wild</span>
</div>
</div>
</div>
{/* Row 3: Quality & DNA */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 items-start">
{/* Quality Selection */}
<div className="bg-stone-50/50 rounded-2xl p-4 border border-stone-100 h-full">
<label className="text-[10px] font-black uppercase tracking-widest text-stone-400 mb-3 block">Base Quality (Draft Scale)</label>
<div className="grid grid-cols-3 gap-2">
{[
{ id: "SD", l: "Standard", desc: "Fast & Stable" },
{ id: "HD", l: "High Def", desc: "Detailed" },
{ id: "4K", l: "Ultra", desc: "Best Finish" }
].map((q) => (
<button
key={q.id}
onClick={() => setSelectedSize(q.id as ImageSize)}
className={`p-2 rounded-xl border-2 transition-all flex flex-col items-center gap-0.5 ${selectedSize === q.id ? 'border-stone-900 bg-white shadow-sm' : 'border-transparent bg-stone-100 hover:bg-stone-200'}`}
>
<span className={`text-[10px] font-black ${selectedSize === q.id ? 'text-stone-900' : 'text-stone-400'}`}>{q.id}</span>
<span className="text-[8px] font-bold text-stone-400 uppercase tracking-tighter">{q.l}</span>
</button>
))}
</div>
<p className="mt-3 text-[9px] text-stone-400 font-medium">Drafts are generated at these scales. Upscaling to 6000px+ is available in the next step.</p>
</div>
{/* Visual DNA */}
<div className="bg-stone-50/50 rounded-2xl p-4 border border-stone-100 h-full">
<div className="flex flex-col gap-4 mb-4">
<div className="flex items-center justify-between">
<label className="text-[10px] font-black uppercase tracking-widest text-stone-400 flex items-center gap-2">
<span>Visual DNA (References)</span>
<span className="bg-stone-200 text-stone-600 px-1.5 py-0.5 rounded text-[9px]">{referenceImages.length}</span>
</label>
{/* STYLE vs STRUCTURE TOGGLE */}
<button
onClick={() => setUseExactReference(!useExactReference)}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-wider transition-all border ${useExactReference
? 'bg-indigo-50 text-indigo-600 border-indigo-200 shadow-sm'
: 'bg-white text-stone-400 border-stone-200 hover:text-stone-600 hover:border-stone-300'
}`}
>
<span className="text-sm">{useExactReference ? '📐 Structure' : '🎨 Style'}</span>
<Tooltip content={useExactReference ? "Matches exact composition & shapes" : "Captures vibe, colors & textures"} position="bottom">
<span className="ml-1 opacity-50 cursor-help"></span>
</Tooltip>
</button>
</div>
<div className="flex flex-wrap items-center gap-2">
{/* DNA DROPDOWN */}
<select
value={activeDnaProfile?.id || ""}
onChange={(e) => {
if (e.target.value === "") {
setActiveDnaProfile(null);
setReferenceImages([]);
localStorage.removeItem('activeDnaProfileId');
} else {
loadDnaProfile(e.target.value);
}
}}
className="bg-white border border-stone-200 text-stone-600 text-[9px] font-bold rounded-lg px-2 py-1.5 outline-none focus:border-stone-400 cursor-pointer uppercase tracking-wide flex-1 min-w-[120px]"
>
<option value="">📂 {activeDnaProfile ? 'Unsaved / New' : 'Load DNA...'}</option>
{dnaProfiles.map((p: any) => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
{/* DNA ACTIONS (Visible if images exist) */}
{referenceImages.length > 0 && (
<div className="flex items-center gap-1.5">
{activeDnaProfile && (
<>
<button
onClick={async () => {
if (window.confirm(`⚠️ ATTENTION: You are about to overwrite '${activeDnaProfile.name}'. Proceed?`)) {
try {
await axios.post('/api/profiles', { name: activeDnaProfile.name, referenceImages, overwrite: true });
alert("Profile Updated!");
const r = await axios.get('/api/profiles');
setDnaProfiles(r.data.profiles);
} catch (e) { alert("Update failed"); }
}
}}
className="text-emerald-600 bg-emerald-50 border border-emerald-100 p-1.5 rounded-lg hover:bg-emerald-100 transition-colors shadow-sm"
title="Update Profile"
>
💾
</button>
<button
onClick={() => handleDeleteDnaProfile(activeDnaProfile.id)}
className="text-red-500 bg-red-50 border border-red-100 p-1.5 rounded-lg hover:bg-red-100 transition-colors shadow-sm"
title="Delete Profile"
>
🗑
</button>
</>
)}
<button
onClick={async () => {
const name = prompt(activeDnaProfile ? "Name this copy:" : "Name this DNA Profile:");
if (!name) return;
const existing = dnaProfiles.find((p: any) => p.name.toLowerCase() === name.toLowerCase());
if (existing && !window.confirm(`⚠️ COLLISION: A profile named '${name}' already exists. Overwrite?`)) return;
try {
await axios.post('/api/profiles', { name, referenceImages, overwrite: !!existing });
alert("Profile Saved!");
const r = await axios.get('/api/profiles');
setDnaProfiles(r.data.profiles);
const np = r.data.profiles.find((p: any) => p.name === name);
if (np) { setActiveDnaProfile(np); localStorage.setItem('activeDnaProfileId', np.id); }
} catch (e) { alert("Save failed"); }
}}
className={`text-[9px] font-black uppercase tracking-widest px-2.5 py-1.5 rounded-lg border shadow-sm transition-all ${activeDnaProfile ? 'bg-white text-indigo-600 border-indigo-200' : 'bg-indigo-600 text-white border-indigo-700'}`}
>
{activeDnaProfile ? 'Copy' : 'Save DNA'}
</button>
</div>
)}
</div>
</div>
<div className="flex gap-2 overflow-x-auto pb-2 scrollbar-hide">
{referenceImages.map((src, i) => (
<div key={i} className="relative aspect-square w-14 h-14 shrink-0 rounded-lg overflow-hidden group border border-stone-200">
<img src={src} className="w-full h-full object-cover" />
<button onClick={() => setReferenceImages(p => p.filter((_, idx) => idx !== i))} className="absolute inset-0 bg-black/50 text-white opacity-0 group-hover:opacity-100 flex items-center justify-center font-bold text-xs transition-opacity">×</button>
</div>
))}
<div className="relative aspect-square w-14 h-14 shrink-0 border-2 border-dashed border-stone-200 rounded-lg flex flex-col items-center justify-center hover:bg-white hover:border-purple-300 cursor-pointer overflow-hidden transition-all group bg-white">
<span className="text-lg text-stone-300 group-hover:text-purple-400 transition-colors">+</span>
<input id="wizard_dna_upload" type="file" multiple onChange={e => {
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" />
</div>
</div>
</div>
</div>
</div>
<div className="mt-8">
<CreditButton
cost={1} // DRAFT COST
onClick={handleCreateProject}
disabled={!niche}
className="w-full bg-stone-900 text-white py-4 rounded-xl text-sm font-black uppercase tracking-widest hover:scale-[1.01] transition-transform shadow-xl disabled:opacity-50"
>
{isLoading ? <Loader2 className="animate-spin w-4 h-4" /> : `Start Creation`}
</CreditButton>
</div>
</div>
</div>
) : (
// ----------------------------------------------------------------------
// PROJECT EDITOR
// ----------------------------------------------------------------------
<div className="max-w-[1600px] mx-auto p-6 md:p-12">
{isLoading && (
<div className="fixed inset-0 bg-white/80 backdrop-blur-sm z-50 flex items-center justify-center">
<Loader message={loadingMsg} />
</div>
)}
{/* CREATION SUMMARY PANEL (Blueprint) */}
<details className="mb-8 bg-white rounded-2xl shadow-sm border border-stone-200 overflow-hidden group">
<summary className="cursor-pointer px-6 py-4 flex items-center justify-between hover:bg-stone-50 transition-colors">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-stone-100 flex items-center justify-center text-stone-500 font-black">📋</div>
<h3 className="font-bold text-sm text-stone-900 uppercase tracking-wide">Project Blueprint</h3>
</div>
<span className="text-stone-300 text-xs group-open:rotate-180 transition-transform"></span>
</summary>
<div className="px-6 py-6 border-t border-stone-100 bg-stone-50/50 space-y-6">
{/* Prompt Niche */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-stone-400 block mb-2">Original Prompt / Niche</label>
<textarea
value={niche}
onChange={(e) => setNiche(e.target.value)}
className="w-full bg-white border border-stone-200 rounded-xl px-4 py-3 text-stone-800 font-bold text-xs focus:border-stone-900 focus:outline-none transition-all resize-none shadow-sm"
rows={2}
/>
</div>
{/* Parameters Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Product Type */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-stone-400 mb-2 block">Product Type</label>
<select
value={productType}
onChange={(e) => setProductType(e.target.value as ProductType)}
className="w-full bg-white border border-stone-200 text-stone-700 px-3 py-2 rounded-lg text-xs font-bold focus:ring-1 focus:ring-stone-300 cursor-pointer outline-none shadow-sm"
>
{["Wall Art", "Sticker", "Planner", "Bookmark", "Label"].map(t => <option key={t} value={t}>{t}</option>)}
</select>
</div>
{/* Sticker Set Configuration (Blueprint) */}
{productType === "Sticker" && (
<div className="bg-purple-50/50 p-3 rounded-xl border border-purple-100 flex flex-col justify-center gap-2">
<div className="flex items-center justify-between">
<label className="text-[10px] font-black uppercase tracking-widest text-purple-900/70">Sticker Set</label>
<div className={`w-8 h-4 rounded-full p-0.5 cursor-pointer transition-colors relative ${isStickerSet ? 'bg-purple-500' : 'bg-stone-300'}`} onClick={() => setIsStickerSet(!isStickerSet)}>
<div className={`w-3 h-3 bg-white rounded-full shadow-sm transition-transform ${isStickerSet ? 'translate-x-4' : 'translate-x-0'}`} />
</div>
</div>
{isStickerSet && (
<div className="flex items-center gap-2">
<select
value={setSize}
onChange={(e) => setSetSize(e.target.value)}
className="w-full bg-white border border-purple-200 text-purple-900 text-xs font-bold rounded-lg px-2 py-1.5 outline-none focus:ring-1 focus:ring-purple-400 cursor-pointer"
>
{[6, 9, 12, 16, 20, 25].map(n => <option key={n} value={n}>{n} Variations</option>)}
</select>
</div>
)}
</div>
)}
{/* Aspect Ratio */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-stone-400 mb-2 block flex justify-between">
<span>Orientation / Ratio</span>
<span className="text-stone-900">{aspectRatio}</span>
</label>
<div className="flex items-center gap-2">
<select
value={selectedRatio}
onChange={(e) => {
const r = e.target.value as AspectRatio;
setSelectedRatio(r);
setAspectRatio(r);
}}
className="w-full bg-white border border-stone-200 text-stone-700 px-3 py-2 rounded-lg text-xs font-bold focus:ring-1 focus:ring-stone-300 cursor-pointer outline-none appearance-none shadow-sm"
>
{productType === "Sticker" ? (
// STICKER MODE: Only Paper Sizes
["A4", "A5", "Letter"].map(r => (
<option key={r} value={r}>{r} (Paper)</option>
))
) : (
// STANDARD MODE: All Ratios
["1:1", "3:4", "4:3", "2:3", "3:2", "9:16", "16:9", "4:5", "5:4", "A4", "Letter"].map(r => (
<option key={r} value={r}>{r} {['A4', 'Letter', 'A5'].includes(r) ? '(Paper)' : r === '1:1' ? '(Square)' : (parseInt(r.split(':')[0]) > parseInt(r.split(':')[1]) ? '(Landscape)' : '(Portrait)')}</option>
))
)}
</select>
{(() => {
const parts = aspectRatio.split(':');
let w = parseFloat(parts[0]);
let h = parseFloat(parts[1]);
if (aspectRatio === 'A4' || aspectRatio === 'A5') { w = 1; h = 1.414; }
if (aspectRatio === 'Letter') { w = 8.5; h = 11; }
if (isNaN(w)) w = 1;
if (isNaN(h)) h = 1;
// Handle 2.35:1
if (aspectRatio === '2.35:1') { w = 2.35; h = 1; }
const ratio = w / h;
// Base height 24px
const baseH = 24;
const baseW = baseH * ratio;
return (
<div className="h-8 w-14 flex items-center justify-center bg-stone-100 rounded border border-stone-200 ml-2" title={`Preview: ${aspectRatio}`}>
<div
style={{
width: `${Math.min(baseW, 40)}px`,
height: `${Math.min(baseW, 40) / ratio}px`,
maxHeight: '24px',
maxWidth: '40px'
}}
className="bg-stone-800 rounded-sm shadow-sm transition-all duration-300"
/>
</div>
);
})()}
</div>
</div>
{/* Creativity */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-stone-400 mb-2 block flex justify-between">
<span>Creative Freedom</span>
<span className="text-stone-900">{creativity}</span>
</label>
<div className="flex flex-col gap-1.5 pt-1">
<input
type="range"
min="0" max="2" step="1"
value={creativity === "Conservative" ? 0 : creativity === "Balanced" ? 1 : 2}
onChange={(e) => 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"
/>
<div className="flex justify-between text-[8px] font-bold uppercase tracking-widest text-stone-300">
<span>Strict</span>
<span>Balanced</span>
<span>Wild</span>
</div>
</div>
</div>
</div>
{/* Quality & DNA */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 items-start">
{/* Quality */}
<div className="bg-white rounded-xl border border-stone-200 p-4 shadow-sm h-full">
<label className="text-[10px] font-black uppercase tracking-widest text-stone-400 mb-3 block">Draft Quality</label>
<div className="grid grid-cols-3 gap-2">
{["SD", "HD", "4K"].map((q) => (
<button
key={q}
onClick={() => setSelectedSize(q as ImageSize)}
className={`p-2 rounded-xl border-2 transition-all flex flex-col items-center gap-0.5 ${selectedSize === q ? 'border-stone-900 bg-stone-50' : 'border-transparent bg-stone-50/50 hover:bg-stone-100'}`}
>
<span className={`text-[10px] font-black ${selectedSize === q ? 'text-stone-900' : 'text-stone-400'}`}>{q}</span>
</button>
))}
</div>
</div>
{/* Visual DNA Management */}
<div className="bg-white rounded-xl border border-stone-200 p-4 shadow-sm h-full">
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<label className="text-[10px] font-black uppercase tracking-widest text-stone-400 flex items-center gap-2">
Visual DNA <span className="bg-stone-100 px-1.5 rounded text-[9px] font-bold">{referenceImages.length}</span>
</label>
<button
onClick={() => setUseExactReference(!useExactReference)}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-wider transition-all border ${useExactReference
? 'bg-indigo-50 text-indigo-600 border-indigo-200'
: 'bg-white text-stone-300 border-stone-100 hover:text-stone-500'
}`}
>
<span className="text-sm">{useExactReference ? '📐 Structure' : '🎨 Style'}</span>
</button>
</div>
<div className="flex flex-wrap items-center gap-2">
<select
value={activeDnaProfile?.id || ""}
onChange={(e) => e.target.value === "" ? (setActiveDnaProfile(null), setReferenceImages([])) : loadDnaProfile(e.target.value)}
className="bg-stone-50 border border-stone-200 text-stone-600 text-[10px] font-bold rounded-lg px-2 py-2 outline-none focus:border-stone-400 cursor-pointer uppercase flex-1 min-w-[150px]"
>
<option value="">📂 {activeDnaProfile ? 'Unsaved / New' : 'Load DNA...'}</option>
{dnaProfiles.map((p: any) => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
{referenceImages.length > 0 && (
<div className="flex items-center gap-1.5">
{activeDnaProfile && (
<>
<button onClick={async () => {
if (!window.confirm(`Update '${activeDnaProfile.name}'?`)) return;
await axios.post('/api/profiles', { name: activeDnaProfile.name, referenceImages, overwrite: true });
const r = await axios.get('/api/profiles'); setDnaProfiles(r.data.profiles);
}} className="text-emerald-600 bg-emerald-50 px-2.5 py-2 rounded-lg border border-emerald-100">💾</button>
<button onClick={() => handleDeleteDnaProfile(activeDnaProfile.id)} className="text-red-500 bg-red-50 px-2.5 py-2 rounded-lg border border-red-100">🗑</button>
</>
)}
<button onClick={async () => {
const name = prompt("Name this DNA Profile:");
if (!name) return;
await axios.post('/api/profiles', { name, referenceImages });
const r = await axios.get('/api/profiles'); setDnaProfiles(r.data.profiles);
}} className="bg-indigo-600 text-white px-4 py-2 rounded-lg font-black text-[10px] uppercase">{activeDnaProfile ? 'Copy' : 'Save DNA'}</button>
</div>
)}
</div>
<div className="flex gap-2 overflow-x-auto pb-1 scrollbar-hide">
{referenceImages.map((src, i) => (
<div key={i} className="relative w-12 h-12 shrink-0 group">
<img src={src} className="w-full h-full object-cover rounded-lg border border-stone-200" />
<button onClick={() => setReferenceImages(p => p.filter((_, idx) => idx !== i))} className="absolute -top-1 -right-1 bg-red-500 text-white w-4 h-4 rounded-full flex items-center justify-center text-[10px] opacity-0 group-hover:opacity-100">×</button>
</div>
))}
<div onClick={() => document.getElementById('wizard_dna_upload')?.click()} className="w-12 h-12 shrink-0 border border-dashed border-stone-300 rounded-lg flex items-center justify-center cursor-pointer hover:bg-stone-50 text-stone-300">
<span className="text-xl">+</span>
</div>
</div>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex justify-end gap-3 pt-4">
<button
onClick={() => window.confirm("Reroll with these Blueprint settings?") && handleCreateProject()}
disabled={isLoading}
className="bg-stone-900 text-white px-8 py-3 rounded-xl font-black uppercase tracking-widest text-[10px] hover:scale-105 transition-transform disabled:opacity-50 shadow-xl"
>
{isLoading ? 'Rerolling...' : '🚀 Reroll from Blueprint'}
</button>
</div>
</div>
</details>
<div className="grid grid-cols-1 xl:grid-cols-12 gap-8">
{/* LEFT COLUMN: Art Director Station */}
<div className="xl:col-span-6 space-y-8">
<section className="bg-white rounded-[3rem] p-8 shadow-2xl border border-stone-100 relative overflow-hidden">
<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-purple-500 via-pink-500 to-yellow-500" />
<div className="flex justify-between items-center mb-8">
<h3 className="font-black text-2xl tracking-tighter text-stone-900 flex items-center gap-2">
<span className="text-3xl">🎨</span> Art Director Station
</h3>
</div>
{/* ACTIVE MASTER CANVAS */}
{(() => {
// FALLBACK: Select any visible asset if no specific ID is active
const visibleAssets = projectData.assets?.filter((a: any) => ['master', 'upscaled', 'revision'].includes(a.type))
.sort((a: any, b: any) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) || [];
const activeAsset = projectData.assets?.find((a: any) => a.id === activeAssetId) || visibleAssets[0];
if (!activeAsset) return (
<div className="aspect-[3/4] bg-stone-900 rounded-[3rem] flex items-center justify-center border border-white/10">
<p className="text-white/20 font-black text-2xl uppercase tracking-widest">No Assets Found</p>
</div>
);
return (
<div className="relative group">
<div className="absolute top-6 left-6 z-10 flex flex-col gap-2">
{(() => {
const isUpscaled = activeAsset.type === 'upscaled';
const displayQuality = isUpscaled ? 'MASTER' : (activeAsset.quality || 'MASTER');
const isDraft = displayQuality === 'DRAFT';
return (
<span className={`px-4 py-1.5 rounded-full text-[10px] font-black uppercase tracking-widest backdrop-blur-md border shadow-2xl ${isDraft ? 'bg-amber-400/90 text-amber-900 border-amber-300' : 'bg-purple-500/90 text-white border-white/20'}`}>
{displayQuality} QUALITY
</span>
);
})()}
</div>
{/* DELETE BUTTON FOR MASTER/DRAFT */}
<button
type="button"
onMouseDown={(e) => {
e.stopPropagation();
e.preventDefault();
setIsDeleteDialogOpen(true);
}}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
// Delay confirm to next tick so ZoomableImage sees disabled=true
setTimeout(() => {
const confirmDelete = window.confirm(`Delete this ${activeAsset.type === 'upscaled' ? 'upscaled' : activeAsset.quality || 'master'} image?`);
setIsDeleteDialogOpen(false);
if (confirmDelete) {
handleDeleteAsset(activeAsset.id, activeAsset.type);
}
}, 10);
}}
className="absolute top-6 right-6 z-30 bg-red-500 hover:bg-red-600 text-white w-10 h-10 rounded-full flex items-center justify-center text-lg font-bold opacity-0 group-hover:opacity-100 transition-all shadow-xl hover:scale-110"
title="Delete this image"
>
🗑
</button>
{/* LEGACY ZIP DOWNLOADS (The "Real" 3 Links) */}
<ZoomableImage
src={`/storage/${activeAsset.path}`}
alt="Master Asset"
className={`w-full rounded-[3rem] shadow-2xl border-4 ${activeAsset.quality === 'DRAFT' ? 'border-amber-400/50' : 'border-purple-500/30'}`}
disabled={isDeleteDialogOpen}
/>
</div>
);
})()}
{/* VISUAL HISTORY STRIP (Moved) */}
{projectData.assets && projectData.assets.length > 0 && (
<div className="flex gap-4 overflow-x-auto pb-6 pt-2 snap-x mt-8 border-t border-stone-100 pt-8">
{projectData.assets.filter((a: any) => ['master', 'revision', 'upscaled'].includes(a.type)).sort((a: any, b: any) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()).map((asset: any) => (
<div key={asset.id} className="relative group/item flex-none w-24 h-32">
<button
onClick={() => setActiveAssetId(asset.id)}
className={`w-full h-full rounded-2xl overflow-hidden border-2 transition-all bg-stone-100 ${activeAssetId === asset.id ? 'border-purple-500 scale-105 shadow-lg' : 'border-stone-200 hover:border-stone-400'}`}
>
<img src={`/storage/${asset.path}`} className="w-full h-full object-contain" />
<div className={`absolute bottom-0 w-full text-[8px] font-black text-center py-1 uppercase ${asset.quality === 'MASTER' ? 'bg-purple-600 text-white' : 'bg-stone-800 text-white'}`}>
{asset.quality || 'MASTER'}
</div>
</button>
<button
type="button"
onMouseDown={(e) => {
e.stopPropagation();
e.preventDefault();
setTimeout(() => {
const confirmDelete = window.confirm('Delete this version?');
if (confirmDelete) {
handleDeleteAsset(asset.id, asset.type);
}
}, 0);
}}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
className="absolute -top-2 -right-2 bg-red-500 text-white w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold opacity-0 group-hover/item:opacity-100 transition-opacity shadow-lg scale-90 hover:scale-110 z-20"
title="Delete Version"
>
🗑
</button>
</div>
))}
</div>
)}
{/* REFINE CONTROLS */}
<div id="refine-section" className="mt-8 bg-stone-50 p-6 rounded-3xl border border-stone-100">
{/* Display Previous Prompt if available */}
{(() => {
const currentAsset = projectData?.assets?.find((a: any) => a.id === (activeAssetId || projectData.assets[0]?.id));
if (!currentAsset?.meta) return null;
try {
const meta = typeof currentAsset.meta === 'string' ? JSON.parse(currentAsset.meta) : currentAsset.meta;
if (meta.brief) {
return (
<div className="mb-4 bg-white p-4 rounded-2xl border border-purple-100 shadow-sm relative overflow-hidden group">
<div className="absolute top-0 left-0 w-1 h-full bg-purple-400 group-hover:h-full transition-all"></div>
<div className="text-[9px] font-black uppercase tracking-widest text-purple-400 mb-1 flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-purple-500 animate-pulse"></span>
Generated with Prompt
</div>
<p className="text-xs text-stone-700 font-medium italic leading-relaxed pl-2 border-l-2 border-transparent">
"{meta.brief}"
</p>
</div>
);
}
} catch (e) { return null; }
return null;
})()}
<label className="text-[10px] font-black uppercase tracking-widest text-stone-400 mb-4 block">Refine & iterate</label>
<div className="flex gap-2">
<input
type="text"
value={revisionBrief}
onChange={(e) => setRevisionBrief(e.target.value)}
placeholder="e.g., 'Make the colors warmer', 'Remove the tree'..."
className="flex-1 bg-white border border-stone-200 rounded-xl px-4 py-3 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-stone-900"
/>
<button
onClick={handleRefine}
disabled={!revisionBrief || isRefining}
className="bg-stone-900 text-white px-6 rounded-xl font-bold uppercase text-xs tracking-widest hover:bg-stone-800 disabled:opacity-50 flex items-center gap-2 group"
>
{isRefining ? (
<>
<Loader2 className="w-3 h-3 animate-spin" />
<span className="animate-pulse">Refining...</span>
</>
) : 'Refine'}
</button>
</div>
</div>
{/* NEURO-SCORE PANEL (Relocated) */}
<div className="mt-6 p-6 bg-stone-50 rounded-3xl border border-stone-100">
<div className="flex items-center justify-between mb-4">
<h4 className="font-black text-sm uppercase tracking-widest text-purple-900 flex items-center gap-2">
<BrainCircuit className="w-4 h-4 text-purple-600" />
Neuro-Score Analysis
</h4>
<button
onClick={() => {
const targetId = activeAssetId || projectData.assets?.[0]?.id;
if (targetId) handleNeuroAnalyze(targetId);
}}
disabled={isAnalyzing}
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white text-[10px] font-black uppercase rounded-xl transition-all shadow-lg hover:shadow-purple-200 disabled:opacity-50 flex items-center gap-2"
>
{isAnalyzing ? <Loader2 className="w-3 h-3 animate-spin" /> : <BrainCircuit className="w-3 h-3" />}
{neuroAnalysis ? 'Re-Analyze' : 'Analyze Visual DNA'}
</button>
</div>
{/* Inline Scorecard Container (Dark Theme Wrapper) */}
{(isAnalyzing || neuroAnalysis) && (
<div className="mt-4 bg-stone-900 rounded-3xl overflow-hidden shadow-inner border border-stone-800">
<NeuroScorecard
analysis={neuroAnalysis}
loading={isAnalyzing}
onApplyImprovement={(fix) => {
const modal = document.getElementById('neuro-modal-overlay');
if (modal) modal.classList.add('opacity-0', 'pointer-events-none');
handleApplyNeuroFix(fix);
}}
/>
</div>
)}
</div>
{/* UPSCALE MASTER BUTTON */}
<div className="mt-6 p-6 bg-gradient-to-r from-emerald-50 to-green-50 rounded-3xl border border-emerald-200">
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<div>
<h4 className="font-black text-sm uppercase tracking-widest text-emerald-800 mb-1">🎯 Finalize for Print</h4>
<p className="text-xs text-emerald-600">
{upscaledAsset
? `✅ Ready: ${upscaledAsset.printSize} @ 300 DPI`
: 'Upscale to 6000px @ 300 DPI'}
</p>
</div>
</div>
{/* Ratio Override Selector */}
<div className="flex items-center gap-4">
<div className="flex-1">
<label className="text-[10px] font-black uppercase tracking-widest text-emerald-700 mb-1 block">Target Ratio</label>
<select
value={upscaleRatioOverride}
onChange={(e) => setUpscaleRatioOverride(e.target.value)}
className="w-full bg-white border border-emerald-200 text-emerald-800 px-3 py-2 rounded-xl text-xs font-bold focus:ring-2 focus:ring-emerald-400 cursor-pointer outline-none"
disabled={!!upscaledAsset}
>
<option value="auto">📐 Auto (Project: {projectData?.project?.aspectRatio || '3:4'})</option>
<option value="9:16">9:16 3375×6000px (Phone Wallpaper)</option>
<option value="3:4">3:4 4500×6000px (Standard Portrait)</option>
<option value="4:5">4:5 4800×6000px (Instagram / 8x10)</option>
<option value="2:3">2:3 4000×6000px (4x6, 24x36)</option>
<option value="1:1">1:1 6000×6000px (Square)</option>
<option value="16:9">16:9 6000×3375px (Wide)</option>
<option value="A4">A4 (ISO) 4961×7016px (Print)</option>
<option value="Letter">Letter (US) 4636×6000px (Print)</option>
<option value="A5">A5 (ISO) 4243×6000px (Journal)</option>
</select>
</div>
<button
onClick={() => {
const visibleAssets = projectData.assets?.filter((a: any) => ['master', 'upscaled', 'revision'].includes(a.type))
.sort((a: any, b: any) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) || [];
let rawId = activeAssetId || visibleAssets[0]?.id;
const targetId = rawId ? rawId.replace('lightbox_', '') : null;
if (!targetId) return alert("Please select an image to upscale.");
setIsUpscaling(true);
const overrideRatio = upscaleRatioOverride === 'auto' ? undefined : upscaleRatioOverride;
axios.post(`/api/projects/${projectData.project.id}/upscale`, {
assetId: targetId,
overrideRatio: overrideRatio
})
.then(() => handleSelectProject(projectData.project.id))
.catch(e => {
console.error("[Client] Upscale Error:", e);
alert("Upscale failed: " + (e.response?.data?.error || e.message));
})
.finally(() => setIsUpscaling(false));
}}
disabled={isUpscaling || !!upscaledAsset}
className="bg-emerald-600 text-white px-8 py-4 rounded-2xl font-black uppercase tracking-widest hover:bg-emerald-500 transition-all disabled:opacity-50 shadow-lg hover:shadow-emerald-300"
>
{isUpscaling ? '⏳ Upscaling...' : upscaledAsset ? '✅ Done' : '🚀 Upscale'}
</button>
</div>
</div>
{upscaledAsset && (
<div className="flex flex-wrap gap-2 mt-4">
<a
href={`/storage/${upscaledAsset.path}`}
download
className="inline-block bg-emerald-100 text-emerald-700 px-6 py-3 rounded-xl text-xs font-bold hover:bg-emerald-200 transition-all border border-emerald-200"
>
Download Print-Ready Master ({upscaledAsset.width}×{upscaledAsset.height}px)
</a>
<button
onClick={() => handleDownloadCricut(upscaledAsset.id)}
className="inline-block bg-emerald-900 text-emerald-100 px-6 py-3 rounded-xl text-xs font-bold hover:bg-emerald-800 transition-all border border-emerald-700 shadow-md flex items-center gap-2"
>
<span></span> Download Cricut Pack (ZIP)
</button>
</div>
)}
</div>
</section>
{/* VARIANTS GRID */}
<section className="bg-white p-8 rounded-[3rem] shadow-sm border border-stone-100 mt-8">
<div className="flex justify-between items-center mb-8">
<h3 className="font-black text-2xl tracking-tighter text-stone-900 flex items-center gap-2">
<span className="text-3xl">🧩</span> Variants & Explorations
</h3>
<div className="flex items-center gap-3">
{/* Variant Fill Selector */}
<select
value={variantFillType}
onChange={(e) => setVariantFillType(e.target.value)}
className="bg-stone-50 border border-stone-200 text-stone-600 text-[10px] font-bold uppercase tracking-widest px-3 py-2 rounded-xl cursor-pointer outline-none focus:ring-2 focus:ring-purple-400"
disabled={isGeneratingVariants}
title="Fallback fill color if AI Outpainting fails"
>
<option value="auto">🔮 AI Seamless / Auto</option>
<option value="white"> White</option>
<option value="black"> Black</option>
<option value="blur">💧 Blur</option>
</select>
{/* Generate Variants Button - HIDDEN for Sticker Sets */}
{(!projectData?.project?.config || !JSON.parse(projectData.project.config).isStickerSet) && (
<button
onClick={handleRegenerateVariants}
className="bg-stone-900 hover:bg-stone-800 text-white px-5 py-2.5 rounded-full text-xs font-black uppercase tracking-widest transition-colors flex items-center gap-2 shadow-lg shadow-purple-200/50"
disabled={isGeneratingVariants}
>
{isGeneratingVariants ? <Loader2 className="w-3 h-3 animate-spin" /> : (variants.length > 0 ? '↻ New Set' : 'Generate Variants')}
</button>
)}
{/* Sticker Sheet Button */}
{projectData?.project?.config && JSON.parse(projectData.project.config).isStickerSet && (
<button
onClick={handleGenerateStickerSheet}
className="bg-emerald-600 hover:bg-emerald-700 text-white px-5 py-2.5 rounded-full text-xs font-black uppercase tracking-widest transition-colors flex items-center gap-2 shadow-lg shadow-emerald-200/50"
disabled={isLoading}
>
{isLoading && loadingMsg.includes('Compositing') ? <Loader2 className="w-3 h-3 animate-spin" /> : '📄 Generate Sheet'}
</button>
)}
</div>
</div>
{variants.length === 0 && !isGeneratingVariants ? (
<div className="flex flex-col items-center justify-center py-12 border-2 border-dashed border-stone-200 rounded-3xl text-stone-400">
<span className="text-4xl mb-4"></span>
<p className="font-bold uppercase tracking-widest text-xs">No variants generated yet</p>
<button
onClick={handleRegenerateVariants}
className="mt-4 text-purple-600 underline text-xs font-black uppercase tracking-widest"
>
Generate Now
</button>
</div>
) : (
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{variants.map((v: any, i) => {
const meta = v.meta ? JSON.parse(v.meta) : {};
const label = v.label || meta.label || `Variant ${i + 1}`;
const ratio = v.ratio || meta.ratio || 'Unknown';
// LIGHTBOX STATE
const isExpanded = activeAssetId === `lightbox_${v.id}`;
return (
<div
key={v.id || i}
className={`relative group cursor-pointer w-full bg-stone-50 rounded-2xl overflow-hidden border border-stone-100 shadow-sm`}
onClick={() => setActiveAssetId(isExpanded ? null : `lightbox_${v.id}`)}
>
{/* NORMAL CARD */}
<div className={`relative w-full aspect-[3/4] flex items-center justify-center`}>
<img
src={`/storage/${v.path}?t=${new Date().getTime()}`}
className="w-full h-full object-contain"
/>
<div className="absolute bottom-0 left-0 right-0 bg-stone-900/60 p-2 backdrop-blur-sm flex justify-between items-center">
<span className="text-[10px] font-black text-white uppercase tracking-wider">{label}</span>
<span className="text-[9px] font-mono text-stone-300">{ratio}</span>
</div>
{/* HOVER ACTIONS */}
<div className="absolute inset-0 bg-stone-900/90 p-4 flex flex-col justify-center items-center opacity-0 group-hover:opacity-100 transition-opacity text-center gap-2"
onMouseEnter={() => handleVariantHover(v.id)}
>
{variantMetadata[v.id] ? (
<div className="text-[10px] text-stone-300 font-mono mb-2 flex flex-col gap-1">
<span className="text-white font-black">{variantMetadata[v.id].width} x {variantMetadata[v.id].height} px</span>
<span className="uppercase text-amber-400">{variantMetadata[v.id].space}</span>
<span>{(variantMetadata[v.id].size / 1024 / 1024).toFixed(2)} MB</span>
</div>
) : (
<span className="text-[10px] text-stone-500 animate-pulse">Inspecting...</span>
)}
<span className="text-xl mb-1">🔍</span>
<span className="text-[9px] text-white/50 uppercase tracking-widest">Zoom</span>
<div className="flex gap-2 mt-2">
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteAsset(v.id, 'variant');
setVariants(prev => prev.filter(item => item.id !== v.id));
}}
className="p-2 bg-red-500/20 hover:bg-red-500 text-red-200 hover:text-white rounded-full transition-colors"
title="Delete Variant"
>
<span className="text-xs">🗑</span>
</button>
</div>
</div>
</div>
{/* LIGHTBOX OVERLAY */}
{isExpanded && (
<div
className="fixed inset-0 z-[9999] bg-stone-900/95 backdrop-blur-md flex items-center justify-center p-8 cursor-zoom-out"
onClick={(e) => {
e.stopPropagation();
setActiveAssetId(null);
}}
>
<img
src={`/storage/${v.path}?t=${new Date().getTime()}`}
className="max-w-full max-h-full rounded-2xl shadow-2xl animate-in fade-in zoom-in duration-200"
/>
<div className="absolute bottom-10 left-1/2 -translate-x-1/2 flex gap-4">
<a
href={`/storage/${v.path}`}
download
onClick={(e) => e.stopPropagation()}
className="bg-white text-stone-900 px-6 py-3 rounded-full font-black uppercase tracking-widest hover:scale-105 transition-transform"
>
Download Original
</a>
<button
onClick={(e) => {
e.stopPropagation();
alert(`Regenerating ${label}...`);
}}
className="bg-purple-600 text-white px-6 py-3 rounded-full font-black uppercase tracking-widest hover:scale-105 transition-transform"
>
Regenerate This Variant
</button>
</div>
</div>
)}
</div>
);
})}
</div>
)}
</section>
{/* MOCKUP STUDIO SECTION */}
<section className="bg-stone-900 rounded-[3rem] p-8 shadow-2xl relative overflow-hidden text-white">
<div className="relative z-10">
{/* Mockup Controls Toolbar */}
{/* Mockup Controls Toolbar - ELEGANT & COMPACT */}
<div className="bg-white/5 backdrop-blur-xl p-5 rounded-2xl border border-white/10 shadow-xl relative overflow-hidden mb-6">
<div className="absolute inset-0 bg-gradient-to-br from-indigo-500/5 via-transparent to-purple-500/5 pointer-events-none" />
<div className="relative z-10 flex flex-col lg:flex-row items-end gap-4">
{/* Selectors Grid - TWO COLUMN STACKED */}
<div className="flex-1 grid grid-cols-1 sm:grid-cols-2 gap-6">
{/* Column 1: Scenario & Atmosphere */}
<div className="flex flex-col gap-4">
{/* Scenario */}
<div className="space-y-1.5">
<label className="text-[10px] font-bold uppercase tracking-[0.15em] text-white/40 block px-0.5">Scenario</label>
<select
value={mockupScenario}
onChange={(e) => setMockupScenario(e.target.value)}
className="w-full bg-stone-950/40 border border-white/5 text-white text-[11px] font-medium rounded-xl px-3 py-3 outline-none focus:ring-1 focus:ring-indigo-500/50 transition-all cursor-pointer appearance-none hover:bg-stone-900/60"
>
{(() => {
let pType = projectData?.project?.productType || "Wall Art";
if (pType.includes("Planner")) pType = "Planner";
else if (pType.includes("Sticker")) pType = "Sticker";
else if (pType.includes("Social")) pType = "Social Media Kit";
else if (pType.includes("Phone")) pType = "Phone Wallpaper";
else if (pType.includes("Bookmark")) pType = "Bookmark";
else if (pType.includes("Label")) pType = "Label";
else pType = "Wall Art";
// Merge with Custom
const scenarios = {
...(CLIENT_MOCKUP_SCENARIOS[pType] || CLIENT_MOCKUP_SCENARIOS["Wall Art"]),
...CLIENT_MOCKUP_SCENARIOS["Custom"]
};
return Object.entries(scenarios).map(([key, label]) => (
<option key={key} value={key} className="bg-stone-900 text-white">{label}</option>
));
})()}
</select>
</div>
{/* CUSTOM PROMPT INPUT */}
{mockupScenario === 'custom' && (
<div className="animate-fade-in mt-2">
<input
type="text"
value={customMockupPrompt}
onChange={(e) => setCustomMockupPrompt(e.target.value)}
placeholder="Describe your scene (e.g., 'Sticker on a red skateboard')..."
className="w-full bg-stone-950/40 border border-indigo-500/50 text-white text-[10px] font-medium rounded-xl px-3 py-2 outline-none focus:ring-2 focus:ring-indigo-500 transition-all placeholder:text-white/20"
/>
</div>
)}
{/* Atmosphere */}
<div className="space-y-1.5">
<label className="text-[10px] font-bold uppercase tracking-[0.15em] text-white/40 block px-0.5">Atmosphere</label>
<select
value={mockupAtmosphere}
onChange={(e) => setMockupAtmosphere(e.target.value)}
className="w-full bg-stone-950/40 border border-white/5 text-white text-[11px] font-medium rounded-xl px-3 py-3 outline-none focus:ring-1 focus:ring-indigo-500/50 transition-all cursor-pointer appearance-none hover:bg-stone-900/60"
>
{Object.entries(CLIENT_ATMOSPHERES).map(([key, label]) => (
<option key={key} value={key} className="bg-stone-900 text-white">{label}</option>
))}
</select>
</div>
</div>
{/* Column 2: Ratio & Watermark */}
<div className="flex flex-col gap-4">
{/* Ratio */}
<div className="space-y-1.5">
<label className="text-[10px] font-bold uppercase tracking-[0.15em] text-white/40 block px-0.5">Ratio</label>
<select
value={mockupRatio}
onChange={(e) => setMockupRatio(e.target.value)}
className="w-full bg-stone-950/40 border border-white/5 text-white text-[11px] font-medium rounded-xl px-3 py-3 outline-none focus:ring-1 focus:ring-indigo-500/50 transition-all cursor-pointer appearance-none hover:bg-stone-900/60"
>
{CLIENT_ASPECT_RATIOS.map((r) => (
<option key={r} value={r} className="bg-stone-900 text-white">{r}</option>
))}
</select>
</div>
{/* Watermark Toggle */}
<div className="space-y-1.5">
<label className="text-[10px] font-bold uppercase tracking-[0.15em] text-white/40 block px-0.5">Watermark</label>
<button
onClick={() => {
// Check if user has a logo uploaded
if (!useWatermark && !user?.etsyShopLogo) {
alert('⚠️ Logo Required!\n\nTo use watermarks, please upload your logo first in Settings > Store Branding.');
return;
}
setUseWatermark(!useWatermark);
}}
className={`w-full flex items-center justify-between px-3 py-3 rounded-xl border transition-all ${useWatermark
? 'bg-indigo-500/20 border-indigo-500/50 text-indigo-200'
: 'bg-stone-950/40 border-white/5 text-white/30 hover:bg-stone-900/60'}`}
>
<span className="text-[10px] font-bold uppercase">{useWatermark ? 'Enabled' : 'Disabled'}</span>
<div className={`w-6 h-3 rounded-full relative transition-colors ${useWatermark ? 'bg-indigo-500' : 'bg-stone-700'}`}>
<div className={`absolute top-0.5 w-2 h-2 rounded-full bg-white transition-transform ${useWatermark ? 'left-[14px]' : 'left-0.5'}`} />
</div>
</button>
{!user?.etsyShopLogo && (
<p className="text-[9px] text-amber-400/70 px-1">
No logo uploaded. Go to <a href="/settings" className="underline hover:text-amber-300">Settings</a>.
</p>
)}
</div>
</div>
</div>
{/* Actions Station */}
<div className="flex flex-col gap-2 min-w-[120px]">
<CreditButton
cost={1}
onClick={handleGenerateSingleMockup}
disabled={isGeneratingMockups}
className="w-full bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 text-white px-5 py-3 rounded-xl text-[10px] font-black uppercase tracking-widest shadow-lg transition-all disabled:opacity-50 flex items-center justify-center gap-2"
>
{isGeneratingMockups ? <Loader2 className="w-3 h-3 animate-spin" /> : 'Create'}
</CreditButton>
<button
onClick={handleGenerateMockups}
disabled={isGeneratingMockups}
className="w-full px-4 py-2 rounded-xl bg-white/5 hover:bg-white/10 text-white/40 hover:text-white text-[10px] font-black uppercase tracking-widest border border-white/5 transition-all disabled:opacity-50"
>
Batch
</button>
</div>
</div>
</div>
{/* Mockups Grid - COMPACT & REFINED */}
{mockups.length > 0 ? (
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-4">
{mockups.map((m, i) => (
<div key={m.id || i} className="relative group rounded-2xl overflow-hidden border border-white/10 bg-stone-950 aspect-[3/4] shadow-lg transition-transform hover:scale-[1.02]">
{/* NEURO-ANALYZE BUTTON (New) */}
<div className="absolute top-6 left-1/2 -translate-x-1/2 z-30">
<button
onClick={(e) => {
e.stopPropagation();
if (activeAssetId) handleNeuroAnalyze(activeAssetId);
}}
className="flex items-center gap-2 bg-stone-900/90 hover:bg-black text-white px-4 py-1.5 rounded-full backdrop-blur-md border border-white/20 shadow-2xl transition-all hover:scale-105 group/neuro"
>
<BrainCircuit className="w-4 h-4 text-purple-400 group-hover/neuro:animate-pulse" />
<span className="text-[10px] font-black uppercase tracking-widest">Neuro-Score</span>
</button>
</div>
<ZoomableImage
src={`/storage/${m.path}`}
alt={m.scenario}
className="w-full h-full object-cover"
>
{/* Hover Controls */}
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex flex-col justify-between p-3">
<div className="flex justify-end gap-1.5">
<button
onClick={(e) => {
e.stopPropagation();
setMockupScenario(m.scenario);
if (m.aspectRatio) setMockupRatio(m.aspectRatio);
handleGenerateSingleMockup();
}}
disabled={isGeneratingMockups}
className="p-2 rounded-lg bg-white/10 hover:bg-indigo-500 text-white transition-colors"
>
<RefreshCw className="w-3.5 h-3.5" />
</button>
<button
onClick={(e) => { e.stopPropagation(); handleDeleteAsset((m as any).id, 'mockup'); }}
className="p-2 rounded-lg bg-white/10 hover:bg-red-500 text-white transition-colors"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
<div className="text-[10px] font-bold uppercase tracking-widest text-white/70 truncate">
{m.scenario.replace(/_/g, ' ')}
</div>
</div>
</ZoomableImage>
</div>
))}
</div>
) : (
<div className="flex flex-col items-center justify-center py-20 border-2 border-dashed border-white/5 rounded-[2rem]">
<p className="text-white/40 font-bold uppercase tracking-[0.2em] text-[10px]">No Mockups Generated Yet</p>
<p className="text-white/20 text-[9px] mt-2 uppercase tracking-widest">Select your settings above and click "Create"</p>
</div>
)}
{/* Video Gallery - HORIZONTAL SCROLL */}
{videos.length > 0 && (
<div className="mt-12 pt-12 border-t border-white/5 overflow-x-auto custom-scrollbar">
<div className="flex gap-4 pb-4">
{videos.map((v, i) => (
<div key={v.id || i} className="relative group inline-block min-w-[240px] rounded-2xl overflow-hidden border border-white/10 bg-stone-950 aspect-video shadow-lg">
{v.simulated ? (
<div className="w-full h-full relative">
<img src={`/storage/${v.path}`} alt="Video Preview" className="w-full h-full object-cover opacity-50" />
<div className="absolute inset-0 flex items-center justify-center">
<span className="px-3 py-1 bg-red-500 text-white text-[8px] font-black uppercase tracking-widest rounded-full">Simulated</span>
</div>
</div>
) : (
<video src={`/storage/${v.path}`} controls className="w-full h-full object-cover" />
)}
<button
onClick={(e) => { e.stopPropagation(); handleDeleteAsset((v as any).id, 'video'); setVideos(prev => prev.filter(vid => vid.id !== (v as any).id)); }}
className="absolute top-2 right-2 p-2 rounded-lg bg-black/60 opacity-0 group-hover:opacity-100 hover:bg-red-500 text-white transition-all"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
))}
</div>
</div>
)}
{/* VEO VIDEO COMPONENT */}
{/* <VideoGenerator
project={projectData.project}
onVideoGenerated={() => handleSelectProject(projectData.project.id)}
/> */}
</div>
</section>
</div>
{/* RIGHT COLUMN: DATA */}
<div className="xl:col-span-6 space-y-20">
<div className="flex items-center justify-between px-8">
<div>
<h3 className="text-sm font-black uppercase tracking-[0.3em] text-stone-900 flex items-center gap-2">
<span className="w-1.5 h-6 bg-indigo-500 rounded-full"></span>
Listing Strategy
</h3>
<p className="text-[10px] text-stone-400 font-bold uppercase tracking-widest mt-1">Etsy Metadata & Branding</p>
</div>
<button
onClick={async () => {
if (!confirm("This will overwrite your current Title, Description, and Tags with a fresh analysis based on your LATEST profile settings and branding. Continue?")) return;
try {
setIsRegeneratingStrategy(true);
const token = localStorage.getItem('token');
const pId = projectData?.project?.id || projectData?.id;
const res = await axios.post(`/api/projects/${pId}/regenerate-strategy`, {}, {
headers: { Authorization: `Bearer ${token}` }
});
if (res.data.success) {
// Update UI immediately
setProjectData((prev: any) => ({
...prev,
strategy: {
...prev.strategy,
seoTitle: res.data.strategy.seoTitle,
description: res.data.strategy.description,
keywords: res.data.strategy.keywords,
attributes: res.data.strategy.attributes,
categorySuggestion: res.data.strategy.categorySuggestion,
jsonLd: res.data.strategy.jsonLd
}
}));
alert("Listing info regenerated successfully!");
}
} catch (err: any) {
console.error("Failed to regenerate strategy", err);
alert("Error: " + (err.response?.data?.error || err.message));
} finally {
setIsRegeneratingStrategy(false);
}
}}
disabled={isRegeneratingStrategy}
className="px-6 py-3 bg-stone-900 hover:bg-black text-white rounded-2xl text-[10px] font-black uppercase tracking-widest transition-all shadow-sm hover:shadow-md active:scale-95 disabled:bg-stone-300 flex items-center gap-2"
>
{isRegeneratingStrategy ? '⏳ Refreshing...' : '🔄 Refresh Listing Info'}
</button>
</div>
<section className="bg-white p-16 rounded-[5rem] border border-stone-100 shadow-sm space-y-16">
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<label className="text-[10px] font-black uppercase tracking-[0.5em] text-stone-400">SEO Title</label>
<button
onClick={() => {
navigator.clipboard.writeText(projectData.strategy?.seoTitle || "");
alert("Title copied!");
}}
className="text-[10px] font-bold text-indigo-600 hover:text-indigo-700 uppercase tracking-widest bg-indigo-50 px-3 py-1.5 rounded-lg border border-indigo-100 transition-all hover:scale-105 active:scale-95"
>
📋 Copy Title
</button>
</div>
<p className="text-4xl font-black text-stone-900 leading-tight tracking-tight">{projectData.strategy?.seoTitle}</p>
</div>
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<label className="text-[10px] font-black uppercase tracking-[0.5em] text-stone-400">Neuro-Marketing Description</label>
<button
onClick={() => {
navigator.clipboard.writeText(projectData.strategy?.description || "");
alert("Description copied!");
}}
className="text-[10px] font-bold text-indigo-600 hover:text-indigo-700 uppercase tracking-widest bg-indigo-50 px-3 py-1.5 rounded-lg border border-indigo-100 transition-all hover:scale-105 active:scale-95"
>
📋 Copy Desc
</button>
</div>
<p className="text-stone-600 font-medium leading-relaxed whitespace-pre-wrap">{projectData.strategy?.description}</p>
</div>
{/* 13 GOLDEN TAGS SECTION */}
<div className="flex flex-col gap-6 pt-16 border-t border-stone-100">
<div className="flex items-center justify-between">
<div>
<label className="text-[10px] font-black uppercase tracking-[0.5em] text-stone-400">13 Golden Tags (Market Ready)</label>
<p className="text-[9px] text-stone-400 mt-1">Exact 13 tags Under 20 chars Long-tail phrases</p>
</div>
<button
onClick={() => {
const tags = projectData.strategy?.keywords
? (Array.isArray(projectData.strategy.keywords)
? projectData.strategy.keywords.map((k: string) => k.replace(/^#/, "").trim()).join(", ") + ","
: projectData.strategy.keywords.replace(/^#/, ""))
: "";
navigator.clipboard.writeText(tags);
alert("Tags copied!");
}}
className="text-[10px] font-bold text-emerald-600 hover:text-emerald-700 uppercase tracking-widest bg-emerald-50 px-3 py-1.5 rounded-lg border border-emerald-100 transition-all hover:scale-105 active:scale-95"
>
📋 Copy All Tags
</button>
</div>
<div className="flex flex-wrap gap-2">
{(projectData.strategy?.keywords || []).map((tag: string, idx: number) => (
<span key={idx} className="bg-stone-50 border border-stone-200 text-stone-600 px-3 py-1.5 rounded-xl text-[11px] font-bold hover:border-stone-400 transition-colors">
{tag.trim()}
</span>
))}
{(!projectData.strategy?.keywords || projectData.strategy.keywords.length === 0) && (
<p className="text-stone-400 text-xs italic">No tags generated yet</p>
)}
</div>
</div>
{/* ATTRIBUTES & CATEGORY INTELLIGENCE */}
<div className="pt-16 border-t border-stone-100 grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Category */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<label className="text-[10px] font-black uppercase tracking-[0.5em] text-stone-400">Category Path</label>
<button onClick={() => { navigator.clipboard.writeText(projectData.strategy?.categorySuggestion || ""); alert("Category copied!"); }} className="text-stone-400 hover:text-stone-600">📋</button>
</div>
<div className="bg-stone-50 border border-stone-200 rounded-xl p-4">
<p className="text-stone-700 text-xs font-bold font-mono">{projectData.strategy?.categorySuggestion || "Not yet generated"}</p>
</div>
</div>
{/* Hidden Attributes */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<label className="text-[10px] font-black uppercase tracking-[0.5em] text-stone-400">Hidden Attributes</label>
<button onClick={() => {
const attrs = projectData.strategy?.attributes;
const text = attrs ? `Primary Color: ${attrs.primaryColor}\nSecondary Color: ${attrs.secondaryColor}\nHoliday: ${attrs.date}\nOccasion: ${attrs.occasion}\nRoom: ${attrs.room}` : "";
navigator.clipboard.writeText(text);
alert("Attributes copied!");
}} className="text-stone-400 hover:text-stone-600">📋</button>
</div>
<div className="bg-stone-50 border border-stone-200 rounded-xl p-4 space-y-2">
{projectData.strategy?.attributes ? (
<>
<div className="flex justify-between text-[10px]"><span className="text-stone-400 font-bold uppercase">Color 1</span> <span className="font-bold text-stone-700">{projectData.strategy.attributes.primaryColor || "-"}</span></div>
<div className="flex justify-between text-[10px]"><span className="text-stone-400 font-bold uppercase">Color 2</span> <span className="font-bold text-stone-700">{projectData.strategy.attributes.secondaryColor || "-"}</span></div>
<div className="flex justify-between text-[10px]"><span className="text-stone-400 font-bold uppercase">Holiday</span> <span className="font-bold text-stone-700">{projectData.strategy.attributes.date || "-"}</span></div>
<div className="flex justify-between text-[10px]"><span className="text-stone-400 font-bold uppercase">Occasion</span> <span className="font-bold text-stone-700">{projectData.strategy.attributes.occasion || "-"}</span></div>
<div className="flex justify-between text-[10px]"><span className="text-stone-400 font-bold uppercase">Room</span> <span className="font-bold text-stone-700">{projectData.strategy.attributes.room || "-"}</span></div>
</>
) : (
<p className="text-stone-400 text-xs italic">No attributes generated yet</p>
)}
</div>
</div>
</div>
<div className="pt-16 border-t border-stone-50">
</div>
{/* DOWNLOAD STATION */}
<div className="pt-16 border-t border-stone-50">
<label className="text-[10px] font-black uppercase tracking-[0.5em] text-stone-400 block mb-8">Production Assets</label>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* 1. MASTER ZIP */}
<button
onClick={async (e) => {
e.preventDefault();
try {
const token = localStorage.getItem('token');
const pId = projectData.project?.id || projectData.id;
const res = await fetch(`/api/projects/${pId}/download/master-zip`, {
headers: { Authorization: `Bearer ${token}` }
});
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `MasterBundle_${projectData.project?.productType || 'Project'}_${pId.substring(0, 6)}.zip`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
a.remove();
} catch (err: any) {
alert("Download Error: " + err.message);
}
}}
className="flex flex-col items-center justify-center p-6 bg-stone-900 rounded-3xl hover:bg-stone-800 transition-all group shadow-xl cursor-pointer"
title="Download Everything (Source Files)"
>
<span className="text-3xl group-hover:scale-110 transition-transform mb-3">📦</span>
<span className="text-[10px] font-black uppercase tracking-widest text-white">Master Bundle</span>
<span className="text-[9px] text-stone-500 mt-1">Source + Mockups</span>
</button>
{/* 2. CUSTOMER RGB */}
<button
onClick={async (e) => {
e.preventDefault();
try {
const token = localStorage.getItem('token');
const pId = projectData.project?.id || projectData.id;
const res = await fetch(`/api/projects/${pId}/download/customer-rgb-zip`, {
headers: { Authorization: `Bearer ${token}` }
});
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `CustomerDownload_RGB_${pId.substring(0, 6)}.zip`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
a.remove();
} catch (err: any) {
alert("Download Error: " + err.message);
}
}}
className="flex flex-col items-center justify-center p-6 bg-white border border-stone-200 rounded-3xl hover:border-blue-300 hover:bg-blue-50 transition-all group cursor-pointer"
title="Customer Delivery (Digital)"
>
<span className="text-3xl group-hover:scale-110 transition-transform mb-3">🎨</span>
<span className="text-[10px] font-black uppercase tracking-widest text-stone-900">Digital (RGB)</span>
<span className="text-[9px] text-stone-400 mt-1">For Web & Screens</span>
</button>
{/* 3. CUSTOMER CMYK */}
<button
onClick={async (e) => {
e.preventDefault();
try {
const token = localStorage.getItem('token');
const pId = projectData.project?.id || projectData.id;
const res = await fetch(`/api/projects/${pId}/download/customer-cmyk-zip`, {
headers: { Authorization: `Bearer ${token}` }
});
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `CustomerDownload_CMYK_${pId.substring(0, 6)}.zip`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
a.remove();
} catch (err: any) {
alert("Download Error: " + err.message);
}
}}
className="flex flex-col items-center justify-center p-6 bg-white border border-stone-200 rounded-3xl hover:border-cyan-300 hover:bg-cyan-50 transition-all group cursor-pointer"
title="Customer Delivery (Print Ready)"
>
<span className="text-3xl group-hover:scale-110 transition-transform mb-3">🖨</span>
<span className="text-[10px] font-black uppercase tracking-widest text-stone-900">Print (CMYK)</span>
<span className="text-[9px] text-stone-400 mt-1">High-Res Production</span>
</button>
</div>
</div>
</section>
</div>
</div >
</div >
)
}
</main >
{/* NEURO-SCORECARD MODAL */}
{
showNeuroModal && (
<div className="fixed inset-0 z-[60] flex items-center justify-center p-4">
<div className="absolute inset-0 bg-stone-900/80 backdrop-blur-sm" onClick={() => setShowNeuroModal(false)} />
<div className="relative w-full max-w-4xl max-h-[90vh] overflow-y-auto">
<button
onClick={() => setShowNeuroModal(false)}
className="absolute -top-12 right-0 text-white hover:text-stone-300 transition-colors"
>
<X className="w-8 h-8" />
</button>
<NeuroScorecard
analysis={neuroAnalysis}
loading={isAnalyzing}
onApplyImprovement={handleApplyNeuroFix}
/>
</div>
</div>
)
}
</Layout >
);
};
export default Home;