mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-21 00:06:42 +02:00
- 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>
263 lines
No EOL
8 KiB
TypeScript
263 lines
No EOL
8 KiB
TypeScript
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>
|
|
);
|
|
} |