Add semantic search UI to servers management page

Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-10-31 15:16:34 +00:00
parent d8e127d911
commit a8852f7807
4 changed files with 126 additions and 6 deletions

View File

@@ -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<Server[] | null>(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 (
<div>
<div className="flex justify-between items-center mb-8">
@@ -116,6 +146,72 @@ const ServersPage: React.FC = () => {
</div>
</div>
{/* Semantic Search Section */}
<div className="bg-white shadow rounded-lg p-6 mb-6 page-card">
<div className="space-y-4">
<div className="flex space-x-4">
<div className="flex-grow">
<input
type="text"
value={searchQuery}
onChange={(e) => 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"
/>
</div>
<button
onClick={handleSemanticSearch}
disabled={isSearching || !searchQuery.trim()}
className="px-6 py-2 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 flex items-center btn-primary transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSearching ? (
<svg className="animate-spin h-4 w-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clipRule="evenodd" />
</svg>
)}
{t('pages.servers.searchButton')}
</button>
{searchResults && (
<button
onClick={handleClearSearch}
className="border border-gray-300 text-gray-700 font-medium py-2 px-4 rounded hover:bg-gray-50 btn-secondary transition-all duration-200"
>
{t('pages.servers.clearSearch')}
</button>
)}
</div>
<div className="flex items-center space-x-4">
<label className="text-sm text-gray-700 font-medium min-w-max">{t('pages.servers.similarityThreshold')}: {similarityThreshold.toFixed(2)}</label>
<input
type="range"
min="0"
max="1"
step="0.05"
value={similarityThreshold}
onChange={(e) => setSimilarityThreshold(parseFloat(e.target.value))}
className="flex-grow h-2 bg-blue-200 rounded-lg appearance-none cursor-pointer"
/>
<span className="text-xs text-gray-500">{t('pages.servers.similarityThresholdHelp')}</span>
</div>
</div>
</div>
{searchResults && (
<div className="mb-4 bg-blue-50 border-l-4 border-blue-500 p-4 rounded">
<p className="text-blue-800">
{searchResults.length > 0
? t('pages.servers.searchResults', { count: searchResults.length })
: t('pages.servers.noSearchResults')}
</p>
</div>
)}
{error && (
<div className="mb-6 bg-red-50 border-l-4 border-red-500 p-4 rounded shadow-sm error-box">
<div className="flex items-center justify-between">
@@ -145,13 +241,13 @@ const ServersPage: React.FC = () => {
<p className="text-gray-600">{t('app.loading')}</p>
</div>
</div>
) : servers.length === 0 ? (
) : (searchResults ? searchResults : servers).length === 0 ? (
<div className="bg-white shadow rounded-lg p-6 empty-state">
<p className="text-gray-600">{t('app.noServers')}</p>
<p className="text-gray-600">{searchResults ? t('pages.servers.noSearchResults') : t('app.noServers')}</p>
</div>
) : (
<div className="space-y-6">
{servers.map((server, index) => (
{(searchResults || servers).map((server, index) => (
<ServerCard
key={index}
server={server}

View File

@@ -268,7 +268,15 @@
"recentServers": "Recent Servers"
},
"servers": {
"title": "Servers Management"
"title": "Servers Management",
"semanticSearch": "Intelligent search for tools...",
"semanticSearchPlaceholder": "Describe the functionality you need, e.g.: maps, weather, file processing",
"similarityThreshold": "Similarity Threshold",
"similarityThresholdHelp": "Higher values return more precise results, lower values return broader matches",
"searchButton": "Search",
"clearSearch": "Clear Search",
"searchResults": "Found {{count}} matching server(s)",
"noSearchResults": "No matching servers found"
},
"groups": {
"title": "Group Management"

View File

@@ -268,7 +268,15 @@
"recentServers": "Serveurs récents"
},
"servers": {
"title": "Gestion des serveurs"
"title": "Gestion des serveurs",
"semanticSearch": "Recherche intelligente d'outils...",
"semanticSearchPlaceholder": "Décrivez la fonctionnalité dont vous avez besoin, par ex. : cartes, météo, traitement de fichiers",
"similarityThreshold": "Seuil de similarité",
"similarityThresholdHelp": "Des valeurs plus élevées renvoient des résultats plus précis, des valeurs plus faibles des correspondances plus larges",
"searchButton": "Rechercher",
"clearSearch": "Effacer la recherche",
"searchResults": "{{count}} serveur(s) correspondant(s) trouvé(s)",
"noSearchResults": "Aucun serveur correspondant trouvé"
},
"groups": {
"title": "Gestion des groupes"

View File

@@ -269,7 +269,15 @@
"recentServers": "最近的服务器"
},
"servers": {
"title": "服务器管理"
"title": "服务器管理",
"semanticSearch": "智能搜索工具...",
"semanticSearchPlaceholder": "描述您需要的功能,例如:地图、天气、文件处理",
"similarityThreshold": "相似度阈值",
"similarityThresholdHelp": "较高值返回更精确结果,较低值返回更广泛匹配",
"searchButton": "搜索",
"clearSearch": "清除搜索",
"searchResults": "找到 {{count}} 个匹配的服务器",
"noSearchResults": "未找到匹配的服务器"
},
"settings": {
"title": "设置",