"use client"; import React, { useMemo } from "react"; import { GranularityId } from "./LevelPicker"; import { NLP } from "sortug-ai"; type AnalysisEngine = "spacy" | "stanza"; interface Paragraph { id: string; text: string; start_char: number; end_char: number; sentences: NLP.Spacy.Sentence[]; } const segmentByParagraphs = ( inputText: string, allSentences: NLP.Spacy.Sentence[], ): Paragraph[] => { const paragraphs: Paragraph[] = []; const paraTexts = inputText.split(/\n\n+/); let currentDocCharOffset = 0; let sentenceIdx = 0; paraTexts.forEach((paraText, idx) => { const paraStartChar = currentDocCharOffset; const paraEndChar = paraStartChar + paraText.length; const paraSentences: NLP.Spacy.Sentence[] = []; while (sentenceIdx < allSentences.length) { const sent = allSentences[sentenceIdx]!; if (sent.start < paraEndChar) { paraSentences.push(sent); sentenceIdx++; } else { break; } } paragraphs.push({ id: `para-${idx}`, text: paraText, start_char: paraStartChar, end_char: paraEndChar, sentences: paraSentences, }); currentDocCharOffset = paraEndChar + (inputText.substring(paraEndChar).match(/^\n\n+/)?.[0].length || 0); }); return paragraphs; }; // --- Text Viewer --- interface TextViewerProps { nlpData: NLP.Spacy.SpacyRes; engine: AnalysisEngine; granularity: GranularityId; onElementSelect: ( elementType: GranularityId, elementData: any, fullText: string, ) => void; } const TextViewer: React.FC = ({ nlpData, engine, granularity, onElementSelect, }) => { const paragraphs = useMemo( () => segmentByParagraphs(nlpData.input, nlpData.segments), [nlpData], ); const getElementText = (element: any, fullInput: string): string => { if (element.text) return element.text; // Already has text if ("start_char" in element && "end_char" in element) { // Stanza word/token/sentence/entity return fullInput.substring(element.start_char, element.end_char); } if ("start" in element && "end" in element) { // spaCy token/sentence/entity return fullInput.substring(element.start, element.end); } return "N/A"; }; const renderInteractiveSpan = ( key: string | number, text: string, data: any, type: GranularityId, baseClasses: string = "", hoverClasses: string = "hover:bg-yellow-200", ) => ( { e.stopPropagation(); // Prevent clicks bubbling to parent elements onElementSelect(type, data, getElementText(data, nlpData.input)); }} > {text} ); return (
{granularity === "text" ? renderInteractiveSpan( "full-text", nlpData.input, nlpData, "text", "block", "hover:bg-sky-100", ) : paragraphs.map((para) => (
{ e.stopPropagation(); onElementSelect("paragraph", para, para.text); } : undefined } style={granularity === "paragraph" ? { cursor: "pointer" } : {}} > {para.sentences.map((sent, sentIdx) => { const sentenceText = getElementText(sent, nlpData.input); const sentenceKey = `sent-${para.id}-${sentIdx}`; if (granularity === "sentence" || granularity === "clause") { return renderInteractiveSpan( sentenceKey, sentenceText, sent, granularity, "mr-1 inline-block bg-gray-100 shadow-xs", "hover:bg-sky-200", ); } else if ( granularity === "word" || granularity === "syllable" || granularity === "phoneme" ) { let currentWordRenderIndex = 0; // to add spaces correctly return ( {" "} {/* Sentence wrapper */} {sent.words.map((word, idx) => { const wordText = getElementText(word, nlpData.input); const wordKey = `${sentenceKey}-tok-${idx}-word-${word}`; const space = currentWordRenderIndex > 0 ? " " : ""; currentWordRenderIndex++; return ( {space} {renderInteractiveSpan( wordKey, wordText, word, granularity, "bg-gray-50", "hover:bg-yellow-300", )} ); })} ); } // Fallback for paragraph view if no other granularity matches (should not happen if logic is correct) return ( {sentenceText} ); })}
))}
); }; export default TextViewer;