|
1 | 1 | import React, { useState, useEffect, useCallback, useRef } from 'react'; |
2 | | -import { Search, ArrowLeft, X, LayoutGrid, Loader2, Database, Download, ChevronLeft, ChevronRight, Scan } from 'lucide-react'; |
3 | | -import { searchPages, searchSimilar, getClusters, getClusterDetail, getStatus, startIndexing, getIndexStatus, pageImageUrl, exportSnapshot } from './api'; |
| 2 | +import { Search, ArrowLeft, X, LayoutGrid, Loader2, Database, Download, ChevronLeft, ChevronRight, Scan, ImagePlus } from 'lucide-react'; |
| 3 | +import { searchPages, searchSimilar, searchByImage, getClusters, getClusterDetail, getStatus, startIndexing, getIndexStatus, pageImageUrl, exportSnapshot } from './api'; |
4 | 4 |
|
5 | 5 | function App() { |
6 | 6 | const [query, setQuery] = useState(''); |
@@ -36,6 +36,7 @@ function App() { |
36 | 36 | }); |
37 | 37 | const [indexError, setIndexError] = useState(null); |
38 | 38 | const pollRef = useRef(null); |
| 39 | + const fileInputRef = useRef(null); |
39 | 40 |
|
40 | 41 | // Connection status |
41 | 42 | const [backendReady, setBackendReady] = useState(false); |
@@ -156,6 +157,34 @@ function App() { |
156 | 157 | searchContextRef.current = null; |
157 | 158 | }; |
158 | 159 |
|
| 160 | + const handleImageSearch = useCallback(async (file) => { |
| 161 | + setLoading(true); |
| 162 | + setError(null); |
| 163 | + setLightboxIndex(null); |
| 164 | + setCurrentPage(1); |
| 165 | + setActiveClusterId(null); |
| 166 | + setActiveCluster(null); |
| 167 | + setClusterPages([]); |
| 168 | + searchContextRef.current = { type: 'image' }; |
| 169 | + setQuery(`Image: ${file.name}`); |
| 170 | + try { |
| 171 | + const results = await searchByImage(file, FETCH_BATCH); |
| 172 | + setSearchResults(results); |
| 173 | + setFetchedTopK(FETCH_MAX); // disable load-more — file is ephemeral |
| 174 | + } catch (e) { |
| 175 | + setError(e.message); |
| 176 | + setSearchResults([]); |
| 177 | + } finally { |
| 178 | + setLoading(false); |
| 179 | + } |
| 180 | + }, []); |
| 181 | + |
| 182 | + const handleFileChange = (e) => { |
| 183 | + const file = e.target.files?.[0]; |
| 184 | + if (file) handleImageSearch(file); |
| 185 | + e.target.value = ''; |
| 186 | + }; |
| 187 | + |
159 | 188 | const handleExport = async () => { |
160 | 189 | setExporting(true); |
161 | 190 | try { |
@@ -229,7 +258,7 @@ function App() { |
229 | 258 | } |
230 | 259 | // Need more results — fetch next batch and advance |
231 | 260 | const ctx = searchContextRef.current; |
232 | | - if (!ctx || fetchedTopK >= FETCH_MAX) return; |
| 261 | + if (!ctx || ctx.type === 'image' || fetchedTopK >= FETCH_MAX) return; |
233 | 262 | const nextTopK = Math.min(fetchedTopK + FETCH_BATCH, FETCH_MAX); |
234 | 263 | setLoadingMore(true); |
235 | 264 | try { |
@@ -286,20 +315,36 @@ function App() { |
286 | 315 | </div> |
287 | 316 | <input |
288 | 317 | type="text" |
289 | | - className="block w-full pl-4 pr-12 py-4 bg-transparent border-none text-white placeholder-gray-600 focus:outline-none focus:ring-0 text-lg" |
| 318 | + className="block w-full pl-4 pr-20 py-4 bg-transparent border-none text-white placeholder-gray-600 focus:outline-none focus:ring-0 text-lg" |
290 | 319 | placeholder="Search documents..." |
291 | 320 | value={query} |
292 | 321 | onChange={(e) => setQuery(e.target.value)} |
293 | 322 | onKeyDown={handleKeyDown} |
294 | 323 | /> |
295 | | - {query && ( |
| 324 | + <div className="absolute inset-y-0 right-0 pr-4 flex items-center gap-2"> |
| 325 | + {query && ( |
| 326 | + <button |
| 327 | + onClick={clearSearch} |
| 328 | + className="cursor-pointer text-gray-500 hover:text-gray-300" |
| 329 | + > |
| 330 | + <X className="h-5 w-5" /> |
| 331 | + </button> |
| 332 | + )} |
296 | 333 | <button |
297 | | - onClick={clearSearch} |
298 | | - className="absolute inset-y-0 right-0 pr-4 flex items-center cursor-pointer text-gray-500 hover:text-gray-300" |
| 334 | + onClick={() => fileInputRef.current?.click()} |
| 335 | + className="cursor-pointer text-gray-500 hover:text-indigo-400 transition-colors" |
| 336 | + title="Search by image" |
299 | 337 | > |
300 | | - <X className="h-5 w-5" /> |
| 338 | + <ImagePlus className="h-5 w-5" /> |
301 | 339 | </button> |
302 | | - )} |
| 340 | + <input |
| 341 | + ref={fileInputRef} |
| 342 | + type="file" |
| 343 | + accept="image/jpeg,image/png" |
| 344 | + className="hidden" |
| 345 | + onChange={handleFileChange} |
| 346 | + /> |
| 347 | + </div> |
303 | 348 | </div> |
304 | 349 | </div> |
305 | 350 |
|
|
0 commit comments