diff --git a/tools/sprite-generation/gui/src/App.tsx b/tools/sprite-generation/gui/src/App.tsx
index df0b402d..e77ab133 100644
--- a/tools/sprite-generation/gui/src/App.tsx
+++ b/tools/sprite-generation/gui/src/App.tsx
@@ -5,6 +5,7 @@ import { ReviewQueuePage } from './pages/ReviewQueuePage'
import { CategoryPage } from './pages/CategoryPage'
import { SpritePage } from './pages/SpritePage'
import SpriteTheaterPage from './pages/SpriteTheaterPage'
+import VariantPage from './pages/VariantPage'
import { SpriteStream } from './SpriteStream'
import { colors } from './pages/theme'
@@ -69,6 +70,7 @@ export function App(): ReactElement {
} />
} />
} />
+ } />
} />
diff --git a/tools/sprite-generation/gui/src/SpriteStream.tsx b/tools/sprite-generation/gui/src/SpriteStream.tsx
index 37ea49f8..dbbfa68f 100644
--- a/tools/sprite-generation/gui/src/SpriteStream.tsx
+++ b/tools/sprite-generation/gui/src/SpriteStream.tsx
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState, useCallback, type ReactNode, type CSSProperties } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
-import { fetchRecentVariants, variantStreamUrl, rawImageUrl, variantImageUrl } from './api'
+import { fetchRecentVariants, variantStreamUrl, bestImageUrl, rawImageUrl } from './api'
import type { RecentVariant } from './types'
import { colors } from './pages/theme'
@@ -15,10 +15,6 @@ const GLOW_DURATION = 3000
const SCORE_FLASH_DURATION = 4000
const SHIFT_DURATION_MS = 500
-function extractFilename(path: string): string {
- const parts = path.replace(/\\/g, '/').split('/')
- return parts[parts.length - 1]
-}
function formatLabel(entityId: string): string {
return entityId.replace(/_/g, ' ')
@@ -137,16 +133,9 @@ function SpriteCard({
size: number
}): ReactNode {
const [loaded, setLoaded] = useState(false)
- const filename = variant.processed_path
- ? extractFilename(variant.processed_path)
- : variant.raw_path
- ? extractFilename(variant.raw_path)
- : null
- const primarySrc = filename
- ? (variant.processed_path ? variantImageUrl(filename) : rawImageUrl(filename))
- : null
- const fallbackSrc = filename
- ? (variant.processed_path ? rawImageUrl(filename) : variantImageUrl(filename))
+ const primarySrc = bestImageUrl(variant.processed_path, variant.raw_path)
+ const fallbackSrc = variant.raw_path
+ ? rawImageUrl(variant.raw_path.replace(/\\/g, '/').split('/').pop() ?? '')
: null
const [imgSrc, setImgSrc] = useState(primarySrc)
const triedFallback = useRef(false)
diff --git a/tools/sprite-generation/gui/src/api.ts b/tools/sprite-generation/gui/src/api.ts
index c487d340..671f63d4 100644
--- a/tools/sprite-generation/gui/src/api.ts
+++ b/tools/sprite-generation/gui/src/api.ts
@@ -1,4 +1,4 @@
-import type { Sprite, Variant, Stats, GenerationRun, RecentVariant } from './types'
+import type { Sprite, Variant, Stats, GenerationRun, RecentVariant, VariantScore } from './types'
const API_BASE = '/api'
@@ -158,6 +158,20 @@ export function variantImageUrl(path: string): string {
return `/images/variants/${path}`
}
+/** Resolve the best available image URL for a variant.
+ * Prefers processed (background removed) over raw. */
+export function bestImageUrl(processedPath: string | null, rawPath: string | null): string | null {
+ if (processedPath) {
+ const filename = processedPath.split('/').pop() ?? processedPath
+ return variantImageUrl(filename)
+ }
+ if (rawPath) {
+ const filename = rawPath.split('/').pop() ?? rawPath
+ return rawImageUrl(filename)
+ }
+ return null
+}
+
export interface Progress {
total: number
by_status: Record
@@ -187,6 +201,46 @@ export async function fetchRecentVariants(limit = 30): Promise
return request(buildUrl('/variants/recent', { limit }))
}
+export async function fetchVariant(variantId: number): Promise {
+ return request(buildUrl(`/variants/${variantId}`))
+}
+
+export async function fetchVariantScores(variantId: number): Promise {
+ return request(buildUrl(`/variants/${variantId}/scores`))
+}
+
export function variantStreamUrl(): string {
return `${API_BASE}/stream/variants`
}
+
+export interface PipelineState {
+ funnel: {
+ total_completed: number
+ total_processed: number
+ scoring: Record
+ approved: number
+ installed: number
+ }
+ failed_gates: Array<{ gate: string; count: number }>
+ sprite_coverage: Array<{
+ sprite_id: string
+ entity_id: string
+ total_variants: number
+ processed: number
+ tier_counts: Record
+ all_passed: number
+ deficit: number
+ }>
+ recent_scores: Array<{
+ variant_id: number
+ sprite_id: string
+ scorer_name: string
+ gate_passed: boolean
+ confidence: number
+ scored_at: string
+ }>
+}
+
+export async function fetchPipeline(): Promise {
+ return request(buildUrl('/pipeline'))
+}
diff --git a/tools/sprite-generation/gui/src/types.ts b/tools/sprite-generation/gui/src/types.ts
index f02476d8..b0aba56e 100644
--- a/tools/sprite-generation/gui/src/types.ts
+++ b/tools/sprite-generation/gui/src/types.ts
@@ -78,4 +78,26 @@ export interface RecentVariant {
rating: number | null
notes: string | null
is_approved: boolean
+ scored_by: string | null
+ review_tier: number | null
+}
+
+export interface TheaterPage {
+ items: RecentVariant[]
+ total: number
+}
+
+export interface VariantScore {
+ id: number
+ variant_id: number
+ scorer_name: string
+ scorer_model: string
+ tier: number
+ gates: string | null
+ quality: string | null
+ gate_passed: number
+ confidence: number
+ failed_gate_reason: string | null
+ quality_floor_failed: number
+ scored_at: string
}