hf-search / templates /index.html
mrfakename's picture
Upload 3 files
4257f85 verified
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>HF Discussion Search</title>
<style>
:root {
--bg-primary: #ffffff;
--bg-secondary: #f3f4f6;
--text-primary: #111827;
--text-secondary: #4b5563;
--border-color: #e5e7eb;
--accent-color: #2563eb;
--accent-hover: #1d4ed8;
--focus-ring: #3b82f680;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: #1f2937;
--bg-secondary: #111827;
--text-primary: #f9fafb;
--text-secondary: #9ca3af;
--border-color: #374151;
--accent-color: #3b82f6;
--accent-hover: #2563eb;
}
}
* {
box-sizing: border-box;
font-family: system-ui, -apple-system, sans-serif;
}
/* Custom scrollbar styles */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--text-secondary);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-primary);
}
/* Firefox scrollbar */
* {
scrollbar-width: thin;
scrollbar-color: var(--text-secondary) var(--bg-secondary);
}
body {
margin: 0;
padding: 20px;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
}
.container {
max-width: 800px;
margin: 0 auto;
}
h1 {
font-size: 24px;
margin: 0 0 20px 0;
color: var(--text-primary);
}
.search-container {
position: relative;
margin-bottom: 30px;
}
.search-input {
width: 100%;
padding: 12px 16px 12px 40px;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--bg-secondary);
color: var(--text-primary);
font-size: 16px;
transition: border-color 0.2s, box-shadow 0.2s;
}
.search-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: var(--text-secondary);
width: 20px;
height: 20px;
}
.search-input:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px var(--focus-ring);
}
.results {
display: grid;
gap: 16px;
}
.result-card {
padding: 16px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
transition: transform 0.2s;
position: relative;
display: flex;
gap: 16px;
}
.type-icon-container {
flex-shrink: 0;
width: 24px;
height: 24px;
color: var(--text-secondary);
}
.result-content {
flex-grow: 1;
min-width: 0;
}
.result-card.closed {
opacity: 0.75;
}
.result-card:hover {
transform: translateY(-2px);
}
.result-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 8px;
color: var(--accent-color);
}
.result-author {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 12px;
}
.result-excerpt {
font-size: 14px;
line-height: 1.5;
color: var(--text-primary);
}
.result-excerpt mark {
background: var(--accent-color);
color: white;
padding: 0 2px;
border-radius: 2px;
}
.loader {
display: none;
justify-content: center;
margin: 40px 0;
}
.loader::after {
content: "";
width: 30px;
height: 30px;
border: 3px solid var(--border-color);
border-radius: 50%;
border-top-color: var(--accent-color);
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.no-results {
text-align: center;
color: var(--text-secondary);
padding: 40px 0;
}
.status-indicator {
position: absolute;
top: 16px;
right: 16px;
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
padding: 4px 8px;
border-radius: 12px;
}
.status-open {
background: #22c55e20;
color: #22c55e;
}
.status-closed {
background: #ef444420;
color: #ef4444;
}
.filter-controls {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.filter-button {
padding: 6px 12px;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--bg-secondary);
color: var(--text-secondary);
cursor: pointer;
}
.filter-button.active {
background: var(--accent-color);
color: white;
border-color: var(--accent-color);
}
.type-icon {
width: 24px;
height: 24px;
}
</style>
</head>
<body>
<div class="container">
<div class="search-container">
<svg class="search-icon" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="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" clip-rule="evenodd" />
</svg>
<input type="text" id="searchInput" class="search-input" placeholder="Search discussions..." autofocus>
</div>
<div class="filter-controls">
<button class="filter-button active" data-filter="all">All</button>
<button class="filter-button" data-filter="open">Open</button>
<button class="filter-button" data-filter="closed">Closed</button>
</div>
<div id="loader" class="loader"></div>
<div id="results" class="results"></div>
</div>
<script>
const searchInput = document.getElementById('searchInput');
const loader = document.getElementById('loader');
const results = document.getElementById('results');
const filterButtons = document.querySelectorAll('.filter-button');
let currentFilter = 'all';
let searchTimeout;
let currentResults = [];
function getTypeIcon(isPR) {
return isPR ?
`<svg class="type-icon" viewBox="0 0 16 16" fill="currentColor">
<path fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"/>
</svg>` :
`<svg class="type-icon" viewBox="0 0 16 16" fill="currentColor">
<path fill-rule="evenodd" d="M1.5 2.75a.25.25 0 01.25-.25h8.5a.25.25 0 01.25.25v5.5a.25.25 0 01-.25.25h-3.5a.75.75 0 00-.53.22L3.5 11.44V9.25a.75.75 0 00-.75-.75h-1a.25.25 0 01-.25-.25v-5.5z"/>
</svg>`;
}
function renderResults(results) {
const filteredResults = results.filter(result => {
if (currentFilter === 'all') return true;
if (currentFilter === 'open') return result.is_open;
if (currentFilter === 'closed') return !result.is_open;
});
if (filteredResults.length === 0) {
if (currentFilter === 'all') {
return '<div class="no-results">No results found</div>';
} else {
return `<div class="no-results">No ${currentFilter} discussions found</div>`;
}
}
return filteredResults
.map(result => `
<a href="${result.url}"
class="result-card${!result.is_open ? ' closed' : ''}"
target="_blank" style="text-decoration: none;">
<div class="type-icon-container">
${getTypeIcon(result.is_pr)}
</div>
<div class="result-content">
<div class="result-title">${result.title}</div>
<div class="result-author">by ${result.author}</div>
<div class="result-excerpt">${result.excerpt}</div>
<div class="status-indicator ${result.is_open ? 'status-open' : 'status-closed'}">
${result.is_open ? 'Open' : 'Closed'}
</div>
</div>
</a>
`).join('');
}
filterButtons.forEach(button => {
button.addEventListener('click', () => {
filterButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
currentFilter = button.dataset.filter;
results.innerHTML = renderResults(currentResults);
});
});
async function performSearch(query) {
loader.style.display = 'flex';
results.innerHTML = '';
try {
const response = await fetch('/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: query,
repo: '{{ repo_name }}'
})
});
const data = await response.json();
currentResults = data.results;
results.innerHTML = renderResults(currentResults);
} catch (error) {
results.innerHTML = '<div class="no-results">An error occurred while searching</div>';
} finally {
loader.style.display = 'none';
}
}
searchInput.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
const query = e.target.value.trim();
if (query.length === 0) {
results.innerHTML = '';
return;
}
if (query.length < 2) return;
searchTimeout = setTimeout(() => {
performSearch(query);
}, 300);
});
// Pre-fill search input and trigger search if query parameter exists
window.addEventListener('DOMContentLoaded', () => {
const urlParams = new URLSearchParams(window.location.search);
const query = urlParams.get('query');
if (query) {
searchInput.value = query;
performSearch(query);
}
});
</script>
</body>
</html>