import type { TimelineAnnotationSource } from "@opengeni/sdk"; import { QuoteIcon, XIcon } from "lucide-react"; import { Suspense, lazy, useEffect, useLayoutEffect, useRef, useState, type ReactNode, type RefObject, } from "react"; import { cn } from "../lib/cn"; import { AnnotationAccentRow, AnnotationNotePreview, AnnotationQuoteSourceButton, type TimelineAnnotationLike, } from "./timeline-annotation-chrome"; import { ANNOTATION_CARD_STACK_SCROLL_AT } from "./timeline-annotation-layout"; import { TimelineAnnotationSourceRootContext, useTimelineAnnotationSourceRoot, } from "./timeline-annotation-reveal-context"; import { annotationDisplayOrdinal, annotationHasNote, resolveAnnotationRevealRoot, } from "./timeline-annotation-shared"; import { useOpenGeniText } from "../messages/runtime"; export type { TimelineAnnotationLike } from "./timeline-annotation-chrome"; const TimelineAnnotationsDialog = lazy(() => import("./timeline-annotations-dialog")); function AnnotationSourceRootBridge({ triggerRef, children, }: { triggerRef: RefObject; children: ReactNode; }) { const inheritedRoot = useTimelineAnnotationSourceRoot(); const conversationRootRef = useRef(null); if (!inheritedRoot) { conversationRootRef.current = resolveAnnotationRevealRoot(triggerRef.current); } useLayoutEffect(() => { if (inheritedRoot) return; conversationRootRef.current = resolveAnnotationRevealRoot(triggerRef.current); }); if (inheritedRoot) return children; return ( {children} ); } export function TimelineAnnotationCards({ annotations, onRevealSource, className, }: { annotations: readonly TimelineAnnotationLike[]; onRevealSource?: ((source: TimelineAnnotationSource) => boolean) | undefined; className?: string | undefined; }) { const t = useOpenGeniText(); const [unavailableId, setUnavailableId] = useState(null); if (annotations.length === 0) return null; return (
= ANNOTATION_CARD_STACK_SCROLL_AT && "max-h-[min(28rem,55vh)] overflow-x-hidden overflow-y-auto overscroll-contain pr-1", className, )} > {annotations.map((annotation, index) => { const ordinal = annotationDisplayOrdinal(annotation, index); return (

{t("timelineAnnotations.annotationOrdinal", { ordinal })}

{unavailableId === annotation.id ? (

{t("timelineAnnotations.sourceIsOutsideTheLoaded")}

) : null}
); })}
); } export function TimelineAnnotationsChip({ annotations, editable = false, focusAnnotationId, onFocusConsumed, onUpdate, onRemove, onRevealSource, className, compact = false, onCommit, }: { annotations: readonly TimelineAnnotationLike[]; editable?: boolean | undefined; focusAnnotationId?: string | null | undefined; onFocusConsumed?: (() => void) | undefined; onUpdate?: ((id: string, note: string) => void) | undefined; onRemove?: ((id: string) => void) | undefined; /** Return focus to the owning input after all notes are completed with Enter. */ onCommit?: (() => void) | undefined; onRevealSource?: ((source: TimelineAnnotationSource) => boolean) | undefined; className?: string | undefined; /** Dense trigger for single-line rows such as the compact queue dock. */ compact?: boolean | undefined; }) { const t = useOpenGeniText(); const focusRequested = Boolean( focusAnnotationId && annotations.some((item) => item.id === focusAnnotationId), ); const [open, setOpen] = useState(focusRequested); const triggerRef = useRef(null); const openedFocusId = useRef(focusRequested ? (focusAnnotationId ?? null) : null); useEffect(() => { if (!focusRequested || !focusAnnotationId) { if (!focusRequested) openedFocusId.current = null; return; } if (openedFocusId.current === focusAnnotationId) return; openedFocusId.current = focusAnnotationId; setOpen(true); }, [focusAnnotationId, focusRequested]); if (annotations.length === 0) return null; const countLabel = t("counts.annotations", { count: annotations.length }); const incomplete = annotations.some((annotation) => !annotationHasNote(annotation.note)); const canClear = Boolean(editable && onRemove); const dismiss = (restoreFocus: boolean) => { setOpen(false); onFocusConsumed?.(); if (restoreFocus) triggerRef.current?.focus(); }; return ( {canClear ? ( ) : null} {open ? ( { dismiss(!onCommit); onCommit?.(); }} /> ) : null} ); } export default TimelineAnnotationsChip;