File size: 5,337 Bytes
cf165dd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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;