| from __future__ import annotations |
|
|
| import base64 |
| import html |
| import json |
| from collections import defaultdict |
| from datetime import datetime |
| from pathlib import Path |
| from typing import Any |
|
|
| from backend.spaces_index import filter_public_spaces, get_category_distribution |
| from backend.storage import get_recent_feedback |
|
|
|
|
| CARD_DECORATIONS = ["compass", "camera", "lantern", "boot", "pine", "flower", "peak", "map"] |
| CARD_DECORATION_MARKS = { |
| "compass": "🧭", |
| "camera": "📷", |
| "lantern": "🏮", |
| "boot": "🥾", |
| "pine": "🌲", |
| "flower": "🌸", |
| "peak": "🏔", |
| "map": "🗺", |
| } |
|
|
|
|
| def safe(text: Any) -> str: |
| return html.escape("" if text is None else str(text)) |
|
|
|
|
| PROJECT_ROOT = Path(__file__).resolve().parent.parent |
| BANNER_PATH = PROJECT_ROOT / "banner.png" |
| BADGE_PATH = PROJECT_ROOT / "hackathon-badge.png" |
|
|
|
|
| def _image_data_uri(path: Path) -> str: |
| data = path.read_bytes() |
| encoded = base64.b64encode(data).decode("ascii") |
| return f"data:image/png;base64,{encoded}" |
|
|
|
|
| BANNER_SRC = _image_data_uri(BANNER_PATH) |
| BADGE_SRC = _image_data_uri(BADGE_PATH) |
|
|
|
|
| def _js_literal(value: Any) -> str: |
| return html.escape(json.dumps(value, ensure_ascii=False), quote=True) |
|
|
|
|
| def _tags_text(tags: list[str] | None) -> str: |
| tags = [str(tag).strip() for tag in (tags or []) if str(tag).strip()] |
| return ", ".join(tags[:7]) |
|
|
|
|
| def _group_feedback() -> dict[str, list[dict]]: |
| grouped: dict[str, list[dict]] = defaultdict(list) |
| for row in get_recent_feedback(limit=2000): |
| repo_id = str(row.get("repo_id", "")).strip() |
| if repo_id: |
| grouped[repo_id].append(row) |
| return grouped |
|
|
|
|
| def _human_datetime(value: str | None) -> str: |
| if not value: |
| return "Just now" |
| try: |
| parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) |
| except Exception: |
| return str(value) |
| local = parsed.astimezone() |
| return local.strftime("%b %d, %Y at %I:%M %p").replace(" 0", " ") |
|
|
|
|
| COMMENTS_MODAL_HTML = """ |
| <div id="bsqf-comments-modal" class="comment-modal-overlay" aria-hidden="true"> |
| <div class="comment-modal"> |
| <div class="comment-modal-header"> |
| <div> |
| <div class="comment-modal-kicker">Community notes</div> |
| <h3 id="bsqf-comments-title">Comments</h3> |
| </div> |
| <button class="comment-modal-close" type="button" onclick="window.bsqfCloseComments()">Close</button> |
| </div> |
| <div id="bsqf-comments-body" class="comment-modal-body"></div> |
| </div> |
| </div> |
| """ |
|
|
|
|
| COMMENTS_MODAL_SCRIPT = """ |
| <script> |
| (function () { |
| const OWNER_SESSION_KEY = 'bsqf_owner_session_id'; |
| |
| function escapeHtml(value) { |
| return String(value || '').replace(/[&<>"']/g, function (m) { |
| return ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]); |
| }); |
| } |
| |
| function formatCommentDate(value) { |
| if (!value) return ''; |
| const parsed = new Date(value); |
| if (Number.isNaN(parsed.getTime())) return escapeHtml(value); |
| |
| const now = new Date(); |
| const diffMs = now.getTime() - parsed.getTime(); |
| const diffDays = Math.floor(diffMs / 86400000); |
| const timePart = parsed.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }); |
| |
| if (diffDays <= 0) return 'Today, ' + timePart; |
| if (diffDays === 1) return 'Yesterday, ' + timePart; |
| if (diffDays < 7) { |
| return parsed.toLocaleDateString([], { weekday: 'long' }) + ', ' + timePart; |
| } |
| return parsed.toLocaleDateString([], { |
| month: 'short', |
| day: 'numeric', |
| year: 'numeric' |
| }) + ' at ' + timePart; |
| } |
| |
| function getOwnerSessionId() { |
| try { |
| let current = window.localStorage.getItem(OWNER_SESSION_KEY); |
| if (!current) { |
| current = (window.crypto && window.crypto.randomUUID) |
| ? window.crypto.randomUUID() |
| : 'bsqf-' + Math.random().toString(36).slice(2) + Date.now().toString(36); |
| window.localStorage.setItem(OWNER_SESSION_KEY, current); |
| } |
| return current; |
| } catch (error) { |
| if (!window.__bsqfAnonymousSessionId) { |
| window.__bsqfAnonymousSessionId = (window.crypto && window.crypto.randomUUID) |
| ? window.crypto.randomUUID() |
| : 'bsqf-' + Math.random().toString(36).slice(2) + Date.now().toString(36); |
| } |
| return window.__bsqfAnonymousSessionId; |
| } |
| } |
| |
| async function callGradioApi(apiName, args) { |
| const response = await fetch('/gradio_api/run/' + encodeURIComponent(apiName), { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Accept': 'application/json' |
| }, |
| body: JSON.stringify({ |
| data: Array.isArray(args) ? args : [], |
| simple_format: true |
| }) |
| }); |
| |
| const payload = await response.json(); |
| if (!response.ok) { |
| throw new Error(payload && (payload.error || payload.detail) ? (payload.error || payload.detail) : 'Request failed.'); |
| } |
| |
| const result = payload && Array.isArray(payload.data) ? payload.data[0] : null; |
| if (!result || typeof result !== 'object') { |
| throw new Error('Unexpected response from the server.'); |
| } |
| return result; |
| } |
| |
| async function loadComments(repoId) { |
| const ownerSessionId = getOwnerSessionId(); |
| const payload = await callGradioApi('comments_get', [repoId, ownerSessionId]); |
| if (!payload.ok) { |
| throw new Error(payload && (payload.error || payload.detail) ? (payload.error || payload.detail) : 'Could not load comments.'); |
| } |
| updateCommentCount(repoId, Number(payload.count || 0)); |
| return payload; |
| } |
| |
| function bodyScrollLock(locked) { |
| document.documentElement.classList.toggle('bsqf-modal-open', locked); |
| document.body.classList.toggle('bsqf-modal-open', locked); |
| } |
| |
| function renderCommentActions(row) { |
| if (!row || !row.can_edit) return ''; |
| const rawCommentId = String(row.id || ''); |
| const rawRepoId = String(row.repo_id || ''); |
| const commentId = escapeHtml(JSON.stringify(rawCommentId)); |
| const repoId = escapeHtml(JSON.stringify(rawRepoId)); |
| const text = escapeHtml(row.answer || ''); |
| return '' + |
| '<div class="comment-actions">' + |
| '<button class="comment-action-button" type="button" onclick="window.bsqfBeginCommentEdit(' + repoId + ', ' + commentId + ')">Edit</button>' + |
| '<button class="comment-action-button comment-action-button--danger" type="button" onclick="window.bsqfDeleteComment(' + repoId + ', ' + commentId + ')">Delete</button>' + |
| '</div>' + |
| '<div class="comment-edit-shell" data-comment-edit-shell="' + escapeHtml(rawCommentId) + '" hidden>' + |
| '<textarea class="comment-edit-input" data-comment-edit-input="' + escapeHtml(rawCommentId) + '">' + text + '</textarea>' + |
| '<div class="comment-edit-actions">' + |
| '<button class="comment-action-button" type="button" onclick="window.bsqfCancelCommentEdit(' + commentId + ')">Cancel</button>' + |
| '<button class="comment-action-button comment-action-button--primary" type="button" onclick="window.bsqfSaveCommentEdit(' + repoId + ', ' + commentId + ')">Save</button>' + |
| '</div>' + |
| '</div>'; |
| } |
| |
| function renderCommentsHtml(comments) { |
| if (!Array.isArray(comments) || !comments.length) { |
| return '<div class="comment-empty">No comments yet. Be the first to leave a note for this Space.</div>'; |
| } |
| |
| const items = comments.map(function (row) { |
| const commentId = row && row.id != null ? String(row.id) : ''; |
| const answer = row && row.answer ? escapeHtml(row.answer) : ''; |
| const createdAt = row && row.created_at ? formatCommentDate(row.created_at) : ''; |
| const sourceQuery = row && row.source_query ? '<span class="comment-source">Query: ' + escapeHtml(row.source_query) + '</span>' : ''; |
| const ownClass = row && row.can_edit ? ' comment-item--own' : ''; |
| const meta = [ |
| createdAt ? '<span class="comment-date">' + createdAt + '</span>' : '', |
| sourceQuery |
| ].filter(Boolean).join(''); |
| |
| return '<li class="comment-item' + ownClass + '" data-comment-id="' + escapeHtml(commentId) + '">' + |
| '<div class="comment-answer">' + answer + '</div>' + |
| (meta ? '<div class="comment-meta">' + meta + '</div>' : '') + |
| renderCommentActions(row) + |
| '</li>'; |
| }).join(''); |
| |
| return '<ul class="comment-list">' + items + '</ul>'; |
| } |
| |
| function updateCommentCount(repoId, count) { |
| document.querySelectorAll('[data-comment-count-for="' + repoId + '"]').forEach(function (node) { |
| node.textContent = String(count || 0); |
| }); |
| } |
| |
| function showCommentToast(repoId, message, variant) { |
| const toast = document.querySelector('[data-comment-status-for="' + repoId + '"]'); |
| if (!toast) return; |
| |
| if (toast._bsqfTimer) { |
| window.clearTimeout(toast._bsqfTimer); |
| toast._bsqfTimer = null; |
| } |
| |
| toast.classList.remove('is-visible', 'is-error', 'is-success'); |
| toast.classList.add(variant === 'error' ? 'is-error' : 'is-success'); |
| toast.innerHTML = '<span class="card-comment-toast__icon">' + (variant === 'error' ? '!' : '✓') + '</span><span class="card-comment-toast__text">' + escapeHtml(message) + '</span>'; |
| void toast.offsetWidth; |
| toast.classList.add('is-visible'); |
| |
| toast._bsqfTimer = window.setTimeout(function () { |
| toast.classList.remove('is-visible'); |
| window.setTimeout(function () { |
| toast.classList.remove('is-error', 'is-success'); |
| toast.innerHTML = ''; |
| }, 180); |
| }, 5000); |
| } |
| |
| window.bsqfCloseComments = function () { |
| const modal = document.getElementById('bsqf-comments-modal'); |
| if (!modal) return; |
| modal.classList.remove('is-open'); |
| modal.setAttribute('aria-hidden', 'true'); |
| bodyScrollLock(false); |
| }; |
| |
| window.bsqfOpenComments = async function (title, repoId) { |
| const modal = document.getElementById('bsqf-comments-modal'); |
| const titleNode = document.getElementById('bsqf-comments-title'); |
| const body = document.getElementById('bsqf-comments-body'); |
| if (!modal || !titleNode || !body) return; |
| |
| window.bsqfActiveCommentsRepoId = repoId || ''; |
| titleNode.textContent = title || 'Comments'; |
| body.innerHTML = '<div class="comment-empty">Loading comments...</div>'; |
| modal.classList.add('is-open'); |
| modal.setAttribute('aria-hidden', 'false'); |
| bodyScrollLock(true); |
| |
| try { |
| const payload = await loadComments(repoId); |
| body.innerHTML = renderCommentsHtml(payload.comments || []); |
| } catch (error) { |
| body.innerHTML = '<div class="comment-empty">' + escapeHtml(error && error.message ? error.message : 'Could not load comments.') + '</div>'; |
| } |
| }; |
| |
| window.bsqfSubmitComment = async function (repoId, sourceQuery) { |
| const textarea = document.querySelector('[data-comment-input-for="' + repoId + '"]'); |
| const statusNode = document.querySelector('[data-comment-status-for="' + repoId + '"]'); |
| if (!textarea || !statusNode) return; |
| |
| const answer = String(textarea.value || '').trim(); |
| if (!answer) { |
| showCommentToast(repoId, 'Write a quick comment first.', 'error'); |
| return; |
| } |
| |
| try { |
| const payload = await callGradioApi('comments_save', [repoId, answer, sourceQuery || '', getOwnerSessionId()]); |
| if (!payload.ok) { |
| throw new Error(payload && (payload.error || payload.detail) ? (payload.error || payload.detail) : 'Could not save comment.'); |
| } |
| |
| textarea.value = ''; |
| window.bsqfSyncCommentButton(repoId); |
| updateCommentCount(repoId, Number(payload.count || 0)); |
| showCommentToast(repoId, 'Comment saved', 'success'); |
| |
| if (window.bsqfActiveCommentsRepoId === repoId) { |
| const body = document.getElementById('bsqf-comments-body'); |
| if (body) body.innerHTML = renderCommentsHtml(payload.comments || []); |
| } |
| } catch (error) { |
| showCommentToast(repoId, error && error.message ? error.message : 'Could not save comment.', 'error'); |
| } |
| }; |
| |
| window.bsqfSyncCommentButton = function (repoId) { |
| const textarea = document.querySelector('[data-comment-input-for="' + repoId + '"]'); |
| const button = document.querySelector('[data-comment-send-for="' + repoId + '"]'); |
| if (!textarea || !button) return; |
| const hasText = String(textarea.value || '').trim().length > 0; |
| button.disabled = !hasText; |
| button.setAttribute('aria-disabled', hasText ? 'false' : 'true'); |
| }; |
| |
| window.bsqfBeginCommentEdit = function (repoId, commentId) { |
| const shell = document.querySelector('[data-comment-edit-shell="' + commentId + '"]'); |
| const input = document.querySelector('[data-comment-edit-input="' + commentId + '"]'); |
| const item = document.querySelector('[data-comment-id="' + commentId + '"]'); |
| if (!shell || !input || !item) return; |
| item.classList.add('is-editing'); |
| shell.hidden = false; |
| input.focus(); |
| input.setSelectionRange(input.value.length, input.value.length); |
| }; |
| |
| window.bsqfCancelCommentEdit = function (commentId) { |
| const shell = document.querySelector('[data-comment-edit-shell="' + commentId + '"]'); |
| const item = document.querySelector('[data-comment-id="' + commentId + '"]'); |
| if (!shell || !item) return; |
| item.classList.remove('is-editing'); |
| shell.hidden = true; |
| }; |
| |
| window.bsqfSaveCommentEdit = async function (repoId, commentId) { |
| const input = document.querySelector('[data-comment-edit-input="' + commentId + '"]'); |
| if (!input) return; |
| |
| const answer = String(input.value || '').trim(); |
| if (!answer) { |
| showCommentToast(repoId, 'Write a quick comment first.', 'error'); |
| return; |
| } |
| |
| try { |
| const payload = await callGradioApi('comments_update', [repoId, commentId, answer, getOwnerSessionId()]); |
| if (!payload.ok) { |
| throw new Error(payload && (payload.error || payload.detail) ? (payload.error || payload.detail) : 'Could not update comment.'); |
| } |
| |
| updateCommentCount(repoId, Number(payload.count || 0)); |
| showCommentToast(repoId, 'Comment updated', 'success'); |
| if (window.bsqfActiveCommentsRepoId === repoId) { |
| const body = document.getElementById('bsqf-comments-body'); |
| if (body) body.innerHTML = renderCommentsHtml(payload.comments || []); |
| } |
| } catch (error) { |
| showCommentToast(repoId, error && error.message ? error.message : 'Could not update comment.', 'error'); |
| } |
| }; |
| |
| window.bsqfDeleteComment = async function (repoId, commentId) { |
| const confirmed = window.confirm('Delete your comment from this Space?'); |
| if (!confirmed) return; |
| |
| try { |
| const payload = await callGradioApi('comments_delete', [repoId, commentId, getOwnerSessionId()]); |
| if (!payload.ok) { |
| throw new Error(payload && (payload.error || payload.detail) ? (payload.error || payload.detail) : 'Could not delete comment.'); |
| } |
| |
| updateCommentCount(repoId, Number(payload.count || 0)); |
| showCommentToast(repoId, 'Comment deleted', 'success'); |
| if (window.bsqfActiveCommentsRepoId === repoId) { |
| const body = document.getElementById('bsqf-comments-body'); |
| if (body) body.innerHTML = renderCommentsHtml(payload.comments || []); |
| } |
| } catch (error) { |
| showCommentToast(repoId, error && error.message ? error.message : 'Could not delete comment.', 'error'); |
| } |
| }; |
| |
| document.addEventListener('keydown', function (event) { |
| if (event.key === 'Escape') window.bsqfCloseComments(); |
| }); |
| })(); |
| </script> |
| """ |
|
|
|
|
| FIELD_GUIDE_SHELL_SCRIPT = """ |
| <script> |
| (function () { |
| function dispatchInput(node) { |
| if (!node) return; |
| node.dispatchEvent(new Event('input', { bubbles: true })); |
| node.dispatchEvent(new Event('change', { bubbles: true })); |
| } |
| |
| function setTextboxValue(selector, value) { |
| const node = document.querySelector(selector); |
| if (!node) return false; |
| const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set; |
| if (setter) setter.call(node, value); |
| else node.value = value; |
| dispatchInput(node); |
| return true; |
| } |
| |
| function clickButton(selector) { |
| const button = document.querySelector(selector); |
| if (button) button.click(); |
| } |
| |
| function applyPrompt(query, liked) { |
| setTextboxValue('#search-query textarea', query || ''); |
| setTextboxValue('#liked-text textarea', liked || ''); |
| clickButton('#recommend-btn button'); |
| const target = document.querySelector('.search-panels'); |
| if (target) target.scrollIntoView({ behavior: 'smooth', block: 'center' }); |
| } |
| |
| window.bsqfDisableRecommendButton = function () { |
| const button = document.querySelector('#recommend-btn button'); |
| if (!button) return; |
| button.disabled = true; |
| button.setAttribute('aria-busy', 'true'); |
| button.classList.add('is-processing'); |
| }; |
| |
| window.bsqfEnableRecommendButton = function () { |
| const button = document.querySelector('#recommend-btn button'); |
| if (!button) return; |
| button.disabled = false; |
| button.removeAttribute('aria-busy'); |
| button.classList.remove('is-processing'); |
| }; |
| |
| window.bsqfApplyPrompt = applyPrompt; |
| |
| document.addEventListener('click', function (event) { |
| const trigger = event.target.closest('[data-bsqf-fill-query]'); |
| if (!trigger) return; |
| event.preventDefault(); |
| applyPrompt( |
| trigger.getAttribute('data-bsqf-fill-query') || '', |
| trigger.getAttribute('data-bsqf-fill-liked') || '' |
| ); |
| }); |
| })(); |
| </script> |
| """ |
|
|
|
|
| def render_page_shell(stats: dict, *, active_category: str = "All", active_query: str = "") -> str: |
| return f""" |
| <section class="camp-hero-shell"> |
| <div class="camp-hero-art-shell" aria-label="Hackathon Space Recommender hero illustration"> |
| <img id="camp-hero-image" class="camp-hero-art" src="{BANNER_SRC}" alt="Hackathon Space Recommender banner illustration" /> |
| </div> |
| </section> |
| """ |
|
|
|
|
| def render_footer_shell() -> str: |
| return """ |
| <footer class="camp-footer"> |
| <div class="camp-footer__trail"></div> |
| <div class="camp-seal"> |
| <img id="camp-badge-image" class="camp-seal__image" src="{badge_src}" alt="Build Small Hackathon badge" /> |
| </div> |
| </footer> |
| """.format(badge_src=BADGE_SRC) |
|
|
|
|
| def _meta_button(text: str, *, css_class: str = "", attrs: str = "", tag: str = "button") -> str: |
| class_attr = f"card-action {css_class}".strip() |
| if tag == "a": |
| return f'<a class="{class_attr}" {attrs}>{text}</a>' |
| return f'<button class="{class_attr}" type="button" {attrs}>{text}</button>' |
|
|
|
|
| def render_space_card( |
| space: dict, |
| comments_by_repo: dict[str, list[dict]] | None = None, |
| *, |
| show_reason: bool = True, |
| enable_inline_comment: bool = False, |
| source_query: str = "", |
| tone_index: int = 0, |
| ) -> str: |
| comments_by_repo = comments_by_repo or {} |
| repo_id = str(space.get("id", "")).strip() |
| comments = comments_by_repo.get(repo_id, []) |
| comment_count = len(comments) |
| reason = str(space.get("reason", "") or "").strip() |
| category = str(space.get("category", "Other") or "Other").strip() |
| track = str(space.get("track", "") or "").strip() |
| title = str(space.get("name", "") or "").strip() |
| likes = int(space.get("likes", 0) or 0) |
| tag_text = _tags_text(space.get("tags")) |
| deco_key = CARD_DECORATIONS[tone_index % len(CARD_DECORATIONS)] |
| deco_mark = CARD_DECORATION_MARKS[deco_key] |
|
|
| title_js = _js_literal(title) |
| repo_id_js = _js_literal(repo_id) |
| source_query_js = _js_literal(source_query) |
| open_url = safe(space.get("url", "")) |
|
|
| reason_html = "" |
| if show_reason and reason: |
| reason_html = f'<div class="card-why"><span class="card-why__label">Why it fits</span><p>{safe(reason)}</p></div>' |
|
|
| inline_comment_html = "" |
| if enable_inline_comment and repo_id: |
| inline_comment_html = f""" |
| <div class="card-comment-box"> |
| <label class="card-comment-label" for="comment-box-{safe(repo_id)}">What did you like best about the space?</label> |
| <textarea |
| id="comment-box-{safe(repo_id)}" |
| class="card-comment-input" |
| data-comment-input-for="{safe(repo_id)}" |
| placeholder="What stood out to you about this Space?" |
| rows="2" |
| oninput="window.bsqfSyncCommentButton({repo_id_js})" |
| onchange="window.bsqfSyncCommentButton({repo_id_js})" |
| ></textarea> |
| <div class="card-comment-help"><small>Not sure what to write? Open the Space and give it a quick try.</small></div> |
| <div class="card-comment-actions"> |
| <button |
| class="card-comment-send" |
| data-comment-send-for="{safe(repo_id)}" |
| type="button" |
| disabled |
| aria-disabled="true" |
| onclick="window.bsqfSubmitComment({repo_id_js}, {source_query_js})" |
| > |
| Send comment |
| </button> |
| <span class="card-comment-status" data-comment-status-for="{safe(repo_id)}" aria-live="polite" aria-atomic="true"></span> |
| </div> |
| </div> |
| """ |
|
|
| category_chip = f'<span class="card-badge card-badge--category">{safe(category)}</span>' if category else "" |
| track_chip = f'<span class="card-badge card-badge--track">{safe(track)}</span>' if track else "" |
| comment_button = _meta_button( |
| f'💬 <span data-comment-count-for="{safe(repo_id)}">{comment_count}</span>', |
| css_class="card-action--count", |
| attrs=f'onclick="window.bsqfOpenComments({title_js}, {repo_id_js})"', |
| ) |
| like_chip = f'<span class="card-like">♥ {likes}</span>' |
| open_button = _meta_button("Open Space ↗", css_class="card-action--open", attrs=f'href="{open_url}" target="_blank" rel="noreferrer"', tag="a") |
|
|
| return f""" |
| <article class="space-card notebook-card notebook-card--{safe(deco_key)}" data-deco="{safe(deco_mark)}" data-bsqf-search-text="{safe(title)}"> |
| <div class="card-top"> |
| <div class="card-chip-row"> |
| {category_chip} |
| {track_chip} |
| </div> |
| <div class="card-meta-actions"> |
| {like_chip} |
| {comment_button} |
| {open_button} |
| </div> |
| </div> |
| <div class="card-title-block"> |
| <h2><strong>{safe(title)}</strong></h2> |
| </div> |
| {reason_html} |
| {inline_comment_html} |
| <div class="card-tags">{safe(tag_text)}</div> |
| </article> |
| """ |
|
|
|
|
| def render_recommendation_cards(results: list[dict], *, show_reason: bool = False, source_query: str = "") -> str: |
| results = filter_public_spaces(results) |
| comments_by_repo = _group_feedback() |
| if not results: |
| return """ |
| <div class="results-shell" data-bsqf-results-shell> |
| <div class="bsqf-filter-empty" data-bsqf-filter-empty hidden>No spaces match this filter.</div> |
| <div class="results-grid"> |
| <div class="empty-state-card"> |
| <h3>No matching spaces found.</h3> |
| <p>Try to modify your search query</p> |
| </div> |
| </div> |
| </div> |
| """ |
|
|
| cards = "\n".join( |
| render_space_card( |
| result, |
| comments_by_repo, |
| show_reason=show_reason, |
| enable_inline_comment=True, |
| source_query=source_query, |
| tone_index=index, |
| ) |
| for index, result in enumerate(results) |
| ) |
| return f""" |
| <div class="results-shell" data-bsqf-results-shell> |
| <div class="bsqf-filter-empty" data-bsqf-filter-empty hidden>No spaces match this filter.</div> |
| <div class="results-grid">{cards}</div> |
| </div> |
| """ |
|
|
|
|
| def render_category_section(spaces: list[dict], category: str) -> str: |
| spaces = filter_public_spaces(spaces) |
| comments_by_repo = _group_feedback() |
| if not spaces: |
| return """ |
| <div class="results-shell" data-bsqf-results-shell> |
| <div class="bsqf-filter-empty" data-bsqf-filter-empty hidden>No spaces match this filter.</div> |
| <div class="results-grid"> |
| <div class="empty-state-card"> |
| <h3>No Spaces in this category</h3> |
| <p>Nothing in the current index matches this category yet.</p> |
| </div> |
| </div> |
| </div> |
| """ |
|
|
| cards = "\n".join( |
| render_space_card( |
| space, |
| comments_by_repo, |
| show_reason=False, |
| enable_inline_comment=False, |
| tone_index=index, |
| ) |
| for index, space in enumerate(spaces) |
| ) |
| return f""" |
| <div class="results-shell" data-bsqf-results-shell> |
| <div class="bsqf-filter-empty" data-bsqf-filter-empty hidden>No spaces match this filter.</div> |
| <div class="results-grid results-grid--browse">{cards}</div> |
| </div> |
| """ |
|
|