|
<!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="handleSortChange('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> |
|
</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="handleSortChange('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> |
|
</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="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> |
|
|
|
|
|
<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> |
|
|
|
|
|
<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; |
|
} |
|
|
|
|
|
.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> |
|
|
|
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; |
|
|
|
|
|
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"; |
|
|
|
|
|
let currentSort = "similarity"; |
|
let currentType = INITIAL_TYPE; |
|
let currentMinLikes = 0; |
|
let currentMinDownloads = 0; |
|
|
|
|
|
lucide.createIcons(); |
|
|
|
|
|
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"); |
|
|
|
|
|
if (tabId === "trending") { |
|
document.getElementById("resultsContainer").innerHTML = ""; |
|
loadTrendingResources( |
|
document.getElementById("trendingTypeSelect").value |
|
); |
|
} |
|
|
|
|
|
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 }); |
|
} |
|
} |
|
|
|
|
|
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}`; |
|
|
|
|
|
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> |
|
`; |
|
|
|
|
|
if (isDataset) { |
|
checkDatasetValidity(resourceId); |
|
} |
|
|
|
return cardHtml; |
|
} |
|
|
|
|
|
async function checkDatasetValidity(datasetId) { |
|
try { |
|
const response = await fetch( |
|
`https://datasets-server.huggingface.co/is-valid?dataset=${datasetId}` |
|
); |
|
const data = await response.json(); |
|
|
|
|
|
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 |
|
); |
|
} |
|
} |
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
const searchResources = _.debounce(async (query, page = 1) => { |
|
performSearch(query, page); |
|
}, DEBOUNCE_MS); |
|
|
|
|
|
let trendingResourcesCache = { |
|
datasets: null, |
|
models: null, |
|
}; |
|
let cacheTimestamp = { |
|
datasets: null, |
|
models: null, |
|
}; |
|
const CACHE_DURATION = 1000 * 60 * 15; |
|
|
|
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(); |
|
|
|
|
|
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 []; |
|
} |
|
} |
|
|
|
|
|
async function loadTrendingResources(type) { |
|
|
|
document.getElementById("trendingLoader").style.display = "flex"; |
|
document.getElementById("trendingResults").innerHTML = ""; |
|
document.getElementById("errorMessage").classList.add("hidden"); |
|
|
|
|
|
document.getElementById("trendingTypeSelect").value = type; |
|
|
|
|
|
const limit = document.getElementById("trendingLimitSelect").value; |
|
|
|
try { |
|
const endpoint = `${API_URL}/trending/${type}`; |
|
const queryParams = new URLSearchParams({ |
|
limit: limit, |
|
}); |
|
|
|
const response = await fetch(`${endpoint}?${queryParams}`); |
|
|
|
if (!response.ok) { |
|
throw new Error(`HTTP error! status: ${response.status}`); |
|
} |
|
|
|
const data = await response.json(); |
|
|
|
|
|
const resultsContainer = document.getElementById("trendingResults"); |
|
|
|
if (data.results && data.results.length > 0) { |
|
resultsContainer.innerHTML = data.results |
|
.map((result) => createResultCard(result)) |
|
.join(""); |
|
|
|
|
|
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(); |
|
} |
|
|
|
|
|
async function findSimilarResources(page = 1) { |
|
const resourceId = document.getElementById("resourceInput").value; |
|
if (!resourceId) return; |
|
|
|
|
|
updateURL({ |
|
similar: resourceId, |
|
q: null, |
|
type: currentType, |
|
tab: "similar", |
|
}); |
|
|
|
const similarLoader = document.getElementById("similarLoader"); |
|
if (similarLoader) { |
|
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 response = await fetch( |
|
`${endpoint}?${paramName}=${encodeURIComponent(resourceId)}&k=${ |
|
RESULTS_PER_PAGE * page |
|
}&sort_by=${currentSort}&min_likes=${currentMinLikes}&min_downloads=${currentMinDownloads}` |
|
); |
|
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 { |
|
if (similarLoader) { |
|
similarLoader.classList.add("hidden"); |
|
} |
|
} |
|
} |
|
|
|
|
|
function displayResults(results, page = 1) { |
|
const container = document.getElementById("resultsContainer"); |
|
console.log("Displaying results:", results); |
|
if (results && results.length > 0) { |
|
|
|
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> |
|
`; |
|
} |
|
} |
|
|
|
|
|
function showError(message) { |
|
const errorElement = document.getElementById("errorMessage"); |
|
errorElement.textContent = message; |
|
errorElement.classList.remove("hidden"); |
|
} |
|
|
|
|
|
document |
|
.getElementById("searchInput") |
|
.addEventListener("input", (e) => searchResources(e.target.value)); |
|
document |
|
.getElementById("resourceInput") |
|
.addEventListener("keydown", (e) => { |
|
if (e.key === "Enter") findSimilarResources(); |
|
}); |
|
|
|
|
|
function findSimilarFromResult(resourceId, resourceType) { |
|
|
|
switchTab("similar"); |
|
|
|
|
|
currentType = resourceType; |
|
document.getElementById("similarTypeSelect").value = resourceType; |
|
document.getElementById("searchTypeSelect").value = resourceType; |
|
document.getElementById("trendingTypeSelect").value = resourceType; |
|
|
|
|
|
const resourceInput = document.getElementById("resourceInput"); |
|
resourceInput.value = resourceId; |
|
|
|
|
|
const suggestionsBox = document.getElementById("suggestionsBox"); |
|
suggestionsBox.classList.add("hidden"); |
|
|
|
|
|
findSimilarResources(); |
|
} |
|
|
|
|
|
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)"; |
|
} |
|
|
|
|
|
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); |
|
} |
|
} |
|
|
|
|
|
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)"; |
|
} |
|
|
|
|
|
async function shareResults() { |
|
const activeTab = document.querySelector(".tab-trigger.active").id; |
|
const currentURL = new URL(window.location); |
|
|
|
|
|
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") { |
|
|
|
const limit = document.getElementById("trendingLimitSelect").value; |
|
|
|
currentURL.searchParams.set("limit", limit); |
|
currentURL.searchParams.delete("q"); |
|
currentURL.searchParams.delete("similar"); |
|
} |
|
|
|
|
|
currentURL.searchParams.set("tab", activeTab.replace("Tab", "")); |
|
currentURL.searchParams.set("type", currentType); |
|
|
|
try { |
|
await navigator.clipboard.writeText(currentURL.toString()); |
|
|
|
|
|
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); |
|
} |
|
} |
|
|
|
|
|
function selectType(type) { |
|
|
|
currentType = type; |
|
|
|
|
|
document.getElementById("searchTypeSelect").value = type; |
|
document.getElementById("similarTypeSelect").value = type; |
|
document.getElementById("trendingTypeSelect").value = type; |
|
|
|
|
|
const resourceInput = document.getElementById("resourceInput"); |
|
resourceInput.placeholder = |
|
type === "datasets" |
|
? "e.g. openai/gsm8k" |
|
: "e.g. meta-llama/Llama-2-7b"; |
|
|
|
|
|
currentPage = 1; |
|
document.getElementById("resultsContainer").innerHTML = ""; |
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
updateURL({ type: currentType }); |
|
} |
|
|
|
|
|
async function performSearch(query, page = 1) { |
|
if (query.length < MIN_SEARCH_LENGTH) { |
|
document.getElementById("resultsContainer").innerHTML = ""; |
|
updateURL({ q: null, similar: null }); |
|
return; |
|
} |
|
|
|
document.getElementById("searchLoader").classList.remove("hidden"); |
|
document.getElementById("errorMessage").classList.add("hidden"); |
|
|
|
|
|
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, |
|
}); |
|
|
|
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"); |
|
} |
|
} |
|
|
|
|
|
document.addEventListener("DOMContentLoaded", async () => { |
|
const resourceInput = document.getElementById("resourceInput"); |
|
let programmaticFocus = false; |
|
|
|
|
|
if (INITIAL_TYPE) { |
|
document.getElementById("searchTypeSelect").value = INITIAL_TYPE; |
|
document.getElementById("similarTypeSelect").value = INITIAL_TYPE; |
|
document.getElementById("trendingTypeSelect").value = INITIAL_TYPE; |
|
currentType = INITIAL_TYPE; |
|
} |
|
|
|
|
|
resourceInput.addEventListener("input", async (e) => { |
|
const suggestionsBox = document.getElementById("suggestionsBox"); |
|
const value = e.target.value; |
|
|
|
if (!programmaticFocus) { |
|
if (!value) { |
|
|
|
const trending = await fetchTrendingResources(currentType); |
|
displaySuggestions(trending, suggestionsBox); |
|
} else { |
|
|
|
const trending = await fetchTrendingResources(currentType); |
|
const filtered = trending.filter((resource) => |
|
resource.toLowerCase().includes(value.toLowerCase()) |
|
); |
|
displaySuggestions(filtered, suggestionsBox); |
|
} |
|
} |
|
}); |
|
|
|
|
|
resourceInput.addEventListener("focus", async () => { |
|
if (!programmaticFocus) { |
|
const suggestionsBox = document.getElementById("suggestionsBox"); |
|
const trending = await fetchTrendingResources(currentType); |
|
displaySuggestions(trending, suggestionsBox); |
|
} |
|
programmaticFocus = false; |
|
}); |
|
|
|
|
|
if (INITIAL_TAB) { |
|
switchTab(INITIAL_TAB); |
|
} |
|
|
|
if (INITIAL_SEARCH) { |
|
switchTab("search"); |
|
document.getElementById("searchInput").value = INITIAL_SEARCH; |
|
await searchResources(INITIAL_SEARCH); |
|
} else if (INITIAL_SIMILAR) { |
|
switchTab("similar"); |
|
document.getElementById("resourceInput").value = INITIAL_SIMILAR; |
|
await findSimilarResources(); |
|
} else if (INITIAL_TAB === "trending") { |
|
|
|
const limit = URL_PARAMS.get("limit") || 10; |
|
|
|
document.getElementById("trendingLimitSelect").value = limit; |
|
|
|
loadTrendingResources(currentType); |
|
} |
|
}); |
|
|
|
|
|
function handleSortChange(tab) { |
|
const sortSelect = document.getElementById(`${tab}SortSelect`); |
|
currentSort = sortSelect.value; |
|
|
|
|
|
const activeTab = document.querySelector(".tab-trigger.active").id; |
|
if (activeTab === "searchTab") { |
|
const searchQuery = document.getElementById("searchInput").value; |
|
if (searchQuery.length >= MIN_SEARCH_LENGTH) { |
|
searchResources(searchQuery, 1); |
|
} |
|
} else { |
|
const resourceId = document.getElementById("resourceInput").value; |
|
if (resourceId) { |
|
findSimilarResources(1); |
|
} |
|
} |
|
} |
|
|
|
|
|
function toggleFilters(tab) { |
|
const popover = document.getElementById(`${tab}FiltersPopover`); |
|
popover.classList.toggle("hidden"); |
|
|
|
|
|
const otherTab = tab === "search" ? "similar" : "search"; |
|
document |
|
.getElementById(`${otherTab}FiltersPopover`) |
|
.classList.add("hidden"); |
|
} |
|
|
|
|
|
document.addEventListener("click", (event) => { |
|
const searchPopover = document.getElementById("searchFiltersPopover"); |
|
const similarPopover = document.getElementById("similarFiltersPopover"); |
|
const searchButton = event.target.closest( |
|
"button[onclick=\"toggleFilters('search')\"]" |
|
); |
|
const similarButton = event.target.closest( |
|
"button[onclick=\"toggleFilters('similar')\"]" |
|
); |
|
|
|
if (!searchButton && !event.target.closest("#searchFiltersPopover")) { |
|
searchPopover.classList.add("hidden"); |
|
} |
|
if (!similarButton && !event.target.closest("#similarFiltersPopover")) { |
|
similarPopover.classList.add("hidden"); |
|
} |
|
}); |
|
|
|
|
|
function handleFilterChange(tab) { |
|
const minLikesInput = document.getElementById(`${tab}MinLikes`); |
|
const minDownloadsInput = document.getElementById(`${tab}MinDownloads`); |
|
const activeFiltersSpan = document.getElementById( |
|
`${tab}ActiveFilters` |
|
); |
|
|
|
currentMinLikes = parseInt(minLikesInput.value) || 0; |
|
currentMinDownloads = parseInt(minDownloadsInput.value) || 0; |
|
|
|
|
|
const activeCount = |
|
(currentMinLikes > 0 ? 1 : 0) + (currentMinDownloads > 0 ? 1 : 0); |
|
if (activeCount > 0) { |
|
activeFiltersSpan.textContent = activeCount; |
|
activeFiltersSpan.classList.remove("hidden"); |
|
} else { |
|
activeFiltersSpan.classList.add("hidden"); |
|
} |
|
|
|
|
|
if (tab === "search") { |
|
const searchQuery = document.getElementById("searchInput").value; |
|
if (searchQuery.length >= MIN_SEARCH_LENGTH) { |
|
searchResources(searchQuery, 1); |
|
} |
|
} else { |
|
const resourceId = document.getElementById("resourceInput").value; |
|
if (resourceId) { |
|
findSimilarResources(1); |
|
} |
|
} |
|
} |
|
</script> |
|
</body> |
|
</html> |
|
|