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 <noreply@anthropic.com>
This commit is contained in:
parent
95db01b451
commit
cf93b33c8b
|
|
@ -114,7 +114,7 @@ export class FolkSplatViewer extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadMyHistory() {
|
private async loadMyHistory() {
|
||||||
const token = localStorage.getItem("encryptid_token");
|
const token = this.getAuthToken();
|
||||||
if (!token || this._spaceSlug === "demo") return;
|
if (!token || this._spaceSlug === "demo") return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -355,7 +355,7 @@ export class FolkSplatViewer extends HTMLElement {
|
||||||
formData.append("tags", tagsInput.value.trim());
|
formData.append("tags", tagsInput.value.trim());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem("encryptid_token") || "";
|
const token = this.getAuthToken();
|
||||||
const res = await fetch(`/${this._spaceSlug}/rsplat/api/splats`, {
|
const res = await fetch(`/${this._spaceSlug}/rsplat/api/splats`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||||
|
|
@ -496,6 +496,17 @@ export class FolkSplatViewer extends HTMLElement {
|
||||||
{ t: 75, msg: "Almost there..." },
|
{ 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 = () => {
|
const onVisChange = () => {
|
||||||
if (document.hidden) {
|
if (document.hidden) {
|
||||||
hiddenAt = Date.now();
|
hiddenAt = Date.now();
|
||||||
|
|
@ -510,10 +521,12 @@ export class FolkSplatViewer extends HTMLElement {
|
||||||
if (document.hidden) return; // Don't update while hidden
|
if (document.hidden) return; // Don't update while hidden
|
||||||
const elapsed = Math.floor((Date.now() - startTime - hiddenTime) / 1000);
|
const elapsed = Math.floor((Date.now() - startTime - hiddenTime) / 1000);
|
||||||
const phase = [...phases].reverse().find(p => elapsed >= p.t);
|
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) {
|
if (progressText && phase) {
|
||||||
progressText.textContent = `${phase.msg} (${elapsed}s)`;
|
progressText.textContent = `${phase.msg} ${pct}% (${elapsed}s)`;
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 500);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const imageUrl = await this.stageImage(selectedFile!);
|
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 };
|
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";
|
progress.style.display = "none";
|
||||||
const elapsed = Math.floor((Date.now() - startTime - hiddenTime) / 1000);
|
const elapsed = Math.floor((Date.now() - startTime - hiddenTime) / 1000);
|
||||||
status.textContent = `Generated in ${elapsed}s`;
|
status.textContent = `Generated in ${elapsed}s`;
|
||||||
|
|
@ -588,7 +605,7 @@ export class FolkSplatViewer extends HTMLElement {
|
||||||
// ── Auto-save after generation ──
|
// ── Auto-save after generation ──
|
||||||
|
|
||||||
private async autoSave() {
|
private async autoSave() {
|
||||||
const token = localStorage.getItem("encryptid_token");
|
const token = this.getAuthToken();
|
||||||
if (!token || !this._generatedUrl || this._spaceSlug === "demo") return;
|
if (!token || !this._generatedUrl || this._spaceSlug === "demo") return;
|
||||||
|
|
||||||
try {
|
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() {
|
private async saveToGallery() {
|
||||||
const saveBtn = this.querySelector("#splat-save-btn") as HTMLButtonElement;
|
const saveBtn = this.querySelector("#splat-save-btn") as HTMLButtonElement;
|
||||||
if (!saveBtn || !this._generatedUrl) return;
|
if (!saveBtn || !this._generatedUrl) return;
|
||||||
|
|
@ -747,7 +770,7 @@ export class FolkSplatViewer extends HTMLElement {
|
||||||
saveBtn.textContent = "Saving...";
|
saveBtn.textContent = "Saving...";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem("encryptid_token") || "";
|
const token = this.getAuthToken();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
saveBtn.textContent = "Sign in to save";
|
saveBtn.textContent = "Sign in to save";
|
||||||
saveBtn.disabled = false;
|
saveBtn.disabled = false;
|
||||||
|
|
@ -768,7 +791,7 @@ export class FolkSplatViewer extends HTMLElement {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
saveBtn.textContent = "Sign in to save";
|
saveBtn.textContent = "Session expired — sign in again";
|
||||||
saveBtn.disabled = false;
|
saveBtn.disabled = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -340,8 +340,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.splat-generate__progress-bar {
|
.splat-generate__progress-bar {
|
||||||
height: 4px;
|
--splat-progress: 0%;
|
||||||
border-radius: 2px;
|
height: 6px;
|
||||||
|
border-radius: 3px;
|
||||||
background: var(--splat-border);
|
background: var(--splat-border);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -350,16 +351,13 @@
|
||||||
.splat-generate__progress-bar::after {
|
.splat-generate__progress-bar::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
top: 0;
|
||||||
background: var(--splat-accent);
|
left: 0;
|
||||||
width: 40%;
|
bottom: 0;
|
||||||
border-radius: 2px;
|
width: var(--splat-progress);
|
||||||
animation: splat-progress-slide 1.2s ease-in-out infinite;
|
background: linear-gradient(90deg, var(--splat-accent), #a78bfa);
|
||||||
}
|
border-radius: 3px;
|
||||||
|
transition: width 0.5s ease-out;
|
||||||
@keyframes splat-progress-slide {
|
|
||||||
0% { transform: translateX(-100%); }
|
|
||||||
100% { transform: translateX(350%); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.splat-generate__progress-text {
|
.splat-generate__progress-text {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue