Files
fusion/packages/dashboard/app/hooks/usePreviewEmbed.ts
Fusion 8bd0538a72 feat(FN-2263): merge fusion/fn-2263 (auto-resolved)
- docs(FN-2263): update README with scaffolded scope and deferral documentation
- test(FN-2263): verify hermes runtime plugin build and tests
- feat(FN-2263): implement plugin entrypoint with runtime registration shell
- feat(FN-2263): scaffold Hermes runtime plugin package
- test(FN-2256): add mocks for createResolvedAgentSession in tests
- docs(FN-2256): update architecture docs and add changeset
- fix(FN-2256): fix lint errors and TypeScript issues
- feat(FN-2256): add runtime selection regression tests — Step 4
- feat(FN-2256): route engine session creation through runtime resolver — Step 3
- feat(FN-2256): extend PluginRunner runtime lookup surface — Step 2
- feat(FN-2256): add runtime abstraction and resolver — Step 1
2026-04-22 14:49:34 -07:00

177 lines
4.7 KiB
TypeScript

import { useCallback, useEffect, useRef, useState, type RefObject } from "react";
export type EmbedStatus = "unknown" | "loading" | "embedded" | "blocked" | "error";
export type EmbedDetectionMethod = "auto" | "manual" | null;
interface UsePreviewEmbedOptions {
loadTimeoutMs?: number;
detectionMethod?: EmbedDetectionMethod;
}
interface UsePreviewEmbedResult {
embedStatus: EmbedStatus;
isEmbedded: boolean;
isBlocked: boolean;
blockReason: string | null;
detectionMethod: EmbedDetectionMethod;
iframeRef: RefObject<HTMLIFrameElement | null>;
resetEmbedStatus: () => void;
// Extended API for direct status control (backward compatibility)
setEmbedStatus: (status: EmbedStatus) => void;
retry: () => void;
// Legacy alias for backward compatibility
embedContext: string | null;
handleIframeLoad: () => void;
handleIframeError: () => void;
}
const DEFAULT_LOAD_TIMEOUT_MS = 10_000;
const BLOCKED_CONTEXT = "This preview appears to block iframe embedding. Open it in a new tab instead.";
const ERROR_CONTEXT = "The preview URL could not be loaded. Verify the server is running and the URL is correct.";
const TIMEOUT_CONTEXT = "Preview is taking longer than expected and may block iframe embedding.";
function getContextForStatus(status: EmbedStatus): string | null {
if (status === "blocked") {
return BLOCKED_CONTEXT;
}
if (status === "error") {
return ERROR_CONTEXT;
}
return null;
}
export function usePreviewEmbed(url: string | null, options: UsePreviewEmbedOptions = {}): UsePreviewEmbedResult {
const { loadTimeoutMs = DEFAULT_LOAD_TIMEOUT_MS, detectionMethod: initialDetectionMethod = null } = options;
const iframeRef = useRef<HTMLIFrameElement | null>(null);
const timeoutRef = useRef<number | null>(null);
const [embedStatus, setEmbedStatusState] = useState<EmbedStatus>("unknown");
const [blockReason, setBlockReason] = useState<string | null>(null);
const [detectionMethod] = useState<EmbedDetectionMethod>(initialDetectionMethod);
const clearLoadingTimeout = useCallback(() => {
if (timeoutRef.current !== null) {
window.clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
}, []);
const setEmbedStatus = useCallback(
(status: EmbedStatus) => {
clearLoadingTimeout();
setEmbedStatusState(status);
setBlockReason(getContextForStatus(status));
},
[clearLoadingTimeout],
);
const resetEmbedStatus = useCallback(() => {
clearLoadingTimeout();
setEmbedStatusState("unknown");
setBlockReason(null);
}, [clearLoadingTimeout]);
const retry = useCallback(() => {
resetEmbedStatus();
}, [resetEmbedStatus]);
const handleIframeLoad = useCallback(() => {
const iframeEl = iframeRef.current;
if (!iframeEl) {
setEmbedStatus("embedded");
return;
}
try {
const frameHref = iframeEl.contentWindow?.location?.href;
if (frameHref === "about:blank" && iframeEl.src !== "about:blank") {
setEmbedStatus("blocked");
return;
}
} catch {
// Cross-origin access can throw; assume successful embed.
}
setEmbedStatus("embedded");
}, [setEmbedStatus]);
const handleIframeError = useCallback(() => {
setEmbedStatus("error");
}, [setEmbedStatus]);
useEffect(() => {
clearLoadingTimeout();
if (!url) {
setEmbedStatusState("unknown");
setBlockReason(null);
return;
}
setEmbedStatusState("unknown");
setBlockReason(null);
let canceled = false;
queueMicrotask(() => {
if (canceled) {
return;
}
setEmbedStatusState("loading");
setBlockReason(null);
});
return () => {
canceled = true;
clearLoadingTimeout();
};
}, [clearLoadingTimeout, url]);
useEffect(() => {
if (!url || embedStatus !== "loading") {
clearLoadingTimeout();
return;
}
const timer = window.setTimeout(() => {
timeoutRef.current = null;
setEmbedStatusState("blocked");
setBlockReason(TIMEOUT_CONTEXT);
}, loadTimeoutMs);
timeoutRef.current = timer;
return () => {
window.clearTimeout(timer);
if (timeoutRef.current === timer) {
timeoutRef.current = null;
}
};
}, [clearLoadingTimeout, embedStatus, loadTimeoutMs, url]);
useEffect(() => {
return clearLoadingTimeout;
}, [clearLoadingTimeout]);
const isEmbedded = embedStatus === "embedded";
const isBlocked = embedStatus === "blocked" || embedStatus === "error";
return {
embedStatus,
isEmbedded,
isBlocked,
blockReason,
detectionMethod,
iframeRef,
resetEmbedStatus,
setEmbedStatus,
retry,
embedContext: blockReason,
handleIframeLoad,
handleIframeError,
};
}