import React, { useState, useEffect, useRef } from 'react'; import { useFirstVisit } from '../hooks/useFirstVisit'; import { View, StyleSheet, Alert, ActivityIndicator, Platform, TouchableOpacity, Keyboard, useWindowDimensions, ScrollView, TextInput, Linking, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; import { MaterialIcons, MaterialCommunityIcons } from '@expo/vector-icons'; import Text from '../components/atoms/Text'; import Button from '../components/atoms/Button'; import TextField from '../components/atoms/TextField'; import { Image } from 'react-native'; import * as ImagePicker from 'expo-image-picker'; import CommonHeader from '../components/molecules/CommonHeader'; import TabSwitcher from '../components/molecules/TabSwitcher'; import PremiumCuddlyToyCard from '../components/molecules/PremiumCuddlyToyCard'; import { fetchWithAuth, isCreditError } from '../src/utils/api'; import { dataService } from '../src/utils/dataService'; import { usePostHog } from '../src/hooks/usePostHog'; import MagicalLoadingScreen from '../components/molecules/MagicalLoadingScreen'; import { router } from 'expo-router'; import type { Character, CustomImage } from '../types/character'; import { showErrorAlert } from '../src/components/ErrorAlert'; import { analytics } from '../src/services/analytics'; const useResponsiveLayout = () => { const { width: windowWidth, height: windowHeight } = useWindowDimensions(); const isTablet = windowWidth >= 768 && windowWidth < 1400; const isDesktop = windowWidth >= 1400; const isLandscape = windowWidth > windowHeight; const isTabletPortrait = isTablet && !isLandscape; const isTabletLandscape = isTablet && isLandscape; // Font sizes const sectionTitleSize = isTablet || isDesktop ? 26 : 18; const sectionInfoSize = isTablet || isDesktop ? 22 : 18; const infoIconSize = isTablet || isDesktop ? 32 : 24; const uploadTextPrimarySize = isTablet || isDesktop ? 24 : 18; const uploadTextSecondarySize = isTablet || isDesktop ? 18 : 14; const tipTextSize = isTablet || isDesktop ? 17 : 13; return { isTablet, isTabletPortrait, isTabletLandscape, isDesktop, isLandscape, windowWidth, sectionTitleSize, sectionInfoSize, infoIconSize, uploadTextPrimarySize, uploadTextSecondarySize, tipTextSize, }; }; const SectionHeader = ({ title, showInfo, onToggleInfo, titleSize, iconSize, }: { title: string; showInfo: boolean; onToggleInfo: () => void; titleSize: number; iconSize: number; }) => ( {title} ); export default function CreateCharacter() { const [isLoading, setIsLoading] = useState(false); const [characterDescription, setCharacterDescription] = useState(''); const [characterName, setCharacterName] = useState(''); const [showNameInfo, setShowNameInfo] = useState(false); const [showDescriptionInfo, setShowDescriptionInfo] = useState(false); const [showPhotoInfo, setShowPhotoInfo] = useState(false); const [generatedImages, setGeneratedImages] = useState>([]); const [error, setError] = useState(''); const [selectedImage, setSelectedImage] = useState(null); const [characterId, setCharacterId] = useState(''); const [uploadedImage, setUploadedImage] = useState(null); const [activeTab, setActiveTab] = useState<'photo' | 'description'>('photo'); const [loadingContext, setLoadingContext] = useState<'character' | 'cuddly_toy'>('character'); // Ref to prevent race conditions from double-taps/rapid button presses // This guards against multiple simultaneous requests before React re-renders const isGeneratingRef = useRef(false); const { isTablet, isDesktop, sectionTitleSize, sectionInfoSize, infoIconSize, uploadTextPrimarySize, uploadTextSecondarySize, tipTextSize, } = useResponsiveLayout(); const dynamicStyles = { container: { maxWidth: 800, width: '100%', alignSelf: 'center', paddingHorizontal: 16, }, descriptionInput: { minHeight: isTablet || isDesktop ? 160 : 120, }, }; const { showAllTooltips } = useFirstVisit('createCharacter'); const posthog = usePostHog(); useEffect(() => { posthog?.capture('character_write_page_viewed'); if (showAllTooltips) { setShowNameInfo(true); setShowDescriptionInfo(true); setShowPhotoInfo(true); } }, [showAllTooltips]); interface GeneratedImageResponse { data: { images: Array<{ description: string; imageUrl: string; }>; characterId: string; }; error?: string; } const pickImage = async (useCamera: boolean = false) => { try { if (useCamera) { // Check camera permission const cameraPermission = await ImagePicker.getCameraPermissionsAsync(); if (cameraPermission.status === 'undetermined') { const { status } = await ImagePicker.requestCameraPermissionsAsync(); if (status !== 'granted') { Alert.alert( 'Berechtigung erforderlich', 'Wir benötigen Zugriff auf Ihre Kamera, um ein Foto zu machen.', [{ text: 'OK' }] ); return; } } else if (cameraPermission.status === 'denied') { Alert.alert( 'Berechtigung erforderlich', 'Der Kamerazugriff wurde verweigert. Bitte aktivieren Sie die Berechtigung in den Einstellungen.', [ { text: 'Abbrechen', style: 'cancel' }, { text: 'Einstellungen öffnen', onPress: () => Linking.openSettings() }, ] ); return; } else if (cameraPermission.status !== 'granted') { Alert.alert('Berechtigung erforderlich', 'Wir benötigen Zugriff auf Ihre Kamera.', [ { text: 'OK' }, ]); return; } // Launch camera const result = await ImagePicker.launchCameraAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsEditing: true, aspect: [1, 1], quality: 0.8, }); if (!result.canceled && result.assets && result.assets.length > 0) { const imageUri = result.assets[0].uri; console.log('Camera photo captured:', imageUri); setUploadedImage(imageUri); } } else { // Original library picker code const permissionResult = await ImagePicker.getMediaLibraryPermissionsAsync(); if (permissionResult.status === 'undetermined') { const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (status !== 'granted') { Alert.alert( 'Berechtigung erforderlich', 'Wir benötigen Ihre Erlaubnis, um auf Ihre Fotos zuzugreifen.', [{ text: 'OK' }] ); return; } } else if (permissionResult.status === 'denied') { Alert.alert( 'Berechtigung erforderlich', 'Der Zugriff auf Ihre Fotobibliothek wurde verweigert. Bitte aktivieren Sie die Berechtigung in den Einstellungen.', [ { text: 'Abbrechen', style: 'cancel' }, { text: 'Einstellungen öffnen', onPress: () => Linking.openSettings() }, ] ); return; } else if (permissionResult.status !== 'granted') { Alert.alert( 'Berechtigung erforderlich', 'Wir benötigen Ihre Erlaubnis, um auf Ihre Fotos zuzugreifen.', [{ text: 'OK' }] ); return; } const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsEditing: true, aspect: [1, 1], quality: 0.8, }); if (!result.canceled && result.assets && result.assets.length > 0) { const imageUri = result.assets[0].uri; console.log('Image selected:', imageUri); setUploadedImage(imageUri); } else { console.log('Image selection canceled or no assets'); } } } catch (error) { console.error('Error with image picker:', error); Alert.alert( 'Fehler', 'Beim Öffnen ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.\n\nBei anhaltenden Problemen helfen wir gerne unter support@manacore.ai weiter.', [{ text: 'OK' }] ); } }; const showImageSourceOptions = () => { Alert.alert('Foto auswählen', 'Woher möchten Sie das Foto hochladen?', [ { text: 'Kamera', onPress: () => pickImage(true) }, { text: 'Fotobibliothek', onPress: () => pickImage(false) }, { text: 'Abbrechen', style: 'cancel' }, ]); }; const handleGenerateFromImage = async () => { // CRITICAL: Check ref first to prevent race conditions from double-taps // This check happens synchronously BEFORE any state updates if (isGeneratingRef.current) { console.log('[CreateCharacter] Ignoring duplicate request - generation already in progress'); return; } Keyboard.dismiss(); setGeneratedImages([]); if (!characterName) { Alert.alert('Fehlende Angaben', 'Bitte geben Sie einen Namen ein.'); return; } if (!uploadedImage) { Alert.alert('Kein Bild ausgewählt', 'Bitte wählen Sie ein Foto aus.'); return; } // Track character creation started analytics.track('character_creation_started', { method: 'photo' }); // Set ref immediately to block duplicate requests isGeneratingRef.current = true; setLoadingContext('cuddly_toy'); setIsLoading(true); const startTime = Date.now(); try { // Create FormData object for file upload const formData = new FormData(); // In React Native, we need to handle file uploads differently // Extract file info from the URI const uriParts = uploadedImage.split('.'); const fileType = uriParts[uriParts.length - 1] || 'jpg'; // Create the file object in a React Native compatible way // This is the format that works with most backends including NestJS formData.append('image', { uri: uploadedImage, name: `photo.${fileType}`, type: `image/${fileType}`, } as any); console.log('Image added to form data', { uri: uploadedImage, name: `photo.${fileType}`, type: `image/${fileType}`, }); // Add the character name - make sure we're using the exact field name expected by the backend formData.append('name', characterName || 'Unnamed Character'); // Log the FormData contents for debugging console.log('FormData prepared with name:', characterName); // When sending FormData, do NOT manually set the Content-Type header // The browser/fetch will automatically set it with the correct boundary const response = await fetchWithAuth('/character/generate-animal-from-image', { method: 'POST', // Let fetch set the Content-Type header with the correct boundary body: formData, }); const result = (await response.json()) as GeneratedImageResponse; console.log('API Response:', result); // Debug log if (result.error) { console.error('API Error:', result.error); Alert.alert( 'Error', result.error + '\n\nBei anhaltenden Problemen helfen wir gerne unter support@manacore.ai weiter.' ); return; } if (!result.data?.images || !Array.isArray(result.data.images)) { console.error('Unexpected API response format:', result); Alert.alert( 'Error', 'Unerwartetes Antwortformat vom Server\n\nBei anhaltenden Problemen helfen wir gerne unter support@manacore.ai weiter.' ); return; } // Validate the structure of each image object const validImages = result.data.images.filter( (img) => img && typeof img === 'object' && 'imageUrl' in img && typeof img.imageUrl === 'string' ); if (validImages.length === 0) { console.error('No valid images in response:', result); Alert.alert( 'Error', 'Keine gültigen Bilder vom Server erhalten\n\nBei anhaltenden Problemen helfen wir gerne unter support@manacore.ai weiter.' ); return; } setCharacterId(result.data.characterId); setGeneratedImages(validImages); // Track successful character generation analytics.track('character_generation_completed', { characterId: result.data.characterId, name: characterName, method: 'photo', duration: Date.now() - startTime, }); } catch (error) { console.error('Error generating character from image:', error); // Track failed character generation analytics.track('character_generation_failed', { method: 'photo', error: error instanceof Error ? error.message : 'unknown_error', duration: Date.now() - startTime, }); // Only show alert for non-credit errors (credit errors are handled globally) if (!isCreditError(error)) { showErrorAlert({ error: error as any, onRetry: () => handleGenerateFromImage(), onDismiss: () => console.log('Error dismissed'), }); } } finally { setIsLoading(false); // Reset ref to allow new requests isGeneratingRef.current = false; } }; const handleGenerateImage = async () => { // CRITICAL: Check ref first to prevent race conditions from double-taps // This check happens synchronously BEFORE any state updates if (isGeneratingRef.current) { console.log('[CreateCharacter] Ignoring duplicate request - generation already in progress'); return; } Keyboard.dismiss(); setGeneratedImages([]); if (!characterName || !characterDescription) { Alert.alert('Fehlende Angaben', 'Bitte geben Sie einen Namen und eine Beschreibung ein.'); return; } // Track character creation started and description entered analytics.track('character_creation_started', { method: 'description' }); analytics.track('character_description_entered', { descriptionLength: characterDescription.length, }); // Set ref immediately to block duplicate requests isGeneratingRef.current = true; setIsLoading(true); const startTime = Date.now(); try { const response = await fetchWithAuth('/character/generate-animal', { method: 'POST', body: JSON.stringify({ description: characterDescription, name: characterName || 'Unnamed Character', }), }); const result = (await response.json()) as GeneratedImageResponse; console.log('API Response:', result); // Debug log if (result.error) { console.error('API Error:', result.error); Alert.alert( 'Error', result.error + '\n\nBei anhaltenden Problemen helfen wir gerne unter support@manacore.ai weiter.' ); return; } if (!result.data?.images || !Array.isArray(result.data.images)) { console.error('Unexpected API response format:', result); Alert.alert( 'Error', 'Unerwartetes Antwortformat vom Server\n\nBei anhaltenden Problemen helfen wir gerne unter support@manacore.ai weiter.' ); return; } // Validate the structure of each image object const validImages = result.data.images.filter( (img) => img && typeof img === 'object' && 'imageUrl' in img && typeof img.imageUrl === 'string' ); if (validImages.length === 0) { console.error('No valid images in response:', result); Alert.alert( 'Error', 'Keine gültigen Bilder vom Server erhalten\n\nBei anhaltenden Problemen helfen wir gerne unter support@manacore.ai weiter.' ); return; } setCharacterId(result.data.characterId); setGeneratedImages(validImages); // Track successful character generation analytics.track('character_generation_completed', { characterId: result.data.characterId, name: characterName, method: 'description', duration: Date.now() - startTime, }); } catch (error) { console.error('Error generating character:', error); // Track failed character generation analytics.track('character_generation_failed', { method: 'description', error: error instanceof Error ? error.message : 'unknown_error', duration: Date.now() - startTime, }); // Only show alert for non-credit errors (credit errors are handled globally) if (!isCreditError(error)) { showErrorAlert({ error: error as any, onRetry: () => handleGenerateImage(), onDismiss: () => console.log('Error dismissed'), }); } } finally { setIsLoading(false); // Reset ref to allow new requests isGeneratingRef.current = false; } }; const handleCreateCharacter = async (selectedImage: CustomImage | null) => { Keyboard.dismiss(); if (!characterName) { Alert.alert('Fehlende Angaben', 'Bitte geben Sie einen Namen ein.'); return; } // Wenn generierte Bilder vorhanden sind, ist keine Beschreibung erforderlich if (generatedImages.length === 0 && !characterDescription) { Alert.alert('Fehlende Angaben', 'Bitte geben Sie eine Beschreibung ein.'); return; } try { setIsLoading(true); const characterData = { characterDescriptionPrompt: selectedImage?.description, imageUrl: selectedImage?.imageUrl, }; await dataService.updateCharacter(characterId, characterData); posthog?.capture('character_updated_successfully', { character_id: characterId }); router.replace('/'); } catch (error) { console.error('Error creating character:', error); // Show error alert with retry option showErrorAlert({ error: error as any, onRetry: () => handleCreateCharacter(selectedImage), onDismiss: () => console.log('Error dismissed'), }); } finally { setIsLoading(false); } }; return ( <> {/* Premium Cuddly Toy Feature Card */} {generatedImages.length === 0 && activeTab === 'photo' && !uploadedImage && ( )} {/* Character information section */} setShowNameInfo(!showNameInfo)} titleSize={sectionTitleSize} iconSize={infoIconSize} /> {showNameInfo && ( Gib deinem Charakter einen Namen. Der Name wird in der Geschichte und in allen Dialogen verwendet. )} {/* Character name field */} {/* Tab Switcher für Foto/Beschreibung */} {generatedImages.length === 0 && ( <> setActiveTab(key as 'photo' | 'description')} /> {/* Photo upload section */} {activeTab === 'photo' && ( setShowPhotoInfo(!showPhotoInfo)} titleSize={sectionTitleSize} iconSize={infoIconSize} /> {showPhotoInfo && ( Lade ein Foto von einem Kuscheltier hoch, um daraus einen einzigartigen Charakter zu erstellen. Das Foto wird in einen Charakter für deine Geschichte umgewandelt. Fotos von Menschen funktionieren nicht. )} {showPhotoInfo && ( 💡 Für beste Ergebnisse: gute Beleuchtung, neutraler Hintergrund )} {uploadedImage ? ( Kuscheltier erkannt Anderes Foto wählen ) : ( Foto hochladen Mache ein Foto vom Kuscheltier deines Kindes )}