Spaces:
Running
Running
| <script lang="ts"> | |
| import { onMount, onDestroy } from "svelte"; | |
| import type { IViewer } from "./viewers/IViewer"; | |
| import { createViewer } from "./viewers/ViewerFactory"; | |
| import { Cube, WatsonHealth3DPrintMesh, Information } from "carbon-icons-svelte"; | |
| import { getConfig } from "./utils/getConfig"; | |
| interface Data { | |
| input: string; | |
| input_path: string; | |
| model1: string; | |
| model1_path: string; | |
| model1_displayName: string; | |
| model2: string; | |
| model2_path: string; | |
| model2_displayName: string; | |
| vote_type: 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 token = localStorage.getItem("access_token"); | |
| const response = await fetch(url, { | |
| method: "GET", | |
| headers: { | |
| "Cache-Control": "no-cache", | |
| Authorization: `Bearer ${token}`, | |
| }, | |
| }); | |
| 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() { | |
| viewerA?.dispose(); | |
| viewerB?.dispose(); | |
| 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}`; | |
| const config1 = await getConfig(data.model1); | |
| const config2 = await getConfig(data.model2); | |
| data.model1_displayName = config1.DisplayName || data.model1; | |
| data.model2_displayName = config2.DisplayName || data.model2; | |
| try { | |
| const isTopology = data.vote_type === "topology"; | |
| [viewerA, viewerB] = await Promise.all([ | |
| createViewer( | |
| model1_path, | |
| canvasA, | |
| (progress) => { | |
| loadingBarFillA.style.width = `${progress * 100}%`; | |
| }, | |
| isTopology, | |
| ), | |
| createViewer( | |
| model2_path, | |
| canvasB, | |
| (progress) => { | |
| loadingBarFillB.style.width = `${progress * 100}%`; | |
| }, | |
| isTopology, | |
| ), | |
| ]); | |
| if (isTopology) { | |
| viewerA.setRenderMode("wireframe"); | |
| viewerB.setRenderMode("wireframe"); | |
| } | |
| window.addEventListener("resize", handleResize); | |
| handleResize(); | |
| } catch (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 token = localStorage.getItem("access_token"); | |
| if (!token || token === "null" || token === "undefined") { | |
| window.location.href = "/api/authorize"; | |
| return; | |
| } | |
| const payload = { | |
| input: data.input, | |
| better: option == "A" ? data.model1 : data.model2, | |
| worse: option == "A" ? data.model2 : data.model1, | |
| vote_type: data.vote_type, | |
| }; | |
| 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", | |
| Authorization: `Bearer ${token}`, | |
| }, | |
| body: JSON.stringify(payload), | |
| }); | |
| const elapsedTime = Date.now() - startTime; | |
| const remainingTime = Math.max(1200 - elapsedTime, 0); | |
| if (response.ok) { | |
| const result = await response.json(); | |
| setTimeout(() => { | |
| voteOverlayA.classList.remove("show"); | |
| voteOverlayB.classList.remove("show"); | |
| voteOverlay = false; | |
| loadScenes(); | |
| }, remainingTime); | |
| } else { | |
| if (response.status === 401) { | |
| localStorage.removeItem("access_token"); | |
| statusMessage = "Session expired. Redirecting to login..."; | |
| await new Promise((resolve) => setTimeout(resolve, 1000)); | |
| window.location.href = "/api/authorize"; | |
| } else { | |
| errorMessage = "Failed to process vote."; | |
| voteOverlayA.classList.remove("show"); | |
| voteOverlayB.classList.remove("show"); | |
| voteOverlay = false; | |
| } | |
| } | |
| } catch (error) { | |
| errorMessage = "Failed to process vote."; | |
| statusMessage = ""; | |
| voteOverlayA.classList.remove("show"); | |
| voteOverlayB.classList.remove("show"); | |
| voteOverlay = false; | |
| } | |
| } | |
| 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> | |
| {#if data.vote_type === "topology"} | |
| <div class="topology-warning"> | |
| <Information size={20} class="topology-warning-icon" /> | |
| <span class="topology-warning-text">Topology Mode • Zoom in to see mesh details</span> | |
| </div> | |
| {/if} | |
| <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_displayName}</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 && data.vote_type !== "topology"} | |
| <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_displayName}</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 && data.vote_type !== "topology"} | |
| <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} | |