diff options
Diffstat (limited to 'packages/prosody-ui/src/components')
| -rw-r--r-- | packages/prosody-ui/src/components/Colors.tsx | 198 | ||||
| -rw-r--r-- | packages/prosody-ui/src/components/Sentence.tsx | 173 | ||||
| -rw-r--r-- | packages/prosody-ui/src/components/word/FullWordData.tsx | 156 | ||||
| -rw-r--r-- | packages/prosody-ui/src/components/word/Phonetic.tsx | 92 | ||||
| -rw-r--r-- | packages/prosody-ui/src/components/word/Semantic.tsx | 184 |
5 files changed, 781 insertions, 22 deletions
diff --git a/packages/prosody-ui/src/components/Colors.tsx b/packages/prosody-ui/src/components/Colors.tsx new file mode 100644 index 0000000..d98838f --- /dev/null +++ b/packages/prosody-ui/src/components/Colors.tsx @@ -0,0 +1,198 @@ +import React from "react"; +import { notRandomFromArray, randomFromArrayMany } from "@sortug/lib"; +import "./sentence.css"; +import type { AnalyzeRes, ColorTheme, LangToColor } from "../logic/types"; +import type { POS_CODE } from "../thai/logic/thainlp"; + +export function assignColors(keys: string[], theme?: ColorTheme): string[] { + const background = theme ? theme : "light"; + const colors = colorPalette[background]; + const reduced = randomFromArrayMany(colors, keys.length, false); + const assigned: string[] = []; + for (const key of keys) { + const color = notRandomFromArray(key, reduced); + assigned.push(color); + } + return assigned; +} + +export function ColoredText({ + frags, + fn, + lang, + theme, +}: { + frags: LangToColor<unknown>[]; + fn?: (s: any) => void; + lang?: string; + theme: ColorTheme; +}) { + const colors = colorPalette[theme]; + console.log("coloredText", theme); + + // function getStyle(frags: AnalyzeRes[], i: number) { + // const prev = frags[i - 1]; + // const prevC = prev ? notRandomFromArray(prev.word, colors) : "lol"; + // const color = notRandomFromArray(s, colors); + // const opacity = prev && prevC === color ? 0.8 : 1; + // const style = { color, opacity }; + // return style; + // } + + return ( + <> + {frags.map((s, i) => { + // old code + const prev = frags[i - 1]; + const prevC = prev ? notRandomFromArray(prev.colorBy, colors) : "lol"; + const color = notRandomFromArray(s.colorBy, colors); + const style = !prev ? { color } : { color }; + return ( + <CTInner + lang={lang} + key={s.display + i} + s={s} + style={style} + fn={fn} + /> + ); + })} + </> + ); +} + +export function CTInner({ + s, + style, + fn, + lang, +}: { + s: LangToColor<unknown>; + style: any; + fn?: (s: any) => void; + lang?: string; +}) { + function handleClick(e: React.MouseEvent<HTMLSpanElement>) { + if (fn) { + e.stopPropagation(); + fn(s.data); + } + } + return ( + <span lang={lang} onClick={handleClick} className="word cp" style={style}> + {s.display} + </span> + ); +} + +export const colorPalette: Record<ColorTheme, string[]> = { + light: [ + // Black Standard high contrast + "#000000", + // Charcoal Softer than pure black + "#36454F", + // Slate Grey Cool, dark grey-green + "#2F4F4F", + // Navy Blue Classic professional blue + "#000080", + // Midnight Blue Very deep, rich blue + "#191970", + // Cobalt Vivid, highly legible blue + "#0047AB", + // Teal Distinct blue-green + "#008080", + // Forest Green Nature-inspired dark green + "#006400", + // Pine Green Cooler, bluish green + "#01796F", + // Olive Drab Dark brownish-green + "#4B5320", + // Bronze Metallic brown-orange + "#CD7F32", + // Saddle Brown Robust earthy tone + "#8B4513", + // Chocolate Warm, readable orange-brown + "#D2691E", + // Burnt Sienna Reddish-orange earth tone + "#E97451", + // Firebrick Muted dark red + "#B22222", + // Crimson Vivid, alarming red + "#DC143C", + // Maroon Deep, serious red + "#800000", + // Burgundy Purple-leaning red + "#800020", + // Deep Pink High contrast magenta-pink + "#C71585", + // Dark Violet Vivid purple + "#9400D3", + // Indigo Deep blue-purple + "#4B0082", + // Purple Standard distinct purple + "#800080", + // Rebecca Purple Web-standard bluish purple + "#663399", + // Dim Gray Neutral, medium-dark gray + "#696969", + ], + dark: [ + // White Standard high contrast + "#FFFFFF", + // Silver Soft readable grey + "#C0C0C0", + // Cream Warm white, easier on eyes + "#FFFDD0", + // Cyan The standard terminal blue-green + "#00FFFF", + // Sky Blue Pleasant, airy blue + "#87CEEB", + // Powder Blue Very pale, soft blue + "#B0E0E6", + // Aquamarine Bright neon blue-green + "#7FFFD4", + // Mint Green Soft, pastel green + "#98FB98", + // Lime Classic high-vis terminal green + "#00FF00", + // Chartreuse Yellow-green neon + "#7FFF00", + // Gold Bright yellow-orange + "#FFD700", + // Yellow Standard high-vis yellow + "#FFFF00", + // Khaki Muted, sandy yellow + "#F0E68C", + // Wheat Soft beige/earth tone + "#F5DEB3", + // Orange Standard distinctive orange + "#FFA500", + // Coral Pinkish-orange + "#FF7F50", + // Salmon Soft reddish-pink + "#FA8072", + // Hot Pink Vivid, high-energy pink + "#FF69B4", + // Magenta Pure, digital pink-purple + "#FF00FF", + // Plum Muted, readable purple + "#DDA0DD", + // Violet Bright, distinct purple + "#EE82EE", + // Lavender Very light purple-blue + "#E6E6FA", + // Periwinkle Soft indigo-blue + "#CCCCFF", + // Thistle Desaturated light purple + "#D8BFD8", + ], +}; + +// export const colors = [ +// "#8c2c2c", +// "#000000", +// "#ffd400", +// "#1513a0", +// "#7e7e7e", +// "1eb52d", +// ]; diff --git a/packages/prosody-ui/src/components/Sentence.tsx b/packages/prosody-ui/src/components/Sentence.tsx index 33144ac..1986ba8 100644 --- a/packages/prosody-ui/src/components/Sentence.tsx +++ b/packages/prosody-ui/src/components/Sentence.tsx @@ -1,26 +1,49 @@ import React from "react"; -import { notRandomFromArray } from "sortug"; +import { notRandomFromArray } from "@sortug/lib"; import "./sentence.css"; +import type { AnalyzeRes, ColorTheme, LangToColor } from "../logic/types"; +import type { POS_CODE } from "../thai/logic/thainlp"; export function ColoredText({ frags, fn, lang, + theme, }: { - frags: string[]; - fn?: (s: string) => void; + frags: LangToColor<unknown>[]; + fn?: (s: any) => void; lang?: string; + theme: ColorTheme; }) { + const colors = colorPalette[theme]; + console.log("coloredText", theme); + + // function getStyle(frags: AnalyzeRes[], i: number) { + // const prev = frags[i - 1]; + // const prevC = prev ? notRandomFromArray(prev.word, colors) : "lol"; + // const color = notRandomFromArray(s, colors); + // const opacity = prev && prevC === color ? 0.8 : 1; + // const style = { color, opacity }; + // return style; + // } + return ( <> {frags.map((s, i) => { + // old code const prev = frags[i - 1]; - const prevC = prev ? notRandomFromArray(prev, colors) : "lol"; - const color = notRandomFromArray(s, colors); - const opacity = prev && prevC === color ? 0.8 : 1; - const style = { color, opacity }; - console.log({ style }); - return <CTInner lang={lang} key={s + i} s={s} style={style} fn={fn} />; + const prevC = prev ? notRandomFromArray(prev.colorBy, colors) : "lol"; + const color = notRandomFromArray(s.colorBy, colors); + const style = !prev ? { color } : { color }; + return ( + <CTInner + lang={lang} + key={s.display + i} + s={s} + style={style} + fn={fn} + /> + ); })} </> ); @@ -32,26 +55,132 @@ export function CTInner({ fn, lang, }: { - s: string; + s: LangToColor<unknown>; style: any; - fn?: (s: string) => void; + fn?: (s: any) => void; lang?: string; }) { function handleClick(e: React.MouseEvent<HTMLSpanElement>) { - console.log(!!fn, "fn"); - if (fn) fn(e.currentTarget.innerText.trim()); + if (fn) { + e.stopPropagation(); + fn(s.data); + } } return ( <span lang={lang} onClick={handleClick} className="word cp" style={style}> - {s} + {s.display} </span> ); } -export const colors = [ - "#8c2c2c", - "#000000", - "#ffd400", - "#1513a0", - "#7e7e7e", - "1eb52d", -]; + +export const colorPalette: Record<ColorTheme, string[]> = { + light: [ + // Black Standard high contrast + "#000000", + // Charcoal Softer than pure black + "#36454F", + // Slate Grey Cool, dark grey-green + "#2F4F4F", + // Navy Blue Classic professional blue + "#000080", + // Midnight Blue Very deep, rich blue + "#191970", + // Cobalt Vivid, highly legible blue + "#0047AB", + // Teal Distinct blue-green + "#008080", + // Forest Green Nature-inspired dark green + "#006400", + // Pine Green Cooler, bluish green + "#01796F", + // Olive Drab Dark brownish-green + "#4B5320", + // Bronze Metallic brown-orange + "#CD7F32", + // Saddle Brown Robust earthy tone + "#8B4513", + // Chocolate Warm, readable orange-brown + "#D2691E", + // Burnt Sienna Reddish-orange earth tone + "#E97451", + // Firebrick Muted dark red + "#B22222", + // Crimson Vivid, alarming red + "#DC143C", + // Maroon Deep, serious red + "#800000", + // Burgundy Purple-leaning red + "#800020", + // Deep Pink High contrast magenta-pink + "#C71585", + // Dark Violet Vivid purple + "#9400D3", + // Indigo Deep blue-purple + "#4B0082", + // Purple Standard distinct purple + "#800080", + // Rebecca Purple Web-standard bluish purple + "#663399", + // Dim Gray Neutral, medium-dark gray + "#696969", + ], + dark: [ + // White Standard high contrast + "#FFFFFF", + // Silver Soft readable grey + "#C0C0C0", + // Cream Warm white, easier on eyes + "#FFFDD0", + // Cyan The standard terminal blue-green + "#00FFFF", + // Sky Blue Pleasant, airy blue + "#87CEEB", + // Powder Blue Very pale, soft blue + "#B0E0E6", + // Aquamarine Bright neon blue-green + "#7FFFD4", + // Mint Green Soft, pastel green + "#98FB98", + // Lime Classic high-vis terminal green + "#00FF00", + // Chartreuse Yellow-green neon + "#7FFF00", + // Gold Bright yellow-orange + "#FFD700", + // Yellow Standard high-vis yellow + "#FFFF00", + // Khaki Muted, sandy yellow + "#F0E68C", + // Wheat Soft beige/earth tone + "#F5DEB3", + // Orange Standard distinctive orange + "#FFA500", + // Coral Pinkish-orange + "#FF7F50", + // Salmon Soft reddish-pink + "#FA8072", + // Hot Pink Vivid, high-energy pink + "#FF69B4", + // Magenta Pure, digital pink-purple + "#FF00FF", + // Plum Muted, readable purple + "#DDA0DD", + // Violet Bright, distinct purple + "#EE82EE", + // Lavender Very light purple-blue + "#E6E6FA", + // Periwinkle Soft indigo-blue + "#CCCCFF", + // Thistle Desaturated light purple + "#D8BFD8", + ], +}; + +// export const colors = [ +// "#8c2c2c", +// "#000000", +// "#ffd400", +// "#1513a0", +// "#7e7e7e", +// "1eb52d", +// ]; 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> + ); +} |
