import React, { useState, useMemo, useRef, useCallback, useEffect } from 'react' import { usePage, Link } from '@inertiajs/react' import StudioLayout from '../../Layouts/StudioLayout' import RichTextEditor from '../../components/forum/RichTextEditor' import TextInput from '../../components/ui/TextInput' import Button from '../../components/ui/Button' import Modal from '../../components/ui/Modal' import FormField from '../../components/ui/FormField' import Toggle from '../../components/ui/Toggle' import NovaSelect from '../../components/ui/NovaSelect' import TagPicker from '../../components/tags/TagPicker' import SchedulePublishPicker from '../../components/upload/SchedulePublishPicker' import ArtworkEvolutionSearchPicker from '../../components/artwork/ArtworkEvolutionSearchPicker' const EDIT_SECTIONS = [ { id: 'taxonomy', label: 'Category', hint: 'Content type and category path' }, { id: 'details', label: 'Details', hint: 'Title and description' }, { id: 'evolution', label: 'Evolution', hint: 'Link an older original artwork' }, { id: 'ai-assist', label: 'AI Assist', hint: 'Suggestions and similar matches' }, { id: 'tags', label: 'Tags', hint: 'Search, add, and refine keywords' }, { id: 'visibility', label: 'Visibility', hint: 'Publishing state' }, ] const TABS = [ { id: 'details', label: 'Details', icon: 'fa-solid fa-pen-fancy' }, { id: 'evolution', label: 'Evolution', icon: 'fa-solid fa-code-branch' }, { id: 'tags', label: 'Tags', icon: 'fa-solid fa-tags' }, { id: 'taxonomy', label: 'Category', icon: 'fa-solid fa-palette' }, { id: 'visibility', label: 'Visibility', icon: 'fa-solid fa-eye' }, { id: 'ai', label: 'AI Assist', icon: 'fa-solid fa-wand-magic-sparkles' }, ] // ─── Helpers ───────────────────────────────────────────────────────────────── function getCsrfToken() { return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '' } function formatBytes(bytes) { if (!bytes) return '—' if (bytes < 1024) return bytes + ' B' if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB' return (bytes / 1048576).toFixed(1) + ' MB' } function getContentTypeVisualKey(slug) { const map = { skins: 'skins', wallpapers: 'wallpapers', photography: 'photography', other: 'other', members: 'members' } return map[slug] || 'other' } function buildCategoryTree(contentTypes) { return (contentTypes || []).map((ct) => ({ ...ct, rootCategories: (ct.categories || ct.root_categories || []).map((rc) => ({ ...rc, children: rc.children || [], })), })) } function nextSourceForManualEdit(currentSource) { if (currentSource === 'ai_applied' || currentSource === 'ai_generated') return 'mixed' if (currentSource === 'mixed') return 'mixed' return 'manual' } function statusTone(status) { switch (status) { case 'ready': return 'border-emerald-400/30 bg-emerald-400/10 text-emerald-200' case 'queued': case 'processing': return 'border-sky-400/30 bg-sky-400/10 text-sky-200' case 'failed': return 'border-red-400/30 bg-red-400/10 text-red-200' default: return 'border-white/10 bg-white/[0.04] text-slate-300' } } function statusLabel(status) { switch (status) { case 'queued': return 'Queued' case 'processing': return 'Processing' case 'ready': return 'Ready' case 'failed': return 'Failed' case 'pending': return 'Pending' default: return 'Not analyzed' } } function visibilityLabel(value) { switch (value) { case 'unlisted': return 'Unlisted' case 'private': return 'Private' default: return 'Public' } } function normalizeContributorCredits(contributorIds = [], contributorCredits = {}) { const normalized = {} const ids = Array.isArray(contributorIds) ? contributorIds.map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0) : [] ids.forEach((id) => { const current = contributorCredits?.[id] || contributorCredits?.[String(id)] || {} normalized[id] = { creditRole: typeof current.creditRole === 'string' ? current.creditRole : '', isPrimary: Boolean(current.isPrimary), } }) const leadIds = Object.entries(normalized) .filter(([, value]) => value.isPrimary) .map(([id]) => Number(id)) if (leadIds.length > 1) { leadIds.slice(1).forEach((id) => { normalized[id] = { ...normalized[id], isPrimary: false, } }) } return normalized } function mapContributorCredits(contributorCredits = []) { return (Array.isArray(contributorCredits) ? contributorCredits : []).reduce((accumulator, contributor) => { const userId = Number(contributor?.user_id) if (!Number.isFinite(userId) || userId <= 0) return accumulator accumulator[userId] = { creditRole: typeof contributor?.credit_role === 'string' ? contributor.credit_role : '', isPrimary: Boolean(contributor?.is_primary), } return accumulator }, {}) } // ─── Sub-components ────────────────────────────────────────────────────────── /** Glass-morphism section card (Nova theme) */ function Section({ children, className = '', id = undefined }) { return (
{children}
) } /** Section heading */ function SectionTitle({ icon, children }) { return (

{icon && } {children}

) } function InlineAiButton({ children, onClick, disabled = false, loading = false }) { return ( ) } function FieldLabel({ label, actionLabel, onAction, disabled = false, loading = false }) { return (
{label} {actionLabel}
) } function RightRailCard({ title, children, className = '' }) { return (

{title}

{children}
) } // ─── Main Component ────────────────────────────────────────────────────────── export default function StudioArtworkEdit() { const { props } = usePage() const { artwork, contentTypes: rawContentTypes } = props const groupOptions = Array.isArray(props.groupOptions) ? props.groupOptions : [] const evolutionRelationTypes = Array.isArray(props.evolutionRelationTypes) ? props.evolutionRelationTypes : [] const initialEvolutionRelation = artwork?.evolution_relation || null const contributorOptionsByGroup = props.contributorOptionsByGroup && typeof props.contributorOptionsByGroup === 'object' ? props.contributorOptionsByGroup : {} const contentTypes = useMemo(() => buildCategoryTree(rawContentTypes || []), [rawContentTypes]) // ── State ────────────────────────────────────────────────────────────────── const [contentTypeId, setContentTypeId] = useState(artwork?.content_type_id || null) const [categoryId, setCategoryId] = useState(artwork?.parent_category_id || null) const [subCategoryId, setSubCategoryId] = useState(artwork?.sub_category_id || null) const [title, setTitle] = useState(artwork?.title || '') const [description, setDescription] = useState(artwork?.description || '') const [tagSlugs, setTagSlugs] = useState(() => (artwork?.tags || []).map((t) => t.slug || t.name)) const [visibility, setVisibility] = useState(artwork?.visibility || (artwork?.is_public ? 'public' : 'private')) const [publishMode, setPublishMode] = useState(artwork?.publish_mode || (artwork?.artwork_status === 'scheduled' ? 'schedule' : 'now')) const [scheduledAt, setScheduledAt] = useState(artwork?.publish_at || null) const [groupSlug, setGroupSlug] = useState(artwork?.group_slug || '') const [primaryAuthorUserId, setPrimaryAuthorUserId] = useState(artwork?.primary_author_user_id || null) const [contributorUserIds, setContributorUserIds] = useState(() => (Array.isArray(artwork?.contributor_user_ids) ? artwork.contributor_user_ids.map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0) : [])) const [contributorCredits, setContributorCredits] = useState(() => normalizeContributorCredits(artwork?.contributor_user_ids || [], mapContributorCredits(artwork?.contributor_credits || []))) const [titleSource, setTitleSource] = useState(artwork?.title_source || 'manual') const [descriptionSource, setDescriptionSource] = useState(artwork?.description_source || 'manual') const [tagsSource, setTagsSource] = useState(artwork?.tags_source || 'manual') const [categorySource, setCategorySource] = useState(artwork?.category_source || 'manual') const [evolutionTarget, setEvolutionTarget] = useState(initialEvolutionRelation?.target_artwork || null) const [evolutionRelationType, setEvolutionRelationType] = useState(initialEvolutionRelation?.relation_type || evolutionRelationTypes[0]?.value || 'remake_of') const [evolutionNote, setEvolutionNote] = useState(initialEvolutionRelation?.note || '') const [saving, setSaving] = useState(false) const [saved, setSaved] = useState(false) const [errors, setErrors] = useState({}) const [aiData, setAiData] = useState(null) const [aiLoading, setAiLoading] = useState(false) const [aiAction, setAiAction] = useState('') const [aiDirect, setAiDirect] = useState(false) const [isAiPanelOpen, setIsAiPanelOpen] = useState(true) const [isAiDebugOpen, setIsAiDebugOpen] = useState(false) const [lastAiRequest, setLastAiRequest] = useState(null) const [selectedAiTags, setSelectedAiTags] = useState([]) const [activeTab, setActiveTab] = useState('details') const [isCategoryChooserOpen, setIsCategoryChooserOpen] = useState(() => !artwork?.parent_category_id) const userTimezone = useMemo(() => artwork?.artwork_timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, [artwork?.artwork_timezone]) // File replace const fileInputRef = useRef(null) const [replacing, setReplacing] = useState(false) const [thumbUrl, setThumbUrl] = useState(artwork?.thumb_url_lg || artwork?.thumb_url || null) const [fileMeta, setFileMeta] = useState({ name: artwork?.file_name || '—', size: artwork?.file_size || 0, width: artwork?.width || 0, height: artwork?.height || 0, }) const [versionCount, setVersionCount] = useState(artwork?.version_count ?? 1) const [requiresReapproval, setRequiresReapproval] = useState(artwork?.requires_reapproval ?? false) const [changeNote, setChangeNote] = useState('') const [showChangeNote, setShowChangeNote] = useState(false) // Version history const [showHistory, setShowHistory] = useState(false) const [historyData, setHistoryData] = useState(null) const [historyLoading, setHistoryLoading] = useState(false) const [restoring, setRestoring] = useState(null) // ── Derived ──────────────────────────────────────────────────────────────── const selectedCT = contentTypes.find((ct) => ct.id === contentTypeId) || null const rootCategories = selectedCT?.rootCategories || [] const selectedRoot = rootCategories.find((c) => c.id === categoryId) || null const subCategories = selectedRoot?.children || [] const selectedGroupOption = useMemo(() => groupOptions.find((group) => String(group.slug || '') === String(groupSlug || '')) || null, [groupOptions, groupSlug]) const currentContributorOptions = useMemo(() => { const selectedSlug = String(groupSlug || '') return Array.isArray(contributorOptionsByGroup?.[selectedSlug]) ? contributorOptionsByGroup[selectedSlug] : [] }, [contributorOptionsByGroup, groupSlug]) const aiStatus = aiData?.status || artwork?.ai_status || 'not_analyzed' const aiSuggestedTags = useMemo(() => (aiData?.tag_suggestions || []).map((item) => item.tag).filter(Boolean), [aiData]) const selectedLeafCategoryId = subCategoryId || categoryId || null const visibilitySummary = publishMode === 'schedule' ? `Scheduled as ${visibilityLabel(visibility)}` : visibilityLabel(visibility) const heroMeta = [ selectedCT?.name || 'No content type', selectedRoot?.name || 'No root category', subCategoryId ? subCategories.find((item) => item.id === subCategoryId)?.name : null, ].filter(Boolean) const publishingIdentityOptions = useMemo(() => { const personalOption = { value: '', label: 'Personal profile', icon: