Spaces:
Running
Running
Commit
·
5e88463
1
Parent(s):
5679415
open source filter option
Browse files- src/routes/+page.svelte +54 -1
- src/routes/Leaderboard.svelte +16 -3
- static/global.css +116 -0
src/routes/+page.svelte
CHANGED
|
@@ -4,6 +4,9 @@
|
|
| 4 |
import Viewer from "./Viewer.svelte";
|
| 5 |
import Vote from "./Vote.svelte";
|
| 6 |
import About from "./About.svelte";
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
interface Scene {
|
| 9 |
name: string;
|
|
@@ -14,6 +17,22 @@
|
|
| 14 |
let currentView: "Leaderboard" | "Vote" | "ModelDetails" | "Viewer" | "About" = "Vote";
|
| 15 |
let selectedEntry: { name: string } | null = null;
|
| 16 |
let selectedScene: Scene | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
function goHome() {
|
| 19 |
window.location.href = "/";
|
|
@@ -45,11 +64,45 @@
|
|
| 45 |
<button on:click={() => (currentView = "About")} class={currentView === "About" ? "active" : ""}
|
| 46 |
>About</button
|
| 47 |
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
</div>
|
| 49 |
{/if}
|
| 50 |
|
| 51 |
{#if currentView === "Leaderboard"}
|
| 52 |
-
<Leaderboard onEntryClick={showModelDetails} />
|
| 53 |
{:else if currentView === "Vote"}
|
| 54 |
<Vote />
|
| 55 |
{:else if currentView === "ModelDetails" && selectedEntry}
|
|
|
|
| 4 |
import Viewer from "./Viewer.svelte";
|
| 5 |
import Vote from "./Vote.svelte";
|
| 6 |
import About from "./About.svelte";
|
| 7 |
+
import { Filter, CheckmarkOutline } from "carbon-icons-svelte";
|
| 8 |
+
import { onMount } from "svelte";
|
| 9 |
+
import { CaretDown, Code } from "carbon-icons-svelte";
|
| 10 |
|
| 11 |
interface Scene {
|
| 12 |
name: string;
|
|
|
|
| 17 |
let currentView: "Leaderboard" | "Vote" | "ModelDetails" | "Viewer" | "About" = "Vote";
|
| 18 |
let selectedEntry: { name: string } | null = null;
|
| 19 |
let selectedScene: Scene | null = null;
|
| 20 |
+
let showOnlyOpenSource = false;
|
| 21 |
+
let showFilter = false;
|
| 22 |
+
let filterContainer: HTMLDivElement;
|
| 23 |
+
|
| 24 |
+
function handleClickOutside(event: MouseEvent) {
|
| 25 |
+
if (filterContainer && !filterContainer.contains(event.target as Node)) {
|
| 26 |
+
showFilter = false;
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
onMount(() => {
|
| 31 |
+
document.addEventListener("click", handleClickOutside);
|
| 32 |
+
return () => {
|
| 33 |
+
document.removeEventListener("click", handleClickOutside);
|
| 34 |
+
};
|
| 35 |
+
});
|
| 36 |
|
| 37 |
function goHome() {
|
| 38 |
window.location.href = "/";
|
|
|
|
| 64 |
<button on:click={() => (currentView = "About")} class={currentView === "About" ? "active" : ""}
|
| 65 |
>About</button
|
| 66 |
>
|
| 67 |
+
{#if currentView === "Leaderboard"}
|
| 68 |
+
<div class="filter-container" bind:this={filterContainer}>
|
| 69 |
+
<button
|
| 70 |
+
class="filter-button"
|
| 71 |
+
on:click={() => (showFilter = !showFilter)}
|
| 72 |
+
aria-expanded={showFilter}
|
| 73 |
+
aria-haspopup="true"
|
| 74 |
+
>
|
| 75 |
+
<Filter size={20} />
|
| 76 |
+
<CaretDown size={16} class="caret" />
|
| 77 |
+
</button>
|
| 78 |
+
{#if showFilter}
|
| 79 |
+
<div class="filter-dropdown" role="menu" aria-label="Filter options">
|
| 80 |
+
<div class="filter-section">
|
| 81 |
+
<div class="filter-section-title">Filter Options</div>
|
| 82 |
+
<div
|
| 83 |
+
class="filter-option {showOnlyOpenSource ? 'active' : ''}"
|
| 84 |
+
on:click={() => (showOnlyOpenSource = !showOnlyOpenSource)}
|
| 85 |
+
on:keydown={(e) => e.key === "Enter" && (showOnlyOpenSource = !showOnlyOpenSource)}
|
| 86 |
+
role="menuitemcheckbox"
|
| 87 |
+
aria-checked={showOnlyOpenSource}
|
| 88 |
+
tabindex="0"
|
| 89 |
+
>
|
| 90 |
+
<div class="filter-label">
|
| 91 |
+
<Code size={16} class="filter-icon" />
|
| 92 |
+
Open source
|
| 93 |
+
</div>
|
| 94 |
+
<span class="filter-checkbox">✓</span>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
{/if}
|
| 99 |
+
</div>
|
| 100 |
+
{/if}
|
| 101 |
</div>
|
| 102 |
{/if}
|
| 103 |
|
| 104 |
{#if currentView === "Leaderboard"}
|
| 105 |
+
<Leaderboard onEntryClick={showModelDetails} {showOnlyOpenSource} />
|
| 106 |
{:else if currentView === "Vote"}
|
| 107 |
<Vote />
|
| 108 |
{:else if currentView === "ModelDetails" && selectedEntry}
|
src/routes/Leaderboard.svelte
CHANGED
|
@@ -8,13 +8,16 @@
|
|
| 8 |
rank: number;
|
| 9 |
score: number;
|
| 10 |
votes: number;
|
|
|
|
| 11 |
displayName?: string;
|
| 12 |
}
|
| 13 |
|
| 14 |
export let onEntryClick: (entry: Entry) => void;
|
|
|
|
| 15 |
|
| 16 |
const baseUrl = "https://huggingface.co/datasets/dylanebert/3d-arena/resolve/main/outputs";
|
| 17 |
let leaderboard: Entry[] = [];
|
|
|
|
| 18 |
|
| 19 |
const fetchLeaderboardData = async () => {
|
| 20 |
const url = "/api/leaderboard";
|
|
@@ -34,19 +37,29 @@
|
|
| 34 |
entriesWithDisplayNames.sort((a, b) => a.rank - b.rank);
|
| 35 |
|
| 36 |
leaderboard = entriesWithDisplayNames;
|
|
|
|
| 37 |
};
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
onMount(async () => {
|
| 40 |
await fetchLeaderboardData();
|
| 41 |
});
|
| 42 |
</script>
|
| 43 |
|
| 44 |
-
{#if
|
| 45 |
<div class="grid">
|
| 46 |
-
{#each
|
| 47 |
<button class="grid-item" on:click={() => onEntryClick(entry)}>
|
| 48 |
<img src={`${baseUrl}/${entry.name}/thumbnail.png`} alt={entry.name} class="thumbnail" />
|
| 49 |
-
<div class="ranking">{
|
| 50 |
<div class="title">{entry.displayName}</div>
|
| 51 |
<div class="score-container">
|
| 52 |
<div class="score">
|
|
|
|
| 8 |
rank: number;
|
| 9 |
score: number;
|
| 10 |
votes: number;
|
| 11 |
+
open_source: boolean;
|
| 12 |
displayName?: string;
|
| 13 |
}
|
| 14 |
|
| 15 |
export let onEntryClick: (entry: Entry) => void;
|
| 16 |
+
export let showOnlyOpenSource: boolean;
|
| 17 |
|
| 18 |
const baseUrl = "https://huggingface.co/datasets/dylanebert/3d-arena/resolve/main/outputs";
|
| 19 |
let leaderboard: Entry[] = [];
|
| 20 |
+
let filteredLeaderboard: Entry[] = [];
|
| 21 |
|
| 22 |
const fetchLeaderboardData = async () => {
|
| 23 |
const url = "/api/leaderboard";
|
|
|
|
| 37 |
entriesWithDisplayNames.sort((a, b) => a.rank - b.rank);
|
| 38 |
|
| 39 |
leaderboard = entriesWithDisplayNames;
|
| 40 |
+
updateFilteredLeaderboard();
|
| 41 |
};
|
| 42 |
|
| 43 |
+
const updateFilteredLeaderboard = () => {
|
| 44 |
+
filteredLeaderboard = showOnlyOpenSource ? leaderboard.filter((entry) => entry.open_source) : leaderboard;
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
$: {
|
| 48 |
+
showOnlyOpenSource;
|
| 49 |
+
updateFilteredLeaderboard();
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
onMount(async () => {
|
| 53 |
await fetchLeaderboardData();
|
| 54 |
});
|
| 55 |
</script>
|
| 56 |
|
| 57 |
+
{#if filteredLeaderboard.length > 0}
|
| 58 |
<div class="grid">
|
| 59 |
+
{#each filteredLeaderboard as entry, index}
|
| 60 |
<button class="grid-item" on:click={() => onEntryClick(entry)}>
|
| 61 |
<img src={`${baseUrl}/${entry.name}/thumbnail.png`} alt={entry.name} class="thumbnail" />
|
| 62 |
+
<div class="ranking">{index + 1}</div>
|
| 63 |
<div class="title">{entry.displayName}</div>
|
| 64 |
<div class="score-container">
|
| 65 |
<div class="score">
|
static/global.css
CHANGED
|
@@ -476,6 +476,7 @@ body {
|
|
| 476 |
margin-bottom: 10px;
|
| 477 |
gap: 10px;
|
| 478 |
width: 100%;
|
|
|
|
| 479 |
}
|
| 480 |
|
| 481 |
.tabs button {
|
|
@@ -498,3 +499,118 @@ body {
|
|
| 498 |
.tabs button.active {
|
| 499 |
background-color: #444;
|
| 500 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 476 |
margin-bottom: 10px;
|
| 477 |
gap: 10px;
|
| 478 |
width: 100%;
|
| 479 |
+
align-items: center;
|
| 480 |
}
|
| 481 |
|
| 482 |
.tabs button {
|
|
|
|
| 499 |
.tabs button.active {
|
| 500 |
background-color: #444;
|
| 501 |
}
|
| 502 |
+
|
| 503 |
+
.filter-container {
|
| 504 |
+
position: relative;
|
| 505 |
+
margin-left: auto;
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
.filter-button {
|
| 509 |
+
background-color: #333;
|
| 510 |
+
border: 1px solid #444;
|
| 511 |
+
color: #fff;
|
| 512 |
+
padding: 8px 12px;
|
| 513 |
+
cursor: pointer;
|
| 514 |
+
display: flex;
|
| 515 |
+
align-items: center;
|
| 516 |
+
justify-content: center;
|
| 517 |
+
border-radius: 4px;
|
| 518 |
+
transition: background-color 0.2s ease;
|
| 519 |
+
gap: 4px;
|
| 520 |
+
font-size: 14px;
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
.filter-button:hover {
|
| 524 |
+
background-color: #444;
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
.filter-button .caret {
|
| 528 |
+
transition: transform 0.2s ease;
|
| 529 |
+
margin-left: 2px;
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
.filter-button[aria-expanded="true"] .caret {
|
| 533 |
+
transform: rotate(180deg);
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
.filter-dropdown {
|
| 537 |
+
position: absolute;
|
| 538 |
+
top: calc(100% + 5px);
|
| 539 |
+
right: 0;
|
| 540 |
+
background-color: #1a1b1e;
|
| 541 |
+
border: 1px solid #444;
|
| 542 |
+
border-radius: 4px;
|
| 543 |
+
padding: 4px;
|
| 544 |
+
min-width: 200px;
|
| 545 |
+
z-index: 1000;
|
| 546 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
.filter-option {
|
| 550 |
+
display: flex;
|
| 551 |
+
align-items: center;
|
| 552 |
+
justify-content: space-between;
|
| 553 |
+
padding: 8px 12px;
|
| 554 |
+
color: #ddd;
|
| 555 |
+
cursor: pointer;
|
| 556 |
+
border-radius: 2px;
|
| 557 |
+
white-space: nowrap;
|
| 558 |
+
font-size: 14px;
|
| 559 |
+
transition: all 0.2s ease;
|
| 560 |
+
background-color: transparent;
|
| 561 |
+
position: relative;
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
.filter-option:hover {
|
| 565 |
+
background-color: #2a2d31;
|
| 566 |
+
}
|
| 567 |
+
|
| 568 |
+
.filter-option.active {
|
| 569 |
+
background-color: #333;
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
.filter-label {
|
| 573 |
+
display: flex;
|
| 574 |
+
align-items: center;
|
| 575 |
+
gap: 8px;
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
.filter-icon {
|
| 579 |
+
color: #888;
|
| 580 |
+
transition: color 0.2s ease;
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
.filter-option:hover .filter-icon,
|
| 584 |
+
.filter-option.active .filter-icon {
|
| 585 |
+
color: #ddd;
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
.filter-checkbox {
|
| 589 |
+
position: absolute;
|
| 590 |
+
right: 12px;
|
| 591 |
+
color: transparent;
|
| 592 |
+
transition: color 0.2s ease;
|
| 593 |
+
font-size: 14px;
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
.filter-option.active .filter-checkbox {
|
| 597 |
+
color: #fff;
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
.filter-section {
|
| 601 |
+
padding: 8px 0;
|
| 602 |
+
border-bottom: 1px solid #333;
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
+
.filter-section:last-child {
|
| 606 |
+
border-bottom: none;
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
.filter-section-title {
|
| 610 |
+
padding: 4px 12px;
|
| 611 |
+
color: #888;
|
| 612 |
+
font-size: 12px;
|
| 613 |
+
text-transform: uppercase;
|
| 614 |
+
letter-spacing: 0.5px;
|
| 615 |
+
margin-bottom: 4px;
|
| 616 |
+
}
|