From cf93b33c8b02f12372b538070612fb48d35d87e5 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 16 Mar 2026 13:02:38 -0700 Subject: [PATCH] feat(rsplat): add % progress bar for 3D generation, fix auth token lookup Replace indeterminate sliding animation with a realistic percentage fill bar using logarithmic curve (asymptotes at 95%, based on ~60s typical Trellis 2 timing). Jumps to 100% on completion. Fix "sign in to save" showing for authenticated users by checking both localStorage and cookie for auth token, and improving the 401 message to "Session expired" when a token exists locally. Co-Authored-By: Claude Opus 4.6 --- .../rsplat/components/folk-splat-viewer.ts | 37 +++++++++++++++---- modules/rsplat/components/splat.css | 22 +++++------ 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/modules/rsplat/components/folk-splat-viewer.ts b/modules/rsplat/components/folk-splat-viewer.ts index a5c7a3f..c735168 100644 --- a/modules/rsplat/components/folk-splat-viewer.ts +++ b/modules/rsplat/components/folk-splat-viewer.ts @@ -114,7 +114,7 @@ export class FolkSplatViewer extends HTMLElement { } private async loadMyHistory() { - const token = localStorage.getItem("encryptid_token"); + const token = this.getAuthToken(); if (!token || this._spaceSlug === "demo") return; try { @@ -355,7 +355,7 @@ export class FolkSplatViewer extends HTMLElement { formData.append("tags", tagsInput.value.trim()); try { - const token = localStorage.getItem("encryptid_token") || ""; + const token = this.getAuthToken(); const res = await fetch(`/${this._spaceSlug}/rsplat/api/splats`, { method: "POST", headers: token ? { Authorization: `Bearer ${token}` } : {}, @@ -496,6 +496,17 @@ export class FolkSplatViewer extends HTMLElement { { t: 75, msg: "Almost there..." }, ]; + // Realistic progress curve — typical Trellis 2 takes 45-75s + const EXPECTED_SECONDS = 60; + const progressBar = progress.querySelector(".splat-generate__progress-bar") as HTMLElement; + + const estimatePercent = (elapsed: number): number => { + if (elapsed <= 0) return 0; + // Logarithmic curve: fast start, slows toward 95% asymptote + const ratio = elapsed / EXPECTED_SECONDS; + return Math.min(95, 100 * (1 - Math.exp(-2.5 * ratio))); + }; + const onVisChange = () => { if (document.hidden) { hiddenAt = Date.now(); @@ -510,10 +521,12 @@ export class FolkSplatViewer extends HTMLElement { if (document.hidden) return; // Don't update while hidden const elapsed = Math.floor((Date.now() - startTime - hiddenTime) / 1000); const phase = [...phases].reverse().find(p => elapsed >= p.t); + const pct = Math.round(estimatePercent(elapsed)); + if (progressBar) progressBar.style.setProperty("--splat-progress", `${pct}%`); if (progressText && phase) { - progressText.textContent = `${phase.msg} (${elapsed}s)`; + progressText.textContent = `${phase.msg} ${pct}% (${elapsed}s)`; } - }, 1000); + }, 500); try { const imageUrl = await this.stageImage(selectedFile!); @@ -552,6 +565,10 @@ export class FolkSplatViewer extends HTMLElement { } const data = await res.json() as { url: string; format: string }; + // Jump to 100% before hiding + if (progressBar) progressBar.style.setProperty("--splat-progress", "100%"); + if (progressText) progressText.textContent = "Complete!"; + await new Promise(r => setTimeout(r, 400)); progress.style.display = "none"; const elapsed = Math.floor((Date.now() - startTime - hiddenTime) / 1000); status.textContent = `Generated in ${elapsed}s`; @@ -588,7 +605,7 @@ export class FolkSplatViewer extends HTMLElement { // ── Auto-save after generation ── private async autoSave() { - const token = localStorage.getItem("encryptid_token"); + const token = this.getAuthToken(); if (!token || !this._generatedUrl || this._spaceSlug === "demo") return; try { @@ -739,6 +756,12 @@ export class FolkSplatViewer extends HTMLElement { } } + private getAuthToken(): string { + return localStorage.getItem("encryptid_token") + || document.cookie.match(/encryptid_token=([^;]+)/)?.[1] + || ""; + } + private async saveToGallery() { const saveBtn = this.querySelector("#splat-save-btn") as HTMLButtonElement; if (!saveBtn || !this._generatedUrl) return; @@ -747,7 +770,7 @@ export class FolkSplatViewer extends HTMLElement { saveBtn.textContent = "Saving..."; try { - const token = localStorage.getItem("encryptid_token") || ""; + const token = this.getAuthToken(); if (!token) { saveBtn.textContent = "Sign in to save"; saveBtn.disabled = false; @@ -768,7 +791,7 @@ export class FolkSplatViewer extends HTMLElement { }); if (res.status === 401) { - saveBtn.textContent = "Sign in to save"; + saveBtn.textContent = "Session expired — sign in again"; saveBtn.disabled = false; return; } diff --git a/modules/rsplat/components/splat.css b/modules/rsplat/components/splat.css index 1e54c19..e770a27 100644 --- a/modules/rsplat/components/splat.css +++ b/modules/rsplat/components/splat.css @@ -340,8 +340,9 @@ } .splat-generate__progress-bar { - height: 4px; - border-radius: 2px; + --splat-progress: 0%; + height: 6px; + border-radius: 3px; background: var(--splat-border); overflow: hidden; position: relative; @@ -350,16 +351,13 @@ .splat-generate__progress-bar::after { content: ""; position: absolute; - inset: 0; - background: var(--splat-accent); - width: 40%; - border-radius: 2px; - animation: splat-progress-slide 1.2s ease-in-out infinite; -} - -@keyframes splat-progress-slide { - 0% { transform: translateX(-100%); } - 100% { transform: translateX(350%); } + top: 0; + left: 0; + bottom: 0; + width: var(--splat-progress); + background: linear-gradient(90deg, var(--splat-accent), #a78bfa); + border-radius: 3px; + transition: width 0.5s ease-out; } .splat-generate__progress-text {