mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 10:19:40 +02:00
Projects included: - maerchenzauber (NestJS backend + Expo mobile + SvelteKit web + Astro landing) - manacore (Expo mobile + SvelteKit web + Astro landing) - manadeck (NestJS backend + Expo mobile + SvelteKit web) - memoro (Expo mobile + SvelteKit web + Astro landing) This commit preserves the current state before monorepo restructuring. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
318 lines
8.1 KiB
TypeScript
318 lines
8.1 KiB
TypeScript
import { create } from 'zustand';
|
|
import {
|
|
SupabaseAIService as AIService,
|
|
GeneratedCard,
|
|
GenerationOptions,
|
|
} from '../utils/supabaseAIService';
|
|
import { supabase } from '../utils/supabase';
|
|
import { Card } from './cardStore';
|
|
|
|
interface AIState {
|
|
// State
|
|
isGenerating: boolean;
|
|
generatedCards: GeneratedCard[];
|
|
error: string | null;
|
|
usage: {
|
|
tokensUsed: number;
|
|
cost: number;
|
|
};
|
|
audioRecording: {
|
|
isRecording: boolean;
|
|
uri: string | null;
|
|
duration: number;
|
|
};
|
|
|
|
// Actions
|
|
generateCardsFromText: (text: string, options?: GenerationOptions) => Promise<GeneratedCard[]>;
|
|
generateCardsFromAudio: (audioUri: string) => Promise<GeneratedCard[]>;
|
|
generateCardsFromImage: (imageUri: string, context?: string) => Promise<GeneratedCard[]>;
|
|
enhanceCard: (card: Card) => Promise<Card>;
|
|
generateRelatedCards: (card: Card) => Promise<GeneratedCard[]>;
|
|
|
|
// Audio Recording
|
|
startRecording: () => Promise<void>;
|
|
stopRecording: () => Promise<string>;
|
|
|
|
// Utility
|
|
clearGeneratedCards: () => void;
|
|
saveGeneratedCards: (deckId: string, cards: GeneratedCard[]) => Promise<void>;
|
|
trackUsage: (tokens: number, cost: number) => void;
|
|
}
|
|
|
|
export const useAIStore = create<AIState>((set, get) => ({
|
|
// Initial State
|
|
isGenerating: false,
|
|
generatedCards: [],
|
|
error: null,
|
|
usage: {
|
|
tokensUsed: 0,
|
|
cost: 0,
|
|
},
|
|
audioRecording: {
|
|
isRecording: false,
|
|
uri: null,
|
|
duration: 0,
|
|
},
|
|
|
|
// Generate cards from text
|
|
generateCardsFromText: async (text: string, options?: GenerationOptions) => {
|
|
set({ isGenerating: true, error: null });
|
|
|
|
try {
|
|
const cards = await AIService.generateCardsFromText(text, options);
|
|
|
|
set((state) => ({
|
|
generatedCards: [...state.generatedCards, ...cards],
|
|
isGenerating: false,
|
|
}));
|
|
|
|
// Track usage (estimated)
|
|
get().trackUsage(text.length / 4, text.length * 0.00002);
|
|
|
|
return cards;
|
|
} catch (error: any) {
|
|
set({
|
|
error: error.message || 'Fehler beim Generieren der Karten',
|
|
isGenerating: false,
|
|
});
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
// Generate cards from audio
|
|
generateCardsFromAudio: async (audioUri: string) => {
|
|
set({ isGenerating: true, error: null });
|
|
|
|
try {
|
|
// Read audio file and convert to base64
|
|
const { FileSystem } = await import('expo-file-system');
|
|
const audioBase64 = await FileSystem.readAsStringAsync(audioUri, {
|
|
encoding: FileSystem.EncodingType.Base64,
|
|
});
|
|
|
|
const cards = await AIService.generateCardsFromSpeech(audioBase64);
|
|
|
|
set((state) => ({
|
|
generatedCards: [...state.generatedCards, ...cards],
|
|
isGenerating: false,
|
|
}));
|
|
|
|
// Track usage (estimated for Whisper + GPT)
|
|
get().trackUsage(1000, 0.02);
|
|
|
|
return cards;
|
|
} catch (error: any) {
|
|
set({
|
|
error: error.message || 'Fehler beim Verarbeiten der Audioaufnahme',
|
|
isGenerating: false,
|
|
});
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
// Generate cards from image
|
|
generateCardsFromImage: async (imageUri: string, context?: string) => {
|
|
set({ isGenerating: true, error: null });
|
|
|
|
try {
|
|
// Read image file and convert to base64
|
|
const { FileSystem } = await import('expo-file-system');
|
|
const imageBase64 = await FileSystem.readAsStringAsync(imageUri, {
|
|
encoding: FileSystem.EncodingType.Base64,
|
|
});
|
|
|
|
const cards = await AIService.generateCardsFromImage(imageBase64, context);
|
|
|
|
set((state) => ({
|
|
generatedCards: [...state.generatedCards, ...cards],
|
|
isGenerating: false,
|
|
}));
|
|
|
|
// Track usage (estimated for Vision API)
|
|
get().trackUsage(2000, 0.03);
|
|
|
|
return cards;
|
|
} catch (error: any) {
|
|
set({
|
|
error: error.message || 'Fehler beim Verarbeiten des Bildes',
|
|
isGenerating: false,
|
|
});
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
// Enhance existing card
|
|
enhanceCard: async (card: Card) => {
|
|
set({ isGenerating: true, error: null });
|
|
|
|
try {
|
|
const contentString = JSON.stringify(card.content);
|
|
const enhancedContent = await AIService.enhanceCardContent(contentString, card.type);
|
|
|
|
const enhancedCard: Card = {
|
|
...card,
|
|
content: JSON.parse(enhancedContent),
|
|
};
|
|
|
|
set({ isGenerating: false });
|
|
|
|
// Track usage (estimated)
|
|
get().trackUsage(500, 0.01);
|
|
|
|
return enhancedCard;
|
|
} catch (error: any) {
|
|
set({
|
|
error: error.message || 'Fehler beim Verbessern der Karte',
|
|
isGenerating: false,
|
|
});
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
// Generate related cards
|
|
generateRelatedCards: async (card: Card) => {
|
|
set({ isGenerating: true, error: null });
|
|
|
|
try {
|
|
const cards = await AIService.generateRelatedCards(card.content);
|
|
|
|
set((state) => ({
|
|
generatedCards: [...state.generatedCards, ...cards],
|
|
isGenerating: false,
|
|
}));
|
|
|
|
// Track usage (estimated)
|
|
get().trackUsage(800, 0.015);
|
|
|
|
return cards;
|
|
} catch (error: any) {
|
|
set({
|
|
error: error.message || 'Fehler beim Generieren verwandter Karten',
|
|
isGenerating: false,
|
|
});
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
// Audio Recording
|
|
startRecording: async () => {
|
|
try {
|
|
const { Audio } = await import('expo-av');
|
|
|
|
// Request permissions
|
|
const { status } = await Audio.requestPermissionsAsync();
|
|
if (status !== 'granted') {
|
|
throw new Error('Mikrofonzugriff verweigert');
|
|
}
|
|
|
|
// Configure audio
|
|
await Audio.setAudioModeAsync({
|
|
allowsRecordingIOS: true,
|
|
playsInSilentModeIOS: true,
|
|
});
|
|
|
|
// Create and start recording
|
|
const recording = new Audio.Recording();
|
|
await recording.prepareToRecordAsync(Audio.RecordingOptionsPresets.HIGH_QUALITY);
|
|
await recording.startAsync();
|
|
|
|
// Store recording instance (we'll need to manage this differently in production)
|
|
(global as any).currentRecording = recording;
|
|
|
|
set({
|
|
audioRecording: {
|
|
isRecording: true,
|
|
uri: null,
|
|
duration: 0,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error('Error starting recording:', error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
stopRecording: async () => {
|
|
try {
|
|
const recording = (global as any).currentRecording;
|
|
if (!recording) {
|
|
throw new Error('Keine aktive Aufnahme');
|
|
}
|
|
|
|
await recording.stopAndUnloadAsync();
|
|
const uri = recording.getURI();
|
|
|
|
if (!uri) {
|
|
throw new Error('Keine Aufnahme-URI erhalten');
|
|
}
|
|
|
|
// Clean up
|
|
delete (global as any).currentRecording;
|
|
|
|
set({
|
|
audioRecording: {
|
|
isRecording: false,
|
|
uri,
|
|
duration: 0,
|
|
},
|
|
});
|
|
|
|
return uri;
|
|
} catch (error) {
|
|
console.error('Error stopping recording:', error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
// Clear generated cards
|
|
clearGeneratedCards: () => {
|
|
set({ generatedCards: [], error: null });
|
|
},
|
|
|
|
// Save generated cards to deck
|
|
saveGeneratedCards: async (deckId: string, cards: GeneratedCard[]) => {
|
|
try {
|
|
const {
|
|
data: { user },
|
|
} = await supabase.auth.getUser();
|
|
if (!user) throw new Error('Nicht authentifiziert');
|
|
|
|
// Convert generated cards to database format
|
|
const cardsToInsert = cards.map((card, index) => ({
|
|
deck_id: deckId,
|
|
type: card.type,
|
|
content: card.content,
|
|
position: index,
|
|
created_by: user.id,
|
|
}));
|
|
|
|
const { error } = await supabase.from('cards').insert(cardsToInsert);
|
|
|
|
if (error) throw error;
|
|
|
|
// Track AI-generated cards
|
|
const aiTracking = cards.map((card) => ({
|
|
generation_method: card.metadata.source,
|
|
confidence_score: card.metadata.confidence,
|
|
source_data: { tags: card.metadata.tags },
|
|
}));
|
|
|
|
await supabase.from('ai_generated_cards').insert(aiTracking);
|
|
|
|
set({ generatedCards: [] });
|
|
} catch (error) {
|
|
console.error('Error saving generated cards:', error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
// Track API usage
|
|
trackUsage: (tokens: number, cost: number) => {
|
|
set((state) => ({
|
|
usage: {
|
|
tokensUsed: state.usage.tokensUsed + tokens,
|
|
cost: state.usage.cost + cost,
|
|
},
|
|
}));
|
|
},
|
|
}));
|