From ba2dbc660c229d3e86662d35513dfa7c904d9870 Mon Sep 17 00:00:00 2001 From: polwex Date: Sun, 23 Nov 2025 13:29:28 +0700 Subject: wew --- .../src/components/word/FullWordData.tsx | 156 +++++++++++++++++ .../prosody-ui/src/components/word/Phonetic.tsx | 92 +++++++++++ .../prosody-ui/src/components/word/Semantic.tsx | 184 +++++++++++++++++++++ 3 files changed, 432 insertions(+) create mode 100644 packages/prosody-ui/src/components/word/FullWordData.tsx create mode 100644 packages/prosody-ui/src/components/word/Phonetic.tsx create mode 100644 packages/prosody-ui/src/components/word/Semantic.tsx (limited to 'packages/prosody-ui/src/components/word') 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([]); + 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 ( +
+ + +
+ ({ + data: s, + display: s.spelling, + colorBy: s.tone.name, + }))} + theme={theme} + /> +
+ +
+

{data.phonetic.ipa}

+ +
+
+ {loading ? ( + + ) : ( + data.senses.map((m) => ( +
+
+ {m.pos} +
+
    + {m.glosses.map((t, i) => ( +
  1. +

    {t}

    +
  2. + ))} +
+
+ )) + )} + {error &&
{error}
} +
+
+
+ ); +} + +export default Word; + + +
+ +
+ + Meanings + Grammar + Examples + +
+ +
+ + + + + +
+
+

+ Tone Analysis +

+
+ {tones.map((tone, idx) => ( + + Syl {idx + 1}:{" "} + + {tone} + + + ))} +
+
+ +
+

+ Word Structure +

+

+ This word consists of {syls.length} syllable + {syls.length > 1 ? "s" : ""}. The tone pattern is essential for + conveying the correct meaning. +

+
+
+
+ + + s.examples || [])} + /> + +
+
+
+
; 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 ( +
+
+

{data.phonetic.ipa}

+ {loading ? ( + + ) : ( + + )} +
+ +
+ ); +} + +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 ( +
+ {data.phonetic.syllables.map((syl) => ( +
+ {syl.tone.letters && } + {syl.spelling} +
+ ))} +
+ ); +} +function Tone({ tone }: { tone: Tone }) { + return ( +
+ {tone.letters} +
+ ); +} 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 ( +
+
+
+ {data.senses.map((sense, i) => ( +
+
+ {sense.pos &&
{sense.pos}
} + +
    + {sense.glosses.map((gloss, idx: number) => ( +
  • + {gloss} +
  • + ))} +
+ + {sense.etymology && ( +
+ Etymology: {sense.etymology} +
+ )} + + {sense.categories.length > 0 && ( +
+ Categories: {sense.categories.join(", ")} +
+ )} + {sense.derivation.length > 0 && ( +
+ Derived forms: + {sense.derivation.map((dr, i) => ( +
+ {dr.type}: {dr.text} - {dr.tags} +
+ ))} +
+ )} + {sense.examples.length > 0 && ( + + )} +
+
+ ))} +
+
+
+ ); +} + +export default Semantic; + +function ExamplesTab({ + data, + examples, +}: { + data: FullWordData; + examples: Example[]; +}) { + const [isGenerating, setIsGenerating] = useState(false); + const [generatedExamples, setGeneratedExamples] = useState([]); + + 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 ( +
+
+

Usage Examples

+ + {/* Generate More Button */} +
+ +
+ + {/* Examples Display */} +
+ {examples.map((example, idx) => ( +
+

+ {example?.text || ""} +

+ {example.ref && ( +

+ Source: {example.ref} +

+ )} +
+ ))} + + {generatedExamples.length > 0 && ( + <> +
+ AI-Generated Examples: +
+ {generatedExamples.map((example, idx) => ( +
+

+ {example.thai} +

+

+ {example.english} +

+ {example.context && ( +

+ Context: {example.context} +

+ )} +
+ ))} + + )} + + {/* No Examples */} + {!moreExamples?.length && !generatedExamples.length && ( +
+

+ No examples available for this word. Click "Generate More + Example Sentences" to get AI-generated examples. +

+
+ )} +
+
+
+ ); +} -- cgit v1.2.3