davanstrien's picture
davanstrien HF staff
add param filter
ecc20d1
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hub Semantic Search</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
</head>
<body>
<div class="w-full max-w-4xl mx-auto p-4 space-y-8">
<h1 class="text-3xl font-bold text-gray-800">Hub Semantic Search</h1>
<div
class="bg-gradient-to-br from-blue-50 to-indigo-50 p-6 rounded-xl shadow-sm border border-blue-100 mb-6"
>
<h2
class="text-lg font-semibold mb-2 text-gray-800 flex items-center gap-2"
>
<i data-lucide="search" class="text-blue-500"></i>
Welcome to Hub Semantic Search
</h2>
<p class="text-gray-700 mb-2 text-sm">
Find and explore the 🤗 Hub using via semantic search on LLM generated
summaries! Summaries are generated using
<a
href="https://huggingface.co/davanstrien/Smol-Hub-tldr"
target="_blank"
class="text-blue-500 hover:text-blue-700 hover:underline"
>
Smol-Hub-tldr </a
>.
</p>
<div
class="bg-blue-100 text-blue-800 px-3 py-1.5 rounded-md mb-2 text-sm"
>
<p class="flex items-center gap-2">
<i data-lucide="info"></i> Search for datasets and models using
semantic search!
</p>
</div>
<button
onclick="toggleAccordion()"
id="accordionButton"
class="text-blue-500 hover:text-blue-700 flex items-center gap-2 text-sm"
>
<i
data-lucide="chevron-right"
id="accordionIcon"
class="transition-transform"
></i>
<span>How it works</span>
</button>
<div id="accordionContent" class="hidden">
<ul
class="list-disc list-inside space-y-1 text-gray-600 ml-4 mt-2 text-sm"
>
<li>
<strong>AI-Generated Summaries:</strong> Each dataset is indexed
using a concise summary generated by an LLM
</li>
<li>
<strong>Semantic Search:</strong> Find semantically similar
resources based on these summaries
</li>
<li>
<strong>Find Similar:</strong> Discover related resources using
semantic matching
</li>
</ul>
</div>
</div>
<div class="tabs w-full">
<div class="tab-list flex gap-2 border-b mb-6">
<button
onclick="switchTab('search')"
id="searchTab"
class="tab-trigger active px-4 sm:px-6 py-3 flex items-center gap-2 border-b-2 border-transparent hover:bg-gray-50 transition-colors flex-1 justify-center"
>
<i data-lucide="search"></i> Search
</button>
<button
onclick="switchTab('similar')"
id="similarTab"
class="tab-trigger px-4 sm:px-6 py-3 flex items-center gap-2 border-b-2 border-transparent hover:bg-gray-50 transition-colors flex-1 justify-center"
>
<i data-lucide="arrow-right"></i> Find Similar
</button>
<button
onclick="switchTab('trending')"
id="trendingTab"
class="tab-trigger px-4 sm:px-6 py-3 flex items-center gap-2 border-b-2 border-transparent hover:bg-gray-50 transition-colors flex-1 justify-center"
>
<i data-lucide="trending-up"></i> Trending
</button>
</div>
<div id="searchContent" class="tab-content space-y-4">
<div
class="card bg-white p-8 rounded-xl shadow-sm border border-gray-100"
>
<div
class="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between mb-4"
>
<p class="text-gray-600">
Enter keywords to search through descriptions. The search will
automatically update as you type.
</p>
<div class="flex flex-wrap gap-2">
<select
id="searchTypeSelect"
class="text-sm border rounded-lg px-3 py-2 bg-white text-gray-700 focus:ring-2 focus:ring-blue-100 focus:border-blue-300 transition-all outline-none"
onchange="selectType(this.value)"
>
<option value="datasets">Datasets</option>
<option value="models">Models</option>
</select>
<select
id="searchSortSelect"
class="text-sm border rounded-lg px-3 py-2 bg-white text-gray-700 focus:ring-2 focus:ring-blue-100 focus:border-blue-300 transition-all outline-none"
onchange="handleFilterChange('search')"
>
<option value="similarity">Sort by relevance</option>
<option value="likes">Sort by likes</option>
<option value="downloads">Sort by downloads</option>
<option value="trending">Sort by trending</option>
</select>
<div class="relative">
<button
onclick="toggleFilters('search')"
class="text-sm border rounded-lg px-3 py-2 bg-white text-gray-700 hover:bg-gray-50 focus:ring-2 focus:ring-blue-100 focus:border-blue-300 transition-all outline-none flex items-center gap-2"
>
<i data-lucide="filter"></i>
Filters
<span
id="searchActiveFilters"
class="hidden px-1.5 py-0.5 bg-blue-100 text-blue-700 text-xs rounded-full"
></span>
</button>
<div
id="searchFiltersPopover"
class="hidden absolute right-0 mt-2 w-64 bg-white border border-gray-200 rounded-lg shadow-lg p-4 z-10"
>
<div class="space-y-4">
<div>
<label
class="block text-sm font-medium text-gray-700 mb-1"
>Minimum Likes</label
>
<input
type="number"
id="searchMinLikes"
placeholder="0"
min="0"
class="w-full text-sm border rounded-lg px-3 py-2 bg-white text-gray-700 focus:ring-2 focus:ring-blue-100 focus:border-blue-300 transition-all outline-none"
onchange="handleFilterChange('search')"
/>
</div>
<div>
<label
class="block text-sm font-medium text-gray-700 mb-1"
>Minimum Downloads</label
>
<input
type="number"
id="searchMinDownloads"
placeholder="0"
min="0"
class="w-full text-sm border rounded-lg px-3 py-2 bg-white text-gray-700 focus:ring-2 focus:ring-blue-100 focus:border-blue-300 transition-all outline-none"
onchange="handleFilterChange('search')"
/>
</div>
<div id="searchParamRange" class="hidden">
<label
class="block text-sm font-medium text-gray-700 mb-1"
>Parameter Range</label
>
<select
id="searchParamRangeSelect"
class="w-full text-sm border rounded-lg px-3 py-2 bg-white text-gray-700 focus:ring-2 focus:ring-blue-100 focus:border-blue-300 transition-all outline-none"
onchange="handleFilterChange('search')"
>
<option value="any">Any</option>
<option value="0-1">0-1B</option>
<option value="1-7">1-7B</option>
<option value="7-20">7-20B</option>
<option value="20-70">20-70B</option>
<option value="70+">70B+</option>
</select>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="relative">
<input
type="text"
id="searchInput"
placeholder="Type to search (minimum 3 characters)..."
class="w-full p-3 border rounded-lg pr-10 focus:ring-2 focus:ring-blue-100 focus:border-blue-300 transition-all outline-none"
/>
<div id="searchLoader" class="hidden absolute right-3 top-2">
<i data-lucide="loader-2" class="animate-spin"></i>
</div>
</div>
</div>
</div>
<div id="similarContent" class="hidden tab-content space-y-4">
<div
class="card bg-white p-8 rounded-xl shadow-sm border border-gray-100"
>
<div
class="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between mb-4"
>
<p class="text-gray-600">
Enter an ID to find similar resources. Popular items will appear
as you type.
</p>
<div class="flex flex-wrap gap-2">
<select
id="similarTypeSelect"
class="text-sm border rounded-lg px-3 py-2 bg-white text-gray-700 focus:ring-2 focus:ring-blue-100 focus:border-blue-300 transition-all outline-none"
onchange="selectType(this.value)"
>
<option value="datasets">Datasets</option>
<option value="models">Models</option>
</select>
<select
id="similarSortSelect"
class="text-sm border rounded-lg px-3 py-2 bg-white text-gray-700 focus:ring-2 focus:ring-blue-100 focus:border-blue-300 transition-all outline-none"
onchange="handleFilterChange('similar')"
>
<option value="similarity">Sort by relevance</option>
<option value="likes">Sort by likes</option>
<option value="downloads">Sort by downloads</option>
<option value="trending">Sort by trending</option>
</select>
<div class="relative">
<button
onclick="toggleFilters('similar')"
class="text-sm border rounded-lg px-3 py-2 bg-white text-gray-700 hover:bg-gray-50 focus:ring-2 focus:ring-blue-100 focus:border-blue-300 transition-all outline-none flex items-center gap-2"
>
<i data-lucide="filter"></i>
Filters
<span
id="similarActiveFilters"
class="hidden px-1.5 py-0.5 bg-blue-100 text-blue-700 text-xs rounded-full"
></span>
</button>
<div
id="similarFiltersPopover"
class="hidden absolute right-0 mt-2 w-64 bg-white border border-gray-200 rounded-lg shadow-lg p-4 z-10"
>
<div class="space-y-4">
<div>
<label
class="block text-sm font-medium text-gray-700 mb-1"
>Minimum Likes</label
>
<input
type="number"
id="similarMinLikes"
placeholder="0"
min="0"
class="w-full text-sm border rounded-lg px-3 py-2 bg-white text-gray-700 focus:ring-2 focus:ring-blue-100 focus:border-blue-300 transition-all outline-none"
onchange="handleFilterChange('similar')"
/>
</div>
<div>
<label
class="block text-sm font-medium text-gray-700 mb-1"
>Minimum Downloads</label
>
<input
type="number"
id="similarMinDownloads"
placeholder="0"
min="0"
class="w-full text-sm border rounded-lg px-3 py-2 bg-white text-gray-700 focus:ring-2 focus:ring-blue-100 focus:border-blue-300 transition-all outline-none"
onchange="handleFilterChange('similar')"
/>
</div>
<div id="similarParamRange" class="hidden">
<label
class="block text-sm font-medium text-gray-700 mb-1"
>Parameter Range</label
>
<select
id="similarParamRangeSelect"
class="w-full text-sm border rounded-lg px-3 py-2 bg-white text-gray-700 focus:ring-2 focus:ring-blue-100 focus:border-blue-300 transition-all outline-none"
onchange="handleFilterChange('similar')"
>
<option value="any">Any</option>
<option value="0-1">0-1B</option>
<option value="1-7">1-7B</option>
<option value="7-20">7-20B</option>
<option value="20-70">20-70B</option>
<option value="70+">70B+</option>
</select>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="flex gap-3">
<div class="relative w-full">
<input
type="text"
id="resourceInput"
class="w-full p-3 border border-gray-200 rounded-lg"
placeholder="e.g. openai/gsm8k or meta-llama/Llama-2-7b"
/>
<div id="similarLoader" class="hidden absolute right-3 top-3">
<i data-lucide="loader-2" class="animate-spin"></i>
</div>
<div
id="suggestionsBox"
class="hidden absolute w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-10 max-h-60 overflow-y-auto"
></div>
</div>
<button onclick="findSimilarResources()" class="btn-primary">
Find Similar
</button>
</div>
</div>
</div>
<!-- Start: New Trending Tab Content -->
<div id="trendingContent" class="hidden tab-content space-y-6">
<div class="bg-white p-8 rounded-xl shadow-sm border border-gray-100">
<div
class="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between mb-6"
>
<h2
class="text-xl font-semibold text-gray-800 flex items-center gap-2"
>
<i data-lucide="trending-up" class="text-blue-500"></i>
Trending on Hugging Face Hub
</h2>
<div class="flex gap-2">
<select
id="trendingTypeSelect"
class="text-sm border rounded-lg px-3 py-2 bg-white text-gray-700 focus:ring-2 focus:ring-blue-100 focus:border-blue-300 transition-all outline-none"
onchange="loadTrendingResources(this.value)"
>
<option value="datasets">Datasets</option>
<option value="models">Models</option>
</select>
<select
id="trendingLimitSelect"
class="text-sm border rounded-lg px-3 py-2 bg-white text-gray-700 focus:ring-2 focus:ring-blue-100 focus:border-blue-300 transition-all outline-none"
onchange="loadTrendingResources(document.getElementById('trendingTypeSelect').value)"
>
<option value="10">Show 10</option>
<option value="20">Show 20</option>
<option value="30">Show 30</option>
</select>
</div>
</div>
<div id="trendingLoader" class="flex justify-center py-6">
<i
data-lucide="loader-2"
class="animate-spin text-blue-500 w-8 h-8"
></i>
</div>
<div id="trendingResults" class="space-y-4"></div>
</div>
</div>
<!-- End: New Trending Tab Content -->
<div
id="errorMessage"
class="hidden mt-4 p-4 text-red-600 bg-red-50 rounded-md"
></div>
<div id="resultsContainer" class="mt-6 space-y-4"></div>
</div>
</div>
<style>
.tab-trigger.active {
border-bottom-color: #3b82f6;
color: #3b82f6;
}
.btn-primary {
background-color: #3b82f6;
color: white;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: background-color 0.2s;
}
.btn-primary:hover {
background-color: #2563eb;
}
/* Add card hover effect */
.resource-card {
transition: transform 0.2s, box-shadow 0.2s;
}
.resource-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
</style>
<script>
// Configuration
const API_URL =
"https://davanstrien-huggingface-datasets-search-v2.hf.space";
const MIN_SEARCH_LENGTH = 3;
const DEBOUNCE_MS = 300;
const RESULTS_PER_PAGE = 5;
const MAX_RESULTS = 100;
let currentPage = 1;
// Add these constants near the top with other configurations
const URL_PARAMS = new URLSearchParams(window.location.search);
const INITIAL_SEARCH = URL_PARAMS.get("q");
const INITIAL_SIMILAR = URL_PARAMS.get("similar");
const INITIAL_TYPE = URL_PARAMS.get("type") || "datasets";
const INITIAL_TAB = URL_PARAMS.get("tab") || "search";
// Add these variables with other configurations
let currentSort = "similarity";
let currentType = INITIAL_TYPE;
let currentMinLikes = 0;
let currentMinDownloads = 0;
let currentParamFilter = "any";
// Initialize Lucide icons
lucide.createIcons();
// Tab switching
function switchTab(tabId) {
currentPage = 1;
document
.querySelectorAll(".tab-content")
.forEach((content) => content.classList.add("hidden"));
document
.querySelectorAll(".tab-trigger")
.forEach((trigger) => trigger.classList.remove("active"));
document.getElementById(`${tabId}Content`).classList.remove("hidden");
document.getElementById(`${tabId}Tab`).classList.add("active");
// Remove reference to non-existent elements
// Display parameter filter UI if needed
updateParamFilterUI();
// Clear results container when switching to trending tab
if (tabId === "trending") {
document.getElementById("resultsContainer").innerHTML = "";
loadTrendingResources(
document.getElementById("trendingTypeSelect").value
);
}
// Update URL parameters when switching tabs
if (tabId === "search") {
updateURL({ tab: "search", similar: null });
} else if (tabId === "similar") {
updateURL({ tab: "similar", q: null });
} else if (tabId === "trending") {
updateURL({ tab: "trending", q: null, similar: null });
}
// Update parameter filter UI after tab switch
updateParamFilterUI();
}
// Create result card
function createResultCard(result) {
const isDataset = "dataset_id" in result;
const resourceId = isDataset ? result.dataset_id : result.model_id;
const resourceType = isDataset ? "datasets" : "models";
const resourceIcon = isDataset ? "database" : "box";
const resourceUrl = isDataset
? `https://huggingface.co/${resourceType}/${resourceId}`
: `https://huggingface.co/${resourceId}`;
// Check if we're in the trending tab
const isTrendingTab =
document
.getElementById("trendingContent")
.classList.contains("hidden") === false;
const cardHtml = `
<div class="card resource-card bg-white p-4 sm:p-6 rounded-lg shadow hover:shadow-md transition-shadow">
<div class="space-y-2 w-full">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<div class="flex items-center gap-2">
<i data-lucide="${resourceIcon}" class="text-blue-500"></i>
<h3 class="text-lg font-semibold">${resourceId}</h3>
</div>
<div class="flex flex-wrap items-center gap-2">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span class="flex items-center gap-1">
<i data-lucide="heart" class="w-4 h-4"></i>
${result.likes}
</span>
<span class="flex items-center gap-1">
<i data-lucide="download" class="w-4 h-4"></i>
${result.downloads}
</span>
</div>
${
!isTrendingTab && result.similarity
? `<span class="bg-blue-50 px-2 py-1 rounded text-sm">
${(result.similarity * 100).toFixed(
1
)}% match
</span>`
: ""
}
<button
onclick="findSimilarFromResult('${resourceId}', '${resourceType}')"
class="flex items-center gap-1 text-sm text-blue-500 hover:text-blue-700"
>
<i data-lucide="arrow-right"></i>
Find Similar
</button>
</div>
</div>
<p class="text-sm text-gray-600">${result.summary}</p>
${
isDataset
? `
<!-- Add preview section that starts hidden -->
<div id="preview-section-${resourceId}" class="mt-4 border-t pt-4 hidden">
<button
onclick="togglePreview('${resourceId}')"
class="flex items-center gap-2 text-sm text-gray-600 hover:text-gray-800"
>
<i data-lucide="chevron-right" id="preview-icon-${resourceId}" class="transition-transform"></i>
Preview Dataset
</button>
<div id="preview-content-${resourceId}" class="hidden mt-4">
<iframe
src="https://huggingface.co/datasets/${resourceId}/embed/viewer/default/train"
frameborder="0"
width="100%"
height="560px"
></iframe>
</div>
</div>
`
: ""
}
<a href="${resourceUrl}"
target="_blank"
class="inline-flex items-center gap-1 text-sm text-blue-500 hover:text-blue-700 mt-2">
<i data-lucide="external-link" class="w-4 h-4"></i>
View on Hugging Face Hub
</a>
</div>
</div>
`;
// After rendering the card, check if preview is available for datasets
if (isDataset) {
checkDatasetValidity(resourceId);
}
return cardHtml;
}
// Add function to check dataset validity
async function checkDatasetValidity(datasetId) {
try {
const response = await fetch(
`https://datasets-server.huggingface.co/is-valid?dataset=${datasetId}`
);
const data = await response.json();
// Show preview section only if viewer is available
if (data.viewer) {
const previewSection = document.getElementById(
`preview-section-${datasetId}`
);
if (previewSection) {
previewSection.classList.remove("hidden");
}
}
} catch (error) {
console.error(
`Failed to check validity for dataset ${datasetId}:`,
error
);
}
}
// Add this function to update the URL
function updateURL(params) {
const newURL = new URL(window.location);
Object.entries(params).forEach(([key, value]) => {
if (value) {
newURL.searchParams.set(key, value);
} else {
newURL.searchParams.delete(key);
}
});
window.history.pushState({}, "", newURL);
}
// Add this function before the searchResources definition
// This function needs to be defined before searchResources uses it
async function performSearch(query, page = 1) {
if (query.length < MIN_SEARCH_LENGTH) {
document.getElementById("resultsContainer").innerHTML = "";
updateURL({ q: null, similar: null }); // Clear URL params
return;
}
document.getElementById("searchLoader").classList.remove("hidden");
document.getElementById("errorMessage").classList.add("hidden");
// Update URL with search query and type
updateURL({
q: query,
similar: null,
type: currentType,
tab: "search",
});
try {
const endpoint = `${API_URL}/search/${currentType}`;
const params = new URLSearchParams({
query: query,
k: RESULTS_PER_PAGE * page,
sort_by: currentSort,
min_likes: currentMinLikes,
min_downloads: currentMinDownloads,
});
// Add parameter filters for models
if (currentType === "models" && currentParamFilter !== "any") {
if (currentParamFilter.includes("-")) {
const [min, max] = currentParamFilter.split("-");
params.append("min_param_count", parseInt(min) * 1e9);
params.append("max_param_count", parseInt(max) * 1e9);
} else if (currentParamFilter.endsWith("+")) {
params.append(
"min_param_count",
parseInt(currentParamFilter.replace("+", "")) * 1e9
);
}
}
const response = await fetch(`${endpoint}?${params}`);
if (!response.ok) throw new Error("Search failed");
const data = await response.json();
displayResults(data.results, page);
} catch (error) {
console.error("Search error:", error);
showError("Failed to perform search. Please try again.");
} finally {
document.getElementById("searchLoader").classList.add("hidden");
}
}
// Add this function to handle toggling filters
// This needs to be defined before it's referenced in the HTML
function toggleFilters(tab) {
const popover = document.getElementById(`${tab}FiltersPopover`);
popover.classList.toggle("hidden");
// Close other popovers
const otherTab = tab === "search" ? "similar" : "search";
document
.getElementById(`${otherTab}FiltersPopover`)
.classList.add("hidden");
}
// Helper function for syncing param values between tabs
function syncParamFilterValues() {
const searchParamSelect = document.getElementById(
"searchParamRangeSelect"
);
const similarParamSelect = document.getElementById(
"similarParamRangeSelect"
);
// When updating one, update both and the global variable
if (searchParamSelect && similarParamSelect) {
if (searchParamSelect.value !== currentParamFilter) {
currentParamFilter = searchParamSelect.value;
similarParamSelect.value = currentParamFilter;
} else if (similarParamSelect.value !== currentParamFilter) {
currentParamFilter = similarParamSelect.value;
searchParamSelect.value = currentParamFilter;
}
}
}
// Modify the search function to handle both datasets and models
const searchResources = _.debounce(async (query, page = 1) => {
performSearch(query, page);
}, DEBOUNCE_MS);
// Cache for trending resources
let trendingResourcesCache = {
datasets: null,
models: null,
};
let cacheTimestamp = {
datasets: null,
models: null,
};
const CACHE_DURATION = 1000 * 60 * 15; // 15 minutes
async function fetchTrendingResources(type) {
if (
trendingResourcesCache[type] &&
cacheTimestamp[type] &&
Date.now() - cacheTimestamp[type] < CACHE_DURATION
) {
return trendingResourcesCache[type];
}
try {
const response = await fetch(`https://huggingface.co/api/${type}`);
const data = await response.json();
// Just take the first 20 resource IDs since they're already sorted
const trendingResources = data
.slice(0, 20)
.map((resource) => resource.id);
trendingResourcesCache[type] = trendingResources;
cacheTimestamp[type] = Date.now();
return trendingResources;
} catch (error) {
console.error(`Error fetching trending ${type}:`, error);
return [];
}
}
// New function to load trending resources from our API
async function loadTrendingResources(type) {
// Show loader and hide any errors
document.getElementById("trendingLoader").style.display = "flex";
document.getElementById("trendingResults").innerHTML = "";
document.getElementById("errorMessage").classList.add("hidden");
// Update selector to match current type
document.getElementById("trendingTypeSelect").value = type;
// Get filter values
const limit = document.getElementById("trendingLimitSelect").value;
try {
const endpoint = `${API_URL}/trending/${type}`;
const queryParams = new URLSearchParams({
limit: limit,
});
// Add parameter filters for models
if (type === "models" && currentParamFilter !== "any") {
if (currentParamFilter.includes("-")) {
const [min, max] = currentParamFilter.split("-");
queryParams.append("min_param_count", parseInt(min) * 1e9);
queryParams.append("max_param_count", parseInt(max) * 1e9);
} else if (currentParamFilter.endsWith("+")) {
queryParams.append(
"min_param_count",
parseInt(currentParamFilter.replace("+", "")) * 1e9
);
}
}
const response = await fetch(`${endpoint}?${queryParams}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// Display trending results
const resultsContainer = document.getElementById("trendingResults");
if (data.results && data.results.length > 0) {
resultsContainer.innerHTML = data.results
.map((result) => createResultCard(result))
.join("");
// Initialize icons for the new cards
lucide.createIcons();
} else {
resultsContainer.innerHTML = `
<div class="text-center p-8 bg-gray-50 rounded-lg">
<p class="text-gray-500">No trending ${type} found matching your criteria</p>
</div>
`;
}
} catch (error) {
console.error("Error loading trending resources:", error);
document.getElementById("trendingResults").innerHTML = `
<div class="text-center p-8 bg-gray-50 rounded-lg">
<p class="text-red-500">Failed to load trending ${type}. Please try again later.</p>
</div>
`;
} finally {
document.getElementById("trendingLoader").style.display = "none";
}
}
function displaySuggestions(resources, suggestionsBox) {
if (resources.length > 0) {
const resourceIcon = currentType === "datasets" ? "database" : "box";
suggestionsBox.innerHTML = resources
.map(
(resourceId) => `
<div
class="p-3 hover:bg-gray-50 cursor-pointer border-b last:border-b-0"
onclick="selectSuggestion('${resourceId}')"
>
<div class="flex items-center gap-2">
<i data-lucide="${resourceIcon}" class="w-4 h-4 text-blue-500"></i>
<span>${resourceId}</span>
</div>
</div>
`
)
.join("");
suggestionsBox.classList.remove("hidden");
lucide.createIcons();
} else {
suggestionsBox.classList.add("hidden");
}
}
function selectSuggestion(resource) {
const resourceInput = document.getElementById("resourceInput");
const suggestionsBox = document.getElementById("suggestionsBox");
resourceInput.value = resource;
suggestionsBox.classList.add("hidden");
findSimilarResources();
}
// Modify the findSimilarResources function to handle both datasets and models
async function findSimilarResources(page = 1) {
const resourceId = document.getElementById("resourceInput").value;
if (!resourceId) return;
// Update URL with similar resource ID and type
updateURL({
similar: resourceId,
q: null,
type: currentType,
tab: "similar",
});
// Show loader and hide error message
document.getElementById("similarLoader").classList.remove("hidden");
document.getElementById("errorMessage").classList.add("hidden");
try {
const endpoint = `${API_URL}/similarity/${currentType}`;
const paramName =
currentType === "datasets" ? "dataset_id" : "model_id";
const params = new URLSearchParams({
[paramName]: resourceId,
k: RESULTS_PER_PAGE * page,
sort_by: currentSort,
min_likes: currentMinLikes,
min_downloads: currentMinDownloads,
});
// Add parameter filters for models
if (currentType === "models" && currentParamFilter !== "any") {
if (currentParamFilter.includes("-")) {
const [min, max] = currentParamFilter.split("-");
params.append("min_param_count", parseInt(min) * 1e9);
params.append("max_param_count", parseInt(max) * 1e9);
} else if (currentParamFilter.endsWith("+")) {
params.append(
"min_param_count",
parseInt(currentParamFilter.replace("+", "")) * 1e9
);
}
}
const response = await fetch(`${endpoint}?${params}`);
if (!response.ok) throw new Error("Similarity search failed");
const data = await response.json();
displayResults(data.results, page);
} catch (error) {
showError(`Failed to find similar ${currentType}. Please try again.`);
} finally {
// Hide the loader when done
document.getElementById("similarLoader").classList.add("hidden");
}
}
// Display results
function displayResults(results, page = 1) {
const container = document.getElementById("resultsContainer");
console.log("Displaying results:", results);
if (results && results.length > 0) {
// Sort results if not using similarity
if (currentSort !== "similarity") {
results.sort((a, b) => b[currentSort] - a[currentSort]);
}
container.innerHTML = `
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold">Results</h2>
<div class="flex items-center gap-4">
<span class="text-sm text-gray-500">Found ${
results.length
} results</span>
<button
onclick="shareResults()"
class="flex items-center gap-1 text-sm px-3 py-1.5 bg-white border border-gray-200 rounded-lg hover:bg-gray-50"
title="Copy link to these search results"
>
<i data-lucide="link"></i>
Copy Search Link
</button>
</div>
</div>
${results.map((result) => createResultCard(result)).join("")}
${
results.length >= RESULTS_PER_PAGE * page &&
RESULTS_PER_PAGE * (page + 1) <= MAX_RESULTS
? `<div class="mt-4 flex items-center justify-between">
<button
onclick="loadMore()"
class="px-6 py-3 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors flex items-center gap-2"
>
<i data-lucide="more-horizontal"></i>
Load More Results
</button>
<button
onclick="shareResults()"
class="flex items-center gap-1 text-sm px-3 py-1.5 bg-white border border-gray-200 rounded-lg hover:bg-gray-50"
title="Copy link to these search results"
>
<i data-lucide="link"></i>
Copy Search Link
</button>
</div>`
: results.length >= MAX_RESULTS
? `<div class="text-center mt-4 p-6 bg-blue-50 rounded-lg">
<p class="text-gray-700 mb-3">You've reached the end of our journey! (${MAX_RESULTS} results)</p>
<p class="text-gray-600 mb-4">Can't find what you're looking for? Why not create and share your own?</p>
<div class="flex items-center justify-center gap-4">
<a href="https://huggingface.co/docs/${
currentType === "datasets"
? "datasets/upload_dataset"
: "hub/upload"
}"
target="_blank"
class="inline-flex items-center gap-2 text-blue-500 hover:text-blue-700">
<i data-lucide="external-link"></i>
Learn how to share on Hugging Face
</a>
<button
onclick="shareResults()"
class="flex items-center gap-1 text-sm px-3 py-1.5 bg-white border border-gray-200 rounded-lg hover:bg-gray-50"
title="Copy link to these search results"
>
<i data-lucide="link"></i>
Copy Search Link
</button>
</div>
</div>`
: ""
}
`;
lucide.createIcons();
} else {
container.innerHTML = `
<div class="text-center text-gray-500">
No results found
</div>
`;
}
}
// Show error message
function showError(message) {
const errorElement = document.getElementById("errorMessage");
errorElement.textContent = message;
errorElement.classList.remove("hidden");
}
// Event listeners
document
.getElementById("searchInput")
.addEventListener("input", (e) => searchResources(e.target.value));
document
.getElementById("resourceInput")
.addEventListener("keydown", (e) => {
if (e.key === "Enter") findSimilarResources();
});
// Update the findSimilarFromResult function
function findSimilarFromResult(resourceId, resourceType) {
// Switch to the similar tab
switchTab("similar");
// Set the resource type
currentType = resourceType;
document.getElementById("similarTypeSelect").value = resourceType;
document.getElementById("searchTypeSelect").value = resourceType;
document.getElementById("trendingTypeSelect").value = resourceType;
// Set the resource ID in the input without triggering the focus event
const resourceInput = document.getElementById("resourceInput");
resourceInput.value = resourceId;
// Hide suggestions box explicitly
const suggestionsBox = document.getElementById("suggestionsBox");
suggestionsBox.classList.add("hidden");
// Trigger the search
findSimilarResources();
}
// Add accordion functionality
function toggleAccordion() {
const content = document.getElementById("accordionContent");
const icon = document.getElementById("accordionIcon");
content.classList.toggle("hidden");
icon.style.transform = content.classList.contains("hidden")
? "rotate(0deg)"
: "rotate(90deg)";
}
// Add the loadMore function
function loadMore() {
currentPage += 1;
const activeTab = document.querySelector(".tab-trigger.active").id;
if (activeTab === "searchTab") {
const searchQuery = document.getElementById("searchInput").value;
searchResources(searchQuery, currentPage);
} else {
findSimilarResources(currentPage);
}
}
// Add this new function for toggling the preview
function togglePreview(datasetId) {
const content = document.getElementById(`preview-content-${datasetId}`);
const icon = document.getElementById(`preview-icon-${datasetId}`);
content.classList.toggle("hidden");
icon.style.transform = content.classList.contains("hidden")
? "rotate(0deg)"
: "rotate(90deg)";
}
// Update the share function name and remove the datasetId parameter
async function shareResults() {
const activeTab = document.querySelector(".tab-trigger.active").id;
const currentURL = new URL(window.location);
// Update URL based on active tab
if (activeTab === "searchTab") {
const searchQuery = document.getElementById("searchInput").value;
currentURL.searchParams.set("q", searchQuery);
currentURL.searchParams.delete("similar");
} else if (activeTab === "similarTab") {
const resourceId = document.getElementById("resourceInput").value;
currentURL.searchParams.set("similar", resourceId);
currentURL.searchParams.delete("q");
} else if (activeTab === "trendingTab") {
// For trending tab, save the selected options
const limit = document.getElementById("trendingLimitSelect").value;
currentURL.searchParams.set("limit", limit);
currentURL.searchParams.delete("q");
currentURL.searchParams.delete("similar");
}
// Always include the current tab and type
currentURL.searchParams.set("tab", activeTab.replace("Tab", ""));
currentURL.searchParams.set("type", currentType);
try {
await navigator.clipboard.writeText(currentURL.toString());
// Show success message
const buttons = document.querySelectorAll(
'button[onclick="shareResults()"]'
);
buttons.forEach((button) => {
const originalHTML = button.innerHTML;
button.innerHTML =
'<i data-lucide="check"></i> Search Link Copied!';
button.classList.add(
"bg-green-50",
"border-green-200",
"text-green-600"
);
lucide.createIcons();
setTimeout(() => {
button.innerHTML = originalHTML;
button.classList.remove(
"bg-green-50",
"border-green-200",
"text-green-600"
);
lucide.createIcons();
}, 2000);
});
} catch (error) {
console.error("Error copying to clipboard:", error);
}
}
// Update selectType to preserve parameter filter selection
function selectType(type) {
// Store current filter value before changing type
const currentParamValue = currentParamFilter;
// Update the current type
currentType = type;
// Update the type selectors to maintain consistency
document.getElementById("searchTypeSelect").value = type;
document.getElementById("similarTypeSelect").value = type;
// We no longer need to show/hide parameter filter sections
// since we've removed them from the popovers
if (type !== "models") {
// Reset parameter filters when switching away from models
currentParamFilter = "any";
}
// Update filter indicators
updateFilterIndicators("search");
updateFilterIndicators("similar");
// Update the parameter filter UI - this will show/hide based on type
updateParamFilterUI();
// Re-run search if needed
const activeTab = document.querySelector(".tab-trigger.active").id;
if (activeTab === "searchTab") {
const searchQuery = document.getElementById("searchInput").value;
if (searchQuery.length >= MIN_SEARCH_LENGTH) {
performSearch(searchQuery, 1);
}
} else if (activeTab === "similarTab") {
const resourceId = document.getElementById("resourceInput").value;
if (resourceId) {
findSimilarResources(1);
}
} else if (activeTab === "trendingTab") {
loadTrendingResources(type);
}
}
// Add this helper function to update filter indicators consistently
function updateFilterIndicators(tab) {
const activeFiltersSpan = document.getElementById(
`${tab}ActiveFilters`
);
// Count active filters
let activeCount =
(currentMinLikes > 0 ? 1 : 0) +
(currentMinDownloads > 0 ? 1 : 0) +
(currentType === "models" && currentParamFilter !== "any" ? 1 : 0);
if (activeCount > 0) {
activeFiltersSpan.textContent = activeCount;
activeFiltersSpan.classList.remove("hidden");
} else {
activeFiltersSpan.classList.add("hidden");
}
}
// Improved handleFilterChange function
function handleFilterChange(tab) {
const minLikesInput = document.getElementById(`${tab}MinLikes`);
const minDownloadsInput = document.getElementById(`${tab}MinDownloads`);
const paramRangeSelect = document.getElementById(
`${tab}ParamRangeSelect`
);
const sortSelect = document.getElementById(`${tab}SortSelect`);
const activeFiltersSpan = document.getElementById(
`${tab}ActiveFilters`
);
// Update the global filter values
currentMinLikes = parseInt(minLikesInput.value) || 0;
currentMinDownloads = parseInt(minDownloadsInput.value) || 0;
// Update sort value from the select element
if (sortSelect) {
currentSort = sortSelect.value;
}
// Update parameter filter if we're on models and sync between tabs
if (currentType === "models" && paramRangeSelect) {
currentParamFilter = paramRangeSelect.value;
syncParamFilterValues();
}
// Update active filters indicator
let activeCount =
(currentMinLikes > 0 ? 1 : 0) +
(currentMinDownloads > 0 ? 1 : 0) +
(currentType === "models" && currentParamFilter !== "any" ? 1 : 0);
if (activeCount > 0) {
activeFiltersSpan.textContent = activeCount;
activeFiltersSpan.classList.remove("hidden");
} else {
activeFiltersSpan.classList.add("hidden");
}
// Re-run the current search with new filters
if (tab === "search") {
const searchQuery = document.getElementById("searchInput").value;
if (searchQuery.length >= MIN_SEARCH_LENGTH) {
currentPage = 1;
performSearch(searchQuery, 1);
}
} else {
const resourceId = document.getElementById("resourceInput").value;
if (resourceId) {
currentPage = 1;
findSimilarResources(1);
}
}
}
// Add this function to create a parameter filter UI above the results
function updateParamFilterUI() {
// Check if we should show the filter (only for models)
if (currentType !== "models") {
// Remove the filter UI if it exists
const existingFilter = document.getElementById("paramFilterBar");
if (existingFilter) {
existingFilter.remove();
}
return;
}
// Check if we already have the filter UI
let paramFilterBar = document.getElementById("paramFilterBar");
// If not, create it
if (!paramFilterBar) {
paramFilterBar = document.createElement("div");
paramFilterBar.id = "paramFilterBar";
paramFilterBar.className =
"bg-white p-4 rounded-lg shadow-sm border border-gray-100 mb-4";
// Create the filter content
paramFilterBar.innerHTML = `
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<h3 class="text-sm font-medium text-gray-700">Filter by Parameter Size</h3>
<button onclick="clearParamFilter()" class="text-xs text-blue-500 hover:text-blue-700">Clear</button>
</div>
<div class="flex flex-wrap gap-2 mt-1" id="paramFilterButtons">
<button onclick="applyParamFilter('any')" class="filter-btn ${
currentParamFilter === "any" ? "active" : ""
}">
Any size
</button>
<button onclick="applyParamFilter('0-1')" class="filter-btn ${
currentParamFilter === "0-1" ? "active" : ""
}">
Small (0-1B)
</button>
<button onclick="applyParamFilter('1-7')" class="filter-btn ${
currentParamFilter === "1-7" ? "active" : ""
}">
Medium (1-7B)
</button>
<button onclick="applyParamFilter('7-20')" class="filter-btn ${
currentParamFilter === "7-20" ? "active" : ""
}">
Large (7-20B)
</button>
<button onclick="applyParamFilter('20-70')" class="filter-btn ${
currentParamFilter === "20-70" ? "active" : ""
}">
XL (20-70B)
</button>
<button onclick="applyParamFilter('70+')" class="filter-btn ${
currentParamFilter === "70+" ? "active" : ""
}">
XXL (70B+)
</button>
</div>
</div>
`;
// Insert it before the results container
const resultsContainer = document.getElementById("resultsContainer");
resultsContainer.parentNode.insertBefore(
paramFilterBar,
resultsContainer
);
// Add CSS for the filter buttons
const style = document.createElement("style");
style.textContent = `
.filter-btn {
padding: 6px 12px;
border-radius: 4px;
font-size: 0.875rem;
background-color: #f3f4f6;
color: #4b5563;
transition: all 0.2s;
}
.filter-btn:hover {
background-color: #e5e7eb;
}
.filter-btn.active {
background-color: #3b82f6;
color: white;
}
`;
document.head.appendChild(style);
} else {
// Just update the active state on existing buttons
const buttons = paramFilterBar.querySelectorAll(".filter-btn");
buttons.forEach((btn) => {
btn.classList.remove("active");
const filterValue = btn.getAttribute("onclick").match(/'(.*?)'/)[1];
if (filterValue === currentParamFilter) {
btn.classList.add("active");
}
});
}
}
// Function to apply parameter filter
function applyParamFilter(value) {
// Update the filter value
currentParamFilter = value;
// Update the filter UI
updateParamFilterUI();
// Update filter indicators
updateFilterIndicators("search");
updateFilterIndicators("similar");
// Re-run the current search
const activeTab = document.querySelector(".tab-trigger.active").id;
if (activeTab === "searchTab") {
const searchQuery = document.getElementById("searchInput").value;
if (searchQuery.length >= MIN_SEARCH_LENGTH) {
currentPage = 1;
performSearch(searchQuery, 1);
}
} else if (activeTab === "similarTab") {
const resourceId = document.getElementById("resourceInput").value;
if (resourceId) {
currentPage = 1;
findSimilarResources(1);
}
} else if (activeTab === "trendingTab") {
loadTrendingResources(currentType);
}
}
// Function to clear parameter filter
function clearParamFilter() {
applyParamFilter("any");
}
// Add this to initialize the UI when the page loads
document.addEventListener("DOMContentLoaded", function () {
// Set initial type selection
selectType(INITIAL_TYPE);
// Initialize parameter filter UI
updateParamFilterUI();
// Switch to the initial tab from URL
switchTab(INITIAL_TAB);
// Initialize with search query if present in URL
if (INITIAL_SEARCH && INITIAL_SEARCH.length >= MIN_SEARCH_LENGTH) {
document.getElementById("searchInput").value = INITIAL_SEARCH;
performSearch(INITIAL_SEARCH, 1);
}
// Initialize with similar resource if present in URL
if (INITIAL_SIMILAR) {
document.getElementById("resourceInput").value = INITIAL_SIMILAR;
findSimilarResources(1);
}
// Add input event listener for resource suggestions
document
.getElementById("resourceInput")
.addEventListener("input", function (e) {
const query = e.target.value.trim();
if (query.length >= 2) {
fetchResourceSuggestions(query);
} else {
document.getElementById("suggestionsBox").classList.add("hidden");
}
});
// Close suggestions when clicking outside
document.addEventListener("click", function (e) {
if (
!e.target.closest("#resourceInput") &&
!e.target.closest("#suggestionsBox")
) {
document.getElementById("suggestionsBox").classList.add("hidden");
}
});
});
// Add function to fetch resource suggestions
async function fetchResourceSuggestions(query) {
if (query.length < 2) return;
try {
const response = await fetch(
`${API_URL}/suggest/${currentType}?q=${encodeURIComponent(query)}`
);
if (!response.ok) throw new Error("Failed to fetch suggestions");
const data = await response.json();
if (data && data.suggestions && data.suggestions.length > 0) {
displaySuggestions(
data.suggestions,
document.getElementById("suggestionsBox")
);
} else {
document.getElementById("suggestionsBox").classList.add("hidden");
}
} catch (error) {
console.error("Error fetching suggestions:", error);
document.getElementById("suggestionsBox").classList.add("hidden");
}
}
</script>
</body>
</html>