diff --git a/frontend/src/pages/ServersPage.tsx b/frontend/src/pages/ServersPage.tsx index 02edf28..ecfe752 100644 --- a/frontend/src/pages/ServersPage.tsx +++ b/frontend/src/pages/ServersPage.tsx @@ -8,6 +8,7 @@ import EditServerForm from '@/components/EditServerForm'; import { useServerData } from '@/hooks/useServerData'; import DxtUploadForm from '@/components/DxtUploadForm'; import JSONImportForm from '@/components/JSONImportForm'; +import { apiGet } from '@/utils/fetchInterceptor'; const ServersPage: React.FC = () => { const { t } = useTranslation(); @@ -27,6 +28,10 @@ const ServersPage: React.FC = () => { const [isRefreshing, setIsRefreshing] = useState(false); const [showDxtUpload, setShowDxtUpload] = useState(false); const [showJsonImport, setShowJsonImport] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const [similarityThreshold, setSimilarityThreshold] = useState(0.65); + const [isSearching, setIsSearching] = useState(false); + const [searchResults, setSearchResults] = useState(null); const handleEditClick = async (server: Server) => { const fullServerData = await handleServerEdit(server); @@ -63,6 +68,31 @@ const ServersPage: React.FC = () => { triggerRefresh(); }; + const handleSemanticSearch = async () => { + if (!searchQuery.trim()) { + return; + } + + setIsSearching(true); + try { + const result = await apiGet(`/servers/search?query=${encodeURIComponent(searchQuery)}&threshold=${similarityThreshold}`); + if (result.success && result.data) { + setSearchResults(result.data.servers); + } else { + setError(result.message || 'Search failed'); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Search failed'); + } finally { + setIsSearching(false); + } + }; + + const handleClearSearch = () => { + setSearchQuery(''); + setSearchResults(null); + }; + return (
@@ -116,6 +146,72 @@ const ServersPage: React.FC = () => {
+ {/* Semantic Search Section */} +
+
+
+
+ setSearchQuery(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && handleSemanticSearch()} + placeholder={t('pages.servers.semanticSearchPlaceholder')} + className="shadow appearance-none border border-gray-200 rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input" + /> +
+ + {searchResults && ( + + )} +
+
+ + setSimilarityThreshold(parseFloat(e.target.value))} + className="flex-grow h-2 bg-blue-200 rounded-lg appearance-none cursor-pointer" + /> + {t('pages.servers.similarityThresholdHelp')} +
+
+
+ + {searchResults && ( +
+

+ {searchResults.length > 0 + ? t('pages.servers.searchResults', { count: searchResults.length }) + : t('pages.servers.noSearchResults')} +

+
+ )} + {error && (
@@ -145,13 +241,13 @@ const ServersPage: React.FC = () => {

{t('app.loading')}

- ) : servers.length === 0 ? ( + ) : (searchResults ? searchResults : servers).length === 0 ? (
-

{t('app.noServers')}

+

{searchResults ? t('pages.servers.noSearchResults') : t('app.noServers')}

) : (
- {servers.map((server, index) => ( + {(searchResults || servers).map((server, index) => (