summaryrefslogtreecommitdiff
path: root/packages/prosody-ui/src/components/word
diff options
context:
space:
mode:
Diffstat (limited to 'packages/prosody-ui/src/components/word')
-rw-r--r--packages/prosody-ui/src/components/word/FullWordData.tsx156
-rw-r--r--packages/prosody-ui/src/components/word/Phonetic.tsx92
-rw-r--r--packages/prosody-ui/src/components/word/Semantic.tsx184
3 files changed, 432 insertions, 0 deletions
diff --git a/packages/prosody-ui/src/components/word/FullWordData.tsx b/packages/prosody-ui/src/components/word/FullWordData.tsx
new file mode 100644
index 0000000..9b1fc69
--- /dev/null
+++ b/packages/prosody-ui/src/components/word/FullWordData.tsx
@@ -0,0 +1,156 @@
+import React, { useCallback, useEffect, useState } from "react";
+import spinner from "../assets/icons/spinner.svg";
+import likeIcon from "../assets/icons/heart.svg";
+import commentsIcon from "../assets/icons/quote.svg";
+import shareIcon from "../assets/icons/share.svg";
+import fontIcon from "../assets/icons/font.svg";
+import bookmarkIcon from "@/assets/icons/bookmark.svg";
+import speakerIcon from "@/assets/icons/speaker.svg";
+import type { AnalyzeRes, ColorTheme, Meaning } from "@/logic/types";
+import { ColoredText } from "../Sentence.tsx";
+import { P, Span, useSpeechSynthesis } from "@/hooks/useLang.tsx";
+import type { FullWordData } from "@sortug/langlib";
+import { cycleNext } from "@sortug/lib";
+import FontChanger from "@/fonts/FontChanger.tsx";
+import Phonetic from "./Phonetic.tsx";
+
+function Word({
+ data,
+ lang,
+ theme,
+}: {
+ data: FullWordData;
+ lang: string;
+ theme: ColorTheme;
+}) {
+ async function load() {
+ // const wiki = await fetchWiki(data.word);
+ // console.log(wiki, "wiki res");
+ // if ("ok" in wiki) setM(wiki.ok.meanings);
+ // else setError(wiki.error);
+ // setLoading(false);
+ }
+ useEffect(() => {
+ load();
+ }, []);
+ const [error, setError] = useState("");
+ const [loading, setLoading] = useState(false);
+ const [meanings, setM] = useState<Meaning[]>([]);
+ const [fontIdx, setFont] = useState(0);
+
+ const { voices, speaking, speak, stop } = useSpeechSynthesis();
+ function playAudio() {
+ console.log({ voices, speaking });
+ console.log("word", data);
+ speak(data.spelling);
+ }
+ console.log({ data });
+
+ async function saveW() {}
+
+ return (
+ <div id="word-modal" title={data.spelling}>
+ <FontChanger text={data.spelling}>
+ <img className="save-icon cp" onClick={saveW} src={bookmarkIcon} />
+ <div className="original">
+ <ColoredText
+ frags={data.phonetic.syllables.map((s) => ({
+ data: s,
+ display: s.spelling,
+ colorBy: s.tone.name,
+ }))}
+ theme={theme}
+ />
+ </div>
+ <Phonetic data={data} lang={lang} theme={theme} />
+ <div className="pronunciation IPA flex1 flex-center">
+ <P>{data.phonetic.ipa}</P>
+ <img onClick={playAudio} className="icon cp" src={speakerIcon} />
+ </div>
+ <div className="meanings">
+ {loading ? (
+ <img src={spinner} className="spinner bc" />
+ ) : (
+ data.senses.map((m) => (
+ <div key={JSON.stringify(m)} className="meaning">
+ <div className="pos">
+ <Span>{m.pos}</Span>
+ </div>
+ <ol>
+ {m.glosses.map((t, i) => (
+ <li key={t + i} className="translation">
+ <P>{t}</P>
+ </li>
+ ))}
+ </ol>
+ </div>
+ ))
+ )}
+ {error && <div className="error">{error}</div>}
+ </div>
+ </FontChanger>
+ </div>
+ );
+}
+
+export default Word;
+
+<Card className="absolute inset-0 backface-hidden rotate-y-180 flex flex-col overflow-hidden border-slate-200 dark:border-slate-800 shadow-lg bg-slate-50/50">
+ <div className="flex-1 overflow-hidden flex flex-col">
+ <Tabs defaultValue="meanings" className="flex-1 flex flex-col">
+ <div className="px-6 pt-6 pb-2 bg-white border-b">
+ <TabsList className="grid w-full grid-cols-3">
+ <TabsTrigger value="meanings">Meanings</TabsTrigger>
+ <TabsTrigger value="grammar">Grammar</TabsTrigger>
+ <TabsTrigger value="examples">Examples</TabsTrigger>
+ </TabsList>
+ </div>
+
+ <div className="flex-1 overflow-y-auto p-6">
+ <TabsContent value="meanings" className="mt-0 space-y-4">
+ <EnhancedWordMeanings word={word} />
+ </TabsContent>
+
+ <TabsContent value="grammar" className="mt-0 space-y-6">
+ <div className="space-y-4">
+ <div className="bg-blue-50 p-4 rounded-lg border border-blue-100">
+ <h3 className="font-semibold text-blue-900 mb-2">
+ Tone Analysis
+ </h3>
+ <div className="flex flex-wrap gap-2">
+ {tones.map((tone, idx) => (
+ <Badge key={idx} variant="outline" className="bg-white">
+ Syl {idx + 1}:{" "}
+ <span
+ className={cn("ml-1 font-bold", getColorByTone(tone))}
+ >
+ {tone}
+ </span>
+ </Badge>
+ ))}
+ </div>
+ </div>
+
+ <div className="bg-slate-100 p-4 rounded-lg border border-slate-200">
+ <h3 className="font-semibold text-slate-900 mb-2">
+ Word Structure
+ </h3>
+ <p className="text-sm text-slate-600">
+ This word consists of {syls.length} syllable
+ {syls.length > 1 ? "s" : ""}. The tone pattern is essential for
+ conveying the correct meaning.
+ </p>
+ </div>
+ </div>
+ </TabsContent>
+
+ <TabsContent value="examples" className="mt-0 space-y-4">
+ <ExamplesTab
+ word={word}
+ moreExamples={word.senses?.flatMap((s) => s.examples || [])}
+ />
+ </TabsContent>
+ </div>
+ </Tabs>
+ </div>
+</Card>;
diff --git a/packages/prosody-ui/src/components/word/Phonetic.tsx b/packages/prosody-ui/src/components/word/Phonetic.tsx
new file mode 100644
index 0000000..db3d0cb
--- /dev/null
+++ b/packages/prosody-ui/src/components/word/Phonetic.tsx
@@ -0,0 +1,92 @@
+import React, { useCallback, useEffect, useState } from "react";
+import spinner from "../assets/icons/spinner.svg";
+import likeIcon from "../assets/icons/heart.svg";
+import commentsIcon from "../assets/icons/quote.svg";
+import shareIcon from "../assets/icons/share.svg";
+import fontIcon from "../assets/icons/font.svg";
+import bookmarkIcon from "../assets/icons/bookmark.svg";
+import type { AnalyzeRes, ColorTheme, Meaning } from "@/logic/types";
+import { P, Span, useSpeechSynthesis } from "@/hooks/useLang.tsx";
+import type { FullWordData, Syllable, Tone } from "@sortug/langlib";
+import { cycleNext } from "@sortug/lib";
+import FontChanger from "../fonts/FontChanger.tsx";
+import { assignColors } from "../Colors.tsx";
+import { IconBadgeFilled, IconSpeakerphone } from "@tabler/icons-react";
+
+function Phonetic({
+ data,
+ lang,
+ theme,
+}: {
+ data: FullWordData;
+ lang: string;
+ theme: ColorTheme;
+}) {
+ async function load() {
+ // const wiki = await fetchWiki(data.word);
+ // console.log(wiki, "wiki res");
+ // if ("ok" in wiki) setM(wiki.ok.meanings);
+ // else setError(wiki.error);
+ // setLoading(false);
+ }
+ useEffect(() => {
+ load();
+ }, []);
+ const [loading, setLoading] = useState(false);
+
+ const { voices, speaking, speak, stop } = useSpeechSynthesis();
+ function playAudio() {
+ setLoading(true);
+ console.log({ voices, speaking });
+ console.log("word", data);
+ speak(data.spelling);
+ setLoading(false);
+ }
+ console.log({ data });
+
+ async function saveW() {}
+
+ return (
+ <div className="phonetic-data">
+ <div className="pronunciation IPA flex1 flex-center">
+ <P>{data.phonetic.ipa}</P>
+ {loading ? (
+ <img src={spinner} className="spinner bc" />
+ ) : (
+ <IconSpeakerphone onClick={playAudio} />
+ )}
+ </div>
+ <Syllables data={data} />
+ </div>
+ );
+}
+
+export default Phonetic;
+
+function Syllables({ data }: { data: FullWordData }) {
+ const syllables = data.phonetic.syllables;
+
+ console.log(data.phonetic.tone_sequence);
+ const isTonal = !!data.phonetic.tone_sequence;
+ const colorMap = isTonal
+ ? (s: Syllable) => s.tone.name
+ : (s: Syllable) => (s.stressed ? "stressed" : "neuter");
+ const colors = assignColors(syllables.map(colorMap));
+ return (
+ <div className="syllables">
+ {data.phonetic.syllables.map((syl) => (
+ <div className="syllable">
+ {syl.tone.letters && <Tone tone={syl.tone} />}
+ <span>{syl.spelling}</span>
+ </div>
+ ))}
+ </div>
+ );
+}
+function Tone({ tone }: { tone: Tone }) {
+ return (
+ <div className="tone">
+ <IconBadgeFilled>{tone.letters}</IconBadgeFilled>
+ </div>
+ );
+}
diff --git a/packages/prosody-ui/src/components/word/Semantic.tsx b/packages/prosody-ui/src/components/word/Semantic.tsx
new file mode 100644
index 0000000..059194c
--- /dev/null
+++ b/packages/prosody-ui/src/components/word/Semantic.tsx
@@ -0,0 +1,184 @@
+import { useEffect, useState } from "react";
+import type { Example, FullWordData } from "@sortug/langlib";
+import { IconBadgeFilled, IconSparkles } from "@tabler/icons-react";
+
+type Tab = "meanings" | "grammar" | "examples";
+function Semantic({ data }: { data: FullWordData }) {
+ return (
+ <div className="">
+ <div className="flex-col">
+ <div className="tab-container">
+ {data.senses.map((sense, i) => (
+ <div>
+ <div key={data.spelling + sense.etymology + i} className="">
+ {sense.pos && <div className="">{sense.pos}</div>}
+
+ <ul className="">
+ {sense.glosses.map((gloss, idx: number) => (
+ <li key={idx} className="text-gray-700">
+ {gloss}
+ </li>
+ ))}
+ </ul>
+
+ {sense.etymology && (
+ <div className="">
+ <strong>Etymology:</strong> {sense.etymology}
+ </div>
+ )}
+
+ {sense.categories.length > 0 && (
+ <div className="">
+ <strong>Categories:</strong> {sense.categories.join(", ")}
+ </div>
+ )}
+ {sense.derivation.length > 0 && (
+ <div className="">
+ <strong>Derived forms:</strong>
+ {sense.derivation.map((dr, i) => (
+ <div key={dr.text + i}>
+ {dr.type}: {dr.text} - {dr.tags}
+ </div>
+ ))}
+ </div>
+ )}
+ {sense.examples.length > 0 && (
+ <Examples data={data} examples={sense.examples} />
+ )}
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ </div>
+ );
+}
+
+export default Semantic;
+
+function ExamplesTab({
+ data,
+ examples,
+}: {
+ data: FullWordData;
+ examples: Example[];
+}) {
+ const [isGenerating, setIsGenerating] = useState(false);
+ const [generatedExamples, setGeneratedExamples] = useState<any[]>([]);
+
+ const generateExamples = async () => {
+ setIsGenerating(true);
+
+ try {
+ // Get the primary meaning from the first sense
+ const primaryMeaning =
+ data.senses?.[0]?.glosses?.[0] || "unknown meaning";
+
+ const response = await fetch("/api/generate-examples", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ word: data.spelling,
+ meaning: primaryMeaning,
+ examples,
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error("Failed to generate examples");
+ }
+
+ const j = await response.json();
+ setGeneratedExamples(j.examples || []);
+ } catch (err) {
+ console.error("Error generating examples:", err);
+ } finally {
+ setIsGenerating(false);
+ }
+ };
+ return (
+ <div className="">
+ <div className="">
+ <h4 className="">Usage Examples</h4>
+
+ {/* Generate More Button */}
+ <div className="">
+ <button
+ onClick={generateExamples}
+ disabled={isGenerating}
+ className=""
+ >
+ {isGenerating ? (
+ <>
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white" />
+ Generating Examples...
+ </>
+ ) : (
+ <>
+ <IconSparkles size={16} />
+ Generate More Example Sentences
+ </>
+ )}
+ </button>
+ </div>
+
+ {/* Examples Display */}
+ <div className="">
+ {examples.map((example, idx) => (
+ <div
+ key={`original-${idx}`}
+ className="p-3 bg-white rounded border-l-4 border-blue-400"
+ >
+ <p className="text-sm text-gray-700 italic">
+ {example?.text || ""}
+ </p>
+ {example.ref && (
+ <p className="text-xs text-gray-500 mt-1">
+ Source: {example.ref}
+ </p>
+ )}
+ </div>
+ ))}
+
+ {generatedExamples.length > 0 && (
+ <>
+ <h5 className="text-sm font-medium text-gray-700 mb-2 mt-4">
+ AI-Generated Examples:
+ </h5>
+ {generatedExamples.map((example, idx) => (
+ <div
+ key={`generated-${idx}`}
+ className="p-3 bg-white rounded border-l-4 border-green-400"
+ >
+ <p className="text-sm text-gray-800 font-medium mb-1">
+ {example.thai}
+ </p>
+ <p className="text-sm text-gray-600 mb-1">
+ {example.english}
+ </p>
+ {example.context && (
+ <p className="text-xs text-gray-500 italic">
+ Context: {example.context}
+ </p>
+ )}
+ </div>
+ ))}
+ </>
+ )}
+
+ {/* No Examples */}
+ {!moreExamples?.length && !generatedExamples.length && (
+ <div className="p-3 bg-white rounded border-l-4 border-orange-400">
+ <p className="text-sm text-gray-600 italic">
+ No examples available for this word. Click "Generate More
+ Example Sentences" to get AI-generated examples.
+ </p>
+ </div>
+ )}
+ </div>
+ </div>
+ </div>
+ );
+}