diff options
author | polwex <polwex@sortug.com> | 2025-05-15 21:29:24 +0700 |
---|---|---|
committer | polwex <polwex@sortug.com> | 2025-05-15 21:29:24 +0700 |
commit | 4f2bd597beaa778476b84c10b571db1b13524301 (patch) | |
tree | daffaae0250d0b88b3a03d0de3821c680aeb337e | |
parent | fd86dc15734f3b7126d88f0130897c597100e30a (diff) |
m
-rw-r--r-- | src/actions/lang.ts | 15 | ||||
-rw-r--r-- | src/picker/App.tsx | 404 | ||||
-rw-r--r-- | src/picker/LevelPicker.tsx | 64 | ||||
-rw-r--r-- | src/picker/TextViewer.tsx | 194 | ||||
-rw-r--r-- | src/zoom/ServerWord.tsx | 2 | ||||
-rw-r--r-- | src/zoom/index.ts | 13 |
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, +}; |