221 lines
6.3 KiB
TypeScript
221 lines
6.3 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
||
import {
|
||
View,
|
||
Text,
|
||
FlatList,
|
||
TouchableOpacity,
|
||
ActivityIndicator,
|
||
Alert,
|
||
KeyboardAvoidingView,
|
||
Platform,
|
||
Linking,
|
||
} from 'react-native';
|
||
import { useDispatch, useSelector } from 'react-redux';
|
||
import type { RootState, AppDispatch } from '../../store';
|
||
import {
|
||
searchHuggingFaceModels,
|
||
saveHuggingFaceApiKey,
|
||
loadHuggingFaceApiKey,
|
||
setSearchQuery,
|
||
clearHFError,
|
||
} from '../../store/modelsSlice';
|
||
import HFModelItem from './HFModelItem';
|
||
import ApiKeyEditor from './ApiKeyEditor';
|
||
import SearchSection from './SearchSection';
|
||
import { handleModelDownload } from './downloadHelper';
|
||
import { createStyles } from './styles';
|
||
import { useTheme } from '../../theme/ThemeProvider';
|
||
|
||
export default function HuggingFaceModelsScreen() {
|
||
const { colors } = useTheme();
|
||
const styles = createStyles(colors);
|
||
const dispatch = useDispatch<AppDispatch>();
|
||
const {
|
||
huggingFaceModels,
|
||
huggingFaceApiKey,
|
||
searchQuery,
|
||
isLoadingHF,
|
||
hfError,
|
||
modelsDirectory,
|
||
downloadProgress,
|
||
} = useSelector((state: RootState) => state.models);
|
||
|
||
const [showApiKeyInput, setShowApiKeyInput] = useState(false);
|
||
const [tempApiKey, setTempApiKey] = useState('');
|
||
const [localSearchQuery, setLocalSearchQuery] = useState(searchQuery);
|
||
|
||
useEffect(() => {
|
||
// Load saved API key on mount
|
||
dispatch(loadHuggingFaceApiKey());
|
||
}, [dispatch]);
|
||
|
||
useEffect(() => {
|
||
setTempApiKey(huggingFaceApiKey);
|
||
}, [huggingFaceApiKey]);
|
||
|
||
useEffect(() => {
|
||
if (hfError) {
|
||
Alert.alert('Erreur', hfError, [
|
||
{ text: 'OK', onPress: () => dispatch(clearHFError()) },
|
||
]);
|
||
}
|
||
}, [hfError, dispatch]);
|
||
|
||
const handleSearch = () => {
|
||
if (localSearchQuery.trim()) {
|
||
dispatch(setSearchQuery(localSearchQuery.trim()));
|
||
dispatch(searchHuggingFaceModels({
|
||
query: localSearchQuery.trim(),
|
||
apiKey: huggingFaceApiKey || undefined,
|
||
}));
|
||
}
|
||
};
|
||
|
||
const handleSaveApiKey = () => {
|
||
dispatch(saveHuggingFaceApiKey(tempApiKey.trim()));
|
||
setShowApiKeyInput(false);
|
||
Alert.alert('Succès', 'Clé API enregistrée');
|
||
};
|
||
|
||
const handleOpenHuggingFace = () => {
|
||
Linking.openURL('https://huggingface.co/settings/tokens');
|
||
};
|
||
|
||
const handleModelPress = (modelId: string) => {
|
||
const url = `https://huggingface.co/${modelId}`;
|
||
Linking.openURL(url);
|
||
};
|
||
|
||
const handleDownload = (modelId: string) => {
|
||
handleModelDownload(modelId, modelsDirectory, dispatch);
|
||
};
|
||
|
||
const renderModelItem = ({ item }: { item: typeof huggingFaceModels[0] }) => {
|
||
const modelProgress = downloadProgress[item.id];
|
||
const isDownloading = !!modelProgress;
|
||
const progressPercent = modelProgress
|
||
? (modelProgress.bytesWritten / modelProgress.contentLength) * 100
|
||
: 0;
|
||
|
||
return (
|
||
<HFModelItem
|
||
model={item}
|
||
onPress={handleModelPress}
|
||
onDownload={handleDownload}
|
||
isDownloading={isDownloading}
|
||
downloadProgress={progressPercent}
|
||
bytesWritten={modelProgress?.bytesWritten || 0}
|
||
contentLength={modelProgress?.contentLength || 0}
|
||
/>
|
||
);
|
||
};
|
||
|
||
const renderEmptyList = () => {
|
||
if (isLoadingHF) {
|
||
return null;
|
||
}
|
||
|
||
return (
|
||
<View style={styles.emptyContainer}>
|
||
{searchQuery ? (
|
||
<>
|
||
<Text style={styles.emptyText}>Aucun modèle trouvé</Text>
|
||
<Text style={styles.emptySubtext}>
|
||
Essayez une autre recherche
|
||
</Text>
|
||
</>
|
||
) : (
|
||
<>
|
||
<Text style={styles.emptyText}>
|
||
Recherchez des modèles GGUF
|
||
</Text>
|
||
<Text style={styles.emptySubtext}>
|
||
Entrez un terme de recherche ci-dessus
|
||
</Text>
|
||
</>
|
||
)}
|
||
</View>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<KeyboardAvoidingView
|
||
style={styles.container}
|
||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||
>
|
||
{/* API Key Section */}
|
||
<View style={styles.apiKeySection}>
|
||
<View style={styles.apiKeyHeader}>
|
||
<Text style={styles.sectionTitle}>Clé API HuggingFace</Text>
|
||
<TouchableOpacity
|
||
style={styles.infoButton}
|
||
onPress={handleOpenHuggingFace}
|
||
>
|
||
<Text style={styles.infoButtonText}>ℹ️ Obtenir</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
{showApiKeyInput ? (
|
||
<ApiKeyEditor
|
||
tempApiKey={tempApiKey}
|
||
onChangeText={setTempApiKey}
|
||
onCancel={() => {
|
||
setTempApiKey(huggingFaceApiKey);
|
||
setShowApiKeyInput(false);
|
||
}}
|
||
onSave={handleSaveApiKey}
|
||
/>
|
||
) : (
|
||
<View style={styles.apiKeyDisplay}>
|
||
<Text style={styles.apiKeyText}>
|
||
{huggingFaceApiKey ? '•••••••••••••' : 'Non configurée'}
|
||
</Text>
|
||
<TouchableOpacity
|
||
style={styles.changeButton}
|
||
onPress={() => setShowApiKeyInput(true)}
|
||
>
|
||
<Text style={styles.changeButtonText}>
|
||
{huggingFaceApiKey ? 'Modifier' : 'Configurer'}
|
||
</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
)}
|
||
|
||
<Text style={styles.apiKeyHint}>
|
||
Optionnel - Permet d'augmenter les limites de recherche
|
||
</Text>
|
||
</View>
|
||
|
||
{/* Search Section */}
|
||
<SearchSection
|
||
searchQuery={localSearchQuery}
|
||
onChangeText={setLocalSearchQuery}
|
||
onSearch={handleSearch}
|
||
isLoading={isLoadingHF}
|
||
/>
|
||
|
||
{/* Results Section */}
|
||
<View style={styles.resultsSection}>
|
||
<Text style={styles.sectionTitle}>
|
||
Résultats ({huggingFaceModels.length})
|
||
</Text>
|
||
|
||
{isLoadingHF && huggingFaceModels.length === 0 ? (
|
||
<View style={styles.loadingContainer}>
|
||
<ActivityIndicator size="large" color="#007AFF" />
|
||
<Text style={styles.loadingText}>Recherche en cours...</Text>
|
||
</View>
|
||
) : (
|
||
<FlatList
|
||
data={huggingFaceModels}
|
||
renderItem={renderModelItem}
|
||
keyExtractor={(item) => item.id}
|
||
ListEmptyComponent={renderEmptyList}
|
||
contentContainerStyle={styles.listContent}
|
||
/>
|
||
)}
|
||
</View>
|
||
</KeyboardAvoidingView>
|
||
);
|
||
}
|