summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-05-15 21:29:24 +0700
committerpolwex <polwex@sortug.com>2025-05-15 21:29:24 +0700
commit4f2bd597beaa778476b84c10b571db1b13524301 (patch)
treedaffaae0250d0b88b3a03d0de3821c680aeb337e
parentfd86dc15734f3b7126d88f0130897c597100e30a (diff)
m
-rw-r--r--src/actions/lang.ts15
-rw-r--r--src/picker/App.tsx404
-rw-r--r--src/picker/LevelPicker.tsx64
-rw-r--r--src/picker/TextViewer.tsx194
-rw-r--r--src/zoom/ServerWord.tsx2
-rw-r--r--src/zoom/index.ts13
6 files changed, 379 insertions, 313 deletions
diff --git a/src/actions/lang.ts b/src/actions/lang.ts
index 5242c7a..fc798da 100644
--- a/src/actions/lang.ts
+++ b/src/actions/lang.ts
@@ -1,15 +1,16 @@
"use server";
import { AsyncRes } from "@/lib/types";
import { NLP } from "sortug-ai";
+import ServerWord from "@/zoom/ServerWord";
// import db from "../lib/db";
-// export async function textAction(
-// text: string,
-// lang: string,
-// ): AsyncRes<NLP.Spacy.SpacyRes> {
-// const res = await NLP.Spacy.run(text, lang);
-// return res;
-// }
+export async function wordAction(
+ text: string,
+ lang: string,
+): Promise<React.ReactNode> {
+ console.log("");
+ return ServerWord({ word: text, lang });
+}
// export async function ocrAction(file: File): AsyncRes<string[]> {
// const res = await NLP.ocr(file);
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>
);
}
diff --git a/src/picker/LevelPicker.tsx b/src/picker/LevelPicker.tsx
new file mode 100644
index 0000000..037febf
--- /dev/null
+++ b/src/picker/LevelPicker.tsx
@@ -0,0 +1,64 @@
+//
+"use client";
+
+import React from "react";
+import {
+ TextSelect,
+ Combine,
+ WholeWord,
+ Highlighter,
+ Atom,
+ Mic2,
+ CheckCircle2,
+} from "lucide-react";
+
+// --- Granularity Definition ---
+export 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;
+export type GranularityId = (typeof GRANULARITY_LEVELS)[number]["id"];
+
+// --- 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>
+);
+
+export default GranularityMenu;
diff --git a/src/picker/TextViewer.tsx b/src/picker/TextViewer.tsx
new file mode 100644
index 0000000..0b9115c
--- /dev/null
+++ b/src/picker/TextViewer.tsx
@@ -0,0 +1,194 @@
+"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<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>
+ );
+};
+
+export default TextViewer;
diff --git a/src/zoom/ServerWord.tsx b/src/zoom/ServerWord.tsx
index 26902f5..d98e54b 100644
--- a/src/zoom/ServerWord.tsx
+++ b/src/zoom/ServerWord.tsx
@@ -39,7 +39,7 @@ export default async function Wordd({
lang: string;
}) {
const data = db.fetchWordBySpelling(word, "en");
- console.log({ data });
+ console.log({ data, word });
if (!data) return <p>oh...</p>;
return (
diff --git a/src/zoom/index.ts b/src/zoom/index.ts
index 4ce07ca..bcda5a6 100644
--- a/src/zoom/index.ts
+++ b/src/zoom/index.ts
@@ -4,6 +4,17 @@ import FullText from "./FullText";
import Paragraph from "./Paragraph";
import Sentence from "./Paragraph";
import SpacyClause from "./SpacyClause";
+import Word from "./Word";
+import ServerWord from "./ServerWord";
import type * as Types from "./logic/types";
-export { App, Paragraph, FullText, Sentence, SpacyClause, Types };
+export {
+ App,
+ Paragraph,
+ FullText,
+ Sentence,
+ SpacyClause,
+ Types,
+ Word,
+ ServerWord,
+};