diff options
Diffstat (limited to 'src/picker/App.tsx')
-rw-r--r-- | src/picker/App.tsx | 404 |
1 files changed, 100 insertions, 304 deletions
diff --git a/src/picker/App.tsx b/src/picker/App.tsx index a17a006..a3e4f43 100644 --- a/src/picker/App.tsx +++ b/src/picker/App.tsx @@ -1,331 +1,118 @@ // "use client"; -import React, { useState, useCallback, useMemo, useEffect } from "react"; -import { - TextSelect, - Combine, - WholeWord, - Highlighter, - Atom, - Mic2, - CheckCircle2, - ExternalLink, - Brain, - Zap, -} from "lucide-react"; +import React, { + type ReactNode, + useState, + useCallback, + useMemo, + useEffect, + startTransition, + useTransition, +} from "react"; +import { Brain, Zap, Loader2 } from "lucide-react"; import { NLP } from "sortug-ai"; +import GranularityMenu, { GranularityId } from "./LevelPicker"; +import TextViewer from "./TextViewer"; +import { wordAction } from "@/actions/lang"; +import Modal from "@/components/Modal"; // --- Granularity Definition --- -const GRANULARITY_LEVELS = [ - { id: "text", name: "Text", icon: TextSelect }, - { id: "paragraph", name: "Paragraph", icon: Combine }, - { id: "sentence", name: "Sentence", icon: Highlighter }, - { id: "clause", name: "Clause (Sentence Lvl)", icon: Highlighter }, - { id: "word", name: "Word/Token", icon: WholeWord }, - { id: "syllable", name: "Syllable (Word Lvl)", icon: Mic2 }, - { id: "phoneme", name: "Phoneme (Word Lvl)", icon: Atom }, -] as const; -type GranularityId = (typeof GRANULARITY_LEVELS)[number]["id"]; type AnalysisEngine = "spacy" | "stanza"; -// --- Sample Data (Simplified) --- - -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; -}; - -// --- Granularity Menu --- -interface GranularityMenuProps { - selectedGranularity: GranularityId; - onSelectGranularity: (granularity: GranularityId) => void; -} -const GranularityMenu: React.FC<GranularityMenuProps> = ({ - selectedGranularity, - onSelectGranularity, -}) => ( - <nav className="w-full bg-slate-800 text-slate-100 p-4 space-y-2 rounded-lg shadow-lg"> - <h2 className="text-lg font-semibold text-sky-400 mb-4"> - Granularity Level - </h2> - {GRANULARITY_LEVELS.map((level) => { - const Icon = level.icon; - const isSelected = selectedGranularity === level.id; - return ( - <button - key={level.id} - onClick={() => onSelectGranularity(level.id)} - className={`w-full flex items-center space-x-3 p-3 rounded-md text-left transition-all duration-150 ease-in-out - ${isSelected ? "bg-sky-500 text-white shadow-md scale-105" : "hover:bg-slate-700 hover:text-sky-300"}`} - > - <Icon - size={20} - className={`${isSelected ? "text-white" : "text-sky-400"}`} - /> - <span>{level.name}</span> - {isSelected && ( - <CheckCircle2 size={18} className="ml-auto text-white" /> - )} - </button> - ); - })} - </nav> -); - -// --- Text Viewer --- -interface TextViewerProps { - nlpData: NLP.Spacy.SpacyRes; - engine: AnalysisEngine; - granularity: GranularityId; - onElementSelect: ( - elementType: GranularityId, - elementData: any, - fullText: string, - ) => void; -} - -const TextViewer: React.FC<TextViewerProps> = ({ - 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", - ) => ( - <span - key={key} - className={`cursor-pointer transition-colors duration-150 ${baseClasses} ${hoverClasses} p-0.5 rounded`} - onClick={(e) => { - e.stopPropagation(); // Prevent clicks bubbling to parent elements - onElementSelect(type, data, getElementText(data, nlpData.input)); - }} - > - {text} - </span> - ); - - return ( - <div className="text-lg text-gray-800 leading-relaxed bg-white p-4 sm:p-6 rounded-xl shadow-inner"> - {granularity === "text" - ? renderInteractiveSpan( - "full-text", - nlpData.input, - nlpData, - "text", - "block", - "hover:bg-sky-100", - ) - : paragraphs.map((para) => ( - <div - key={para.id} - className={`mb-4 ${granularity === "paragraph" ? "p-2 rounded-md shadow-sm bg-gray-50" : ""}`} - onClick={ - granularity === "paragraph" - ? (e) => { - 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}`; +// --- Main Application Component --- +export default function NlpTextAnalysisScreen({ + children, +}: { + children: ReactNode; +}) { + const [modalContent, setModalContent] = useState<ReactNode | null>(null); - 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 ( - <span key={sentenceKey} className="mr-1"> - {" "} - {/* 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 ( - <React.Fragment key={wordKey}> - {space} - {renderInteractiveSpan( - wordKey, - wordText, - word, - granularity, - "bg-gray-50", - "hover:bg-yellow-300", - )} - </React.Fragment> - ); - })} - </span> - ); - } - // Fallback for paragraph view if no other granularity matches (should not happen if logic is correct) - return ( - <span key={sentenceKey} className="mr-1"> - {sentenceText} - </span> - ); - })} - </div> - ))} - </div> - ); -}; + const closeModal = () => setModalContent(null); -// --- Main Application Component --- -export default function NlpTextAnalysisScreen() { const [selectedGranularity, setSelectedGranularity] = useState<GranularityId>("word"); - const [currentEngine, setCurrentEngine] = useState<AnalysisEngine>("stanza"); + const [currentEngine, setCurrentEngine] = useState<AnalysisEngine>("spacy"); const [selectedElementInfo, setSelectedElementInfo] = useState<string | null>( null, ); const [activeNlpData, setData] = useState<NLP.Spacy.SpacyRes>(); useEffect(() => { - // const nlpdata = sessionStorage.getItem( - // currentEngine === "spacy" ? "spacyres" : "stanzares", - // ); - // const activeNlpData = JSON.parse(nlpdata!); + const nlpdata = sessionStorage.getItem( + currentEngine === "spacy" ? "spacyres" : "stanzares", + ); + const parsed = JSON.parse(nlpdata!); + setData(parsed); }, []); const handleGranularityChange = useCallback((granularity: GranularityId) => { setSelectedGranularity(granularity); setSelectedElementInfo(null); }, []); - + const [isPending, startTransition] = useTransition(); const handleElementSelect = useCallback( (elementType: GranularityId, elementData: any, elementText: string) => { - let info = `Selected: ${elementType.toUpperCase()} (${currentEngine})\n`; - info += `Text: "${elementText}"\n`; - - if (elementType === "syllable" || elementType === "phoneme") { - info += `(Granularity: ${elementType}, showing parent Word/Token details)\n`; - } - - // Add specific details based on element type and engine if (elementType === "word") { - if (currentEngine === "stanza" && elementData.lemma) { - // StanzaWord - info += `Lemma: ${elementData.lemma}\nUPOS: ${elementData.upos}\nXPOS: ${elementData.xpos}\nDepRel: ${elementData.deprel} (Head ID: ${elementData.head})\n`; - if (elementData.parentToken?.ner) - info += `NER (Token): ${elementData.parentToken.ner}\n`; - } else if (currentEngine === "spacy" && elementData.lemma_) { - // SpacyToken - info += `Lemma: ${elementData.lemma_}\nPOS: ${elementData.pos_}\nTag: ${elementData.tag_}\nDep: ${elementData.dep_} (Head ID: ${elementData.head?.i})\n`; - if (elementData.ent_type_) - info += `Entity: ${elementData.ent_type_} (${elementData.ent_iob_})\n`; - } - } else if (elementType === "sentence") { - if ( - currentEngine === "stanza" && - (elementData as NLP.Stanza.Sentence).sentiment - ) { - info += `Sentiment: ${(elementData as NLP.Stanza.Sentence).sentiment}\n`; - } - if ( - (elementData as NLP.Stanza.Sentence | NLP.Spacy.Sentence).entities - ?.length - ) { - info += `Entities in sentence: ${(elementData.entities as any[]).map((e) => `${e.text} (${e.type || e.label_})`).join(", ")}\n`; - } - } else if (elementType === "paragraph") { - info += `Char range: ${elementData.start_char}-${elementData.end_char}\n`; - info += `Sentence count: ${elementData.sentences.length}\n`; + startTransition(async () => { + const modal = await wordAction(elementData.text, "en"); + setModalContent(modal); + }); } - - info += `Raw Data Keys: ${Object.keys(elementData).slice(0, 5).join(", ")}...`; // Show some keys - setSelectedElementInfo(info); - console.log( - "Selected Element:", - elementType, - elementData, - "Text:", - elementText, - ); }, [currentEngine], ); + // const handleElementSelect = useCallback( + // (elementType: GranularityId, elementData: any, elementText: string) => { + // let info = `Selected: ${elementType.toUpperCase()} (${currentEngine})\n`; + // info += `Text: "${elementText}"\n`; + + // if (elementType === "syllable" || elementType === "phoneme") { + // info += `(Granularity: ${elementType}, showing parent Word/Token details)\n`; + // } + + // // Add specific details based on element type and engine + // if (elementType === "word") { + // if (currentEngine === "stanza" && elementData.lemma) { + // // StanzaWord + // info += `Lemma: ${elementData.lemma}\nUPOS: ${elementData.upos}\nXPOS: ${elementData.xpos}\nDepRel: ${elementData.deprel} (Head ID: ${elementData.head})\n`; + // if (elementData.parentToken?.ner) + // info += `NER (Token): ${elementData.parentToken.ner}\n`; + // } else if (currentEngine === "spacy" && elementData.lemma_) { + // // SpacyToken + // info += `Lemma: ${elementData.lemma_}\nPOS: ${elementData.pos_}\nTag: ${elementData.tag_}\nDep: ${elementData.dep_} (Head ID: ${elementData.head?.i})\n`; + // if (elementData.ent_type_) + // info += `Entity: ${elementData.ent_type_} (${elementData.ent_iob_})\n`; + // } + // } else if (elementType === "sentence") { + // if ( + // currentEngine === "stanza" && + // (elementData as NLP.Stanza.Sentence).sentiment + // ) { + // info += `Sentiment: ${(elementData as NLP.Stanza.Sentence).sentiment}\n`; + // } + // if ( + // (elementData as NLP.Stanza.Sentence | NLP.Spacy.Sentence).entities + // ?.length + // ) { + // info += `Entities in sentence: ${(elementData.entities as any[]).map((e) => `${e.text} (${e.type || e.label_})`).join(", ")}\n`; + // } + // } else if (elementType === "paragraph") { + // info += `Char range: ${elementData.start_char}-${elementData.end_char}\n`; + // info += `Sentence count: ${elementData.sentences.length}\n`; + // } + + // info += `Raw Data Keys: ${Object.keys(elementData).slice(0, 5).join(", ")}...`; // Show some keys + // setSelectedElementInfo(info); + // console.log( + // "Selected Element:", + // elementType, + // elementData, + // "Text:", + // elementText, + // ); + // }, + // [currentEngine], + // ); const toggleEngine = () => { setCurrentEngine((prev) => (prev === "spacy" ? "stanza" : "spacy")); @@ -376,12 +163,16 @@ export default function NlpTextAnalysisScreen() { <main className="flex-1 min-w-0"> {" "} {/* min-w-0 for flex child to prevent overflow */} - <TextViewer - nlpData={activeNlpData} - engine={currentEngine} - granularity={selectedGranularity} - onElementSelect={handleElementSelect} - /> + {activeNlpData ? ( + <TextViewer + nlpData={activeNlpData} + engine={currentEngine} + granularity={selectedGranularity} + onElementSelect={handleElementSelect} + /> + ) : ( + <Loader2 /> + )} </main> </div> @@ -391,6 +182,11 @@ export default function NlpTextAnalysisScreen() { TailwindCSS, and your NLP engine of choice! </p> </footer> + {modalContent && ( + <Modal onClose={closeModal} isOpen={!!modalContent}> + {modalContent} + </Modal> + )} </div> ); } |