import React, { useRef, useState } from 'react'; import { extractHeadings, markdownToHtml, markdownToPlainText, type TocEntry, } from '../visibility/MarkdownView'; import { copyToClipboard } from '../../lib/clipboard'; import { diffToCleanHtml, dropEmptyDiffTags, faqPairsToHtml, htmlToPlainText, injectHeadingIds, looksLikeHtml, parseFaqPairs, splitTitleFromBody, } from '../../lib/inline-result-html'; import type { InlineGenerateMode } from '../../service/content-generator/content-generator.interface'; /** * Rendered-content styling. ```` = additions (green), ```` = * removals (red, struck through), ```` = highlight (green). Scoped with * Tailwind arbitrary-variant selectors so it never leans on a typography * plugin. */ const RENDERED_CLASS = 'space-y-2 text-sm leading-relaxed text-gray-900 [&_h1]:text-lg [&_h1]:font-semibold [&_h2]:mt-3 [&_h2]:font-semibold [&_h3]:mt-3 [&_h3]:font-semibold [&_h4]:font-semibold [&_p]:my-1 [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5 [&_li]:my-0.5 [&_strong]:font-semibold [&_mark]:rounded [&_mark]:bg-emerald-100 [&_mark]:px-0.5 [&_mark]:text-emerald-800 [&_ins]:rounded [&_ins]:bg-emerald-100 [&_ins]:px-0.5 [&_ins]:text-emerald-800 [&_ins]:no-underline [&_del]:rounded [&_del]:bg-red-100 [&_del]:px-0.5 [&_del]:text-red-700 [&_del]:line-through'; type CopyKind = 'title' | 'html' | 'text'; /** Props for {@link InlineResultCard}. */ interface InlineResultCardProps { mode: InlineGenerateMode; content: string; } /** * Renders a generated result the way it will look on the product page: HTML * (with ````/```` diff for gap) rendered, FAQ as Q&A. Title * (gap/rewrite) splits into its own block with its own Copy title. An HTML * toggle, a Sections sidebar (when the body has headings), and Copy HTML / * Copy text (diff marks stripped to the final clean version) round it out. * * @param {InlineResultCardProps} props - Mode and generated content. * @return {JSX.Element} The result card. */ const InlineResultCard = ({ mode, content, }: InlineResultCardProps): JSX.Element => { const [showCode, setShowCode] = useState(false); const [copied, setCopied] = useState(null); const bodyRef = useRef(null); const titled = mode === 'rewrite' || mode === 'gap'; const split = titled ? splitTitleFromBody(content) : { title: '', body: content }; const titleText = split.title; const body = split.body; const isHtml = looksLikeHtml(body); const faqPairs = !isHtml ? parseFaqPairs(body) : []; const isFaq = faqPairs.length > 0; const cleanBody = isHtml ? dropEmptyDiffTags(body) : body; let renderedBody: JSX.Element; let toc: TocEntry[] = []; if (isFaq) { renderedBody = (
{faqPairs.map((pair, pairIndex) => (
{pair.q}
{pair.a}
))}
); } else { const processed = isHtml ? injectHeadingIds(cleanBody) : { html: markdownToHtml(cleanBody), toc: extractHeadings(cleanBody) }; toc = processed.toc; renderedBody = (
); } const htmlVersion = isHtml ? diffToCleanHtml(cleanBody) : isFaq ? faqPairsToHtml(faqPairs) : markdownToHtml(cleanBody); const textVersion = isHtml ? htmlToPlainText(diffToCleanHtml(cleanBody)) : isFaq ? body : markdownToPlainText(cleanBody); const flash = (kind: CopyKind): void => { setCopied(kind); window.setTimeout(() => setCopied(null), 2000); }; const copy = async (kind: 'html' | 'text'): Promise => { if (await copyToClipboard(kind === 'html' ? htmlVersion : textVersion)) { flash(kind); } }; const copyTitle = async (): Promise => { if (titleText && (await copyToClipboard(titleText))) flash('title'); }; const scrollToHeading = (slug: string): void => { bodyRef.current ?.querySelector(`[id="${CSS.escape(slug)}"]`) ?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }; const copyButtonClass = 'inline-flex items-center gap-1.5 rounded-md border border-gray-200 bg-white px-2 py-1 text-xs font-medium text-gray-700 hover:bg-gray-50'; return (
{!isFaq && ( )}
{titled && titleText && (

Title

{titleText}

)}
0 && !showCode ? 'grid min-w-0 gap-4 lg:grid-cols-[200px_minmax(0,1fr)]' : 'min-w-0' } > {toc.length > 0 && !showCode && ( )}
{showCode ? (
              {htmlVersion}
            
) : ( renderedBody )}
); }; export default InlineResultCard;