import { useActionQuery, useSession } from "@agent-native/core/client/hooks"; import { injectSessionReplayIframeBootstrap, SESSION_REPLAY_IFRAME_ATTRIBUTE, } from "@agent-native/core/client/host"; import { useT } from "@agent-native/core/client/i18n"; import { ReviewStatusBadge, useReviewComments, } from "@agent-native/core/client/review"; import { buildSignInReturnHref } from "@agent-native/core/client/ui"; import { readDesignReviewSummary } from "@shared/review-summary"; import { IconMessageCircle } from "@tabler/icons-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Link, useParams, useNavigate } from "react-router"; import { appendHitTestResponder } from "@/components/design/design-canvas/hit-test"; import { ReviewCommentsPanel } from "@/components/design/ReviewCommentsPanel"; import { QueryErrorState } from "@/components/QueryErrorState"; import { Button } from "@/components/ui/button"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, } from "@/components/ui/sheet"; import { Skeleton } from "@/components/ui/skeleton"; import { ReviewCanvasPins } from "@/components/visual-editor/ReviewCanvasPins"; import { resolvePresentEscapeAction, shouldBlockPresentPageNavigation, } from "./present-review-state"; interface DesignFile { id: string; filename: string; fileType: string; content: string; } interface DesignData { id: string; title: string; files: DesignFile[]; accessRole?: "viewer" | "editor" | "admin" | "owner"; } export default function Present() { const t = useT(); const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const { session } = useSession(); const [currentPage, setCurrentPage] = useState(0); const [commentMode, setCommentMode] = useState(false); const [commentsOpen, setCommentsOpen] = useState(false); const { data: design, isLoading, isError, isFetching, refetch, } = useActionQuery("get-design", { id: id! }); const files: DesignFile[] = design?.files ?? []; const activeFile = files[currentPage] ?? files[0]; const reviewQuery = useReviewComments( { resourceType: "design", resourceId: id ?? "", targetId: activeFile?.id ?? undefined, includeResolved: false, limit: 500, }, { enabled: Boolean(id) }, ); const canPost = Boolean(session?.email); const canResolve = Boolean( design?.accessRole === "owner" || design?.accessRole === "admin" || design?.accessRole === "editor", ); const reviewableContent = useMemo( () => injectSessionReplayIframeBootstrap( appendHitTestResponder(activeFile?.content ?? ""), ), [activeFile?.content], ); const reviewCommentCount = readDesignReviewSummary(reviewQuery.data)?.openCount ?? new Set( (reviewQuery.data?.comments ?? []) .filter( (comment) => comment.status === "open" && comment.parentCommentId === null, ) .map((comment) => comment.threadId), ).size; const signInHref = buildSignInReturnHref( typeof window === "undefined" ? undefined : { returnTo: window.location.pathname }, ); // Keyboard navigation const handleKeyDown = useCallback( (e: KeyboardEvent) => { if (e.key === "Escape") { const action = resolvePresentEscapeAction({ commentsOpen, commentMode, }); if (action === "close-comments") setCommentsOpen(false); if (action === "exit-presentation") navigate(`/design/${id}`); // ReviewCanvasPins owns "defer-to-comment-mode" so it can dismiss an // active draft before it exits the tool. return; } // Freeze slide navigation while review UI is active so typing a space // or using arrow keys in the sheet cannot change the anchored screen. if (shouldBlockPresentPageNavigation({ commentsOpen, commentMode })) return; if (files.length <= 1) return; if (e.key === "ArrowRight" || e.key === "ArrowDown" || e.key === " ") { e.preventDefault(); setCurrentPage((p) => Math.min(p + 1, files.length - 1)); } if (e.key === "ArrowLeft" || e.key === "ArrowUp") { e.preventDefault(); setCurrentPage((p) => Math.max(p - 1, 0)); } }, [commentMode, commentsOpen, files.length, id, navigate], ); useEffect(() => { document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [handleKeyDown]); if (!id) { navigate("/"); return null; } if (isLoading) { return (
); } if (isError) { return (
void refetch()} retrying={isFetching} />
); } if (!design || files.length === 0) { return (

{t("pages.presentEmpty")}

{t("pages.presentBackToEditor")}
); } return (