From fd86dc15734f3b7126d88f0130897c597100e30a Mon Sep 17 00:00:00 2001 From: polwex Date: Thu, 15 May 2025 20:32:25 +0700 Subject: m --- src/picker/App.tsx | 396 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/picker/Old.tsx | 252 +++++++++++++++++++++++++++++++++ src/picker/index.ts | 0 3 files changed, 648 insertions(+) create mode 100644 src/picker/App.tsx create mode 100644 src/picker/Old.tsx create mode 100644 src/picker/index.ts (limited to 'src/picker') diff --git a/src/picker/App.tsx b/src/picker/App.tsx new file mode 100644 index 0000000..a17a006 --- /dev/null +++ b/src/picker/App.tsx @@ -0,0 +1,396 @@ +// +"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 { NLP } from "sortug-ai"; + +// --- 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 = ({ + selectedGranularity, + onSelectGranularity, +}) => ( + +); + +// --- 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} + + ); + })} +
+ ))} +
+ ); +}; + +// --- Main Application Component --- +export default function NlpTextAnalysisScreen() { + const [selectedGranularity, setSelectedGranularity] = + useState("word"); + const [currentEngine, setCurrentEngine] = useState("stanza"); + const [selectedElementInfo, setSelectedElementInfo] = useState( + null, + ); + const [activeNlpData, setData] = useState(); + useEffect(() => { + // const nlpdata = sessionStorage.getItem( + // currentEngine === "spacy" ? "spacyres" : "stanzares", + // ); + // const activeNlpData = JSON.parse(nlpdata!); + }, []); + + const handleGranularityChange = useCallback((granularity: GranularityId) => { + setSelectedGranularity(granularity); + setSelectedElementInfo(null); + }, []); + + 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")); + setSelectedElementInfo(null); + }; + + return ( +
+
+

+ NLP Text Analyzer +

+ +

+ Currently viewing with:{" "} + {currentEngine.toUpperCase()} +

+
+ +
+ + +
+ {" "} + {/* min-w-0 for flex child to prevent overflow */} + +
+
+ +
+

+ © {new Date().getFullYear()} NLP Analysis Tool. Powered by React, + TailwindCSS, and your NLP engine of choice! +

+
+
+ ); +} diff --git a/src/picker/Old.tsx b/src/picker/Old.tsx new file mode 100644 index 0000000..f02e538 --- /dev/null +++ b/src/picker/Old.tsx @@ -0,0 +1,252 @@ +import React, { useState, useMemo, useCallback } from "react"; +import { + TextSelect, + Combine, + WholeWord, + Highlighter, + Atom, + Mic2, + ChevronRight, + CheckCircle2, +} from "lucide-react"; + +// Define granularity levels +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", icon: Highlighter }, // Simplified + { id: "word", name: "Word", icon: WholeWord }, + { id: "syllable", name: "Syllable", icon: Mic2 }, // Conceptual + { id: "phoneme", name: "Phoneme", icon: Atom }, // Conceptual +] as const; + +type GranularityId = (typeof GRANULARITY_LEVELS)[number]["id"]; + +// Granularity Menu Component +interface GranularityMenuProps { + selectedGranularity: GranularityId; + onSelectGranularity: (granularity: GranularityId) => void; +} + +const GranularityMenu: React.FC = ({ + selectedGranularity, + onSelectGranularity, +}) => { + return ( + + ); +}; + +// Text Viewer Component +interface TextViewerProps { + document: TextDocument; + granularity: GranularityId; + onElementSelect: (elementType: GranularityId, element: any) => void; // element type can be more specific +} + +const TextViewer: React.FC = ({ + document, + granularity, + onElementSelect, +}) => { + const handleElementClick = (type: GranularityId, data: any) => { + // For syllable/phoneme, pass the parent word data for now + if ((type === "syllable" || type === "phoneme") && data.type === "word") { + onElementSelect(type, { ...data, originalClickType: type }); + } else { + onElementSelect(type, data); + } + }; + + const renderContent = () => { + if (granularity === "text") { + return ( +
handleElementClick("text", document)} + > + {document.paragraphs.map((p) => ( +

+ {p.text} +

+ ))} +
+ ); + } + + return document.paragraphs.map((paragraph) => ( +
handleElementClick("paragraph", paragraph) + : undefined + } + > + {paragraph.sentences.map((sentence) => ( + handleElementClick(granularity, sentence) + : undefined + } + > + {granularity === "word" || + granularity === "syllable" || + granularity === "phoneme" + ? sentence.words + .map((word, wordIndex) => ( + handleElementClick(granularity, word)} // Syllable/Phoneme click conceptually targets word + > + {word.text} + + )) + .reduce( + (prev, curr, idx) => ( + <> + {prev} + {idx > 0 && " "} + {curr} + + ), + <>, + ) // Add spaces between words + : sentence.text} + + ))} +
+ )); + }; + + return ( +
+ {renderContent()} +
+ ); +}; + +// Main Application Component +export default function TextAnalysisScreen() { + const [selectedGranularity, setSelectedGranularity] = + useState("word"); + const [currentDocument, setCurrentDocument] = + useState(sampleTextDocument); + const [selectedElementInfo, setSelectedElementInfo] = useState( + null, + ); + + const handleGranularityChange = useCallback((granularity: GranularityId) => { + setSelectedGranularity(granularity); + setSelectedElementInfo(null); // Clear selection when granularity changes + }, []); + + const handleElementSelect = useCallback( + (elementType: GranularityId, elementData: any) => { + let info = `Selected: ${elementType.toUpperCase()}\n`; + if (elementData.text) { + info += `Text: "${elementData.text.substring(0, 100)}${elementData.text.length > 100 ? "..." : ""}"\n`; + } + info += `ID: ${elementData.id}`; + if ( + elementData.originalClickType && + elementData.originalClickType !== elementType + ) { + info += `\n(Clicked as ${elementData.originalClickType}, showing parent Word)`; + } + setSelectedElementInfo(info); + // Here you would typically navigate to a detail view or open a modal + // For example: router.push(`/details/${elementType}/${elementData.id}`); + console.log("Selected Element:", elementType, elementData); + }, + [], + ); + + return ( +
+
+

+ Text Analyzer +

+
+ +
+ {/* Sticky container for the menu */} +
+ {" "} + {/* Ensure menu is sticky on larger screens */} + + {selectedElementInfo && ( +
+

+ Selection Details: +

+
+                {selectedElementInfo}
+              
+
+ )} +
+ +
+ {" "} + {/* min-w-0 for flex child */} + +
+
+ + +
+ ); +} diff --git a/src/picker/index.ts b/src/picker/index.ts new file mode 100644 index 0000000..e69de29 -- cgit v1.2.3