"use client"; import { ArrowLeft, Clock3, Download, Loader2, LogOut, Sparkles, Upload } from "lucide-react"; import { signOut, useSession } from "next-auth/react"; import { type CSSProperties, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent, useCallback, useEffect, useMemo, useRef, useState, } from "react"; import useSWR from "swr"; import { toast } from "sonner"; import { BrandTopBar, BrandTopBarButton } from "@/components/layout/brand-top-bar"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, } from "@/components/ui/select"; import { imageGenerationService } from "@/services/canvas"; import { downloadCanvasImageAsPng } from "@/lib/client/canvas-image-download"; import { cn, fetcher } from "@/lib/utils"; import type { PublicStyleProCategory, PublicStyleProCustomField, PublicStyleProConfig, StyleProCustomInputMap, StyleProGenerationMode, StyleProHistoryItem, StyleProPersistedState, StyleProSelectionMap, } from "@/lib/style-pro/config"; type BrandingSettings = { siteName?: string; siteLogo?: string | null; siteLogoDark?: string | null; }; type StyleProConfigResponse = { success: boolean; config?: PublicStyleProConfig; error?: string; }; type StyleProModelOption = { id: string; modelId: string; displayName: string; avatarUrl?: string | null; providerName: string; providerType: string; isDefault: boolean; unitCost: number; creditDiscountLabel?: string | null; availabilityStatus?: "available" | "maintenance" | "testing"; availabilityLabel?: string | null; selectable?: boolean; disabledReason?: string | null; }; type StyleProModelsResponse = { success: boolean; defaultModelId?: string | null; realisticUnitCost?: number | null; models?: StyleProModelOption[]; error?: string; }; type StyleProMembershipSummaryResponse = { currentPlan?: { name?: string; monthlyCredits?: number; dailyFreeCredits?: number; }; summary?: { currentCredits?: number; dailyFreeCredits?: number; }; }; type StyleProAccountCenterResponse = { profile?: { name?: string; email?: string; currentPlanName?: string; availablePoints?: number; monthlyCredits?: number; dailyFreeCredits?: number; }; }; type StyleProGenerateResponse = { success: boolean; error?: string; taskId?: string; status?: string; queueStatus?: { status?: "pending" | "running" | "succeeded" | "failed"; queuePosition?: number; sameModelPendingCount?: number; sameModelRunningCount?: number; concurrencyLimit?: number; estimatedWaitMs?: number; source?: "model_queue" | "batch_queue"; }; task?: { id: string; status: "pending" | "running" | "succeeded" | "failed"; attempts: number; error?: string | null; createdAt?: string | Date | null; startedAt?: string | Date | null; completedAt?: string | Date | null; queueStatus?: StyleProGenerateResponse["queueStatus"]; result?: { imageUrl: string; thumbnailUrl?: string; model?: string; modelRef?: string; gallerySavedImageCount?: number; gallerySaveError?: string; generationMode?: StyleProGenerationMode; chargedPoints?: number; selections: StyleProSelectionMap; customInputs?: StyleProCustomInputMap; optionLabels: string[]; } | null; }; }; type StyleProWorkspaceProps = { canvasId: string; initialSourceImageUrl?: string | null; persistedState?: StyleProPersistedState | null; onAddToCanvas?: (item: StyleProHistoryItem) => void | Promise; onBack?: () => void; onPersistState?: (state: StyleProPersistedState) => void | Promise; }; const STYLE_PRO_HISTORY_LIMIT = 4; const STYLE_PRO_TASK_POLL_INTERVAL_MS = 1600; const STYLE_PRO_TASK_TIMEOUT_MS = 10 * 60 * 1000; type StyleProTaskState = { taskId: string; status: "pending" | "running" | "succeeded" | "failed"; queueStatus?: StyleProGenerateResponse["queueStatus"]; error?: string | null; generationMode: StyleProGenerationMode; }; function getHistoryId() { if (typeof crypto !== "undefined" && "randomUUID" in crypto) { return crypto.randomUUID(); } return `style-pro-${Date.now()}-${Math.random().toString(36).slice(2)}`; } function createStyleProRequestId() { if (typeof crypto !== "undefined" && "randomUUID" in crypto) { return crypto.randomUUID(); } return undefined; } function loadImageDimensions(url: string) { return new Promise<{ width?: number; height?: number }>((resolve) => { if (typeof window === "undefined" || !url) { resolve({}); return; } const image = new window.Image(); image.onload = () => { resolve({ width: image.naturalWidth || undefined, height: image.naturalHeight || undefined, }); }; image.onerror = () => resolve({}); image.src = url; }); } function sleep(ms: number) { return new Promise((resolve) => window.setTimeout(resolve, ms)); } function normalizeHistory(history: StyleProHistoryItem[] | undefined) { return Array.isArray(history) ? history.slice(0, STYLE_PRO_HISTORY_LIMIT) : []; } function getLocalStyleProStateKey(canvasId: string) { return `style-pro-state:${canvasId || "draft"}`; } function readLocalStyleProState( canvasId: string ): StyleProPersistedState | null { if (typeof window === "undefined") return null; try { const raw = window.localStorage.getItem(getLocalStyleProStateKey(canvasId)); if (!raw) return null; const parsed = JSON.parse(raw) as StyleProPersistedState; return { sourceImageUrl: typeof parsed.sourceImageUrl === "string" ? parsed.sourceImageUrl : undefined, history: normalizeHistory( Array.isArray(parsed.history) ? parsed.history.filter( (item) => item && typeof item.sourceImageUrl === "string" && typeof item.resultImageUrl === "string" ) : [] ), }; } catch { return null; } } function writeLocalStyleProState( canvasId: string, state: StyleProPersistedState ) { if (typeof window === "undefined") return; try { window.localStorage.setItem( getLocalStyleProStateKey(canvasId), JSON.stringify({ sourceImageUrl: state.sourceImageUrl, history: normalizeHistory(state.history), }) ); } catch { // Local persistence is a safety net; server persistence remains authoritative. } } function mergeStyleProHistory( ...historyGroups: Array ) { const byKey = new Map(); for (const history of historyGroups) { for (const item of history || []) { if (!item?.sourceImageUrl || !item?.resultImageUrl) continue; const key = item.id || `${item.sourceImageUrl}::${item.resultImageUrl}`; if (!byKey.has(key)) { byKey.set(key, item); } } } return Array.from(byKey.values()) .sort((left, right) => { const leftTime = Date.parse(left.createdAt || "") || 0; const rightTime = Date.parse(right.createdAt || "") || 0; return rightTime - leftTime; }) .slice(0, STYLE_PRO_HISTORY_LIMIT); } function mergeStyleProState( ...states: Array ): StyleProPersistedState { return { sourceImageUrl: states.find((state) => String(state?.sourceImageUrl || "").trim()) ?.sourceImageUrl || undefined, history: mergeStyleProHistory(...states.map((state) => state?.history)), }; } function cloneCustomInputs( inputs?: StyleProCustomInputMap | null ): StyleProCustomInputMap { return { global: { ...(inputs?.global || {}) }, categories: Object.fromEntries( Object.entries(inputs?.categories || {}).map(([categoryId, values]) => [ categoryId, { ...(values || {}) }, ]) ), }; } function buildDefaultCustomInputs( config?: PublicStyleProConfig | null, current?: StyleProCustomInputMap | null ): StyleProCustomInputMap { const next = cloneCustomInputs(current); for (const field of config?.customFields || []) { if (next.global?.[field.id] === undefined && field.defaultValue) { next.global![field.id] = field.defaultValue; } } for (const category of config?.categories || []) { for (const field of category.customFields || []) { if (!field.defaultValue) continue; next.categories![category.id] = next.categories![category.id] || {}; if (next.categories![category.id][field.id] === undefined) { next.categories![category.id][field.id] = field.defaultValue; } } } return next; } function hasCustomFields(config?: PublicStyleProConfig | null) { return Boolean( (config?.customFields || []).length || (config?.categories || []).some((category) => category.customFields?.length) ); } function getCustomFieldColorValue(value?: string | null) { const trimmed = String(value || "").trim(); const shortHex = trimmed.match(/^#([0-9a-fA-F]{3})$/); if (/^#[0-9a-fA-F]{6}$/.test(trimmed)) { return trimmed; } if (shortHex) { return `#${shortHex[1] .split("") .map((char) => `${char}${char}`) .join("")}`; } return "#ffffff"; } function StyleProCustomFieldInput({ field, onChange, value, }: { field: PublicStyleProCustomField; onChange: (value: string) => void; value?: string; }) { const inputValue = value || ""; const label = ( ); if (field.type === "color") { return (
{label}
onChange(event.target.value)} type="color" value={getCustomFieldColorValue(inputValue || field.defaultValue)} /> onChange(event.target.value)} placeholder={field.placeholder || "#FFFFFF"} value={inputValue} />
{field.description ? (
{field.description}
) : null}
); } if (field.type === "textarea") { return (
{label}