mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 18:06:42 +02:00
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>
This commit is contained in:
parent
c6c4c5a552
commit
c712a2504a
1031 changed files with 189301 additions and 290 deletions
294
picture/apps/mobile/components/batch/BatchGenerationModal.tsx
Normal file
294
picture/apps/mobile/components/batch/BatchGenerationModal.tsx
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
import React, { useState } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
ScrollView,
|
||||
Pressable,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
Alert,
|
||||
ActivityIndicator
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Button } from '../Button';
|
||||
import { useBatchStore, BatchPrompt } from '~/store/batchStore';
|
||||
import { useModelSelection } from '~/store/modelStore';
|
||||
import { aspectRatios } from '~/hooks/useImageGeneration';
|
||||
|
||||
interface BatchGenerationModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess?: (batchId: string) => void;
|
||||
}
|
||||
|
||||
export function BatchGenerationModal({ isOpen, onClose, onSuccess }: BatchGenerationModalProps) {
|
||||
const [batchName, setBatchName] = useState('');
|
||||
const [prompts, setPrompts] = useState<BatchPrompt[]>([{ text: '' }]);
|
||||
const [selectedAspectRatio, setSelectedAspectRatio] = useState(aspectRatios[0]);
|
||||
const [steps, setSteps] = useState(30);
|
||||
const [guidanceScale, setGuidanceScale] = useState(7.5);
|
||||
|
||||
const { createBatch, isCreatingBatch } = useBatchStore();
|
||||
const { models, selectedModel, setSelectedModel, loadModels, isLoading: modelsLoading } = useModelSelection();
|
||||
|
||||
// Models are preloaded at app start, only load if error occurred
|
||||
React.useEffect(() => {
|
||||
if (isOpen && models.length === 0 && !modelsLoading) {
|
||||
loadModels();
|
||||
}
|
||||
}, [isOpen, models.length, modelsLoading]);
|
||||
|
||||
const addPrompt = () => {
|
||||
if (prompts.length < 10) {
|
||||
setPrompts([...prompts, { text: '' }]);
|
||||
}
|
||||
};
|
||||
|
||||
const removePrompt = (index: number) => {
|
||||
if (prompts.length > 1) {
|
||||
setPrompts(prompts.filter((_, i) => i !== index));
|
||||
}
|
||||
};
|
||||
|
||||
const updatePrompt = (index: number, text: string) => {
|
||||
const updated = [...prompts];
|
||||
updated[index] = { ...updated[index], text };
|
||||
setPrompts(updated);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// Validate
|
||||
const validPrompts = prompts.filter(p => p.text.trim().length > 0);
|
||||
if (validPrompts.length === 0) {
|
||||
Alert.alert('Fehler', 'Mindestens ein Prompt ist erforderlich');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedModel) {
|
||||
Alert.alert('Fehler', 'Bitte wähle ein Modell aus');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const batchId = await createBatch(
|
||||
validPrompts,
|
||||
{
|
||||
model_id: selectedModel.id,
|
||||
model_version: selectedModel.version,
|
||||
width: selectedAspectRatio.width,
|
||||
height: selectedAspectRatio.height,
|
||||
steps: steps,
|
||||
guidance_scale: guidanceScale
|
||||
},
|
||||
batchName || undefined
|
||||
);
|
||||
|
||||
// Reset form
|
||||
setBatchName('');
|
||||
setPrompts([{ text: '' }]);
|
||||
|
||||
onSuccess?.(batchId);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
Alert.alert('Fehler', 'Batch konnte nicht erstellt werden');
|
||||
console.error('Batch creation error:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const validPromptCount = prompts.filter(p => p.text.trim().length > 0).length;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={isOpen}
|
||||
animationType="slide"
|
||||
presentationStyle="pageSheet"
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<SafeAreaView className="flex-1 bg-dark-bg">
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
className="flex-1"
|
||||
>
|
||||
{/* Header */}
|
||||
<View className="flex-row items-center justify-between p-4 border-b border-dark-border">
|
||||
<Pressable onPress={onClose} className="p-2">
|
||||
<Ionicons name="close" size={24} color="white" />
|
||||
</Pressable>
|
||||
<Text className="text-lg font-bold text-white">
|
||||
Batch Generation ({validPromptCount}/10)
|
||||
</Text>
|
||||
<View style={{ width: 40 }} />
|
||||
</View>
|
||||
|
||||
<ScrollView className="flex-1 p-4">
|
||||
{/* Batch Name */}
|
||||
<View className="mb-4">
|
||||
<Text className="text-sm font-medium text-gray-300 mb-2">
|
||||
Batch Name (optional)
|
||||
</Text>
|
||||
<TextInput
|
||||
className="border border-dark-border bg-dark-input rounded-lg px-4 py-3 text-base text-gray-100"
|
||||
placeholder="z.B. Landschaften, Portraits..."
|
||||
placeholderTextColor="#6b7280"
|
||||
value={batchName}
|
||||
onChangeText={setBatchName}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Model Selection */}
|
||||
<View className="mb-4">
|
||||
<Text className="text-sm font-medium text-gray-300 mb-2">
|
||||
Modell
|
||||
</Text>
|
||||
{modelsLoading ? (
|
||||
<View className="py-4 bg-dark-surface rounded-lg">
|
||||
<ActivityIndicator size="small" color="#6366f1" />
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||
<View className="flex-row">
|
||||
{models.map((model) => (
|
||||
<Pressable
|
||||
key={model.id}
|
||||
onPress={() => setSelectedModel(model)}
|
||||
className={`mr-2 px-3 py-2 rounded-lg border ${
|
||||
selectedModel?.id === model.id
|
||||
? 'bg-indigo-600 border-indigo-600'
|
||||
: 'bg-dark-surface border-dark-border'
|
||||
}`}
|
||||
>
|
||||
<Text
|
||||
className={
|
||||
selectedModel?.id === model.id ? 'text-white' : 'text-gray-300'
|
||||
}
|
||||
>
|
||||
{model.display_name}
|
||||
</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Aspect Ratio */}
|
||||
<View className="mb-4">
|
||||
<Text className="text-sm font-medium text-gray-300 mb-2">
|
||||
Seitenverhältnis
|
||||
</Text>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||
<View className="flex-row">
|
||||
{aspectRatios.map((ratio) => (
|
||||
<Pressable
|
||||
key={ratio.value}
|
||||
onPress={() => setSelectedAspectRatio(ratio)}
|
||||
className={`mr-2 px-3 py-2 rounded-lg border ${
|
||||
selectedAspectRatio.value === ratio.value
|
||||
? 'bg-indigo-600 border-indigo-600'
|
||||
: 'bg-dark-surface border-dark-border'
|
||||
}`}
|
||||
>
|
||||
<Text
|
||||
className={
|
||||
selectedAspectRatio.value === ratio.value ? 'text-white' : 'text-gray-300'
|
||||
}
|
||||
>
|
||||
{ratio.icon} {ratio.label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* Settings */}
|
||||
<View className="bg-dark-surface rounded-lg p-4 mb-4">
|
||||
<Text className="text-sm font-medium text-gray-300 mb-3">
|
||||
Gemeinsame Einstellungen
|
||||
</Text>
|
||||
<View className="space-y-2">
|
||||
<View className="flex-row justify-between">
|
||||
<Text className="text-gray-400">Größe</Text>
|
||||
<Text className="font-medium text-gray-200">
|
||||
{selectedAspectRatio.width} x {selectedAspectRatio.height}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="flex-row justify-between">
|
||||
<Text className="text-gray-400">Steps</Text>
|
||||
<Text className="font-medium text-gray-200">{steps}</Text>
|
||||
</View>
|
||||
<View className="flex-row justify-between">
|
||||
<Text className="text-gray-400">Guidance Scale</Text>
|
||||
<Text className="font-medium text-gray-200">{guidanceScale}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Prompts */}
|
||||
<View className="mb-4">
|
||||
<Text className="text-sm font-medium text-gray-300 mb-2">
|
||||
Prompts
|
||||
</Text>
|
||||
{prompts.map((prompt, index) => (
|
||||
<View key={index} className="mb-3 flex-row items-start">
|
||||
<Text className="text-gray-400 mr-2 mt-3 min-w-[20px]">
|
||||
{index + 1}.
|
||||
</Text>
|
||||
<TextInput
|
||||
className="flex-1 border border-dark-border bg-dark-input rounded-lg px-4 py-3 text-base text-gray-100 min-h-[80px]"
|
||||
placeholder="Beschreibe dein Bild..."
|
||||
placeholderTextColor="#6b7280"
|
||||
value={prompt.text}
|
||||
onChangeText={(text) => updatePrompt(index, text)}
|
||||
multiline
|
||||
textAlignVertical="top"
|
||||
/>
|
||||
{prompts.length > 1 && (
|
||||
<Pressable
|
||||
onPress={() => removePrompt(index)}
|
||||
className="ml-2 mt-3 p-2"
|
||||
>
|
||||
<Ionicons name="trash-outline" size={20} color="#ef4444" />
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
|
||||
{prompts.length < 10 && (
|
||||
<Pressable
|
||||
onPress={addPrompt}
|
||||
className="flex-row items-center justify-center py-3 px-4 bg-dark-surface rounded-lg border border-dark-border"
|
||||
>
|
||||
<Ionicons name="add-circle-outline" size={20} color="#818cf8" />
|
||||
<Text className="ml-2 text-indigo-400">Prompt hinzufügen</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Submit Buttons */}
|
||||
<View className="flex-row space-x-2 mb-8">
|
||||
<View className="flex-1">
|
||||
<Button
|
||||
title="Abbrechen"
|
||||
onPress={onClose}
|
||||
variant="secondary"
|
||||
disabled={isCreatingBatch}
|
||||
/>
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Button
|
||||
title={isCreatingBatch ? "Erstelle..." : `Batch generieren (${validPromptCount})`}
|
||||
onPress={handleSubmit}
|
||||
disabled={isCreatingBatch || validPromptCount === 0 || !selectedModel}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
263
picture/apps/mobile/components/batch/BatchProgressTracker.tsx
Normal file
263
picture/apps/mobile/components/batch/BatchProgressTracker.tsx
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ScrollView,
|
||||
Pressable,
|
||||
ActivityIndicator
|
||||
} from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useBatchStore } from '~/store/batchStore';
|
||||
import { router } from 'expo-router';
|
||||
|
||||
interface BatchProgressTrackerProps {
|
||||
batchId: string;
|
||||
onComplete?: () => void;
|
||||
onItemClick?: (itemId: string) => void;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export function BatchProgressTracker({
|
||||
batchId,
|
||||
onComplete,
|
||||
onItemClick,
|
||||
compact = false
|
||||
}: BatchProgressTrackerProps) {
|
||||
const {
|
||||
activeBatches,
|
||||
loadBatch,
|
||||
subscribeToBatch,
|
||||
unsubscribeFromBatch,
|
||||
retryFailed,
|
||||
cancelBatch
|
||||
} = useBatchStore();
|
||||
|
||||
const batch = activeBatches.get(batchId);
|
||||
|
||||
useEffect(() => {
|
||||
// Load and subscribe to batch
|
||||
loadBatch(batchId);
|
||||
subscribeToBatch(batchId);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromBatch(batchId);
|
||||
};
|
||||
}, [batchId]);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if batch is complete
|
||||
if (batch && batch.status === 'completed' && onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
}, [batch?.status]);
|
||||
|
||||
if (!batch) {
|
||||
return (
|
||||
<View className="p-4 bg-dark-surface rounded-lg">
|
||||
<ActivityIndicator size="small" color="#818cf8" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const progressPercentage = batch.total_count > 0
|
||||
? ((batch.completed_count + batch.failed_count) / batch.total_count) * 100
|
||||
: 0;
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return <Ionicons name="checkmark-circle" size={20} color="#10b981" />;
|
||||
case 'failed':
|
||||
return <Ionicons name="close-circle" size={20} color="#ef4444" />;
|
||||
case 'processing':
|
||||
return <ActivityIndicator size="small" color="#818cf8" />;
|
||||
case 'pending':
|
||||
return <Ionicons name="time-outline" size={20} color="#6b7280" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return 'text-green-500';
|
||||
case 'failed':
|
||||
return 'text-red-500';
|
||||
case 'processing':
|
||||
return 'text-indigo-500';
|
||||
default:
|
||||
return 'text-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
if (compact) {
|
||||
// Compact view for in-screen display
|
||||
return (
|
||||
<View className="bg-dark-surface rounded-lg p-3 mb-2">
|
||||
<View className="flex-row items-center justify-between mb-2">
|
||||
<Text className="text-sm font-semibold text-white">
|
||||
{batch.name || 'Batch Generation'}
|
||||
</Text>
|
||||
<Text className={`text-xs ${getStatusColor(batch.status)}`}>
|
||||
{batch.completed_count}/{batch.total_count}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<View className="h-2 bg-dark-input rounded-full overflow-hidden">
|
||||
<View
|
||||
className="h-full bg-indigo-600 rounded-full"
|
||||
style={{ width: `${progressPercentage}%` }}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{batch.failed_count > 0 && (
|
||||
<Pressable
|
||||
onPress={() => retryFailed(batchId)}
|
||||
className="mt-2 flex-row items-center"
|
||||
>
|
||||
<Ionicons name="refresh" size={14} color="#ef4444" />
|
||||
<Text className="text-xs text-red-500 ml-1">
|
||||
{batch.failed_count} fehlgeschlagen - Wiederholen
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Full view
|
||||
return (
|
||||
<View className="bg-dark-surface rounded-lg p-4">
|
||||
{/* Header */}
|
||||
<View className="mb-4">
|
||||
<Text className="text-lg font-bold text-white mb-1">
|
||||
{batch.name || 'Batch Generation'}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-400">
|
||||
Status: <Text className={getStatusColor(batch.status)}>{batch.status}</Text>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Overall Progress */}
|
||||
<View className="mb-4">
|
||||
<View className="flex-row justify-between mb-2">
|
||||
<Text className="text-sm text-gray-400">
|
||||
Gesamt: {progressPercentage.toFixed(0)}%
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-400">
|
||||
{batch.completed_count + batch.failed_count}/{batch.total_count}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="h-3 bg-dark-input rounded-full overflow-hidden">
|
||||
<View
|
||||
className="h-full bg-indigo-600 rounded-full"
|
||||
style={{ width: `${progressPercentage}%` }}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Statistics */}
|
||||
<View className="flex-row justify-around mb-4 py-2 border-y border-dark-border">
|
||||
<View className="items-center">
|
||||
<Text className="text-2xl font-bold text-green-500">
|
||||
{batch.completed_count}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-400">Fertig</Text>
|
||||
</View>
|
||||
<View className="items-center">
|
||||
<Text className="text-2xl font-bold text-indigo-500">
|
||||
{batch.processing_count || 0}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-400">Läuft</Text>
|
||||
</View>
|
||||
<View className="items-center">
|
||||
<Text className="text-2xl font-bold text-gray-500">
|
||||
{batch.pending_count || 0}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-400">Wartend</Text>
|
||||
</View>
|
||||
<View className="items-center">
|
||||
<Text className="text-2xl font-bold text-red-500">
|
||||
{batch.failed_count}
|
||||
</Text>
|
||||
<Text className="text-xs text-gray-400">Fehler</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Items List */}
|
||||
{batch.items && batch.items.length > 0 && (
|
||||
<ScrollView className="max-h-64">
|
||||
{batch.items.map((item, index) => (
|
||||
<Pressable
|
||||
key={item.id}
|
||||
onPress={() => {
|
||||
if (item.status === 'completed' && onItemClick) {
|
||||
onItemClick(item.id);
|
||||
}
|
||||
}}
|
||||
disabled={item.status !== 'completed'}
|
||||
className="flex-row items-center py-2 border-b border-dark-border"
|
||||
>
|
||||
<Text className="text-gray-400 mr-3 min-w-[20px]">
|
||||
{index + 1}.
|
||||
</Text>
|
||||
<View className="mr-3">
|
||||
{getStatusIcon(item.status)}
|
||||
</View>
|
||||
<Text
|
||||
className="flex-1 text-sm text-gray-300"
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.prompt}
|
||||
</Text>
|
||||
{item.retry_count && item.retry_count > 0 && (
|
||||
<Text className="text-xs text-yellow-500 ml-2">
|
||||
Retry {item.retry_count}
|
||||
</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
))}
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<View className="flex-row justify-between mt-4">
|
||||
{batch.status === 'processing' && (
|
||||
<Pressable
|
||||
onPress={() => cancelBatch(batchId)}
|
||||
className="flex-row items-center px-3 py-2 bg-red-900/20 rounded-lg"
|
||||
>
|
||||
<Ionicons name="stop-circle-outline" size={16} color="#ef4444" />
|
||||
<Text className="ml-1 text-sm text-red-500">Abbrechen</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
{batch.failed_count > 0 && (
|
||||
<Pressable
|
||||
onPress={() => retryFailed(batchId)}
|
||||
className="flex-row items-center px-3 py-2 bg-yellow-900/20 rounded-lg"
|
||||
>
|
||||
<Ionicons name="refresh-outline" size={16} color="#eab308" />
|
||||
<Text className="ml-1 text-sm text-yellow-500">
|
||||
Fehler wiederholen
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
{batch.status === 'completed' && (
|
||||
<Pressable
|
||||
onPress={() => router.push('/(tabs)')}
|
||||
className="flex-row items-center px-3 py-2 bg-green-900/20 rounded-lg"
|
||||
>
|
||||
<Ionicons name="images-outline" size={16} color="#10b981" />
|
||||
<Text className="ml-1 text-sm text-green-500">
|
||||
Zur Galerie
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue