'use client' import { useState } from 'react' import type { AnnotateResponse, AnnotationEntry } from '@/lib/api' import type { Settings } from '@/types' interface Sentence { text: string annotations: AnnotationEntry[] startAnnotIdx: number } const CHARS_PER_PAGE = 2000 interface Props { result: AnnotateResponse settings: Settings } export default function AnnotationResult({ result, settings }: Props) { const [copied, setCopied] = useState<'annotated' | 'replaced' | null>(null) const [toggled, setToggled] = useState>(new Set()) const [page, setPage] = useState(0) if (!result.valid || !result.annotation) { return (
Annotation failed: {result.reason ?? 'Unknown error.'}
) } const { marked, annotations } = result.annotation const sentences = parseSentences(marked, annotations) const pages = groupByChars(sentences, CHARS_PER_PAGE) const totalPages = Math.max(1, pages.length) const safePage = Math.min(page, totalPages - 1) const pageSentences = pages[safePage] ?? [] const lineHeight = settings.displayMode === 'top' ? `${settings.emojiSize + 44}px` : `${Math.max(32, settings.emojiSize * 1.4)}px` function handleCopy(type: 'annotated' | 'replaced') { const text = type === 'annotated' ? buildAnnotatedText(marked, annotations) : buildReplacedText(marked, annotations) navigator.clipboard.writeText(text).then(() => { setCopied(type) setTimeout(() => setCopied(null), 2000) }) } function handleToggle(globalIdx: number) { setToggled((prev) => { const next = new Set(prev) if (next.has(globalIdx)) next.delete(globalIdx) else next.add(globalIdx) return next }) } return (
{/* Header */}

Result

handleCopy('annotated')} /> handleCopy('replaced')} />
{/* Text */}

{pageSentences.map((sentence, si) => ( {settings.displayMode === 'top' ? renderTop(sentence, settings.emojiSize, si) : renderToggle(sentence, settings.emojiSize, toggled, handleToggle, si)} {si < pageSentences.length - 1 ? ' ' : ''} ))}

{/* Pagination — always visible */}
{safePage + 1} / {totalPages}
) } function groupByChars(sentences: Sentence[], maxChars: number): Sentence[][] { const pages: Sentence[][] = [] let current: Sentence[] = [] let count = 0 for (const s of sentences) { const len = s.text.replace(/<\/?span>/g, '').length if (current.length > 0 && count + len > maxChars) { pages.push(current) current = [s] count = len } else { current.push(s) count += len } } if (current.length > 0) pages.push(current) return pages } function parseSentences(marked: string, annotations: AnnotationEntry[]): Sentence[] { const parts = marked.split(/(?<=[.!?])\s+/) let globalAnnotIdx = 0 return parts.map((text) => { const spanCount = (text.match(//g) ?? []).length const sentence: Sentence = { text: text.trim(), annotations: annotations.slice(globalAnnotIdx, globalAnnotIdx + spanCount), startAnnotIdx: globalAnnotIdx, } globalAnnotIdx += spanCount return sentence }) } function renderTop( sentence: Sentence, emojiSize: number, prefix: number ): React.ReactNode[] { const parts = sentence.text.split(/(.*?<\/span>)/g) let localIdx = 0 return parts.map((part, i) => { const match = part.match(/^(.*?)<\/span>$/) if (match) { const word = match[1] const emoji = sentence.annotations[localIdx++]?.emojis?.trim() ?? '' // Empty emoji → render as plain, unmarked text (no highlight, no emoji slot) if (!emoji) return {word} return ( {emoji} {word} ) } return {part} }) } function renderToggle( sentence: Sentence, emojiSize: number, toggled: Set, onToggle: (globalIdx: number) => void, prefix: number ): React.ReactNode[] { const parts = sentence.text.split(/(.*?<\/span>)/g) let localIdx = 0 return parts.map((part, i) => { const match = part.match(/^(.*?)<\/span>$/) if (match) { const word = match[1] const globalIdx = sentence.startAnnotIdx + localIdx const emoji = sentence.annotations[localIdx++]?.emojis?.trim() ?? '' // Empty emoji → render as plain, unmarked text (no highlight, no toggle) if (!emoji) return {word} const isToggled = toggled.has(globalIdx) return ( ) } return {part} }) } function buildAnnotatedText(marked: string, annotations: AnnotationEntry[]): string { const parts = marked.split(/(.*?<\/span>)/g) let i = 0 return parts .map((part) => { const match = part.match(/^(.*?)<\/span>$/) if (match) { const emoji = annotations[i++]?.emojis.trim() ?? '' // Empty emoji → plain word, no empty [](...) brackets return emoji ? `[${match[1]}](${emoji})` : match[1] } return part }) .join('') } function buildReplacedText(marked: string, annotations: AnnotationEntry[]): string { const parts = marked.split(/(.*?<\/span>)/g) let i = 0 return parts .map((part) => { const match = part.match(/^(.*?)<\/span>$/) if (match) { const emoji = annotations[i++]?.emojis.trim() ?? '' // Empty emoji → keep the original word (nothing to replace it with) return emoji ? emoji : match[1] } return part }) .join('') } function CopyButton({ label, done, onClick }: { label: string; done: boolean; onClick: () => void }) { return ( ) } function CopyIcon() { return ( ) } function CheckIcon() { return ( ) }