Restore toolbar background to bg-nebula; add toolbar backdrop blur
This commit is contained in:
@@ -5,7 +5,7 @@ export default function UploadStepper({ steps = [], activeStep = 1, highestUnloc
|
||||
|
||||
return (
|
||||
<nav aria-label="Upload steps" className="rounded-xl border border-white/50 bg-slate-900/70 px-3 py-3 sm:px-4">
|
||||
<ol className="flex flex-nowrap items-center gap-2 overflow-x-auto sm:gap-3">
|
||||
<ol className="flex flex-nowrap items-center gap-3 overflow-x-auto sm:gap-4">
|
||||
{steps.map((step, index) => {
|
||||
const number = index + 1
|
||||
const isActive = number === safeActive
|
||||
@@ -29,21 +29,21 @@ export default function UploadStepper({ steps = [], activeStep = 1, highestUnloc
|
||||
: 'border-white/20 bg-white/5 text-white/80'
|
||||
|
||||
return (
|
||||
<li key={step.key} className="flex min-w-0 items-center gap-2">
|
||||
<li key={step.key} className="flex-shrink-0 flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => canNavigate && onStepClick?.(number)}
|
||||
disabled={isLocked}
|
||||
aria-disabled={isLocked ? 'true' : 'false'}
|
||||
aria-current={isActive ? 'step' : undefined}
|
||||
className={`${baseBtn} ${stateClass}`}
|
||||
className={`${baseBtn} ${stateClass} flex-shrink-0`}
|
||||
>
|
||||
<span className={`grid h-5 w-5 place-items-center rounded-full border text-[11px] ${circleClass}`}>
|
||||
{isComplete ? '✓' : number}
|
||||
</span>
|
||||
<span className="whitespace-nowrap">{step.label}</span>
|
||||
<span className="whitespace-nowrap pr-3">{step.label}</span>
|
||||
</button>
|
||||
{index < steps.length - 1 && <span className="text-white/50">→</span>}
|
||||
{index < steps.length - 1 && <span className="text-white/50 mx-1 select-none">→</span>}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -354,6 +354,17 @@ export default function UploadWizard({
|
||||
rightsAccepted: false,
|
||||
contentType: '',
|
||||
})
|
||||
const [visionSuggestedTags, setVisionSuggestedTags] = useState([])
|
||||
const [visionDebug, setVisionDebug] = useState({
|
||||
enabled: null,
|
||||
queueConnection: '',
|
||||
queuedJobs: 0,
|
||||
failedJobs: 0,
|
||||
triggered: false,
|
||||
aiTagCount: 0,
|
||||
totalTagCount: 0,
|
||||
lastError: '',
|
||||
})
|
||||
const [isUploadLocked, setIsUploadLocked] = useState(false)
|
||||
const [resolvedArtworkId, setResolvedArtworkId] = useState(() => {
|
||||
const parsed = Number(initialDraftId)
|
||||
@@ -371,6 +382,7 @@ export default function UploadWizard({
|
||||
const requestControllersRef = useRef(new Set())
|
||||
const publishLockRef = useRef(false)
|
||||
const hasAutoAdvancedRef = useRef(false)
|
||||
const lastVisionFetchAtRef = useRef(0)
|
||||
|
||||
const effectiveChunkSize = useMemo(() => {
|
||||
const parsed = Number(chunkSize)
|
||||
@@ -422,6 +434,32 @@ export default function UploadWizard({
|
||||
}, [metadata, requiresSubCategory])
|
||||
|
||||
const detailsValid = Object.keys(metadataErrors).length === 0
|
||||
const mergedSuggestedTags = useMemo(() => {
|
||||
const normalized = new Map()
|
||||
const addTag = (item) => {
|
||||
if (!item) return
|
||||
const key = String(item?.slug || item?.tag || item?.name || item).trim().toLowerCase()
|
||||
if (!key) return
|
||||
if (!normalized.has(key)) {
|
||||
if (typeof item === 'string') {
|
||||
normalized.set(key, item)
|
||||
} else {
|
||||
normalized.set(key, {
|
||||
id: item.id ?? key,
|
||||
name: item.name || item.tag || item.slug || key,
|
||||
slug: item.slug || item.tag || key,
|
||||
usage_count: Number(item.usage_count || 0),
|
||||
is_ai: Boolean(item.is_ai || item.source === 'ai'),
|
||||
source: item.source || (item.is_ai ? 'ai' : 'manual'),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
;(Array.isArray(suggestedTags) ? suggestedTags : []).forEach(addTag)
|
||||
;(Array.isArray(visionSuggestedTags) ? visionSuggestedTags : []).forEach(addTag)
|
||||
return Array.from(normalized.values())
|
||||
}, [suggestedTags, visionSuggestedTags])
|
||||
const highestUnlockedStep = uploadReady ? (detailsValid ? 3 : 2) : 1
|
||||
const showProgress = machine.state !== machineStates.idle && machine.state !== machineStates.cancelled
|
||||
const canStartUpload = isValidForUpload(primaryFile, primaryErrors, isArchive, screenshotErrors)
|
||||
@@ -697,6 +735,68 @@ export default function UploadWizard({
|
||||
}
|
||||
}, [machine.sessionId, machine.uploadToken, fetchProcessingStatus, registerController, unregisterController])
|
||||
|
||||
const fetchVisionSuggestedTags = useCallback(async () => {
|
||||
if (!resolvedArtworkId || resolvedArtworkId <= 0) return
|
||||
|
||||
const now = Date.now()
|
||||
if (now - lastVisionFetchAtRef.current < 3000) return
|
||||
lastVisionFetchAtRef.current = now
|
||||
|
||||
try {
|
||||
const response = await window.axios.get(`/api/artworks/${resolvedArtworkId}/tags`, {
|
||||
params: { trigger: 1 },
|
||||
})
|
||||
const payload = response?.data || {}
|
||||
if (payload?.vision_enabled === false) {
|
||||
setVisionSuggestedTags([])
|
||||
setVisionDebug((current) => ({
|
||||
...current,
|
||||
enabled: false,
|
||||
lastError: '',
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
const aiTags = Array.isArray(payload?.ai_tags) ? payload.ai_tags : []
|
||||
setVisionSuggestedTags(aiTags)
|
||||
const debug = payload?.debug || {}
|
||||
setVisionDebug({
|
||||
enabled: Boolean(payload?.vision_enabled),
|
||||
queueConnection: String(debug?.queue_connection || ''),
|
||||
queuedJobs: Number(debug?.queued_jobs || 0),
|
||||
failedJobs: Number(debug?.failed_jobs || 0),
|
||||
triggered: Boolean(debug?.triggered),
|
||||
aiTagCount: Number(debug?.ai_tag_count || aiTags.length || 0),
|
||||
totalTagCount: Number(debug?.total_tag_count || 0),
|
||||
lastError: '',
|
||||
})
|
||||
if (typeof window !== 'undefined') {
|
||||
window.console?.debug?.('[upload][vision-tags]', {
|
||||
artworkId: resolvedArtworkId,
|
||||
aiTags: aiTags.map((tag) => tag?.slug || tag?.name || tag),
|
||||
debug,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
if (error?.response?.status === 404 || error?.response?.status === 403) return
|
||||
setVisionDebug((current) => ({
|
||||
...current,
|
||||
lastError: error?.response?.data?.message || error?.message || 'Vision tag fetch failed.',
|
||||
}))
|
||||
}
|
||||
}, [resolvedArtworkId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!resolvedArtworkId || activeStep < 2) return
|
||||
|
||||
fetchVisionSuggestedTags()
|
||||
const timer = window.setInterval(() => {
|
||||
fetchVisionSuggestedTags()
|
||||
}, 4000)
|
||||
|
||||
return () => window.clearInterval(timer)
|
||||
}, [resolvedArtworkId, activeStep, fetchVisionSuggestedTags])
|
||||
|
||||
useEffect(() => {
|
||||
if (machine.state !== machineStates.processing) {
|
||||
clearPolling()
|
||||
@@ -745,12 +845,21 @@ export default function UploadWizard({
|
||||
dispatchMachine({ type: 'PUBLISH_START' })
|
||||
|
||||
try {
|
||||
const publishTargetId = machine.sessionId || initialDraftId || resolvedArtworkId
|
||||
const publishTargetId = resolvedArtworkId || initialDraftId || machine.sessionId
|
||||
const publishPayload = {
|
||||
title: String(metadata.title || '').trim() || undefined,
|
||||
description: String(metadata.description || '').trim() || null,
|
||||
}
|
||||
|
||||
if (!machine.sessionId) {
|
||||
if (!publishTargetId) throw new Error('Missing publish id.')
|
||||
const publishController = registerController()
|
||||
await window.axios.post(uploadEndpoints.publish(publishTargetId), {}, { signal: publishController.signal })
|
||||
await window.axios.post(uploadEndpoints.publish(publishTargetId), publishPayload, { signal: publishController.signal })
|
||||
unregisterController(publishController)
|
||||
} else {
|
||||
if (!publishTargetId) throw new Error('Missing publish id.')
|
||||
const publishController = registerController()
|
||||
await window.axios.post(uploadEndpoints.publish(publishTargetId), publishPayload, { signal: publishController.signal })
|
||||
unregisterController(publishController)
|
||||
}
|
||||
|
||||
@@ -764,7 +873,7 @@ export default function UploadWizard({
|
||||
} finally {
|
||||
publishLockRef.current = false
|
||||
}
|
||||
}, [canPublish, machine.sessionId, initialDraftId, resolvedArtworkId, registerController, unregisterController])
|
||||
}, [canPublish, machine.sessionId, initialDraftId, resolvedArtworkId, metadata.title, metadata.description, registerController, unregisterController])
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
clearPolling()
|
||||
@@ -780,6 +889,7 @@ export default function UploadWizard({
|
||||
rightsAccepted: false,
|
||||
contentType: '',
|
||||
})
|
||||
setVisionSuggestedTags([])
|
||||
setResolvedArtworkId(() => {
|
||||
const parsed = Number(initialDraftId)
|
||||
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : null
|
||||
@@ -1059,13 +1169,26 @@ export default function UploadWizard({
|
||||
<UploadSidebar
|
||||
showHeader={false}
|
||||
metadata={metadata}
|
||||
suggestedTags={suggestedTags}
|
||||
suggestedTags={mergedSuggestedTags}
|
||||
errors={metadataErrors}
|
||||
onChangeTitle={(value) => setMetadata((current) => ({ ...current, title: value }))}
|
||||
onChangeTags={(value) => setMetadata((current) => ({ ...current, tags: value }))}
|
||||
onChangeDescription={(value) => setMetadata((current) => ({ ...current, description: value }))}
|
||||
onToggleRights={(value) => setMetadata((current) => ({ ...current, rightsAccepted: Boolean(value) }))}
|
||||
/>
|
||||
|
||||
{resolvedArtworkId ? (
|
||||
<div className="rounded-xl border border-white/10 bg-white/[0.03] p-3 text-xs text-white/70">
|
||||
<div className="font-medium text-white/85">Vision debug</div>
|
||||
<div className="mt-1">
|
||||
enabled: {String(visionDebug.enabled)} · queue: {visionDebug.queueConnection || 'n/a'} · ai tags: {visionDebug.aiTagCount} · total tags: {visionDebug.totalTagCount}
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
queued jobs: {visionDebug.queuedJobs} · failed jobs: {visionDebug.failedJobs} · trigger sent: {String(visionDebug.triggered)}
|
||||
</div>
|
||||
{visionDebug.lastError ? <div className="mt-1 text-red-300">error: {visionDebug.lastError}</div> : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user