3d-arena / src /routes /Vote.svelte
dylanebert's picture
dylanebert HF staff
add oauth
1e16a1f
raw
history blame
10.1 kB
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import type { IViewer } from "./viewers/IViewer";
import { createViewer } from "./viewers/ViewerFactory";
import { Cube, WatsonHealth3DPrintMesh } from "carbon-icons-svelte";
interface Data {
input: string;
input_path: string;
model1: string;
model1_path: string;
model2: string;
model2_path: string;
}
let viewerA: IViewer;
let viewerB: IViewer;
let canvasA: HTMLCanvasElement;
let canvasB: HTMLCanvasElement;
let containerA: HTMLDivElement;
let containerB: HTMLDivElement;
let overlayA: HTMLDivElement;
let overlayB: HTMLDivElement;
let normalToggleA: HTMLInputElement;
let normalToggleB: HTMLInputElement;
let wireframeToggleA: HTMLInputElement;
let wireframeToggleB: HTMLInputElement;
let loadingBarFillA: HTMLDivElement;
let loadingBarFillB: HTMLDivElement;
let voteOverlay: boolean = false;
let voteOverlayA: HTMLDivElement;
let voteOverlayB: HTMLDivElement;
let statusMessage: string = "Loading...";
let errorMessage: string = "";
let data: Data;
async function fetchScenes() {
statusMessage = "Loading...";
errorMessage = "";
try {
const url = "/api/fetchScenes";
const response = await fetch(url, {
method: "GET",
headers: {
"Cache-Control": "no-cache",
},
});
const result = await response.json();
if (result.input) {
data = result;
statusMessage = "";
return true;
} else {
statusMessage = "Voting complete.";
return false;
}
} catch (error) {
errorMessage = "Failed to fetch pair.";
statusMessage = "";
return false;
}
}
async function loadScenes() {
const success = await fetchScenes();
if (!success) return;
overlayA.style.display = "flex";
overlayB.style.display = "flex";
const baseUrl = "https://huggingface.co/datasets/dylanebert/3d-arena/resolve/main/";
const model1_path = `${baseUrl}${data.model1_path}`;
const model2_path = `${baseUrl}${data.model2_path}`;
try {
[viewerA, viewerB] = await Promise.all([
createViewer(model1_path, canvasA, (progress) => {
loadingBarFillA.style.width = `${progress * 100}%`;
}),
createViewer(model2_path, canvasB, (progress) => {
loadingBarFillB.style.width = `${progress * 100}%`;
}),
]);
window.addEventListener("resize", handleResize);
handleResize();
} catch (error) {
console.log(error);
errorMessage = "Failed to load scenes.";
}
overlayA.style.display = "none";
overlayB.style.display = "none";
}
async function vote(option: "A" | "B") {
voteOverlay = true;
voteOverlayA.classList.add("show");
voteOverlayB.classList.add("show");
const payload = {
input: data.input,
better: option == "A" ? data.model1 : data.model2,
worse: option == "A" ? data.model2 : data.model1,
};
const url = `/api/vote`;
const startTime = Date.now();
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Cache-Control": "no-cache",
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
const elapsedTime = Date.now() - startTime;
const remainingTime = Math.max(1200 - elapsedTime, 0);
if (response.ok) {
const result = await response.json();
console.log(result);
setTimeout(() => {
voteOverlayA.classList.remove("show");
voteOverlayB.classList.remove("show");
voteOverlay = false;
loadScenes();
}, remainingTime);
} else {
if (response.status === 401) {
statusMessage = "Unauthorized. Redirecting to login...";
await new Promise((resolve) => setTimeout(resolve, 1000));
window.location.href = "/api/authorize";
} else {
errorMessage = "Failed to process vote.";
}
}
} catch (error) {
errorMessage = "Failed to process vote.";
statusMessage = "";
}
}
function skip() {
loadScenes();
}
function handleResize() {
requestAnimationFrame(() => {
if (canvasA && containerA) {
canvasA.width = containerA.clientWidth;
canvasA.height = containerA.clientHeight;
}
if (canvasB && containerB) {
canvasB.width = containerB.clientWidth;
canvasB.height = containerB.clientHeight;
}
});
}
function setRenderMode(viewer: IViewer, mode: string) {
viewer.setRenderMode(mode);
}
onMount(loadScenes);
onDestroy(() => {
viewerA?.dispose();
viewerB?.dispose();
if (typeof window !== "undefined") {
window.removeEventListener("resize", handleResize);
}
});
</script>
{#if errorMessage}
<p class="center-title muted" style="color: red;">{errorMessage}</p>
{:else if statusMessage}
<p class="center-title muted">{statusMessage}</p>
{:else}
<div class="vote-input">
<img
src={`https://huggingface.co/datasets/dylanebert/3d-arena/resolve/main/${data.input_path}`}
class="input-image"
alt="Input"
/>
</div>
<h2 class="center-title">Which is better?</h2>
<p class="center-subtitle">Use mouse/touch to change the view.</p>
<div class="voting-container">
<div bind:this={containerA} class="canvas-container">
<div bind:this={overlayA} class="loading-overlay">
<div class="loading-bar">
<div bind:this={loadingBarFillA} class="loading-bar-fill" />
</div>
</div>
<div bind:this={voteOverlayA} class="vote-overlay">{data.model1}</div>
<canvas bind:this={canvasA} class="viewer-canvas" id="canvas1"> </canvas>
<div class="stats">
{#if viewerA}
<p>vertex count: {viewerA.vertexCount}</p>
{/if}
</div>
{#if viewerA && !viewerA.topoOnly}
<div class="mode-toggle">
<label>
<input
type="radio"
name="modeA"
value="default"
checked
bind:this={normalToggleA}
on:change={() => setRenderMode(viewerA, "default")}
/>
<Cube class="mode-toggle-icon" />
</label>
<label>
<input
type="radio"
name="modeA"
value="wireframe"
bind:this={wireframeToggleA}
on:change={() => setRenderMode(viewerA, "wireframe")}
/>
<WatsonHealth3DPrintMesh class="mode-toggle-icon" />
</label>
</div>
{/if}
</div>
<div bind:this={containerB} class="canvas-container">
<div bind:this={overlayB} class="loading-overlay">
<div class="loading-bar">
<div bind:this={loadingBarFillB} class="loading-bar-fill" />
</div>
</div>
<div bind:this={voteOverlayB} class="vote-overlay">{data.model2}</div>
<canvas bind:this={canvasB} class="viewer-canvas" id="canvas2"></canvas>
<div class="stats">
{#if viewerB}
<p>vertex count: {viewerB.vertexCount}</p>
{/if}
</div>
{#if viewerB && !viewerB.topoOnly}
<div class="mode-toggle">
<label>
<input
type="radio"
name="modeB"
value="default"
checked
bind:this={normalToggleB}
on:change={() => setRenderMode(viewerB, "default")}
/>
<Cube class="mode-toggle-icon" />
</label>
<label>
<input
type="radio"
name="modeB"
value="wireframe"
bind:this={wireframeToggleB}
on:change={() => setRenderMode(viewerB, "wireframe")}
/>
<WatsonHealth3DPrintMesh class="mode-toggle-icon" />
</label>
</div>
{/if}
</div>
</div>
{#if !voteOverlay}
<div class="vote-buttons-container">
<button class="vote-button" on:click={() => vote("A")}>A is Better</button>
<button class="vote-button" on:click={() => vote("B")}>B is Better</button>
</div>
<div class="skip-container">
<button class="vote-button" on:click={() => skip()}>Skip</button>
</div>
{/if}
{/if}