import type { DraftTimelineAnnotation, TimelineAnnotation, TimelineAnnotationSource, } from "@opengeni/sdk"; import { useLayoutEffect, useRef, useState, type ReactNode } from "react"; import { cn } from "../lib/cn"; import { annotationNoteNeedsDisclosure } from "./timeline-annotation-layout"; import { useTimelineAnnotationSourceRoot } from "./timeline-annotation-reveal-context"; import { annotationSourceLabel, revealLoadedAnnotationSource, truncateAnnotationQuote, } from "./timeline-annotation-shared"; import { useOpenGeniText } from "../messages/runtime"; export type TimelineAnnotationLike = DraftTimelineAnnotation | TimelineAnnotation; const NOTE_MAX_HEIGHT_PX = 160; function autosizeNote(textarea: HTMLTextAreaElement | null): void { if (!textarea) return; textarea.style.height = "auto"; textarea.style.height = `${Math.min(Math.max(textarea.scrollHeight, 32), NOTE_MAX_HEIGHT_PX)}px`; } function quotePreview(quote: string): { text: string; multiline: boolean } { const trimmed = quote.trim(); const multiline = trimmed.includes("\n"); return { text: multiline ? trimmed : trimmed.replace(/\s+/g, " "), multiline, }; } export function AnnotationQuoteSourceButton({ annotation, lines = 2, onRevealSource, onUnavailable, }: { annotation: TimelineAnnotationLike; lines?: 2 | 3; onRevealSource?: ((source: TimelineAnnotationSource) => boolean) | undefined; onUnavailable: (id: string | null) => void; }) { const t = useOpenGeniText(); const preview = quotePreview(annotation.quote); const sourceLabel = annotationSourceLabel(t, annotation.source); const sourceRoot = useTimelineAnnotationSourceRoot(); return ( ); } export function AnnotationAccentRow({ children, className, }: { children: ReactNode; className?: string | undefined; }) { return (
); } export function AnnotationNotePreview({ note, annotationId, ordinal, className, }: { note: string; annotationId: string; ordinal: number; className?: string | undefined; }) { const t = useOpenGeniText(); const [expanded, setExpanded] = useState(false); if (!note) return null; const collapsible = annotationNoteNeedsDisclosure(note); const contentId = `og-annotation-note-${annotationId}`; return (

{note}

{collapsible ? ( ) : null}
); } export function AnnotationNoteField({ annotation, inputRef, onUpdate, onCommit, }: { annotation: TimelineAnnotationLike; inputRef?: ((node: HTMLTextAreaElement | null) => void) | undefined; onUpdate: (id: string, note: string) => void; onCommit?: (() => void) | undefined; }) { const t = useOpenGeniText(); const localRef = useRef(null); const setRef = (node: HTMLTextAreaElement | null) => { localRef.current = node; inputRef?.(node); }; useLayoutEffect(() => { const textarea = localRef.current; if (!textarea) return; // Ref callbacks can reattach on every scroll-driven parent render. Reflowing // unchanged notes there interrupts native scrolling, particularly in WebKit. autosizeNote(textarea); if (typeof ResizeObserver === "undefined") return; let width = textarea.getBoundingClientRect().width; const observer = new ResizeObserver(() => { const nextWidth = textarea.getBoundingClientRect().width; if (nextWidth === width) return; width = nextWidth; autosizeNote(textarea); }); observer.observe(textarea); return () => observer.disconnect(); }, [annotation.note]); return (