Initial commit
This commit is contained in:
220
app/src/screens/HuggingFaceModelsScreen/index.tsx
Normal file
220
app/src/screens/HuggingFaceModelsScreen/index.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user