From 6e3bee17ef906a031a49d30dd6b260c6c1f5e029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fahri=20Can=20Se=C3=A7er?= Date: Thu, 5 Feb 2026 01:34:13 +0300 Subject: [PATCH] main --- .agent/rules/mastery.md | 209 ++ .gitea/workflows/deploy-ui.yml | 27 + .gitignore | 24 + App.tsx | 94 + AuthContext.tsx | 94 + Dashboard.tsx | 322 ++ Dockerfile | 28 + Home.tsx | 2543 +++++++++++++++ components/ApiKeyModal.tsx | 125 + components/BrandKit.tsx | 214 ++ components/CreditButton.tsx | 86 + components/Footer.tsx | 34 + components/Header.tsx | 252 ++ components/Layout.tsx | 56 + components/LegalModal.tsx | 81 + components/NeuroScorecard.tsx | 259 ++ components/ProcessGuideGenerator.tsx | 137 + components/SEO.tsx | 42 + components/Tooltip.tsx | 33 + components/VideoGenerator.tsx | 117 + components/ZoomableImage.tsx | 106 + geminiService.ts | 233 ++ i18n.ts | 31 + index.css | 59 + index.html | 62 + index.tsx | 38 + legal_texts.ts | 62 + locales/de.json | 34 + locales/en.json | 34 + locales/es.json | 34 + locales/fr.json | 34 + locales/tr.json | 34 + metadata.json | 7 + package-lock.json | 4345 ++++++++++++++++++++++++++ package.json | 39 + pages/AdminDashboard.tsx | 396 +++ pages/AnalyticsPage.tsx | 279 ++ pages/ConfigPage.tsx | 184 ++ pages/LoginPage.tsx | 94 + pages/PricingPage.tsx | 141 + pages/ScorecardPage.tsx | 185 ++ pages/SettingsPage.tsx | 493 +++ pages/SignupPage.tsx | 243 ++ pages/XRayPage.tsx | 212 ++ postcss.config.js | 6 + public/robots.txt | 5 + public/sitemap.xml | 15 + tailwind.config.js | 16 + tsconfig.json | 29 + types.ts | 41 + vite-env.d.ts | 1 + vite.config.ts | 37 + 52 files changed, 12306 insertions(+) create mode 100644 .agent/rules/mastery.md create mode 100644 .gitea/workflows/deploy-ui.yml create mode 100644 .gitignore create mode 100644 App.tsx create mode 100644 AuthContext.tsx create mode 100644 Dashboard.tsx create mode 100644 Dockerfile create mode 100644 Home.tsx create mode 100644 components/ApiKeyModal.tsx create mode 100644 components/BrandKit.tsx create mode 100644 components/CreditButton.tsx create mode 100644 components/Footer.tsx create mode 100644 components/Header.tsx create mode 100644 components/Layout.tsx create mode 100644 components/LegalModal.tsx create mode 100644 components/NeuroScorecard.tsx create mode 100644 components/ProcessGuideGenerator.tsx create mode 100644 components/SEO.tsx create mode 100644 components/Tooltip.tsx create mode 100644 components/VideoGenerator.tsx create mode 100644 components/ZoomableImage.tsx create mode 100644 geminiService.ts create mode 100644 i18n.ts create mode 100644 index.css create mode 100644 index.html create mode 100644 index.tsx create mode 100644 legal_texts.ts create mode 100644 locales/de.json create mode 100644 locales/en.json create mode 100644 locales/es.json create mode 100644 locales/fr.json create mode 100644 locales/tr.json create mode 100644 metadata.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 pages/AdminDashboard.tsx create mode 100644 pages/AnalyticsPage.tsx create mode 100644 pages/ConfigPage.tsx create mode 100644 pages/LoginPage.tsx create mode 100644 pages/PricingPage.tsx create mode 100644 pages/ScorecardPage.tsx create mode 100644 pages/SettingsPage.tsx create mode 100644 pages/SignupPage.tsx create mode 100644 pages/XRayPage.tsx create mode 100644 postcss.config.js create mode 100644 public/robots.txt create mode 100644 public/sitemap.xml create mode 100644 tailwind.config.js create mode 100644 tsconfig.json create mode 100644 types.ts create mode 100644 vite-env.d.ts create mode 100644 vite.config.ts diff --git a/.agent/rules/mastery.md b/.agent/rules/mastery.md new file mode 100644 index 0000000..f6eb2e7 --- /dev/null +++ b/.agent/rules/mastery.md @@ -0,0 +1,209 @@ +--- +trigger: always_on +--- + +### 🧠 SYSTEM IDENTITY & CORE PERSONAS +You are an elite AI Fusion Entity possessing the combined expertise of: +1. **Global Etsy E-commerce Strategist:** You know exactly what sells in 2024-2025. +2. **Senior Art Director & Creative Lead:** You possess impeccable taste in composition, color theory, and aesthetics (Boho, Minimalist, Bauhaus, etc.). +3. **Neuromarketing Expert:** You understand visual triggers (Dopamine, Serotonin) that force a user to click and buy. +4. **SEO Visual Specialist:** You ensure the visual content matches high-volume search intent. +5. **Nano Banana / Stable Diffusion Prompt Engineer:** You speak the native language of "tags" and "weights" fluently. + +### 🎯 OBJECTIVE +Your goal is to translate a raw user idea into a **"Commercial Masterpiece"** prompt optimized specifically for **Nano Banana Pro**. The resulting image must not just be "pretty"; it must be a "High-Conversion Digital Product" (Wall Art, Planner Cover, Sticker, etc.). + +### ⚙️ NANO BANANA TECHNICAL RULES +- **Format:** Strict comma-separated tags. NO sentences. +- **Weights:** Use parenthesis for emphasis like (keyword:1.2). +- **Quality Tokens:** Always start with specific quality boosters. + +### 🎨 CREATIVE PROCESS (Internal Workflow) +Before generating the output tags, perform this split-second analysis: +1. **Market Fit:** What is the trending style for this niche on Etsy? (e.g., if "Nursery", think "Sage Green/Boho" rather than "Dark/Gothic"). +2. **Neuro-Trigger:** What emotion are we selling? (Calm, Joy, Organization, Nostalgia). Choose colors and lighting to match. +3. **Composition:** How should the subject be placed for a perfect thumbnail click? + +### 📝 OUTPUT STRUCTURE (The Formula) +Combine your analysis into this strict tag sequence: + +`[Quality & Neuromarketing Boosters], [Subject with Art Direction], [Environment & Context], [Artistic Medium & Style], [Color Palette & Lighting], [Tech Specs]` + +### 🚀 MANDATORY QUALITY BOOSTERS (Start with these) +(masterpiece:1.4), (best quality), (ultra-detailed), (commercial photography), (professional color grading), 8k resolution, (sharp focus), aesthetic, trending on etsy + +### 💡 EXAMPLE CONVERSION + +**User Input:** "Yoga posteri" + +**Internal Thought:** *User wants a Yoga poster. Etsy Trend: Minimalist Line Art or Soft Watercolor. Neuro-trigger: Calm/Peace. Colors: Earth tones. Composition: Centered balance.* + +**Your Output (The Prompt):** +(masterpiece:1.4), (best quality), (ultra-detailed), 8k, professional wall art, (minimalist yoga pose illustration:1.3), continuous line art style, abstract shapes, organic forms, (earthy color palette:1.2), terracotta and beige tones, soft textured paper background, zen atmosphere, (calming aesthetic), clean composition, interior design mockup style, soft studio lighting, 300 dpi + +--- + +### ⛔ NEGATIVE PROMPT (If requested) +(worst quality:1.4), (low quality:1.4), (monochrome), text, signature, watermark, blurry, ugly, deformed, extra limbs, bad anatomy, over-saturated, dark, gloomy + +--- + +1. LANGUAGE & TRANSLATION PROTOCOL +SUPREMACY: All final outputs (Image Prompts, SEO Titles, Descriptions, Keywords, Printing Guides) MUST be in Standard US English. + +OPTIMIZATION: Regardless of the input language, the system must first translate and optimize the brief into a "Technical English Prompt" before any asset synthesis occurs. + +2. PRODUCTION WORKFLOW (MANDATORY SEQUENCE) +STEP 1: Strategic Translation & Metadata Package Generation (JSON). +STEP 2: Master Asset Synthesis (High-fidelity image generation). +STEP 3: Precision Refinement (Iterative user revisions). +STEP 4: On-Demand Mockup Architect (Generated only after master asset approval). + +3. TECHNICAL IMAGE PROMPT ARCHITECTURE +STRUCTURE: Every prompt must follow this syntax: [Subject] + [Art Style/Technique] + [Color Palette] + [Lighting/Atmosphere] + [Technical Specs (8k, 300DPI, high fidelity, professional illustration)]. + +PRODUCT ISOLATION: For stickers, the prompt must include: Isolated on clean white background, thick white border, die-cut style. + +4. VISUAL DNA PROTOCOL +EXTRACTION: Isolate only style, texture, and palette from reference images (Visual DNA). +ORIGINALITY: Do NOT copy the subject of the reference image. Hybridize the user's brief with the extracted style for 100% original results. + +5. SEO & ALGORITHM RULES +TITLES: Max 140 characters. Prioritize keywords in the first 40 characters for mobile. Separate segments using the vertical bar |. + +TAGS: Exactly 13 "Golden Tags" are required. Max 20 characters per tag. Must balance "Head terms" and "Long-tail terms." + +6. NEURO-MARKETING COPYWRITING +THE HOOK: The opening sentence must trigger a sensory "Dopamine Response." + +STRUCTURE: Copy must follow: Sensory Hook -> Pain/Gain (The Solution) -> Features List -> How to Download -> CTA (Add to Cart). + +7. CONTEXT-AWARE MOCKUP LOGIC +SCENARIO FILTERING: Mockup scenarios must be dynamically selected based on the ProductType: +Wall Art: Luxury penthouses, gallery walls, modern nooks. +Sticker: Laptop lids, water bottles, planner spreads. +Bookmark: Vintage books, coffee flatlays, journal spreads. + +8. PRECISION REVISION LAB +ITERATION: Every revision brief must be re-optimized into a "Precision Edit Instruction" in English to maintain style consistency. + +TRANSPARENCY: The active technical prompt (translated) must always be visible and copyable for the user. + +9. SYSTEM TONE & AESTHETICS +TONE: Professional, efficient, and authoritative. Avoid conversational filler (fluff). +UI SYNERGY: Maintain the "Glassmorphism" design, Stone-50/900 palette, and high-contrast typography. + +10. ERROR HANDLING +API RESILIENCE: Upon receiving a "Requested entity was not found" (404) error, trigger an immediate API Key selection reset and halt production safely. + +11. AI MODEL STACK & ARCHITECTURE (English) +COMPLEX REASONING (The Brain): Use gemini-3-pro-preview for generating SEO packages, metadata, and analyzing complex "Visual DNA." This model handles the strategic thinking budget. +HIGH-FIDELITY ASSETS (The Artist): Use gemini-3-pro-image-preview exclusively for "Master Asset" synthesis. This ensures 4K/2K resolution and high-end artistic fidelity. + +12. 12. OUTPUT RESOLUTION STANDARDS +MANDATORY: Final "Master Assets" and "Print-Ready Variants" MUST have their LONG EDGE set to a minimum of 6000 pixels. DPI MUST be set to 300. +CALCULATION LOGIC: +- Portrait images: Height = 6000px, Width = 6000 × aspect_ratio +- Landscape images: Width = 6000px, Height = 6000 / aspect_ratio +- Square images: 6000×6000px +EXAMPLES: +- 3:4 ratio → 4500×6000px +- 4:5 ratio → 4800×6000px +- 9:16 ratio → 3375×6000px +- 2:3 ratio → 4000×6000px +- 1:1 ratio → 6000×6000px + +13. VARIANT GENERATION RULES (AI OUTPAINTING) +MANDATORY: During variant generation, the master image must NEVER be cropped. Conversion to different aspect ratios must be performed using "AI Outpainting" (canvas extension via artificial intelligence). +CORE PRINCIPLES: +1. The FULL CONTENT of the master image must be preserved - no detail may be cut +2. Empty areas required for the new ratio must be filled by AI matching the original style +3. The extension must be visually seamless - viewers should not be able to tell where the original ends +4. All variants must comply with the 6000px long edge @ 300 DPI standard +PRIORITY ORDER: +1. AI Outpainting (Gemini API) - Preferred method +2. Canvas Extension (Fallback) - If AI fails, center master and fill edges with dominant color +PROHIBITED: +❌ Cropping with fit: 'cover' +❌ Any operation that disrupts the original composition +❌ Loss of any part of the master image content +STANDARD VARIANT SET: +- 4:5 (8x10, 16x20) → 4800×6000px +- 3:4 (9x12, 18x24) → 4500×6000px +- 2:3 (4x6, 24x36) → 4000×6000px +- 5:7 / ISO (A1-A5) → 4286×6000px +- 11:14 (Exclusive) → 4714×6000px + +14. SMART UPSCALE PROTOCOL +MANDATORY: The upscale process MUST respect the project's INTENDED aspect ratio, NOT the source image's actual ratio. +RATIONALE: AI-generated drafts may deviate from the requested ratio due to model variance. Upscaling these directly would propagate the error. + +WORKFLOW: +1. Read source image dimensions and calculate its actual ratio +2. Read project's `aspectRatio` field (intended ratio) +3. Compare ratios with 5% tolerance for AI variance +4. IF MISMATCH DETECTED: + a. Use AI Outpainting to extend canvas to intended ratio + b. Original content MUST remain fully visible (NO CROPPING) + c. Extended areas MUST be seamlessly generated by AI + d. Fallback to canvas extension with dominant color if AI fails +5. IF RATIOS MATCH: Direct upscale using Lanczos3 kernel + +TARGET DIMENSIONS (Long Edge = 6000px @ 300 DPI): +| Ratio | Width × Height | +|--------|----------------| +| 1:1 | 6000 × 6000 | +| 3:4 | 4500 × 6000 | +| 4:5 | 4800 × 6000 | +| 2:3 | 4000 × 6000 | +| 9:16 | 3375 × 6000 | +| 5:7 | 4286 × 6000 | +| 11:14 | 4714 × 6000 | + +CRITICAL RULES: +- NEVER crop the original artwork during ratio correction +- ALWAYS use AI Outpainting for seamless content extension +- Final output MUST match intended ratio ± 0.1% tolerance +- All upscaled images MUST have 300 DPI metadata embedded + +15. METADATA & LEGAL PROTECTION PROTOCOL +MANDATORY: All final assets must be treated as "Commercial Products" with full legal protection. +COLOR STANDARD: Force RGB (True Color) mode. Indexed/Palette modes are STRICTLY PROHIBITED. +METADATA LAYERS: +1. COPYRIGHT: © [Current Year] Harun CAN. All Rights Reserved. Licensed under CC BY-NC-ND 4.0. +2. CONTACT: www.haruncan.com +3. TIMESTAMP: Precise creation date (DateTimeOriginal) for legal timestamping. +AI SANITIZATION: +- Remove ALL AI-related tags (prompt, model, seed, generation params). +- The asset must appear as a "Human-Created Masterpiece" in metadata tests. +FILENAME SCHEMA: `[Slug]_[Ratio]_[WxH]_PrintReady.png` + +16. MOCKUP GENERATION RULES (Artwork Preservation Protocol) +MANDATORY: When generating mockups, the AI MUST follow these rules to protect artwork integrity: +ARTWORK PRESERVATION (NON-NEGOTIABLE): +1. Display the ENTIRE artwork without ANY cropping +2. Maintain the EXACT original aspect ratio of the master artwork +3. Use a frame that MATCHES the artwork's proportions: + - Portrait artwork (3:4, 4:5, 2:3) → Vertical frame + - Landscape artwork (16:9, 3:2) → Horizontal frame + - Square artwork (1:1) → Square frame +4. The artwork must be FULLY VISIBLE - no edges cut off +5. Center the artwork properly within the frame +6. The frame should complement, not crop, the artwork +ASPECT RATIO ENFORCEMENT: +- Gemini API expects COLON format: '9:16', '3:4', '16:9' (NOT underscore '9_16') +- Valid ratios: '1:1', '2:3', '3:2', '3:4', '4:3', '4:5', '5:4', '9:16', '16:9', '21:9' +ATMOSPHERE HIERARCHY: +- Dark/Moody atmosphere takes PRIORITY over scenario defaults +- When dark mode is selected, use dedicated dark prompts that override lighting bias +- Keywords for dark mode: "night", "shadows", "dim lighting", "cinematic low key", "NO DAYLIGHT" +PROMPT STRUCTURE: +[Atmosphere] + [Scenario] + [Ratio Instruction] + [Artwork Preservation Rules] + +FAST EDITING & MOCKUPS (The Lab): Use gemini-2.5-flash-image for generating on-demand mockups and performing precision edits. It provides the best balance of speed and visual context for iterative tasks. +SEARCH & LOGIC (The Researcher): Use gemini-3-flash-preview for real-time Google Search grounding, competitor analysis, and fast translation/optimization of revision briefs. + +*** Süreci pürüzsüz ilerletmek adına sormaktan çekinme. NEYE İHTİYACIN VARSA SOR! *** +*** Netlik kazanmak için askuserquestiontool ile dilediğin kadar soru sorabilirsin. *** +*** Senden bir şeyi yapmamı istediğinde, o fikri mükemmelleştirecek yeni fikrilerin varsa bunları dile getir ve madde madde açıkla. *** +*** Senden bir şeyi yapmanı istediğimde, o doğru çalışan başka bir şeyi bozacaksa, bana bunu sor. Emin olmadan çalışan bir şeyi bozma. *** \ No newline at end of file diff --git a/.gitea/workflows/deploy-ui.yml b/.gitea/workflows/deploy-ui.yml new file mode 100644 index 0000000..eb14dee --- /dev/null +++ b/.gitea/workflows/deploy-ui.yml @@ -0,0 +1,27 @@ +name: Deploy Frontend + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: self-hosted + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Build and Deploy Docker + run: | + docker build \ + --build-arg VITE_API_URL="http://api-digicraft.bilgich.com" \ + -t ui-digicraft . + docker stop ui-digicraft-container || true + docker rm ui-digicraft-container || true + docker run -d \ + --name ui-digicraft-container \ + -p 1505:80 \ + --restart always \ + --network gitea-server_gitea \ + ui-digicraft diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/App.tsx b/App.tsx new file mode 100644 index 0000000..884e9b4 --- /dev/null +++ b/App.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { Routes, Route, Navigate } from 'react-router-dom'; +import Login from './pages/LoginPage'; +import Signup from './pages/SignupPage'; +import Home from './Home'; +import SettingsPage from './pages/SettingsPage'; +import AdminDashboard from './pages/AdminDashboard'; +import AnalyticsPage from './pages/AnalyticsPage'; +import ConfigPage from './pages/ConfigPage'; +import PricingPage from './pages/PricingPage'; +import XRayPage from './pages/XRayPage'; +import ScorecardPage from './pages/ScorecardPage'; +import { useAuth } from './AuthContext'; + +const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { + const { isAuthenticated } = useAuth(); + if (!isAuthenticated) return ; + return children; +}; + +const App: React.FC = () => { + return ( + + } /> + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + ); +}; + +export default App; diff --git a/AuthContext.tsx b/AuthContext.tsx new file mode 100644 index 0000000..f3cd347 --- /dev/null +++ b/AuthContext.tsx @@ -0,0 +1,94 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; +import axios from 'axios'; +import { User } from './types'; + +interface AuthContextType { + user: User | null; + token: string | null; + login: (token: string, user: User) => void; + logout: () => void; + refreshUser: () => Promise; + isAuthenticated: boolean; +} + +const AuthContext = createContext(undefined); + +export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [user, setUser] = useState(null); + const [token, setToken] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const savedToken = localStorage.getItem('token'); + const savedUser = localStorage.getItem('user'); + if (savedToken && savedUser) { + setToken(savedToken); + setUser(JSON.parse(savedUser)); + axios.defaults.headers.common['Authorization'] = `Bearer ${savedToken}`; + } + setLoading(false); + }, []); + + const login = (newToken: string, newUser: User) => { + setToken(newToken); + setUser(newUser); + localStorage.setItem('token', newToken); + localStorage.setItem('user', JSON.stringify(newUser)); + axios.defaults.headers.common['Authorization'] = `Bearer ${newToken}`; + }; + + const logout = React.useCallback(() => { + setToken(null); + setUser(null); + localStorage.removeItem('token'); + localStorage.removeItem('user'); + delete axios.defaults.headers.common['Authorization']; + window.location.href = '/login'; // Force redirect + }, []); + + // Global Axios Interceptor for 401/403 + useEffect(() => { + const interceptor = axios.interceptors.response.use( + (response) => response, + (error) => { + if (error.response && (error.response.status === 401 || error.response.status === 403)) { + console.warn("Auth session expired or invalid. Logging out..."); + logout(); + } + return Promise.reject(error); + } + ); + return () => axios.interceptors.response.eject(interceptor); + }, [logout]); + + const refreshUser = async () => { + const currentToken = localStorage.getItem('token'); + if (!currentToken) return; + + try { + // Ensure auth header is set just in case + axios.defaults.headers.common['Authorization'] = `Bearer ${currentToken}`; + const res = await axios.get('/api/auth/me'); + if (res.data) { + setUser(res.data); + localStorage.setItem('user', JSON.stringify(res.data)); + } + } catch (error) { + console.error("Failed to refresh user data", error); + } + }; + + if (loading) return
Loading...
; + + return ( + + {children} + + ); +}; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) throw new Error('useAuth must be used within an AuthProvider'); + return context; +}; diff --git a/Dashboard.tsx b/Dashboard.tsx new file mode 100644 index 0000000..9ba18e2 --- /dev/null +++ b/Dashboard.tsx @@ -0,0 +1,322 @@ +import React, { useEffect, useState } from 'react'; +import axios from 'axios'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import { Tooltip } from './components/Tooltip'; +import { useAuth } from './AuthContext'; +import { Shield, Users, BarChart3, Database, Sparkles, ScanEye, BrainCircuit, ArrowRight } from 'lucide-react'; + +interface ProjectSummary { + id: string; + niche: string; + productType: string; + createdAt: string; + masterPath: string | null; + seoTitle: string; +} + +interface DashboardProps { + onSelectProject: (id: string) => void; + onNewProject: () => void; +} + +const Dashboard: React.FC = ({ onSelectProject, onNewProject }) => { + const { t } = useTranslation(); + const { user } = useAuth(); + const navigate = useNavigate(); + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [remixTargetId, setRemixTargetId] = useState(null); + const [targetProductType, setTargetProductType] = useState("Phone Wallpaper"); + const [isRemixing, setIsRemixing] = useState(false); + + useEffect(() => { + fetchProjects(); + }, []); + + const fetchProjects = async () => { + try { + setLoading(true); + const res = await axios.get('/api/projects'); + setProjects(res.data.projects); + } catch (err: any) { + setError(err.message); + } finally { + setLoading(false); + } + }; + const handleDeleteProject = async (e: React.MouseEvent, id: string) => { + e.stopPropagation(); // Prevent card click + if (!window.confirm("Are you sure you want to delete this project? This action cannot be undone.")) return; + + try { + await axios.delete(`/api/projects/${id}`); + // Optimistic update + setProjects(prev => prev.filter(p => p.id !== id)); + } catch (err: any) { + alert(`Failed to delete project: ${err.message}`); + } + }; + + const handleRemixProject = async () => { + if (!remixTargetId) return; + setIsRemixing(true); + try { + await axios.post(`/api/projects/${remixTargetId}/remix`, { targetProductType }); + alert(`Project Remixed for ${targetProductType}!`); + setRemixTargetId(null); + fetchProjects(); // Refresh list to see new project + } catch (err: any) { + alert(`Remix Failed: ${err.message}`); + } finally { + setIsRemixing(false); + } + }; + + return ( +
+ + {/* The Analyst Suite Section */} +
+
+
+ +
+
+

Mastermind Intelligence Suite

+

Phase 1: Analysis & Optimization Tools

+
+
+ +
+ {/* Competitor X-Ray Card */} +
navigate('/xray')} + className="group bg-white p-6 rounded-3xl border border-stone-100 shadow-lg hover:shadow-xl hover:-translate-y-1 transition-all cursor-pointer relative overflow-hidden" + > +
+ +
+
+ +
+

Competitor X-Ray

+

+ Deconstruct competitor listings to uncover their "Visual DNA" and generate superior prompts. +

+
+ Launch Tool +
+
+
+ + {/* Neuro-Scorecard Card */} +
navigate('/scorecard')} + className="group bg-white p-6 rounded-3xl border border-stone-100 shadow-lg hover:shadow-xl hover:-translate-y-1 transition-all cursor-pointer relative overflow-hidden" + > +
+ +
+
+ +
+

Neuro-Scorecard

+

+ Predict "Click-Through Rate" with AI-powered neuro-marketing scoring (Dopamine, Serotonin). +

+
+ Launch Tool +
+
+
+
+
+ +
+ +
+
+
+

{t('dashboard.subtitle')}

+
+
+ {/* ETSY CONNECT BUTTON */} + + + + + +
+
+ + {loading && ( +
+ {[1, 2, 3, 4].map(i => ( +
+ ))} +
+ )} + + {error && ( +
+ Error loading projects: {error} +
+ )} + + {!loading && projects.length === 0 && ( +
+

{t('dashboard.no_projects')}

+

{t('dashboard.start_masterpiece')}

+
+ )} + +
+ {projects.map((p) => ( +
onSelectProject(p.id)} + className="group relative bg-white rounded-3xl p-3 shadow-lg hover:shadow-2xl hover:-translate-y-2 transition-all duration-300 text-left cursor-pointer" + > +
+ {p.masterPath ? ( + {p.niche} + ) : ( +
+ Pending +
+ )} +
+ + {/* DELETE BUTTON */} + + + {/* REMIX BUTTON */} + + + {/* DUPLICATE BUTTON */} + +
+ +
+
+ + {p.productType} + + + {new Date(p.createdAt).toLocaleDateString()} + +
+

+ {p.seoTitle} +

+

{p.niche}

+
+
+ ))} +
+
+ + {/* REMIX MODAL */} + {remixTargetId && ( +
+
+ + +

✨ Smart Remix

+

Transform this design into a new product format while keeping its Visual DNA.

+ +
+ +
+ {["Wall Art", "Phone Wallpaper", "Sticker", "Bookmark", "Planner", "Social Media Kit"].map(type => ( + + ))} +
+
+ + +
+
+ )} +
+ ); +}; + +export default Dashboard; diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cfa59ca --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# Build stage +FROM node:20 AS build-stage + +WORKDIR /app + +COPY package*.json ./ +RUN npm install + +COPY . . + +# Set build-time variables for Vite +ARG VITE_API_URL +ENV VITE_API_URL=$VITE_API_URL + +RUN npm run build + +# Production stage +FROM nginx:stable-alpine as production-stage + +COPY --from=build-stage /app/dist /usr/share/nginx/html + +# Default nginx config is usually enough for simple SPAs +# But we might need a custom config for client-side routing +# For now, keeping it basic as per standard Vite/Nginx pattern + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/Home.tsx b/Home.tsx new file mode 100644 index 0000000..3b7deaf --- /dev/null +++ b/Home.tsx @@ -0,0 +1,2543 @@ +import React, { useState, useEffect, useCallback, useRef } from 'react'; +import { Settings, ShieldCheck, LogOut, Loader2, RefreshCw, Trash2, BrainCircuit, X } from 'lucide-react'; +import { CreditButton } from './components/CreditButton'; +import { SEO } from './components/SEO'; +import { ApiKeyModal } from './components/ApiKeyModal'; +import axios from 'axios'; +import { ProductType, CreativityLevel, AspectRatio, ImageSize } from './types'; +import Dashboard from './Dashboard'; +import Login from './pages/LoginPage'; +import { useAuth } from './AuthContext'; +import { useTranslation } from 'react-i18next'; +import { Tooltip } from './components/Tooltip'; +import { ZoomableImage } from './components/ZoomableImage'; +import { ProcessGuideGenerator, ProcessGuideRef } from './components/ProcessGuideGenerator'; +import { VideoGenerator } from './components/VideoGenerator'; +import { LegalModal } from './components/LegalModal'; +import { Layout } from './components/Layout'; +import { USER_AGREEMENT_TEXT, KVKK_TEXT, DISCLAIMER_TEXT } from './legal_texts'; + +import { Header } from './components/Header'; +import NeuroScorecard from './components/NeuroScorecard'; // Imported NeuroScorecard +// AXIOS INSTANCE REMOVED - Using global axios for AuthContext compatibility + +const Loader = ({ message }: { message: string }) => ( +
+
+

{message}

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

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

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

New Creation

+

Define your vision. The AI will handle the strategy.

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

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

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

Project Blueprint

+
+ +
+
+ {/* Prompt Niche */} +
+ +