import { useEffect, useMemo, useRef, useState } from "react"; import type { Agent, DocumentAnnotationThreadWithComments } from "@paperclipai/shared"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import type { DocumentAnnotationTarget } from "@/api/document-annotations"; import type { CompanyUserProfile } from "@/lib/company-members"; import { useDocumentAnnotationMutations } from "@/hooks/useDocumentAnnotationMutations"; import type { AnnotationAnchorRect, PendingAnchor } from "./DocumentAnnotationLayer"; import { copyAnnotationLink, ThreadCard, truncate } from "./DocumentAnnotationPanel"; export interface DocumentAnnotationPopoverProps { anchorRect: AnnotationAnchorRect; containerRef: React.RefObject; target: DocumentAnnotationTarget; documentKey: string; baseRevisionId: string | null; baseRevisionNumber: number; pendingAnchor: PendingAnchor | null; thread: DocumentAnnotationThreadWithComments | null; focusedCommentId: string | null; onFocusThread: (threadId: string | null) => void; onClose: () => void; onThreadCreated: (thread: DocumentAnnotationThreadWithComments) => void; newCommentDisabled?: boolean; agentMap?: ReadonlyMap & Partial>>; userProfileMap?: ReadonlyMap; } export function DocumentAnnotationPopover(props: DocumentAnnotationPopoverProps) { const cardRef = useRef(null); const composerRef = useRef(null); const [composer, setComposer] = useState(""); const [reply, setReply] = useState(""); const target = useMemo(() => props.target, [props.target]); const { createThread, addReply, updateStatus, mutationError } = useDocumentAnnotationMutations({ target, baseRevisionId: props.baseRevisionId, baseRevisionNumber: props.baseRevisionNumber, pendingAnchor: props.pendingAnchor, onFocusThread: props.onFocusThread, onThreadCreated: props.onThreadCreated, onReplyAdded: () => setReply(""), }); useEffect(() => composerRef.current?.focus(), [props.pendingAnchor]); useEffect(() => { const dismiss = (event: PointerEvent) => { if (!cardRef.current?.contains(event.target as Node)) props.onClose(); }; const escape = (event: KeyboardEvent) => { if (event.key === "Escape") { event.preventDefault(); props.onClose(); } }; document.addEventListener("pointerdown", dismiss); document.addEventListener("keydown", escape); return () => { document.removeEventListener("pointerdown", dismiss); document.removeEventListener("keydown", escape); }; }, [props.onClose]); const cardHeight = cardRef.current?.offsetHeight ?? 260; const containerHeight = visibleBottomWithinContainer(props.containerRef.current); const below = props.anchorRect.top + props.anchorRect.height + 6; const top = below + cardHeight <= containerHeight ? below : Math.max(0, props.anchorRect.top - cardHeight - 6); const containerWidth = props.containerRef.current?.clientWidth ?? 320; const left = Math.max(0, Math.min(props.anchorRect.left, containerWidth - 320)); const submitComposer = () => { const body = composer.trim(); if (body && !createThread.isPending && props.baseRevisionId) createThread.mutate(body); }; return (
{mutationError ?

{mutationError}

: null} {props.pendingAnchor ? (
{truncate(props.pendingAnchor.selectedText, 120)}