summaryrefslogtreecommitdiff
path: root/src/picker
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-05-15 20:32:25 +0700
committerpolwex <polwex@sortug.com>2025-05-15 20:32:25 +0700
commitfd86dc15734f3b7126d88f0130897c597100e30a (patch)
tree253890a5f0bde7bc460904ce1743581f53a23d5b /src/picker
parent3d4b740e5a512db8fbdd934af2fbc9585fa00f0f (diff)
m
Diffstat (limited to 'src/picker')
-rw-r--r--src/picker/App.tsx396
-rw-r--r--src/picker/Old.tsx252
-rw-r--r--src/picker/index.ts0
3 files changed, 648 insertions, 0 deletions
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<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}`;
+
+ 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>
+ );
+};
+
+// --- Main Application Component ---
+export default function NlpTextAnalysisScreen() {
+ const [selectedGranularity, setSelectedGranularity] =
+ useState<GranularityId>("word");
+ const [currentEngine, setCurrentEngine] = useState<AnalysisEngine>("stanza");
+ 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 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 (
+ <div className="min-h-screen bg-gradient-to-br from-slate-100 to-sky-100 p-4 sm:p-8 font-sans">
+ <header className="mb-6 text-center">
+ <h1 className="text-3xl sm:text-4xl font-bold text-slate-800">
+ NLP Text Analyzer
+ </h1>
+ <button
+ onClick={toggleEngine}
+ className="mt-2 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg shadow-md transition-colors flex items-center mx-auto"
+ >
+ {currentEngine === "spacy" ? (
+ <Zap size={18} className="mr-2" />
+ ) : (
+ <Brain size={18} className="mr-2" />
+ )}
+ Switch to {currentEngine === "spacy" ? "Stanza" : "spaCy"} View
+ </button>
+ <p className="text-sm text-slate-600 mt-1">
+ Currently viewing with:{" "}
+ <span className="font-semibold">{currentEngine.toUpperCase()}</span>
+ </p>
+ </header>
+
+ <div className="flex flex-col lg:flex-row gap-6 max-w-7xl mx-auto">
+ <aside className="lg:w-72 lg:sticky lg:top-8 h-full flex flex-col gap-6">
+ <GranularityMenu
+ selectedGranularity={selectedGranularity}
+ onSelectGranularity={handleGranularityChange}
+ />
+ {selectedElementInfo && (
+ <div className="p-4 bg-white rounded-lg shadow-md text-xs text-slate-700 overflow-auto max-h-96">
+ <h3 className="font-semibold text-sky-600 mb-2 text-sm">
+ Selection Details:
+ </h3>
+ <pre className="whitespace-pre-wrap break-all">
+ {selectedElementInfo}
+ </pre>
+ </div>
+ )}
+ </aside>
+
+ <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}
+ />
+ </main>
+ </div>
+
+ <footer className="text-center mt-12 text-sm text-slate-500">
+ <p>
+ &copy; {new Date().getFullYear()} NLP Analysis Tool. Powered by React,
+ TailwindCSS, and your NLP engine of choice!
+ </p>
+ </footer>
+ </div>
+ );
+}
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<GranularityMenuProps> = ({
+ selectedGranularity,
+ onSelectGranularity,
+}) => {
+ return (
+ <nav className="w-64 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 Component
+interface TextViewerProps {
+ document: TextDocument;
+ granularity: GranularityId;
+ onElementSelect: (elementType: GranularityId, element: any) => void; // element type can be more specific
+}
+
+const TextViewer: React.FC<TextViewerProps> = ({
+ 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 (
+ <div
+ className="p-4 rounded-md bg-white shadow hover:bg-sky-50 cursor-pointer transition-colors"
+ onClick={() => handleElementClick("text", document)}
+ >
+ {document.paragraphs.map((p) => (
+ <p key={p.id} className="mb-4 leading-relaxed">
+ {p.text}
+ </p>
+ ))}
+ </div>
+ );
+ }
+
+ return document.paragraphs.map((paragraph) => (
+ <div
+ key={paragraph.id}
+ className={`p-3 mb-4 rounded-md transition-all duration-150
+ ${granularity === "paragraph" ? "bg-white shadow hover:bg-sky-100 cursor-pointer" : "bg-transparent"}`}
+ onClick={
+ granularity === "paragraph"
+ ? () => handleElementClick("paragraph", paragraph)
+ : undefined
+ }
+ >
+ {paragraph.sentences.map((sentence) => (
+ <span // Sentences are inline for paragraph flow, but can be styled as blocks if needed
+ key={sentence.id}
+ className={`mr-1 transition-all duration-150
+ ${granularity === "sentence" || granularity === "clause" ? "p-1 hover:bg-sky-200 bg-white shadow-sm rounded cursor-pointer" : ""}
+ ${granularity === "syllable" || granularity === "phoneme" ? "" : ""}
+ `}
+ onClick={
+ granularity === "sentence" || granularity === "clause"
+ ? () => handleElementClick(granularity, sentence)
+ : undefined
+ }
+ >
+ {granularity === "word" ||
+ granularity === "syllable" ||
+ granularity === "phoneme"
+ ? sentence.words
+ .map((word, wordIndex) => (
+ <span
+ key={word.id}
+ className="p-0.5 hover:bg-yellow-200 bg-white rounded cursor-pointer transition-colors"
+ onClick={() => handleElementClick(granularity, word)} // Syllable/Phoneme click conceptually targets word
+ >
+ {word.text}
+ </span>
+ ))
+ .reduce(
+ (prev, curr, idx) => (
+ <>
+ {prev}
+ {idx > 0 && " "}
+ {curr}
+ </>
+ ),
+ <></>,
+ ) // Add spaces between words
+ : sentence.text}
+ </span>
+ ))}
+ </div>
+ ));
+ };
+
+ return (
+ <div className="text-lg text-gray-800 leading-relaxed">
+ {renderContent()}
+ </div>
+ );
+};
+
+// Main Application Component
+export default function TextAnalysisScreen() {
+ const [selectedGranularity, setSelectedGranularity] =
+ useState<GranularityId>("word");
+ const [currentDocument, setCurrentDocument] =
+ useState<TextDocument>(sampleTextDocument);
+ const [selectedElementInfo, setSelectedElementInfo] = useState<string | null>(
+ 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 (
+ <div className="min-h-screen bg-gradient-to-br from-slate-100 to-sky-100 p-4 sm:p-8 font-sans">
+ <header className="mb-8 text-center">
+ <h1 className="text-3xl sm:text-4xl font-bold text-slate-800">
+ Text Analyzer
+ </h1>
+ </header>
+
+ <div className="flex flex-col lg:flex-row gap-6 max-w-7xl mx-auto">
+ {/* Sticky container for the menu */}
+ <div className="lg:w-72 lg:sticky lg:top-8 h-full">
+ {" "}
+ {/* Ensure menu is sticky on larger screens */}
+ <GranularityMenu
+ selectedGranularity={selectedGranularity}
+ onSelectGranularity={handleGranularityChange}
+ />
+ {selectedElementInfo && (
+ <div className="mt-6 p-4 bg-white rounded-lg shadow-md text-sm text-slate-700">
+ <h3 className="font-semibold text-sky-600 mb-2">
+ Selection Details:
+ </h3>
+ <pre className="whitespace-pre-wrap break-all">
+ {selectedElementInfo}
+ </pre>
+ </div>
+ )}
+ </div>
+
+ <main className="flex-1 bg-slate-50 p-4 sm:p-6 rounded-xl shadow-xl min-w-0">
+ {" "}
+ {/* min-w-0 for flex child */}
+ <TextViewer
+ document={currentDocument}
+ granularity={selectedGranularity}
+ onElementSelect={handleElementSelect}
+ />
+ </main>
+ </div>
+
+ <footer className="text-center mt-12 text-sm text-slate-500">
+ <p>
+ &copy; {new Date().getFullYear()} Advanced Text Analysis Tool. All
+ rights reserved.
+ </p>
+ </footer>
+ </div>
+ );
+}
diff --git a/src/picker/index.ts b/src/picker/index.ts
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/picker/index.ts