From e12307b43d8a10b8aab8cd2416c6102835fd6422 Mon Sep 17 00:00:00 2001 From: Natalie Date: Sun, 21 Jun 2026 07:59:40 -0500 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=F0=9F=96=A5=EF=B8=8F=20sprite-gen=20worker=20preferences=20+?= =?UTF-8?q?=20operations/coverage=20GUI=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a GUI preferences module and refines the worker engine, operations panel, workers page, and coverage page. Co-Authored-By: Claude Opus 4.8 --- tools/sprite-generation/engine/worker.py | 19 +++----- .../gui/src/pages/OperationsPanel.tsx | 17 +++++-- .../gui/src/pages/SpriteCoveragePage.tsx | 12 +++-- .../gui/src/pages/WorkersPage.tsx | 29 ++++++++++-- .../sprite-generation/gui/src/preferences.ts | 45 +++++++++++++++++++ 5 files changed, 99 insertions(+), 23 deletions(-) create mode 100644 tools/sprite-generation/gui/src/preferences.ts diff --git a/tools/sprite-generation/engine/worker.py b/tools/sprite-generation/engine/worker.py index 75855f41..d4414988 100644 --- a/tools/sprite-generation/engine/worker.py +++ b/tools/sprite-generation/engine/worker.py @@ -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.") \ No newline at end of file + await asyncio.sleep(2) \ No newline at end of file diff --git a/tools/sprite-generation/gui/src/pages/OperationsPanel.tsx b/tools/sprite-generation/gui/src/pages/OperationsPanel.tsx index 45459de4..3a2d0f1c 100644 --- a/tools/sprite-generation/gui/src/pages/OperationsPanel.tsx +++ b/tools/sprite-generation/gui/src/pages/OperationsPanel.tsx @@ -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(['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() diff --git a/tools/sprite-generation/gui/src/pages/SpriteCoveragePage.tsx b/tools/sprite-generation/gui/src/pages/SpriteCoveragePage.tsx index c7b3e959..a010fae7 100644 --- a/tools/sprite-generation/gui/src/pages/SpriteCoveragePage.tsx +++ b/tools/sprite-generation/gui/src/pages/SpriteCoveragePage.tsx @@ -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('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]) diff --git a/tools/sprite-generation/gui/src/pages/WorkersPage.tsx b/tools/sprite-generation/gui/src/pages/WorkersPage.tsx index 0016362f..71a391ba 100644 --- a/tools/sprite-generation/gui/src/pages/WorkersPage.tsx +++ b/tools/sprite-generation/gui/src/pages/WorkersPage.tsx @@ -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(null) const [starter, setStarter] = useState(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>(new Set(['qwen3'])) const [busy, setBusy] = useState(null) const [toast, setToast] = useState(null) @@ -230,7 +252,6 @@ export function WorkersPage(): ReactNode { fetchWorkersOverview() .then((data) => { setOverview(data) - setBackend(data.config.default_backend) setError(null) }) .catch((e: unknown) => { diff --git a/tools/sprite-generation/gui/src/preferences.ts b/tools/sprite-generation/gui/src/preferences.ts new file mode 100644 index 00000000..7e592309 --- /dev/null +++ b/tools/sprite-generation/gui/src/preferences.ts @@ -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 */ } +} \ No newline at end of file