From fd86dc15734f3b7126d88f0130897c597100e30a Mon Sep 17 00:00:00 2001
From: polwex <polwex@sortug.com>
Date: Thu, 15 May 2025 20:32:25 +0700
Subject: m

---
 src/picker/App.tsx | 396 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 396 insertions(+)
 create mode 100644 src/picker/App.tsx

(limited to 'src/picker/App.tsx')

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>
+  );
+}
-- 
cgit v1.2.3

