piclets / src /lib /components /AutoTrainerScanner.svelte
Fraser's picture
t s
5435413
<script lang="ts">
import type { GradioClient } from '$lib/types';
import { TrainerScanService, type TrainerScanState } from '$lib/services/trainerScanService';
import { getScanningStats, resetFailedScans } from '$lib/db/trainerScanning';
interface Props {
joyCaptionClient: GradioClient;
zephyrClient: GradioClient;
fluxClient: GradioClient;
}
let { joyCaptionClient, zephyrClient, fluxClient }: Props = $props();
// Service instance
let scanService: TrainerScanService;
// Component state
let scanState: TrainerScanState = $state({
isScanning: false,
currentImage: null,
currentTrainer: null,
progress: {
total: 0,
completed: 0,
failed: 0,
pending: 0
},
error: null
});
let showAdvanced = $state(false);
let isInitializing = $state(false);
// Initialize service when clients are available
$effect(() => {
if (joyCaptionClient && zephyrClient && fluxClient) {
scanService = new TrainerScanService(joyCaptionClient, zephyrClient, fluxClient);
// Subscribe to state changes
scanService.onStateChange((newState) => {
scanState = newState;
});
// Load initial stats
loadStats();
}
});
async function loadStats() {
try {
const stats = await getScanningStats();
scanState.progress = {
total: stats.total,
completed: stats.completed,
failed: stats.failed,
pending: stats.pending
};
} catch (error) {
console.error('Failed to load scanning stats:', error);
}
}
async function handleStartScanning() {
if (!scanService) return;
try {
isInitializing = true;
await scanService.startScanning();
} catch (error) {
scanState.error = error instanceof Error ? error.message : 'Failed to start scanning';
} finally {
isInitializing = false;
}
}
function handleStopScanning() {
if (scanService) {
scanService.stopScanning();
}
}
async function handleResetFailed() {
try {
const resetCount = await resetFailedScans();
await loadStats();
scanState.error = null;
alert(`Reset ${resetCount} failed scans back to pending`);
} catch (error) {
scanState.error = error instanceof Error ? error.message : 'Failed to reset failed scans';
}
}
async function handleInitializeDatabase() {
if (!scanService) return;
try {
isInitializing = true;
await scanService.initializeFromFile();
await loadStats();
scanState.error = null;
} catch (error) {
scanState.error = error instanceof Error ? error.message : 'Failed to initialize database';
} finally {
isInitializing = false;
}
}
function getProgressPercentage(): number {
if (scanState.progress.total === 0) return 0;
return Math.round((scanState.progress.completed / scanState.progress.total) * 100);
}
function formatTrainerName(name: string | null): string {
if (!name) return '';
return name.replace(/^\d+_/, '').replace(/_/g, ' ');
}
</script>
<div class="auto-trainer-scanner">
<div class="scanner-header">
<h3>🤖 Auto Trainer Scanner</h3>
<button
class="toggle-advanced"
onclick={() => showAdvanced = !showAdvanced}
>
{showAdvanced ? '▼' : '▶'} Advanced
</button>
</div>
<!-- Main Controls -->
<div class="main-controls">
{#if scanState.progress.total === 0}
<div class="setup-section">
<p>Initialize the trainer scanning database to get started.</p>
<button
class="init-button"
onclick={handleInitializeDatabase}
disabled={isInitializing}
>
{isInitializing ? 'Initializing...' : 'Initialize Scanner'}
</button>
</div>
{:else}
<div class="scan-controls">
{#if !scanState.isScanning}
<button
class="start-button"
onclick={handleStartScanning}
disabled={scanState.progress.pending === 0 || isInitializing}
>
{isInitializing ? 'Starting...' : 'Start Auto Scan'}
</button>
{:else}
<button
class="stop-button"
onclick={handleStopScanning}
>
Stop Scanning
</button>
{/if}
</div>
{/if}
</div>
<!-- Progress Display -->
{#if scanState.progress.total > 0}
<div class="progress-section">
<div class="progress-stats">
<div class="stat">
<span class="stat-value">{scanState.progress.completed}</span>
<span class="stat-label">Completed</span>
</div>
<div class="stat">
<span class="stat-value">{scanState.progress.pending}</span>
<span class="stat-label">Pending</span>
</div>
<div class="stat">
<span class="stat-value">{scanState.progress.failed}</span>
<span class="stat-label">Failed</span>
</div>
<div class="stat">
<span class="stat-value">{scanState.progress.total}</span>
<span class="stat-label">Total</span>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: {getProgressPercentage()}%"></div>
<span class="progress-text">{getProgressPercentage()}%</span>
</div>
{#if scanState.isScanning && scanState.currentTrainer}
<div class="current-processing">
<div class="processing-spinner"></div>
<span>Processing: {formatTrainerName(scanState.currentTrainer)}</span>
</div>
{/if}
</div>
{/if}
<!-- Error Display -->
{#if scanState.error}
<div class="error-section">
<div class="error-message">
⚠️ {scanState.error}
</div>
<button
class="error-dismiss"
onclick={() => scanState.error = null}
>
Dismiss
</button>
</div>
{/if}
<!-- Advanced Controls -->
{#if showAdvanced}
<div class="advanced-section">
<h4>Advanced Options</h4>
<div class="advanced-controls">
<button
class="reset-button"
onclick={handleResetFailed}
disabled={scanState.progress.failed === 0}
>
Reset Failed ({scanState.progress.failed})
</button>
<button
class="reinit-button"
onclick={handleInitializeDatabase}
disabled={isInitializing}
>
Re-initialize Database
</button>
</div>
<div class="info-section">
<p><strong>How it works:</strong></p>
<ul>
<li>Loads trainer images from HuggingFace dataset</li>
<li>Generates unique piclets using Joy Caption + Zephyr-7B</li>
<li>Tracks progress in IndexedDB to prevent duplicates</li>
<li>Can be stopped and resumed at any time</li>
</ul>
</div>
</div>
{/if}
</div>
<style>
.auto-trainer-scanner {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
padding: 20px;
margin: 16px 0;
color: white;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
.scanner-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.scanner-header h3 {
margin: 0;
font-size: 1.2rem;
font-weight: 600;
}
.toggle-advanced {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.2s;
}
.toggle-advanced:hover {
background: rgba(255, 255, 255, 0.3);
}
.main-controls {
margin-bottom: 16px;
}
.setup-section {
text-align: center;
padding: 20px;
}
.setup-section p {
margin-bottom: 16px;
opacity: 0.9;
}
.init-button {
background: #4CAF50;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
.init-button:hover:not(:disabled) {
background: #45a049;
}
.init-button:disabled {
background: #666;
cursor: not-allowed;
}
.scan-controls {
display: flex;
justify-content: center;
}
.start-button {
background: #4CAF50;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
.start-button:hover:not(:disabled) {
background: #45a049;
}
.start-button:disabled {
background: #666;
cursor: not-allowed;
}
.stop-button {
background: #f44336;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
.stop-button:hover {
background: #da190b;
}
.progress-section {
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
.progress-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 16px;
}
.stat {
text-align: center;
}
.stat-value {
display: block;
font-size: 1.5rem;
font-weight: bold;
color: #FFD700;
}
.stat-label {
display: block;
font-size: 0.8rem;
opacity: 0.8;
margin-top: 4px;
}
.progress-bar {
position: relative;
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
height: 20px;
overflow: hidden;
margin-bottom: 12px;
}
.progress-fill {
background: linear-gradient(90deg, #FFD700, #FFA500);
height: 100%;
transition: width 0.3s ease;
border-radius: 10px;
}
.progress-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 0.8rem;
font-weight: bold;
color: #333;
}
.current-processing {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.9rem;
opacity: 0.9;
}
.processing-spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top: 2px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.error-section {
background: rgba(244, 67, 54, 0.2);
border: 1px solid rgba(244, 67, 54, 0.5);
border-radius: 8px;
padding: 12px;
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.error-message {
font-size: 0.9rem;
}
.error-dismiss {
background: none;
border: 1px solid rgba(255, 255, 255, 0.5);
color: white;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
}
.error-dismiss:hover {
background: rgba(255, 255, 255, 0.1);
}
.advanced-section {
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 16px;
margin-top: 16px;
}
.advanced-section h4 {
margin: 0 0 12px 0;
font-size: 1rem;
}
.advanced-controls {
display: flex;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.reset-button, .reinit-button {
background: rgba(255, 255, 255, 0.2);
color: white;
border: 1px solid rgba(255, 255, 255, 0.3);
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.2s;
}
.reset-button:hover:not(:disabled), .reinit-button:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.3);
}
.reset-button:disabled, .reinit-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.info-section {
font-size: 0.85rem;
opacity: 0.9;
}
.info-section ul {
margin: 8px 0 0 0;
padding-left: 20px;
}
.info-section li {
margin-bottom: 4px;
}
@media (max-width: 600px) {
.progress-stats {
grid-template-columns: repeat(2, 1fr);
}
.advanced-controls {
flex-direction: column;
}
.reset-button, .reinit-button {
width: 100%;
}
}
</style>