Spaces:
Sleeping
Sleeping
<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> |