import React, { useEffect, useRef, useState } from 'react'; import { cn } from '../copilot/cn'; import { ARTICLE_STATUS } from '../../service/visibility/visibility.constants'; import { getArticle } from '../../service/visibility/visibility.service'; import { markdownToPlainText, stripLeadingH1 } from './MarkdownView'; const PREVIEW_CHARS = 260; // Flipping tabs or filters re-mounts every card; without this each pass // would re-download every content body. const bodyCache = new Map(); function PreviewSkeleton({ pulse = false }: { pulse?: boolean }): JSX.Element { const bar = cn( 'h-2 rounded', pulse ? 'animate-pulse bg-gray-100' : 'bg-gray-100' ); return (
); } interface PreviewFrameProps { heading: string; body?: string | null; loading?: boolean; innerRef?: React.Ref; className?: string; } export function PreviewFrame({ heading, body, loading = false, innerRef, className, }: PreviewFrameProps): JSX.Element { return ( ); } interface ArticlePreviewProps { articleId: string; clientId: string; token: string; title: string; status: string; } function ArticlePreview({ articleId, clientId, token, title, status, }: ArticlePreviewProps): JSX.Element { const frameRef = useRef(null); const [content, setContent] = useState( () => bodyCache.get(articleId) ?? null ); const [failed, setFailed] = useState(false); const isGenerating = status === ARTICLE_STATUS.PENDING || status === ARTICLE_STATUS.GENERATING; useEffect(() => { if (content || failed || isGenerating) return; const frame = frameRef.current; if (!frame) return; let cancelled = false; const load = (): void => { getArticle(clientId, token, articleId) .then(article => { const entry = { heading: article.h1 || article.summary.title || title, body: markdownToPlainText( stripLeadingH1(article.markdown ?? '') ).slice(0, PREVIEW_CHARS), }; bodyCache.set(articleId, entry); if (!cancelled) setContent(entry); }) .catch(() => { // The card still reads fine on its title alone. if (!cancelled) setFailed(true); }); }; const observer = new IntersectionObserver( entries => { if (entries.some(entry => entry.isIntersecting)) { observer.disconnect(); load(); } }, { rootMargin: '200px' } ); observer.observe(frame); return () => { cancelled = true; observer.disconnect(); }; }, [articleId, clientId, token, title, content, failed, isGenerating]); return ( ); } export default ArticlePreview;