ui(sprite-generation): 💄 Enhance sprite generation GUI with new options and improved preview/interaction in SpritePage and TheaterCard components
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
d9177021fe
commit
1793eabc63
2 changed files with 109 additions and 31 deletions
|
|
@ -9,11 +9,34 @@ import {
|
|||
regenerateSprite,
|
||||
updatePrompt,
|
||||
} from '../api'
|
||||
import type { Sprite, Variant } from '../types'
|
||||
import type { RecentVariant, Sprite, Variant } from '../types'
|
||||
import { colors, statusColors } from './theme'
|
||||
import { DimensionSection, ImageModal, VariantCard, variantSrc } from './SpriteComponents'
|
||||
import { DimensionSection, ImageModal, variantSrc } from './SpriteComponents'
|
||||
import { Card } from './TheaterCard'
|
||||
import { Tooltip } from '../components/Tooltip'
|
||||
|
||||
function variantToRecent(v: Variant, sprite: Sprite): RecentVariant {
|
||||
return {
|
||||
variant_id: v.id,
|
||||
sprite_id: v.sprite_id,
|
||||
category: sprite.category,
|
||||
entity_id: sprite.entity_id,
|
||||
raw_path: v.raw_path ?? '',
|
||||
processed_path: v.processed_path,
|
||||
seed: v.seed,
|
||||
created_at: v.created_at,
|
||||
rating: v.rating,
|
||||
notes: v.notes,
|
||||
is_approved: v.is_approved,
|
||||
scored_by: null,
|
||||
review_tier: null,
|
||||
quality_json: null,
|
||||
gates_json: null,
|
||||
reject_reason: v.reject_reason,
|
||||
quality_scorer: null,
|
||||
}
|
||||
}
|
||||
|
||||
export function SpritePage(): ReactNode {
|
||||
const params = useParams()
|
||||
const navigate = useNavigate()
|
||||
|
|
@ -342,29 +365,28 @@ export function SpritePage(): ReactNode {
|
|||
<div style={{ marginBottom: 32 }}>
|
||||
<h2 style={{ color: colors.text, fontSize: 18, marginBottom: 4 }}>Variants</h2>
|
||||
<p style={{ color: colors.muted, fontSize: 12, margin: '0 0 14px 0' }}>
|
||||
Keys 1-{Math.min(8, variants.length)} to select, Enter to approve selected, Esc to deselect
|
||||
Keys 1-{Math.min(8, variants.length)} to select · Enter to approve selected · Esc to deselect
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 12 }}>
|
||||
{variants.map((v, i) => (
|
||||
<VariantCard
|
||||
<div
|
||||
key={v.id}
|
||||
variant={v}
|
||||
index={i}
|
||||
isSelected={selectedIndex === i}
|
||||
onSelect={(): void => {
|
||||
setSelectedIndex(i)
|
||||
const src = variantSrc(v)
|
||||
if (src) {
|
||||
setModalSrc(src)
|
||||
}
|
||||
}}
|
||||
onApprove={(): void => {
|
||||
void handleApprove(v.id)
|
||||
}}
|
||||
onReject={(): void => {
|
||||
void handleRejectVariant(v.id)
|
||||
}}
|
||||
/>
|
||||
style={{ width: 220, flexShrink: 0, outline: selectedIndex === i ? `2px solid ${colors.highlight}` : 'none', borderRadius: 8 }}
|
||||
>
|
||||
<Card
|
||||
v={variantToRecent(v, sprite)}
|
||||
isNew={false}
|
||||
index={i}
|
||||
alwaysShowActions
|
||||
onSelect={(): void => {
|
||||
setSelectedIndex(i)
|
||||
const src = variantSrc(v)
|
||||
if (src) setModalSrc(src)
|
||||
}}
|
||||
onApprove={(): void => { load() }}
|
||||
onReject={(): void => { void handleRejectVariant(v.id) }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -232,9 +232,11 @@ export interface CardProps {
|
|||
onApprove: (variantId: number) => void
|
||||
onReject: (variantId: number) => void
|
||||
onSelect?: (v: RecentVariant) => void
|
||||
/** Show Approve/Reject buttons even when no scoring notes exist */
|
||||
alwaysShowActions?: boolean
|
||||
}
|
||||
|
||||
export const Card = memo(forwardRef<HTMLDivElement, CardProps>(function Card({ v, isNew, index, onApprove, onReject, onSelect }, ref) {
|
||||
export const Card = memo(forwardRef<HTMLDivElement, CardProps>(function Card({ v, isNew, index, onApprove, onReject, onSelect, alwaysShowActions }, ref) {
|
||||
const conf = useMemo(() => confidence(v), [v.notes])
|
||||
const scores = useMemo(() => parseScores(v.notes), [v.notes])
|
||||
const gateOk = useMemo(() => gatePassed(v), [v.notes])
|
||||
|
|
@ -360,15 +362,48 @@ export const Card = memo(forwardRef<HTMLDivElement, CardProps>(function Card({ v
|
|||
{/* ── Art frame ───────────────────────────────────────────── */}
|
||||
<div
|
||||
onClick={() => onSelect?.(v)}
|
||||
style={{ display: 'block', cursor: 'pointer' }}
|
||||
style={{ display: 'block', cursor: 'pointer', position: 'relative' }}
|
||||
title={`#${v.variant_id} — ${v.sprite_id} (click for details)`}
|
||||
>
|
||||
<img
|
||||
src={imgUrl ?? ''}
|
||||
alt={v.entity_id}
|
||||
style={{ width: '100%', display: 'block', aspectRatio: '1/1', objectFit: 'cover' }}
|
||||
loading="lazy"
|
||||
/>
|
||||
{imgUrl ? (
|
||||
<img
|
||||
src={imgUrl}
|
||||
alt={v.entity_id}
|
||||
style={{
|
||||
width: '100%', display: 'block', aspectRatio: '1/1', objectFit: 'cover',
|
||||
opacity: v.rating === -1 ? 0.35 : 1,
|
||||
filter: v.rating === -1 ? 'saturate(0)' : 'none',
|
||||
}}
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<div style={{
|
||||
width: '100%', aspectRatio: '1/1',
|
||||
background: '#0d0f1a',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
flexDirection: 'column', gap: 4,
|
||||
}}>
|
||||
<span style={{ fontSize: 22, opacity: 0.15 }}>▣</span>
|
||||
<span style={{ fontSize: 9, color: '#2a3044', letterSpacing: '0.5px' }}>no image</span>
|
||||
</div>
|
||||
)}
|
||||
{v.rating === -1 && (
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
pointerEvents: 'none',
|
||||
}}>
|
||||
<span style={{
|
||||
fontSize: 13, fontWeight: 800, color: '#ef4444',
|
||||
background: 'rgba(0,0,0,0.7)',
|
||||
border: '1px solid #ef444466',
|
||||
borderRadius: 4, padding: '3px 10px',
|
||||
letterSpacing: '1px', textTransform: 'uppercase',
|
||||
}}>
|
||||
Rejected
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Text box ────────────────────────────────────────────── */}
|
||||
|
|
@ -458,7 +493,28 @@ export const Card = memo(forwardRef<HTMLDivElement, CardProps>(function Card({ v
|
|||
}}>
|
||||
APPROVED
|
||||
</div>
|
||||
) : scores && (
|
||||
) : v.rating === -1 ? (
|
||||
<div style={{ display: 'flex', gap: 6, padding: '0 10px 8px' }}>
|
||||
<Tooltip text="Approve this variant despite prior rejection">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
approveVariant(v.sprite_id, v.variant_id).then(
|
||||
() => { onApprove(v.variant_id) },
|
||||
() => {},
|
||||
)
|
||||
}}
|
||||
style={{
|
||||
flex: 1, padding: '5px 0', borderRadius: 4, fontSize: 11, fontWeight: 600,
|
||||
background: '#10b98133', color: '#10b981', border: '1px solid #10b98155',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Approve anyway
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : (scores || alwaysShowActions) && imgUrl ? (
|
||||
<div style={{ display: 'flex', gap: 6, padding: '0 10px 8px' }}>
|
||||
<Tooltip text="Approve this variant as the final game sprite">
|
||||
<button
|
||||
|
|
@ -494,7 +550,7 @@ export const Card = memo(forwardRef<HTMLDivElement, CardProps>(function Card({ v
|
|||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue