Spaces:
Running
Running
File size: 5,310 Bytes
b03f016 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | import { useState, useEffect, useCallback } from "react";
export interface HashRoute {
page: "experiments" | "viz";
tab: string;
segments: string[];
params: URLSearchParams;
}
const ROUTE_CHANGE = "routechange";
const STORAGE_KEY = "agg-viz-route";
/** Read the saved route hash from localStorage (fallback when hash is empty, e.g. in an iframe). */
function getSavedHash(): string {
try {
return localStorage.getItem(STORAGE_KEY) || "";
} catch {
return "";
}
}
/** Persist the current hash to localStorage so iframe reloads restore state. */
function saveHash(hash: string) {
try {
localStorage.setItem(STORAGE_KEY, hash);
} catch {
// localStorage unavailable — ignore
}
}
/** Get the effective hash: prefer URL hash, fall back to localStorage. */
function effectiveHash(): string {
const hash = window.location.hash;
if (hash && hash !== "#" && hash !== "#/") return hash;
return getSavedHash();
}
export function parseHash(hash?: string): HashRoute {
const raw = (hash ?? effectiveHash()).replace(/^#\/?/, "");
if (!raw) {
return { page: "experiments", tab: "", segments: [], params: new URLSearchParams() };
}
const qIdx = raw.indexOf("?");
const pathPart = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
const params = new URLSearchParams(qIdx >= 0 ? raw.slice(qIdx + 1) : "");
const parts = pathPart.split("/").filter(Boolean);
if (parts[0] === "viz") {
return { page: "viz", tab: parts[1] || "model", segments: parts.slice(2), params };
}
// "experiments" or anything else defaults to experiments
const segments = parts[0] === "experiments" ? parts.slice(1) : parts;
return { page: "experiments", tab: "", segments, params };
}
function buildHash(route: HashRoute): string {
const parts: string[] = [route.page === "viz" ? "viz" : "experiments"];
if (route.page === "viz" && route.tab) parts.push(route.tab);
if (route.segments.length) parts.push(...route.segments);
let hash = "#/" + parts.join("/");
const qs = route.params.toString();
if (qs) hash += "?" + qs;
return hash;
}
function applyRoute(hash: string, push: boolean) {
saveHash(hash);
if (window.location.hash === hash) return;
if (push) {
window.history.pushState(null, "", hash);
} else {
window.history.replaceState(null, "", hash);
}
window.dispatchEvent(new Event(ROUTE_CHANGE));
}
/** Navigate to a new route (creates browser history entry). */
export function navigateTo(update: Partial<HashRoute>) {
const current = parseHash();
const pageChanged = update.page !== undefined && update.page !== current.page;
const tabChanged = update.tab !== undefined && update.tab !== current.tab;
const merged: HashRoute = {
page: update.page ?? current.page,
tab: update.tab ?? (pageChanged ? (update.page === "viz" ? "model" : "") : current.tab),
segments: update.segments ?? ((pageChanged || tabChanged) ? [] : current.segments),
params: update.params ?? ((pageChanged || tabChanged) ? new URLSearchParams() : current.params),
};
applyRoute(buildHash(merged), true);
}
/** Replace current route (no history entry). Use for frequent state changes like indices. */
export function replaceRoute(update: Partial<HashRoute>) {
const current = parseHash();
const merged: HashRoute = {
page: update.page ?? current.page,
tab: update.tab ?? current.tab,
segments: update.segments ?? current.segments,
params: update.params ?? current.params,
};
applyRoute(buildHash(merged), false);
}
/** Build a shareable direct URL for the current route (bypasses HF iframe). */
export function getShareableUrl(): string {
const hash = effectiveHash();
// Use the app's direct origin (works for both .hf.space and localhost)
return `${window.location.origin}${window.location.pathname}${hash}`;
}
/** Hook that re-renders on hash route changes. */
export function useHashRoute(): HashRoute {
const [route, setRoute] = useState<HashRoute>(parseHash);
useEffect(() => {
const handler = () => setRoute(parseHash());
window.addEventListener(ROUTE_CHANGE, handler);
window.addEventListener("popstate", handler);
window.addEventListener("hashchange", handler);
return () => {
window.removeEventListener(ROUTE_CHANGE, handler);
window.removeEventListener("popstate", handler);
window.removeEventListener("hashchange", handler);
};
}, []);
// On mount, if URL hash is empty but localStorage has a saved route, apply it
useEffect(() => {
const urlHash = window.location.hash;
if (!urlHash || urlHash === "#" || urlHash === "#/") {
const saved = getSavedHash();
if (saved) {
window.history.replaceState(null, "", saved);
setRoute(parseHash(saved));
}
}
}, []);
return route;
}
/** Hook for the copy-link button. Returns a callback that copies the shareable URL. */
export function useCopyLink() {
const [copied, setCopied] = useState(false);
const copyLink = useCallback(async () => {
const url = getShareableUrl();
try {
await navigator.clipboard.writeText(url);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
// Fallback: prompt user
window.prompt("Copy this link:", url);
}
}, []);
return { copyLink, copied };
}
|