import React, { useState } from 'react'; import { View, Text, StyleSheet, TextInput, FlatList, TouchableOpacity, Platform, StatusBar, ScrollView, ActivityIndicator, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useRouter, useLocalSearchParams } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { useApp } from '../context/AppContext'; import { useColors } from '../constants/Colors'; import { PlantDatabaseService } from '../services/plantDatabaseService'; import { IdentificationResult } from '../types'; import { DatabaseEntry } from '../services/plantDatabaseService'; import { ResultCard } from '../components/ResultCard'; import { ThemeBackdrop } from '../components/ThemeBackdrop'; import { SafeImage } from '../components/SafeImage'; import { resolveImageUri } from '../utils/imageUri'; export default function LexiconScreen() { const { isDarkMode, colorPalette, language, t, savePlant, getLexiconSearchHistory, saveLexiconSearchQuery, clearLexiconSearchHistory } = useApp(); const colors = useColors(isDarkMode, colorPalette); const insets = useSafeAreaInsets(); const router = useRouter(); const params = useLocalSearchParams(); const categoryIdParam = Array.isArray(params.categoryId) ? params.categoryId[0] : params.categoryId; const categoryLabelParam = Array.isArray(params.categoryLabel) ? params.categoryLabel[0] : params.categoryLabel; const decodeParam = (value?: string | string[]) => { if (!value || typeof value !== 'string') return ''; try { return decodeURIComponent(value); } catch { return value; } }; const initialCategoryId = typeof categoryIdParam === 'string' ? categoryIdParam : null; const initialCategoryLabel = decodeParam(categoryLabelParam); const topInsetFallback = Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : 20; const topInset = insets.top > 0 ? insets.top : topInsetFallback; const [searchQuery, setSearchQuery] = useState(initialCategoryLabel); const [activeCategoryId, setActiveCategoryId] = useState(initialCategoryId); const [selectedItem, setSelectedItem] = useState<(IdentificationResult & { imageUri: string }) | null>(null); const [isAiSearching, setIsAiSearching] = useState(false); const [aiResults, setAiResults] = useState(null); const [searchErrorMessage, setSearchErrorMessage] = useState(null); const [searchHistory, setSearchHistory] = useState([]); const detailParam = Array.isArray(params.detail) ? params.detail[0] : params.detail; const openedWithDetail = Boolean(detailParam); React.useEffect(() => { if (detailParam) { try { const rawParam = detailParam; const decoded = decodeURIComponent(rawParam as string); const detail = JSON.parse(decoded); setSelectedItem(detail); } catch (e) { try { const fallbackRaw = detailParam; const detail = JSON.parse(fallbackRaw as string); setSelectedItem(detail); } catch (fallbackError) { console.error('Failed to parse plant detail', fallbackError); } } } }, [detailParam]); React.useEffect(() => { setActiveCategoryId(initialCategoryId); setSearchQuery(initialCategoryLabel); }, [initialCategoryId, initialCategoryLabel]); React.useEffect(() => { const loadHistory = async () => { const history = getLexiconSearchHistory(); setSearchHistory(history); }; loadHistory(); }, []); const handleResultClose = () => { if (openedWithDetail) { router.back(); return; } setSelectedItem(null); }; const normalizeText = (value: string): string => ( value .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .trim() .replace(/\s+/g, ' ') ); const effectiveSearchQuery = searchQuery; const [lexiconPlants, setLexiconPlants] = useState([]); React.useEffect(() => { if (aiResults) { setLexiconPlants(aiResults); return; } let isCancelled = false; PlantDatabaseService.searchPlants(effectiveSearchQuery, language, { category: activeCategoryId, limit: 500, }).then(results => { if (!isCancelled) setLexiconPlants(results); }).catch(console.error); return () => { isCancelled = true; }; }, [aiResults, effectiveSearchQuery, language, activeCategoryId]); const handleAiSearch = async () => { const query = searchQuery.trim(); if (!query) return; setIsAiSearching(true); setAiResults(null); setSearchErrorMessage(null); try { const response = await PlantDatabaseService.semanticSearchDetailed(query, language); if (response.status === 'success') { setAiResults(response.results); saveLexiconSearchQuery(query); setSearchHistory(getLexiconSearchHistory()); } else if (response.status === 'insufficient_credits') { setSearchErrorMessage((t as any).errorNoCredits || 'Nicht genügend Guthaben für KI-Suche.'); } else if (response.status === 'no_results') { setSearchErrorMessage((t as any).noResultsFound || 'Keine Ergebnisse gefunden.'); setAiResults([]); } else { setSearchErrorMessage((t as any).errorTryAgain || 'Fehler bei der Suche. Bitte später erneut versuchen.'); } } catch (error) { console.error('AI Search failed', error); setSearchErrorMessage((t as any).errorGeneral || 'Etwas ist schiefgelaufen.'); } finally { setIsAiSearching(false); } }; const handleSearchSubmit = async () => { const query = searchQuery.trim(); if (!query) return; saveLexiconSearchQuery(query); setSearchHistory(getLexiconSearchHistory()); }; const handleHistorySelect = (query: string) => { setActiveCategoryId(null); setSearchQuery(query); }; const handleClearHistory = () => { clearLexiconSearchHistory(); setSearchHistory([]); }; const showSearchHistory = searchQuery.trim().length === 0 && !activeCategoryId && searchHistory.length > 0; if (selectedItem) { return ( { savePlant(selectedItem, resolveImageUri(selectedItem.imageUri)); router.back(); }} onClose={handleResultClose} t={t} isDark={isDarkMode} colorPalette={colorPalette} /> ); } return ( {/* Header */} router.back()} style={styles.backBtn}> {t.lexiconTitle} {/* Search */} {(searchQuery || activeCategoryId) ? ( { setSearchQuery(''); setActiveCategoryId(null); setAiResults(null); }} hitSlop={8} > ) : null} {/* AI Search Trigger block removed */} {searchErrorMessage && ( {searchErrorMessage} )} {aiResults && ( { setAiResults(null); setSearchErrorMessage(null); }} > {(t as any).clearAiResults || 'KI-Ergebnisse löschen'} )} {showSearchHistory ? ( {t.searchHistory} {t.clearHistory} {searchHistory.map((item, index) => ( handleHistorySelect(item)} activeOpacity={0.8} > {item} ))} ) : null} {/* Grid */} i.toString()} contentContainerStyle={styles.grid} columnWrapperStyle={styles.gridRow} showsVerticalScrollIndicator={false} initialNumToRender={12} maxToRenderPerBatch={6} windowSize={3} ListEmptyComponent={ {t.noResults} } renderItem={({ item }) => ( setSelectedItem(item as any)} > {item.name} {item.botanicalName} )} /> ); } const styles = StyleSheet.create({ container: { flex: 1, overflow: 'hidden' }, header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingBottom: 16, borderBottomWidth: 1, gap: 14, }, backBtn: { marginLeft: -8, padding: 4 }, title: { fontSize: 19, fontWeight: '700', letterSpacing: 0.2 }, searchBar: { flexDirection: 'row', alignItems: 'center', borderWidth: 1, borderRadius: 16, paddingHorizontal: 14, paddingVertical: 10, gap: 10, shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.14, shadowRadius: 8, elevation: 2, }, searchInput: { flex: 1, fontSize: 15 }, historySection: { paddingHorizontal: 20, paddingTop: 12, paddingBottom: 8 }, historyHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8, }, historyTitle: { fontSize: 11, fontWeight: '700', letterSpacing: 0.8, textTransform: 'uppercase', }, clearHistoryText: { fontSize: 12, fontWeight: '700' }, historyContent: { gap: 8, paddingRight: 20 }, historyChip: { flexDirection: 'row', alignItems: 'center', gap: 6, borderWidth: 1, borderRadius: 16, paddingHorizontal: 10, paddingVertical: 8, }, historyChipText: { fontSize: 12, fontWeight: '600' }, grid: { padding: 20, paddingBottom: 40 }, gridRow: { gap: 10, marginBottom: 10 }, card: { flex: 1, borderRadius: 18, borderWidth: 1, overflow: 'hidden', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.14, shadowRadius: 6, elevation: 2, }, cardImage: { width: '100%', aspectRatio: 1, resizeMode: 'cover' }, cardContent: { padding: 8 }, cardName: { fontSize: 12, fontWeight: '700' }, cardBotanical: { fontSize: 9, fontStyle: 'italic' }, empty: { paddingVertical: 40, alignItems: 'center' }, aiSearchBtn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 12, paddingHorizontal: 16, borderRadius: 14, borderWidth: 1, borderStyle: 'dashed', gap: 10, }, aiSearchText: { fontSize: 14, fontWeight: '600', }, errorBox: { flexDirection: 'row', alignItems: 'center', padding: 12, borderRadius: 12, gap: 10, }, errorText: { fontSize: 14, fontWeight: '500', flex: 1, }, clearAiResultsBtn: { flexDirection: 'row', alignItems: 'center', alignSelf: 'flex-start', paddingVertical: 6, paddingHorizontal: 12, borderRadius: 20, gap: 6, }, });