'use client'; import { useEffect, useState } from 'react'; import { useEndpointsRuntime } from '../contexts/endpoints-runtime-context'; import { toClaudeMirrorPath } from '../utils/embed-url-converters'; export type ClaudeMirrorStatus = /** No mirror candidate at all (no proxy configured, or a non-mirrorable url). */ | 'absent' /** Probing the host for a self-hosted mirror (or minting one on first view). */ | 'probing' /** A mirror exists and `src` points at it. */ | 'found'; export interface ClaudeMirrorState { /** The mirror src when found (with a cache-bust once a re-publish is * detected), else `null`. */ src: string | null; status: ClaudeMirrorStatus; } /** * Ask the host's storage-view proxy whether the cached mirror is still the * artifact's CURRENT version, re-minting it silently if a re-publish moved * it. Returns a cache-bust token (the new version) when the mirror changed, * else null. Best-effort: any failure just leaves the shown mirror in place. */ async function revalidateMirror(mirrorPath: string): Promise { try { const res = await fetch(`${mirrorPath}?revalidate=1`); if (!res.ok) return null; const body = (await res.json()) as { updated?: boolean; version?: string | null }; return body?.updated ? String(body.version ?? Date.now()) : null; } catch { return null; } } /** * The host's SELF-HOSTED MIRROR src for a claude artifact url, plus a * `status` so the viewer can show a loading state while it resolves — THE * hook behind `ClaudeEmbed`'s transparent mirror leg, extracted so the * component stays presentational. * * Fully under the hood, three moves: * 1. Derive the candidate mirror path from the url's artifact id under the * host's configured storage-view proxy base * (`EndpointsRuntime.storageViewBaseUrl`) and probe it with a 1-byte * ranged GET. On a cold first view that probe IS the server-side * self-heal (~1-2s), which is exactly why `status: 'probing'` exists — * the viewer shows a spinner instead of the empty "no embeddable view" * flash. Exists → `found`; missing / no proxy / blip → `absent`, and the * caller's claude.ai fallback stands (the pre-mirror behavior). * 2. Once found, fire ONE background revalidation. If the artifact was * re-published since the mirror was cached, the server re-mints it and * reports the new version; the hook bumps a cache-bust on `src` so the * iframe silently reloads the fresh bytes. Unchanged → nothing happens. * * A host that mounts no EndpointsRuntime provider (or omits the entry) skips * the mirror leg entirely (`absent`). */ export function useClaudeMirrorSrc(url: string): ClaudeMirrorState { const endpoints = useEndpointsRuntime(); const mirrorPath = toClaudeMirrorPath(url, endpoints?.storageViewBaseUrl); // Keyed on `mirrorPath` and DERIVED rather than seeded by the effect: when // the url changes, the fallback below is what a reader sees, so a previous // artifact's `found` src can never be shown for a new one. Doing that with a // synchronous `setState` in the effect body is the cascading-render pattern // `react-hooks/set-state-in-effect` rules out; the effect now only settles // state from its async callbacks. const [resolved, setResolved] = useState<{ key: string; value: ClaudeMirrorState } | null>(null); const state: ClaudeMirrorState = resolved && resolved.key === mirrorPath ? resolved.value : { src: null, status: mirrorPath ? 'probing' : 'absent' }; useEffect(() => { if (!mirrorPath) return undefined; let cancelled = false; const settle = (value: ClaudeMirrorState) => { if (!cancelled) setResolved({ key: mirrorPath, value }); }; fetch(mirrorPath, { headers: { Range: 'bytes=0-0' } }) .then(res => { if (cancelled) return; if (!(res.ok || res.status === 206)) { settle({ src: null, status: 'absent' }); return; } settle({ src: mirrorPath, status: 'found' }); // Background freshness check — reload in place only if it changed. void revalidateMirror(mirrorPath).then(bust => { if (bust) settle({ src: `${mirrorPath}?v=${encodeURIComponent(bust)}`, status: 'found' }); }); }) .catch(() => { // No proxy on this host / network blip — the caller's claude.ai // fallback stands, same as before mirrors existed. settle({ src: null, status: 'absent' }); }); return () => { cancelled = true; }; }, [mirrorPath]); return state; }