managarten/picture/apps/mobile/store/batchStore.ts
Till-JS c712a2504a feat: integrate uload and picture, unify package naming
- Add uload project with apps/web structure
  - Reorganize from flat to monorepo structure
  - Remove PocketBase binary and local data
  - Update to pnpm and @uload/web namespace

- Add picture project to monorepo
  - Remove embedded git repository

- Unify all package names to @{project}/{app} schema:
  - @maerchenzauber/* (was @storyteller/*)
  - @manacore/* (was manacore-*, manacore)
  - @manadeck/* (was web, backend, manadeck)
  - @memoro/* (was memoro-web, landing, memoro)
  - @picture/* (already unified)
  - @uload/web

- Add convenient dev scripts for all apps:
  - pnpm dev:{project}:web
  - pnpm dev:{project}:landing
  - pnpm dev:{project}:mobile
  - pnpm dev:{project}:backend

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 04:00:36 +01:00

416 lines
No EOL
11 KiB
TypeScript

import { create } from 'zustand';
import { supabase } from '~/utils/supabase';
import { RealtimeChannel } from '@supabase/supabase-js';
export interface BatchPrompt {
text: string;
negative_prompt?: string;
seed?: number;
tags?: string[];
}
export interface SharedSettings {
model_id: string;
model_version: string;
width: number;
height: number;
steps: number;
guidance_scale: number;
}
export interface BatchGeneration {
id: string;
name: string;
total_count: number;
completed_count: number;
failed_count: number;
processing_count?: number;
pending_count?: number;
status: 'pending' | 'processing' | 'completed' | 'partial' | 'failed';
created_at: string;
completed_at?: string;
items?: BatchItem[];
}
export interface BatchItem {
id: string;
index: number;
prompt: string;
status: string;
error_message?: string;
retry_count?: number;
image_url?: string;
}
interface BatchStore {
// State
activeBatches: Map<string, BatchGeneration>;
currentBatch: BatchGeneration | null;
subscriptions: Map<string, RealtimeChannel>;
// UI State
isBatchModalOpen: boolean;
isCreatingBatch: boolean;
// Actions
createBatch: (prompts: BatchPrompt[], settings: SharedSettings, name?: string) => Promise<string>;
loadBatch: (batchId: string) => Promise<void>;
loadUserBatches: () => Promise<void>;
// Subscriptions
subscribeToBatch: (batchId: string) => void;
unsubscribeFromBatch: (batchId: string) => void;
unsubscribeAll: () => void;
// Batch Actions
retryFailed: (batchId: string) => Promise<void>;
cancelBatch: (batchId: string) => Promise<void>;
deleteBatch: (batchId: string) => Promise<void>;
// UI Actions
openBatchModal: () => void;
closeBatchModal: () => void;
setCurrentBatch: (batch: BatchGeneration | null) => void;
// Cleanup
reset: () => void;
}
export const useBatchStore = create<BatchStore>((set, get) => ({
// Initial State
activeBatches: new Map(),
currentBatch: null,
subscriptions: new Map(),
isBatchModalOpen: false,
isCreatingBatch: false,
// Create a new batch
createBatch: async (prompts, settings, name) => {
set({ isCreatingBatch: true });
try {
// Get the session to ensure we have a valid token
const { data: { session } } = await supabase.auth.getSession();
if (!session) throw new Error('Not authenticated');
// Call the batch-generate edge function with explicit auth header
const response = await supabase.functions.invoke('batch-generate', {
body: {
prompts: prompts.map(p => ({
text: p.text,
negative_prompt: p.negative_prompt,
seed: p.seed,
tags: p.tags
})),
shared_settings: settings,
batch_name: name
},
headers: {
Authorization: `Bearer ${session.access_token}`,
}
});
if (response.error) throw response.error;
const { batch } = response.data;
// Add to active batches
const newBatch: BatchGeneration = {
id: batch.id,
name: batch.name,
total_count: batch.total_count,
completed_count: 0,
failed_count: 0,
status: 'processing',
created_at: new Date().toISOString(),
items: batch.generations
};
set(state => {
const newBatches = new Map(state.activeBatches);
newBatches.set(batch.id, newBatch);
return {
activeBatches: newBatches,
currentBatch: newBatch,
isCreatingBatch: false
};
});
// Subscribe to updates
get().subscribeToBatch(batch.id);
return batch.id;
} catch (error) {
console.error('Error creating batch:', error);
set({ isCreatingBatch: false });
throw error;
}
},
// Load a specific batch
loadBatch: async (batchId) => {
try {
const { data: batch, error } = await supabase
.from('batch_progress')
.select('*')
.eq('id', batchId)
.single();
if (error) throw error;
const batchData: BatchGeneration = {
id: batch.id,
name: batch.name,
total_count: batch.total_count,
completed_count: batch.completed_count,
failed_count: batch.failed_count,
processing_count: batch.processing_count,
pending_count: batch.pending_count,
status: batch.status,
created_at: batch.created_at,
items: batch.items
};
set(state => {
const newBatches = new Map(state.activeBatches);
newBatches.set(batchId, batchData);
return {
activeBatches: newBatches,
currentBatch: batchData
};
});
} catch (error) {
console.error('Error loading batch:', error);
throw error;
}
},
// Load all user batches
loadUserBatches: async () => {
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) return;
const { data: batches, error } = await supabase
.from('batch_generations')
.select('*')
.eq('user_id', user.id)
.order('created_at', { ascending: false })
.limit(20);
if (error) throw error;
set(state => {
const newBatches = new Map(state.activeBatches);
batches?.forEach(batch => {
newBatches.set(batch.id, {
id: batch.id,
name: batch.name,
total_count: batch.total_count,
completed_count: batch.completed_count,
failed_count: batch.failed_count,
status: batch.status,
created_at: batch.created_at,
completed_at: batch.completed_at
});
});
return { activeBatches: newBatches };
});
} catch (error) {
console.error('Error loading batches:', error);
}
},
// Subscribe to batch updates
subscribeToBatch: (batchId) => {
const state = get();
// Don't subscribe if already subscribed
if (state.subscriptions.has(batchId)) return;
const channel = supabase
.channel(`batch_${batchId}`)
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'batch_generations',
filter: `id=eq.${batchId}`
},
(payload) => {
console.log('Batch update:', payload);
if (payload.new) {
set(state => {
const newBatches = new Map(state.activeBatches);
const existing = newBatches.get(batchId);
if (existing) {
newBatches.set(batchId, {
...existing,
...payload.new,
});
}
return { activeBatches: newBatches };
});
}
}
)
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'image_generations',
filter: `batch_id=eq.${batchId}`
},
async (payload) => {
console.log('Generation update:', payload);
// Reload the batch to get updated items
await get().loadBatch(batchId);
}
)
.subscribe();
set(state => {
const newSubs = new Map(state.subscriptions);
newSubs.set(batchId, channel);
return { subscriptions: newSubs };
});
},
// Unsubscribe from batch updates
unsubscribeFromBatch: (batchId) => {
const state = get();
const channel = state.subscriptions.get(batchId);
if (channel) {
channel.unsubscribe();
set(state => {
const newSubs = new Map(state.subscriptions);
newSubs.delete(batchId);
return { subscriptions: newSubs };
});
}
},
// Unsubscribe from all
unsubscribeAll: () => {
const state = get();
state.subscriptions.forEach(channel => channel.unsubscribe());
set({ subscriptions: new Map() });
},
// Retry failed generations in a batch
retryFailed: async (batchId) => {
try {
// Reset failed generations to pending
const { error } = await supabase
.from('image_generations')
.update({
status: 'pending',
error_message: null,
retry_count: 0
})
.eq('batch_id', batchId)
.eq('status', 'failed');
if (error) throw error;
// Update batch status
await supabase
.from('batch_generations')
.update({
status: 'processing',
failed_count: 0
})
.eq('id', batchId);
// Trigger queue processing
await supabase.functions.invoke('process-queue');
// Reload batch
await get().loadBatch(batchId);
} catch (error) {
console.error('Error retrying batch:', error);
throw error;
}
},
// Cancel a batch
cancelBatch: async (batchId) => {
try {
// Update pending generations to cancelled
await supabase
.from('image_generations')
.update({ status: 'failed', error_message: 'Cancelled by user' })
.eq('batch_id', batchId)
.in('status', ['pending']);
// Update batch status
await supabase
.from('batch_generations')
.update({
status: 'failed',
completed_at: new Date().toISOString()
})
.eq('id', batchId);
// Reload batch
await get().loadBatch(batchId);
} catch (error) {
console.error('Error cancelling batch:', error);
throw error;
}
},
// Delete a batch and all its generations
deleteBatch: async (batchId) => {
try {
// Delete will cascade to image_generations
const { error } = await supabase
.from('batch_generations')
.delete()
.eq('id', batchId);
if (error) throw error;
// Remove from state
set(state => {
const newBatches = new Map(state.activeBatches);
newBatches.delete(batchId);
return {
activeBatches: newBatches,
currentBatch: state.currentBatch?.id === batchId ? null : state.currentBatch
};
});
// Unsubscribe
get().unsubscribeFromBatch(batchId);
} catch (error) {
console.error('Error deleting batch:', error);
throw error;
}
},
// UI Actions
openBatchModal: () => set({ isBatchModalOpen: true }),
closeBatchModal: () => set({ isBatchModalOpen: false }),
setCurrentBatch: (batch) => set({ currentBatch: batch }),
// Reset store
reset: () => {
get().unsubscribeAll();
set({
activeBatches: new Map(),
currentBatch: null,
subscriptions: new Map(),
isBatchModalOpen: false,
isCreatingBatch: false
});
}
}));