// components/HybridSearch.tsx
import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react';
import { Sparkles, Zap, Search, Loader } from 'lucide-react';
import CustomInstantSearchResults from './CustomInstantSearchResults';
import ErrorBoundary from './ErrorBoundary';
import AlgoliaStatus from './AlgoliaStatus';
import type { PokemonData, RecommendationData } from '../types/pokemon';
interface BattleQueryResult {
pokemon: PokemonData[];
recommendations: RecommendationData[];
summary?: string;
intent?: string;
}
interface HybridSearchProps {
onResultsReceived: (results: BattleQueryResult) => void;
onPokemonSelect: (pokemon: PokemonData, sourceImageRef?: HTMLImageElement) => void;
}
const HybridSearch: React.FC<HybridSearchProps> = ({ onResultsReceived, onPokemonSelect }) => {
const [currentQuery, setCurrentQuery] = useState('');
const [searchMode, setSearchMode] = useState<'auto' | 'instant' | 'nlp' | 'demo'>('auto');
const [showModeSelector, setShowModeSelector] = useState(false);
const [instantSearchDisabled, setInstantSearchDisabled] = useState(false);
const [useDemoMode, setUseDemoMode] = useState(false);
const [isSearching, setIsSearching] = useState(false);
const [searchResults, setSearchResults] = useState<BattleQueryResult | null>(null);
const [lastSearchQuery, setLastSearchQuery] = useState('');
const [instantSearchLoading, setInstantSearchLoading] = useState(false);
const [instantSearchError, setInstantSearchError] = useState<{hasError: boolean, message: string}>({hasError: false, message: ''});
const isMountedRef = useRef(true);
const searchInputRef = useRef<HTMLInputElement>(null);
const debounceTimer = useRef<number | undefined>(undefined);
const currentQueryRef = useRef(currentQuery);
const isSearchingRef = useRef(isSearching);
// Query analysis functions
const isSimpleQuery = useCallback((query: string): boolean => {
const trimmed = query.trim();
// Empty query
if (!trimmed) return false;
// Single word (likely Pokemon name)
const words = trimmed.split(/\s+/);
if (words.length === 1) return true;
// Two words where second might be a type or generation
if (words.length === 2) {
const secondWord = words[1].toLowerCase();
const simpleModifiers = [
'fire', 'water', 'grass', 'electric', 'psychic', 'fighting',
'poison', 'ground', 'flying', 'bug', 'rock', 'ghost',
'dragon', 'dark', 'steel', 'fairy', 'ice', 'normal',
'gen1', 'gen2', 'gen3', 'gen4', 'gen5', 'gen6', 'gen7', 'gen8', 'gen9',
'kanto', 'johto', 'hoenn', 'sinnoh', 'unova', 'kalos', 'alola', 'galar', 'paldea'
];
return simpleModifiers.includes(secondWord);
}
return false;
}, []);
const isComplexQuery = useCallback((query: string): boolean => {
const lowerQuery = query.toLowerCase();
const complexKeywords = [
'counter', 'beat', 'against', 'vs', 'versus', 'defeat', 'stop',
'team', 'build', 'squad', 'composition', 'core',
'move', 'learn', 'knows', 'attack', 'tm', 'tutor',
'ability', 'hidden', 'nature', 'item', 'strategy',
'best', 'good', 'effective', 'weak', 'strong',
'what', 'how', 'which', 'where', 'why', 'when'
];
return complexKeywords.some(keyword => lowerQuery.includes(keyword));
}, []);
const determineSearchMode = useCallback((query: string): 'instant' | 'nlp' => {
if (isSimpleQuery(query)) return 'instant';
if (isComplexQuery(query)) return 'nlp';
// Default to instant for ambiguous cases
return 'instant';
}, [isSimpleQuery, isComplexQuery]);
// Calculate activeMode BEFORE using it in refs
const activeMode = useMemo(() => {
// If demo mode is explicitly enabled, use it
if (useDemoMode || searchMode === 'demo') {
return 'demo';
}
// If instant search is disabled due to errors, always use NLP
if (instantSearchDisabled) {
return 'nlp';
}
if (searchMode === 'auto') {
return determineSearchMode(currentQuery);
}
return searchMode === 'instant' ? 'instant' : 'nlp';
}, [searchMode, currentQuery, instantSearchDisabled, useDemoMode, determineSearchMode]);
const activeModeRef = useRef(activeMode);
// Keep refs in sync - only update with current values, no circular dependencies
useEffect(() => {
currentQueryRef.current = currentQuery;
activeModeRef.current = activeMode;
isSearchingRef.current = isSearching;
}, [currentQuery, activeMode, isSearching]);
const isAutoMode = searchMode === 'auto';
// Debounced mode detection to prevent rapid switching - simplified to avoid loops
const debouncedModeDetection = useCallback((query: string) => {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
debounceTimer.current = window.setTimeout(() => {
// Only update if there's a significant change and we're in auto mode
if (searchMode === 'auto' && query.trim() && query.trim() !== lastSearchQuery.trim()) {
// Only update lastSearchQuery when actually performing a search, not on every query change
console.log('Mode detection - query changed significantly:', query);
}
}, 300);
}, [searchMode, lastSearchQuery]);
const handleQueryChange = useCallback((query: string) => {
setCurrentQuery(query);
debouncedModeDetection(query);
}, [debouncedModeDetection]);
const handleSearch = useCallback(async () => {
const query = currentQueryRef.current;
const mode = activeModeRef.current;
if (!query.trim() || !isMountedRef.current) return;
// Prevent multiple simultaneous searches
if (isSearchingRef.current) {
console.log('[HybridSearch] Search already in progress, skipping...');
return;
}
// Additional guard - minimum query length
if (query.trim().length < 2) {
console.log('[HybridSearch] Query too short, skipping search:', query);
return;
}
setIsSearching(true);
// Update lastSearchQuery to prevent repeated searches for the same query
setLastSearchQuery(query);
try {
let result: BattleQueryResult;
if (mode === 'demo') {
// Handle demo search
const { searchMockPokemon } = await import('../data/mockPokemon');
const results = searchMockPokemon(query);
result = {
pokemon: results,
recommendations: [],
summary: `Found ${results.length} Pokemon in demo data`,
intent: 'pokemon_search'
};
} else if (mode === 'instant') {
// For instant search, create placeholder result
// Real InstantSearch will be handled separately
result = {
pokemon: [],
recommendations: [],
summary: 'Instant search results will be displayed below',
intent: 'instant_search'
};
} else {
// AI Analysis mode
const { enhancedNlSearchService } = await import('../services/naturalLanguageSearchEnhanced');
const searchResponse = await enhancedNlSearchService.processNaturalLanguageQuery(query);
result = {
pokemon: (searchResponse.results.pokemon || []) as PokemonData[],
recommendations: searchResponse.suggestions.map(suggestion => ({
pokemon: suggestion.pokemon,
types: [],
battleRole: 'Unknown',
tier: 'Unknown',
matchupRating: suggestion.matchupScore,
reasoning: suggestion.reasoning,
recommendedMoves: [],
recommendedAbility: '',
recommendedItem: '',
stats: { hp: 0, attack: 0, defense: 0, specialAttack: 0, specialDefense: 0, speed: 0, total: 0 },
abilities: []
})) as RecommendationData[],
summary: searchResponse.summary,
intent: searchResponse.intent.type
};
}
// Only update state if component is still mounted
if (isMountedRef.current) {
setSearchResults(result);
onResultsReceived(result);
}
} catch (error) {
console.error('Search error:', error);
} finally {
// Only update state if component is still mounted
if (isMountedRef.current) {
setIsSearching(false);
}
}
}, [onResultsReceived]); // Only stable dependency
// Cleanup debounce timer on unmount and track mounted state
useEffect(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
};
}, []);
// Auto-trigger search DISABLED FOR DEBUGGING - this may be causing the loop
// useEffect(() => {
// // All auto-search logic temporarily disabled
// }, []);
// Manual search only for now
// Handlers for instant search state
const handleInstantSearchLoading = useCallback((loading: boolean) => {
setInstantSearchLoading(loading);
}, []);
const handleInstantSearchError = useCallback((hasError: boolean, message: string) => {
setInstantSearchError({ hasError, message });
if (hasError) {
setInstantSearchDisabled(true);
}
}, []);
const handleInstantPokemonSelect = useCallback((pokemon: PokemonData, sourceImageRef?: HTMLImageElement) => {
// Convert single Pokemon to BattleQueryResult format and trigger analysis
const result: BattleQueryResult = {
pokemon: [pokemon],
recommendations: [],
summary: `Selected ${pokemon.name} for battle analysis`,
intent: 'pokemon_selection'
};
setSearchResults(result);
onResultsReceived(result);
onPokemonSelect(pokemon, sourceImageRef);
}, [onResultsReceived, onPokemonSelect]);
return (
<div className="space-y-6">
{/* Algolia Connection Status */}
<ErrorBoundary>
<AlgoliaStatus />
</ErrorBoundary>
{/* Search Mode Indicator & Selector */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="flex items-center space-x-2">
{activeMode === 'demo' ? (
<span className="text-orange-400">📋</span>
) : activeMode === 'instant' ? (
<Zap className="text-yellow-400" size={16} />
) : (
<Sparkles className="text-purple-400" size={16} />
)}
<span className="text-sm font-medium text-gray-300">
{isAutoMode ? (
<>Auto Mode: {activeMode === 'demo' ? 'Demo Search' : activeMode === 'instant' ? 'Instant Search' : 'AI Analysis'}</>
) : (
<>Manual: {activeMode === 'demo' ? 'Demo Search' : activeMode === 'instant' ? 'Instant Search' : 'AI Analysis'}</>
)}
</span>
</div>
{/* Temporarily disabled to debug loops */}
{/* {isAutoMode && currentQuery && (
<div className="text-xs text-gray-500 flex items-center space-x-1">
<ArrowRight size={12} />
<span>
{isSimpleQuery(currentQuery) ? 'Simple query detected' : 'Complex query detected'}
</span>
</div>
)} */}
</div>
<button
onClick={() => setShowModeSelector(!showModeSelector)}
className="text-xs text-gray-400 hover:text-white transition-colors px-2 py-1 rounded hover:bg-white/10"
>
Change Mode
</button>
</div>
{/* Mode Selector */}
{showModeSelector && (
<div className="bg-gray-800/50 rounded-lg p-4 border border-white/10">
<h4 className="text-sm font-semibold text-white mb-3">Search Mode</h4>
<div className="space-y-2">
<label className="flex items-center space-x-3 cursor-pointer">
<input
type="radio"
name="searchMode"
value="auto"
checked={searchMode === 'auto'}
onChange={(e) => {
setSearchMode(e.target.value as 'auto');
setShowModeSelector(false);
}}
className="text-purple-400"
/>
<div>
<span className="text-white text-sm font-medium">Auto (Recommended)</span>
<p className="text-xs text-gray-400">Automatically chooses the best search method</p>
</div>
</label>
<label className="flex items-center space-x-3 cursor-pointer">
<input
type="radio"
name="searchMode"
value="instant"
checked={searchMode === 'instant'}
onChange={(e) => {
setSearchMode(e.target.value as 'instant');
setShowModeSelector(false);
}}
className="text-purple-400"
/>
<div>
<span className="text-white text-sm font-medium">Instant Search</span>
<p className="text-xs text-gray-400">Lightning-fast Pokemon name search</p>
</div>
</label>
<label className="flex items-center space-x-3 cursor-pointer">
<input
type="radio"
name="searchMode"
value="nlp"
checked={searchMode === 'nlp'}
onChange={(e) => {
setSearchMode(e.target.value as 'nlp');
setShowModeSelector(false);
}}
className="text-purple-400"
/>
<div>
<span className="text-white text-sm font-medium">AI Analysis</span>
<p className="text-xs text-gray-400">Natural language battle strategy queries</p>
</div>
</label>
<label className="flex items-center space-x-3 cursor-pointer">
<input
type="radio"
name="searchMode"
value="demo"
checked={searchMode === 'demo'}
onChange={(e) => {
setSearchMode(e.target.value as 'demo');
setUseDemoMode(true);
setShowModeSelector(false);
}}
className="text-purple-400"
/>
<div>
<span className="text-white text-sm font-medium">Demo Mode</span>
<p className="text-xs text-gray-400">Search sample Pokemon data offline</p>
</div>
</label>
</div>
</div>
)}
{/* Unified Persistent Search Interface */}
<div className="mb-8 bg-gradient-to-r from-purple-900/20 to-blue-900/20 backdrop-blur-lg rounded-2xl p-6 border border-white/20">
<div className="mb-6">
<h2 className="text-2xl font-bold mb-2 flex items-center text-white">
🔍 Pokémon Search
</h2>
<p className="text-gray-300">
Search for Pokémon and battle strategies
</p>
</div>
{/* Persistent Search Input */}
<div className="space-y-4">
<div className="relative">
<Search className="absolute left-4 top-4 text-gray-400 z-10" size={20} />
{(isSearching || (activeMode === 'instant' && instantSearchLoading)) && (
<Loader className="absolute right-4 top-4 text-purple-400 animate-spin z-10" size={20} />
)}
<input
ref={searchInputRef}
type="text"
value={currentQuery}
onChange={(e) => handleQueryChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && currentQuery.trim()) {
handleSearch();
}
}}
placeholder="Search Pokémon or ask battle questions..."
className="w-full pl-12 pr-12 py-4 bg-white/5 border border-white/20 rounded-xl text-white placeholder-gray-400 focus:outline-none focus:border-purple-400 focus:ring-2 focus:ring-purple-400/20 transition-all text-lg"
/>
</div>
{/* Search button - hidden for instant mode since it's real-time */}
{activeMode !== 'instant' && (
<button
onClick={handleSearch}
disabled={isSearching || !currentQuery.trim()}
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 text-white py-3 px-6 rounded-xl font-semibold hover:from-purple-700 hover:to-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all transform hover:scale-105"
>
{isSearching ? 'Searching...' :
activeMode === 'demo' ? 'Search Demo Data' :
'Ask Battle AI'}
</button>
)}
{/* Real-time indicator for instant mode */}
{activeMode === 'instant' && (
<div className="text-center py-2">
<div className="inline-flex items-center space-x-2 text-sm text-gray-400">
<Zap size={16} className="text-yellow-400" />
<span>Real-time search - results appear as you type</span>
</div>
</div>
)}
</div>
{/* Custom Instant Search Integration - No infinite loops! */}
{activeMode === 'instant' && (
<div className="mt-6">
{instantSearchError.hasError ? (
<div className="bg-red-900/20 border border-red-500/30 rounded-lg p-4">
<div className="flex items-start">
<span className="text-red-400 mr-2">⚠️</span>
<div>
<h4 className="text-red-300 font-semibold text-sm mb-1">Instant Search Error</h4>
<p className="text-red-200 text-sm">{instantSearchError.message}</p>
</div>
</div>
</div>
) : (
<ErrorBoundary
fallback={
<div className="bg-red-900/20 border border-red-500/30 rounded-lg p-4">
<div className="flex items-start">
<span className="text-red-400 mr-2">⚠️</span>
<div>
<h4 className="text-red-300 font-semibold text-sm mb-1">Instant Search Error</h4>
<p className="text-red-200 text-sm">
The instant search component failed to load. Try switching to Demo mode or AI Analysis mode.
</p>
</div>
</div>
</div>
}
>
<CustomInstantSearchResults
query={currentQuery}
onPokemonSelect={handleInstantPokemonSelect}
onLoadingChange={handleInstantSearchLoading}
onErrorChange={handleInstantSearchError}
/>
</ErrorBoundary>
)}
</div>
)}
{/* Search Results Display */}
{searchResults && (
<div className="mt-8 space-y-4">
{/* AI Summary */}
{searchResults.summary && (
<div className="p-4 bg-gradient-to-r from-green-900/20 to-teal-900/20 border border-green-500/30 rounded-lg">
<div className="flex items-start">
<Sparkles className="text-green-400 mr-2 mt-1 flex-shrink-0" size={16} />
<div>
<h4 className="text-green-300 font-semibold text-sm mb-1">AI Summary</h4>
<p className="text-green-100 text-sm leading-relaxed">{searchResults.summary}</p>
</div>
</div>
</div>
)}
{/* Recommendations */}
{searchResults.recommendations && searchResults.recommendations.length > 0 && (
<div className="bg-gradient-to-r from-purple-900/20 to-pink-900/20 border border-purple-500/30 rounded-lg p-4">
<h4 className="text-purple-300 font-semibold text-sm mb-3 flex items-center">
<span className="mr-2">💡</span>
Battle Recommendations ({searchResults.recommendations.length})
</h4>
<div className="grid gap-3">
{searchResults.recommendations.slice(0, 3).map((rec, index) => (
<div key={index} className="bg-white/5 rounded-lg p-3 border border-white/10">
<div className="flex items-center justify-between mb-2">
<span className="text-white font-medium">{rec.pokemon}</span>
<span className="text-xs text-purple-400">
{rec.matchupRating}% match
</span>
</div>
<p className="text-gray-400 text-xs">{rec.reasoning}</p>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
{/* Developer Notice */}
{instantSearchDisabled && (
<div className="mt-4 p-4 bg-yellow-900/20 border border-yellow-500/30 rounded-lg">
<div className="flex items-start space-x-2">
<span className="text-yellow-400 text-sm">⚠️</span>
<div className="text-sm text-yellow-200">
<p className="font-semibold mb-1">Instant Search Unavailable</p>
<p className="text-xs text-yellow-300">
The Pokémon search index needs to be set up in Algolia.
Using AI Analysis mode as fallback.
</p>
</div>
</div>
</div>
)}
</div>
);
};
export default HybridSearch;