managarten/picture/apps/mobile/store/modelStore.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

197 lines
No EOL
5.2 KiB
TypeScript

import { create } from 'zustand';
import { Model, getActiveModels } from '~/services/models';
import { logger } from '~/utils/logger';
interface ModelState {
// State
models: Model[];
selectedModel: Model | null;
isLoading: boolean;
error: string | null;
lastFetch: number | null;
// Actions
loadModels: (force?: boolean) => Promise<void>;
setSelectedModel: (model: Model | null) => void;
clearError: () => void;
resetLoadingState: () => void;
// Derived state
getDefaultModel: () => Model | null;
isStale: () => boolean;
}
// Cache TTL in milliseconds (5 minutes)
const CACHE_TTL = 5 * 60 * 1000;
export const useModelStore = create<ModelState>((set, get) => ({
// Initial state
models: [],
selectedModel: null,
isLoading: false,
error: null,
lastFetch: null,
// Load models with caching
loadModels: async (force = false) => {
const state = get();
// Check if we should skip loading (cache is fresh and not forced)
// IMPORTANT: Don't use cache if there was an error or no models
if (!force && state.models.length > 0 && !state.error && !state.isStale()) {
logger.debug('Using cached models');
// If no model is selected yet, select the default one
if (!state.selectedModel && state.models.length > 0) {
const defaultModel = state.getDefaultModel();
if (defaultModel) {
set({ selectedModel: defaultModel });
}
}
return;
}
// If already loading, just return without waiting
// This prevents multiple components from creating race conditions
if (state.isLoading && !force) {
logger.debug('Model loading already in progress, skipping...');
return;
}
logger.info('Loading models from server');
set({ isLoading: true, error: null });
// Set up a safety timeout to prevent stuck loading state
const timeoutId = setTimeout(() => {
const currentState = get();
if (currentState.isLoading) {
logger.warn('Model loading timeout, resetting state');
set({
isLoading: false,
error: 'Timeout beim Laden der Modelle. Bitte erneut versuchen.',
});
}
}, 10000); // 10 second timeout
try {
const models = await getActiveModels();
const now = Date.now();
// Clear the timeout since we completed successfully
clearTimeout(timeoutId);
// Check if we actually got models
if (!models || models.length === 0) {
logger.warn('No models returned from API');
set({
models: [],
selectedModel: null,
isLoading: false,
error: 'Keine Modelle verfügbar. Bitte wende dich an den Support.',
lastFetch: now,
});
return;
}
// Find default model
const defaultModel = models.find(m => m.is_default) || models[0] || null;
set({
models,
selectedModel: state.selectedModel || defaultModel,
isLoading: false,
error: null,
lastFetch: now,
});
logger.success(`Loaded ${models.length} models`);
} catch (error: any) {
// Clear the timeout since we completed (with error)
clearTimeout(timeoutId);
logger.error('Error loading models:', error);
// More specific error messages
let errorMessage = 'Fehler beim Laden der Modelle';
if (error.message?.includes('Failed to fetch')) {
errorMessage = 'Netzwerkfehler. Bitte überprüfe deine Internetverbindung.';
} else if (error.message?.includes('401') || error.message?.includes('403')) {
errorMessage = 'Authentifizierungsfehler. Bitte melde dich erneut an.';
} else if (error.message) {
errorMessage = error.message;
}
set({
models: [],
selectedModel: null,
isLoading: false,
error: errorMessage,
lastFetch: null, // Don't cache failed requests
});
}
},
// Set selected model
setSelectedModel: (model) => {
set({ selectedModel: model });
},
// Clear error
clearError: () => {
set({ error: null });
},
// Reset loading state - emergency function to fix stuck loading
resetLoadingState: () => {
set({ isLoading: false });
logger.debug('Loading state reset');
},
// Get default model
getDefaultModel: () => {
const { models } = get();
return models.find(m => m.is_default) || models[0] || null;
},
// Check if cache is stale
isStale: () => {
const { lastFetch } = get();
if (!lastFetch) return true;
return Date.now() - lastFetch > CACHE_TTL;
},
}));
// Helper hook for convenient access to commonly used values
export const useModelSelection = () => {
const {
models,
selectedModel,
isLoading,
error,
setSelectedModel,
loadModels,
clearError,
resetLoadingState,
} = useModelStore();
return {
models,
selectedModel,
isLoading,
error,
hasModels: models.length > 0,
setSelectedModel,
loadModels,
clearError,
resetLoadingState,
};
};
// Background loader - can be called from app initialization
export const preloadModels = async () => {
logger.info('Preloading models in background');
const store = useModelStore.getState();
await store.loadModels();
};