feat(@projects/@magic-civilization): 🖥️ sprite-gen worker preferences + operations/coverage GUI updates

Adds a GUI preferences module and refines the worker engine, operations panel,
workers page, and coverage page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-21 07:59:40 -05:00
parent f3c1cb5564
commit e12307b43d
5 changed files with 99 additions and 23 deletions

View file

@ -236,17 +236,17 @@ class GenerationWorker:
submitted_this_loop = 0
if to_submit:
self._log(f"\n[loop {loop_count}] Submitting {len(to_submit)} sprites x {variants} variants...")
on_complete = _on_complete if config["backend"] == "model-boss" else None
submit_cb = on_complete if config["backend"] == "model-boss" else None
submitted_this_loop = await gen.submit_batch(
sprite_ids=to_submit,
variants_per=variants,
priority="high",
on_complete=on_complete,
on_complete=submit_cb,
)
collected = 0
if config["backend"] == "model-boss":
collected = await gen.collect_pending(on_complete=_on_complete)
collected = await gen.collect_pending(on_complete=on_complete)
if collected:
self._log(f" Collected {collected} images")
@ -285,14 +285,7 @@ class GenerationWorker:
)
if st["needed"] == 0 and st["queued_variants"] == 0:
self._log("\nAll sprites processed. Worker idle.")
for _ in range(15):
if self._stop.is_set():
break
await asyncio.sleep(2)
if self._stop.is_set():
break
self._log("\nAll sprites processed. Worker idle (polling).")
await asyncio.sleep(10)
else:
await asyncio.sleep(2)
self._log("Worker stopped.")
await asyncio.sleep(2)

View file

@ -11,6 +11,7 @@ import {
stopWorker,
type StarterStatus,
} from '../api'
import { loadStoredBackend, loadStoredVariants, saveBackend, saveVariants } from '../preferences'
import { colors } from './theme'
import { Tooltip } from '../components/Tooltip'
@ -33,13 +34,20 @@ export function OperationsPanel({
onBackendChange?: (b: string) => void
}): ReactNode {
const [backends, setBackends] = useState<string[]>(['model-boss', 'grok'])
const [internalBackend, setInternalBackend] = useState('model-boss')
const [internalBackend, setInternalBackend] = useState(() =>
loadStoredBackend(['model-boss', 'grok'], 'model-boss'),
)
const backend = controlledBackend ?? internalBackend
const setBackend = (b: string): void => {
saveBackend(b)
setInternalBackend(b)
onBackendChange?.(b)
}
const [variants, setVariants] = useState(3)
const [variants, setVariantsState] = useState(() => loadStoredVariants(3))
const setVariants = (n: number): void => {
saveVariants(n)
setVariantsState(n)
}
const [workerRunning, setWorkerRunning] = useState(false)
const [workerLog, setWorkerLog] = useState('')
const [workerStats, setWorkerStats] = useState({ queued: 0, needed: 0, review: 0 })
@ -75,7 +83,10 @@ export function OperationsPanel({
fetchConfig()
.then((c) => {
setBackends(c.backends)
setBackend(c.default_backend)
if (controlledBackend === undefined) {
const preferred = loadStoredBackend(c.backends, c.default_backend)
setBackend(preferred)
}
})
.catch(() => { /* defaults */ })
refreshWorker()

View file

@ -1,11 +1,12 @@
import { useCallback, useEffect, useState, type CSSProperties, type ReactNode } from 'react'
import { useNavigate } from 'react-router-dom'
import { FlashNumber, useFlashRow } from '@lilith/ui-animated'
import { fetchConfig, fetchPipeline, fetchVariants, triggerGenerate, type PipelineState } from '../api'
import { fetchPipeline, fetchVariants, triggerGenerate, type PipelineState } from '../api'
import type { Variant } from '../types'
import { colors, SCORER_COLORS, SCORER_ORDER, statusColors } from './theme'
import { Tooltip } from '../components/Tooltip'
import { OperationsPanel } from './OperationsPanel'
import { loadStoredBackend, saveBackend } from '../preferences'
type CoverageRow = PipelineState['sprite_coverage'][number]
type BtnState = 'idle' | 'loading' | 'queued' | 'error'
@ -453,7 +454,13 @@ export function SpriteCoveragePage(): ReactNode {
const [search, setSearch] = useState('')
const [categoryFilter, setCategoryFilter] = useState('all')
const [batchState, setBatchState] = useState<BtnState>('idle')
const [backend, setBackend] = useState('model-boss')
const [backend, setBackendState] = useState(() =>
loadStoredBackend(['model-boss', 'grok'], 'model-boss'),
)
const setBackend = (b: string): void => {
saveBackend(b)
setBackendState(b)
}
const load = useCallback((): void => {
fetchPipeline()
@ -470,7 +477,6 @@ export function SpriteCoveragePage(): ReactNode {
useEffect((): (() => void) => {
load()
fetchConfig().then((c) => setBackend(c.default_backend)).catch(() => { /* default */ })
const interval = setInterval(load, 5000)
return () => clearInterval(interval)
}, [load])

View file

@ -19,6 +19,14 @@ import {
type StarterStatus,
type WorkersOverview,
} from '../api'
import {
loadStoredBackend,
loadStoredBatchSize,
loadStoredVariants,
saveBackend,
saveBatchSize,
saveVariants,
} from '../preferences'
import { colors, SCORER_COLORS, SCORER_ORDER, SCORER_TOOLTIPS, type ScorerName } from './theme'
import { Tooltip } from '../components/Tooltip'
@ -213,9 +221,23 @@ const actionBtn = (color: string, disabled: boolean): CSSProperties => ({
export function WorkersPage(): ReactNode {
const [overview, setOverview] = useState<WorkersOverview | null>(null)
const [starter, setStarter] = useState<StarterStatus | null>(null)
const [backend, setBackend] = useState('model-boss')
const [variants, setVariants] = useState(3)
const [batchSize, setBatchSize] = useState(4)
const [backend, setBackendState] = useState(() =>
loadStoredBackend(['model-boss', 'grok'], 'model-boss'),
)
const [variants, setVariantsState] = useState(() => loadStoredVariants(3))
const [batchSize, setBatchSizeState] = useState(() => loadStoredBatchSize(4))
const setBackend = (b: string): void => {
saveBackend(b)
setBackendState(b)
}
const setVariants = (n: number): void => {
saveVariants(n)
setVariantsState(n)
}
const setBatchSize = (n: number): void => {
saveBatchSize(n)
setBatchSizeState(n)
}
const [selectedScorers, setSelectedScorers] = useState<Set<ScorerName>>(new Set(['qwen3']))
const [busy, setBusy] = useState<string | null>(null)
const [toast, setToast] = useState<string | null>(null)
@ -230,7 +252,6 @@ export function WorkersPage(): ReactNode {
fetchWorkersOverview()
.then((data) => {
setOverview(data)
setBackend(data.config.default_backend)
setError(null)
})
.catch((e: unknown) => {

View file

@ -0,0 +1,45 @@
const BACKEND_KEY = 'spritegen.backend'
const VARIANTS_KEY = 'spritegen.variants'
const BATCH_SIZE_KEY = 'spritegen.batch_size'
export function loadStoredBackend(allowed: string[], fallback: string): string {
try {
const stored = localStorage.getItem(BACKEND_KEY)
if (stored && allowed.includes(stored)) return stored
} catch { /* private browsing */ }
return fallback
}
export function saveBackend(backend: string): void {
try {
localStorage.setItem(BACKEND_KEY, backend)
} catch { /* ignore */ }
}
export function loadStoredVariants(fallback = 3): number {
try {
const n = parseInt(localStorage.getItem(VARIANTS_KEY) ?? '', 10)
if (n >= 1 && n <= 12) return n
} catch { /* ignore */ }
return fallback
}
export function saveVariants(n: number): void {
try {
localStorage.setItem(VARIANTS_KEY, String(Math.max(1, Math.min(12, n))))
} catch { /* ignore */ }
}
export function loadStoredBatchSize(fallback = 4): number {
try {
const n = parseInt(localStorage.getItem(BATCH_SIZE_KEY) ?? '', 10)
if (n >= 1 && n <= 20) return n
} catch { /* ignore */ }
return fallback
}
export function saveBatchSize(n: number): void {
try {
localStorage.setItem(BATCH_SIZE_KEY, String(Math.max(1, Math.min(20, n))))
} catch { /* ignore */ }
}