| import React, { useState, useCallback } from 'react'; | |
| import { groundedSearch } from '../services/geminiService'; | |
| import { Spinner } from '../components/Spinner'; | |
| import { GroundingChunk } from '../types'; | |
| import { MarkdownRenderer } from '../components/MarkdownRenderer'; | |
| const GroundedSearchModule: React.FC = () => { | |
| const [prompt, setPrompt] = useState(''); | |
| const [tool, setTool] = useState<'googleSearch' | 'googleMaps'>('googleSearch'); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [error, setError] = useState<string | null>(null); | |
| const [response, setResponse] = useState<string | null>(null); | |
| const [chunks, setChunks] = useState<GroundingChunk[]>([]); | |
| const [location, setLocation] = useState<GeolocationCoordinates | null>(null); | |
| const [locationError, setLocationError] = useState<string | null>(null); | |
| const handleSearch = useCallback(async () => { | |
| if (!prompt.trim()) { | |
| setError('Please enter a search query.'); | |
| return; | |
| } | |
| setIsLoading(true); | |
| setError(null); | |
| setResponse(null); | |
| setChunks([]); | |
| let searchLocation: GeolocationCoordinates | undefined = undefined; | |
| if(tool === 'googleMaps') { | |
| if (!location) { | |
| try { | |
| const pos = await new Promise<GeolocationPosition>((resolve, reject) => { | |
| navigator.geolocation.getCurrentPosition(resolve, reject); | |
| }); | |
| setLocation(pos.coords); | |
| searchLocation = pos.coords; | |
| } catch (err) { | |
| setLocationError('Could not get location. Please enable location services. Search will proceed without it.'); | |
| } | |
| } else { | |
| searchLocation = location; | |
| } | |
| } | |
| try { | |
| const result = await groundedSearch(prompt, tool, searchLocation); | |
| setResponse(result.text); | |
| if (result.candidates?.[0]?.groundingMetadata?.groundingChunks) { | |
| setChunks(result.candidates[0].groundingMetadata.groundingChunks); | |
| } | |
| } catch (e: any) { | |
| setError(e.message || 'An unknown error occurred.'); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }, [prompt, tool, location]); | |
| return ( | |
| <div className="flex flex-col h-full w-full max-w-4xl mx-auto"> | |
| <h2 className="text-2xl font-bold text-cyan-300 mb-4 text-center">Grounded Search</h2> | |
| <p className="text-gray-400 mb-6 text-center">Get up-to-date, verifiable information by grounding Gemini's responses with Google Search or Google Maps.</p> | |
| <div className="flex flex-col sm:flex-row gap-2 mb-4"> | |
| <input | |
| type="text" | |
| value={prompt} | |
| onChange={(e) => setPrompt(e.target.value)} | |
| placeholder="e.g., Who won the latest F1 race? or Good Italian restaurants nearby?" | |
| className="flex-grow bg-gray-700 border border-gray-600 rounded-md p-3 focus:outline-none focus:ring-2 focus:ring-cyan-500 text-white" | |
| disabled={isLoading} | |
| /> | |
| <button | |
| onClick={handleSearch} | |
| disabled={isLoading} | |
| className="bg-cyan-600 hover:bg-cyan-500 text-white font-bold py-3 px-4 rounded-md disabled:bg-gray-500 transition-colors" | |
| > | |
| {isLoading ? 'Searching...' : 'Search'} | |
| </button> | |
| </div> | |
| <div className="flex items-center justify-center gap-4 mb-6"> | |
| <label className="flex items-center gap-2 cursor-pointer"> | |
| <input type="radio" name="tool" value="googleSearch" checked={tool === 'googleSearch'} onChange={() => setTool('googleSearch')} className="form-radio text-cyan-500 bg-gray-800"/> | |
| Google Search | |
| </label> | |
| <label className="flex items-center gap-2 cursor-pointer"> | |
| <input type="radio" name="tool" value="googleMaps" checked={tool === 'googleMaps'} onChange={() => setTool('googleMaps')} className="form-radio text-cyan-500 bg-gray-800"/> | |
| Google Maps | |
| </label> | |
| </div> | |
| {locationError && <p className="text-yellow-400 text-center mb-4">{locationError}</p>} | |
| <div className="flex-grow overflow-y-auto p-4 bg-gray-800/50 rounded-lg border border-cyan-500/10 min-h-[40vh]"> | |
| {isLoading && <Spinner text="Retrieving grounded information..." />} | |
| {error && <p className="text-red-400 text-center">{error}</p>} | |
| {response && ( | |
| <div> | |
| <MarkdownRenderer content={response} /> | |
| {chunks.length > 0 && ( | |
| <div className="mt-8 border-t border-cyan-500/20 pt-4"> | |
| <h4 className="font-bold text-cyan-400 mb-2">Sources:</h4> | |
| <ul className="list-disc list-inside text-sm"> | |
| {chunks.map((chunk, index) => { | |
| const uri = chunk.web?.uri || chunk.maps?.uri; | |
| const title = chunk.web?.title || chunk.maps?.title; | |
| if (!uri) return null; | |
| return <li key={index}><a href={uri} target="_blank" rel="noopener noreferrer" className="text-purple-300 hover:underline">{title}</a></li> | |
| })} | |
| </ul> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default GroundedSearchModule; |