summaryrefslogtreecommitdiff
path: root/packages/prosody-ui/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/prosody-ui/src')
-rw-r--r--packages/prosody-ui/src/LangText.tsx80
-rw-r--r--packages/prosody-ui/src/Paragraph.tsx2
-rw-r--r--packages/prosody-ui/src/assets/fonts/Hani/dingliezhuhaifont-20240831GengXinBan-2.ttf (renamed from packages/prosody-ui/src/assets/fonts/Hani/dingliezhuhaifont-20240831GengXinBan)-2.ttf)bin5359608 -> 5359608 bytes
-rw-r--r--packages/prosody-ui/src/components/Colors.tsx198
-rw-r--r--packages/prosody-ui/src/components/Sentence.tsx173
-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
-rw-r--r--packages/prosody-ui/src/fonts/FontChanger.tsx90
-rw-r--r--packages/prosody-ui/src/fonts/Jpan.tsx14
-rw-r--r--packages/prosody-ui/src/fonts/Latn.tsx14
-rw-r--r--packages/prosody-ui/src/fonts/Thai.tsx6
-rw-r--r--packages/prosody-ui/src/fonts/useLangFont.tsx2
-rw-r--r--packages/prosody-ui/src/latin/LatinText.tsx4
-rw-r--r--packages/prosody-ui/src/logic/stanza.ts2
-rw-r--r--packages/prosody-ui/src/logic/types.ts3
-rw-r--r--packages/prosody-ui/src/logic/utils.ts2
-rw-r--r--packages/prosody-ui/src/logic/wiki.ts2
-rw-r--r--packages/prosody-ui/src/sortug.css248
-rw-r--r--packages/prosody-ui/src/styles/styles.css37
-rw-r--r--packages/prosody-ui/src/thai/ThaiText.tsx38
-rw-r--r--packages/prosody-ui/src/thai/logic/thainlp.ts10
-rw-r--r--packages/prosody-ui/src/themes/ThemeSwitcher.tsx130
-rw-r--r--packages/prosody-ui/src/themes/themes.ts321
-rw-r--r--packages/prosody-ui/src/zoom/FullText.tsx2
-rw-r--r--packages/prosody-ui/src/zoom/Paragraph.tsx2
-rw-r--r--packages/prosody-ui/src/zoom/Sentence.tsx2
-rw-r--r--packages/prosody-ui/src/zoom/SpacyClause.tsx2
-rw-r--r--packages/prosody-ui/src/zoom/logic/types.ts2
29 files changed, 1424 insertions, 394 deletions
diff --git a/packages/prosody-ui/src/LangText.tsx b/packages/prosody-ui/src/LangText.tsx
index 790c499..ab9d4f4 100644
--- a/packages/prosody-ui/src/LangText.tsx
+++ b/packages/prosody-ui/src/LangText.tsx
@@ -2,27 +2,31 @@ import { franc } from "franc-all";
import React, { useEffect, useState } from "react";
import ThaiText from "./thai/ThaiText";
import { ColoredText } from "./components/Sentence";
-import type { AnalyzeRes, WordData } from "./logic/types";
+import type { AnalyzeRes, ColorTheme, WordData } from "./logic/types";
import { detectScript, scriptFromLang } from "./logic/utils";
import LatinText from "./latin/LatinText";
import { buildWiktionaryURL, parseWiktionary } from "./logic/wiki";
-import type { Result } from "sortug";
+import FullWord from "./components/word/FullWordData";
+import type { AsyncRes, Result } from "@sortug/lib";
+import type { FullWordData } from "@sortug/langlib";
export default function LangText({
text,
lang,
theme,
- fetchWiki,
handleWord,
+ handleError,
}: {
text: string;
+ theme?: ColorTheme;
lang?: string;
- theme?: string;
- fetchWiki?: (url: string) => Promise<string>;
- handleWord?: (wd: Result<WordData>) => any;
+ handleWord?: (word: AnalyzeRes) => any;
+ handleError?: (error: string) => any;
}) {
+ const background: ColorTheme = theme ? theme : "light";
const [llang, setLang] = useState("");
const [script, setScript] = useState(scriptFromLang(lang || "", text));
+ const [modal, setWordModal] = useState<FullWordData | null>(null);
useEffect(() => {
if (!lang) {
const res = franc(text);
@@ -31,7 +35,27 @@ export default function LangText({
}, [text]);
console.log("langtext", { text, llang, script });
- async function openWord(word: string) {
+ async function openWord(word: AnalyzeRes) {
+ if (handleWord) handleWord(word);
+ else {
+ const body = JSON.stringify({
+ getWordFull: { spelling: word.word, lang: llang },
+ });
+ const opts = {
+ method: "POST",
+ body,
+ headers: { "Content-type": "application/json" },
+ };
+ const res = await fetch("/api/db", opts);
+ const j = (await res.json()) as Result<FullWordData>;
+ console.log({ j });
+ if ("error" in j) {
+ if (handleError) handleError(j.error);
+ else console.error("error opening word", j.error);
+ } else {
+ setWordModal(j.ok);
+ }
+ }
// console.log("looking up", word);
// const url = buildWiktionaryURL(word);
// const html = await fetchWiki(url);
@@ -56,12 +80,20 @@ export default function LangText({
return (
<div className="lang-text-container">
{script === "Thai" ? (
- <ThaiText text={text} openWord={openWord} />
+ <ThaiText text={text} theme={background} openWord={openWord} />
) : script === "Latin" ? (
<LatinText text={text} lang={llang} openWord={openWord} />
) : (
<Generic text={text} lang={llang} />
)}
+ {modal && (
+ <WordModal
+ word={modal}
+ lang={llang}
+ theme={background}
+ onClose={() => setWordModal(null)}
+ />
+ )}
</div>
);
}
@@ -76,3 +108,35 @@ function Generic({ text, lang }: { text: string; lang: string }) {
// {data && <ColoredText frags={Object.keys(data)} />}
return <div className="lang-text-div"></div>;
}
+
+function WordModal({
+ word,
+ lang,
+ theme,
+ onClose,
+}: {
+ word: FullWordData;
+ lang: string;
+ theme: ColorTheme;
+ onClose: () => void;
+}) {
+ return (
+ <div
+ id="modal-bg"
+ role="dialog"
+ aria-modal="true"
+ onMouseDown={(event) => {
+ if (event.target === event.currentTarget) {
+ onClose();
+ }
+ }}
+ >
+ <div
+ id="modal-fg"
+ style={{ backgroundColor: "white", border: "5px solid black" }}
+ >
+ <FullWord data={word} lang={lang} theme={"light"} />
+ </div>
+ </div>
+ );
+}
diff --git a/packages/prosody-ui/src/Paragraph.tsx b/packages/prosody-ui/src/Paragraph.tsx
index 72c43a7..b911fa0 100644
--- a/packages/prosody-ui/src/Paragraph.tsx
+++ b/packages/prosody-ui/src/Paragraph.tsx
@@ -6,7 +6,7 @@ import type { AnalyzeRes, WordData } from "./logic/types";
import { detectScript, langFromScript } from "./logic/utils";
import LatinText from "./latin/LatinText";
import { buildWiktionaryURL, parseWiktionary } from "./logic/wiki";
-import type { Result } from "sortug";
+import type { Result } from "@sortug/lib";
import * as Stanza from "./logic/stanza";
import { iso6393To1 } from "./logic/iso6393to1";
diff --git a/packages/prosody-ui/src/assets/fonts/Hani/dingliezhuhaifont-20240831GengXinBan)-2.ttf b/packages/prosody-ui/src/assets/fonts/Hani/dingliezhuhaifont-20240831GengXinBan-2.ttf
index b387fc5..b387fc5 100644
--- a/packages/prosody-ui/src/assets/fonts/Hani/dingliezhuhaifont-20240831GengXinBan)-2.ttf
+++ b/packages/prosody-ui/src/assets/fonts/Hani/dingliezhuhaifont-20240831GengXinBan-2.ttf
Binary files differ
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>
+ );
+}
diff --git a/packages/prosody-ui/src/fonts/FontChanger.tsx b/packages/prosody-ui/src/fonts/FontChanger.tsx
index 15c932e..9ae9c84 100644
--- a/packages/prosody-ui/src/fonts/FontChanger.tsx
+++ b/packages/prosody-ui/src/fonts/FontChanger.tsx
@@ -1,64 +1,68 @@
import React, { useEffect, useState, type ReactNode } from "react";
import fontIcon from "../assets/icons/font.svg";
-import { getScriptPredictor } from "glotscript";
+import { getScriptPredictor, type ISO_15924_CODE } from "@sortug/langlib";
import ThaiFontLoader from "./Thai";
+import HanFontLoader from "./Hani";
+import LatnFontLoader from "./Latn";
+import JpanFontLoader from "./Jpan";
-function FontChanger({ text }: { text: string }) {
- const [script, setScript] = useState<string | null>(null);
+function findFontCount(lang: ISO_15924_CODE): number {
+ if (lang === "Thai") return 7;
+ if (lang === "Jpan") return 6;
+ // TODO get more latin fonts
+ if (lang === "Latn") return 1;
+ if ((lang as any) === "IPA") return 6;
+ if (lang.startsWith("Han")) return 23;
+ return 0;
+}
+
+function FontChanger({
+ text,
+ script,
+ children,
+}: {
+ text: string;
+ script?: ISO_15924_CODE;
+ lang?: string;
+ children: ReactNode;
+}) {
+ const [script2, setScript] = useState<ISO_15924_CODE | null>(script || null);
useEffect(() => {
+ if (script) return;
const predictor = getScriptPredictor();
const res = predictor(text);
console.log("script predicted", res);
- setScript(res[0]);
+ const rescript: ISO_15924_CODE | null = res[0];
+ if (!rescript) {
+ console.error("script undetected", text);
+ return;
+ }
+ setScript(rescript);
+ setFontCount(findFontCount(rescript));
}, [text]);
- useEffect(() => {
- if (script === "Hani") setFontCount(12);
- else if (script === "Thai") setFontCount(6);
- else if (script === "Jpan") setFontCount(5);
- // else if (script === "Latn") setFontCount(6)
- }, [script]);
+
const [fontIdx, setFont] = useState(0);
const [fontCount, setFontCount] = useState(0);
function changeFont() {
if (fontIdx === fontCount) setFont(0);
else setFont((prev) => prev + 1);
}
+ if (!script2)
+ return <div className="error">Couldn't detect script of {text}</div>;
return (
- <div
- className={`font-changer font-${script}-${fontIdx}`}
- lang={script || ""}
- >
- <img
- className="font-icon cp"
- style={{ width: 25 }}
- onClick={changeFont}
- src={fontIcon}
- />
- {script === "Thai" ? <ThaiFontLoader text={text} /> : null}
+ <div className={`font-changer font-${script}-${fontIdx}`}>
+ <img className="font-icon cp" onClick={changeFont} src={fontIcon} />
+ {script2 === "Thai" ? (
+ <ThaiFontLoader>{children}</ThaiFontLoader>
+ ) : script2.startsWith("Han") ? (
+ <HanFontLoader>{children}</HanFontLoader>
+ ) : script2 === "Jpan" ? (
+ <JpanFontLoader>{children}</JpanFontLoader>
+ ) : script2 === "Latn" ? (
+ <LatnFontLoader>{children}</LatnFontLoader>
+ ) : null}
</div>
);
}
-// function FontChanger({
-// lang,
-// children,
-// }: {
-// lang: string;
-// children: ReactNode;
-// }) {
-// useEffect(() => {}, []);
-// const [script, setScript] = useState("Latn");
-// const [fontIdx, setFont] = useState(0);
-// const fontCount = 6;
-// function changeFont() {
-// if (fontIdx === fontCount) setFont(0);
-// else setFont((prev) => prev + 1);
-// }
-// return (
-// <div className="font-changer" lang={script}>
-// <img className="font-icon cp" onClick={changeFont} src={fontIcon} />
-// {children}
-// </div>
-// );
-// }
export default FontChanger;
diff --git a/packages/prosody-ui/src/fonts/Jpan.tsx b/packages/prosody-ui/src/fonts/Jpan.tsx
new file mode 100644
index 0000000..f9cc602
--- /dev/null
+++ b/packages/prosody-ui/src/fonts/Jpan.tsx
@@ -0,0 +1,14 @@
+import React, { useState, type ReactNode } from "react";
+import "../assets/fonts/Hani/style.css";
+
+function ChineseFontLoader({ children }: { children: ReactNode }) {
+ const [fontIdx, setFont] = useState(0);
+ const fontCount = 12;
+ function changeFont() {
+ if (fontIdx === fontCount) setFont(0);
+ else setFont((prev) => prev + 1);
+ }
+ return <div>{children}</div>;
+}
+
+export default ChineseFontLoader;
diff --git a/packages/prosody-ui/src/fonts/Latn.tsx b/packages/prosody-ui/src/fonts/Latn.tsx
new file mode 100644
index 0000000..f9cc602
--- /dev/null
+++ b/packages/prosody-ui/src/fonts/Latn.tsx
@@ -0,0 +1,14 @@
+import React, { useState, type ReactNode } from "react";
+import "../assets/fonts/Hani/style.css";
+
+function ChineseFontLoader({ children }: { children: ReactNode }) {
+ const [fontIdx, setFont] = useState(0);
+ const fontCount = 12;
+ function changeFont() {
+ if (fontIdx === fontCount) setFont(0);
+ else setFont((prev) => prev + 1);
+ }
+ return <div>{children}</div>;
+}
+
+export default ChineseFontLoader;
diff --git a/packages/prosody-ui/src/fonts/Thai.tsx b/packages/prosody-ui/src/fonts/Thai.tsx
index 0048316..62b886b 100644
--- a/packages/prosody-ui/src/fonts/Thai.tsx
+++ b/packages/prosody-ui/src/fonts/Thai.tsx
@@ -1,8 +1,8 @@
-import React, { useState, type ReactNode } from "react";
+import { type ReactNode } from "react";
import "../assets/fonts/Thai/style.css";
-function ThaiFontLoader({ text }: { text: string }) {
- return <div>{text}</div>;
+function ThaiFontLoader({ children }: { children: ReactNode }) {
+ return <>{children}</>;
}
export default ThaiFontLoader;
diff --git a/packages/prosody-ui/src/fonts/useLangFont.tsx b/packages/prosody-ui/src/fonts/useLangFont.tsx
index 36fa603..5467b18 100644
--- a/packages/prosody-ui/src/fonts/useLangFont.tsx
+++ b/packages/prosody-ui/src/fonts/useLangFont.tsx
@@ -1,6 +1,6 @@
import React, { useEffect, useState, type ReactNode } from "react";
import fontIcon from "../assets/icons/font.svg";
-import { getScriptPredictor } from "glotscript";
+import { getScriptPredictor } from "@sortug/langlib";
function useLangFont({ text }: { text: string }) {
useEffect(() => {
diff --git a/packages/prosody-ui/src/latin/LatinText.tsx b/packages/prosody-ui/src/latin/LatinText.tsx
index e5b13ff..073baff 100644
--- a/packages/prosody-ui/src/latin/LatinText.tsx
+++ b/packages/prosody-ui/src/latin/LatinText.tsx
@@ -30,7 +30,7 @@ export default function LatinText({
}: {
text: string;
lang: string;
- openWord?: (word: string) => void;
+ openWord?: (word: AnalyzeRes) => void;
}) {
useEffect(() => {
const sentences = segmentate(text, lang, "sentence");
@@ -55,7 +55,7 @@ function Sentence({
text: string;
lang: string;
- openWord?: (word: string) => void;
+ openWord?: (word: AnalyzeRes) => void;
}) {
useEffect(() => {
const w = segmentate(text, lang, "word");
diff --git a/packages/prosody-ui/src/logic/stanza.ts b/packages/prosody-ui/src/logic/stanza.ts
index 9e59450..b74a064 100644
--- a/packages/prosody-ui/src/logic/stanza.ts
+++ b/packages/prosody-ui/src/logic/stanza.ts
@@ -1,4 +1,4 @@
-import type { AsyncRes, Result } from "sortug";
+import type { AsyncRes, Result } from "@sortug/lib";
const ENDPOINT = "http://localhost:8102";
export async function segmenter(text: string, lang: string) {
diff --git a/packages/prosody-ui/src/logic/types.ts b/packages/prosody-ui/src/logic/types.ts
index ac308cf..cdae30e 100644
--- a/packages/prosody-ui/src/logic/types.ts
+++ b/packages/prosody-ui/src/logic/types.ts
@@ -46,3 +46,6 @@ export type WordData = {
meanings: Meaning[];
references?: any;
};
+
+export type ColorTheme = "light" | "dark";
+export type LangToColor<T> = { display: string; colorBy: string; data: T };
diff --git a/packages/prosody-ui/src/logic/utils.ts b/packages/prosody-ui/src/logic/utils.ts
index 737a6ec..90b2e1e 100644
--- a/packages/prosody-ui/src/logic/utils.ts
+++ b/packages/prosody-ui/src/logic/utils.ts
@@ -1,4 +1,4 @@
-import type { Result } from "sortug";
+import type { Result } from "@sortug/lib";
export function detectScript(text: string): Result<string> {
const scripts = {
diff --git a/packages/prosody-ui/src/logic/wiki.ts b/packages/prosody-ui/src/logic/wiki.ts
index 1325c0f..d3c56ee 100644
--- a/packages/prosody-ui/src/logic/wiki.ts
+++ b/packages/prosody-ui/src/logic/wiki.ts
@@ -1,4 +1,4 @@
-import type { AsyncRes, Result } from "sortug";
+import type { AsyncRes, Result } from "@sortug/lib";
import type { Meaning } from "./types";
export function buildWiktionaryURL(word: string) {
diff --git a/packages/prosody-ui/src/sortug.css b/packages/prosody-ui/src/sortug.css
deleted file mode 100644
index c6280c0..0000000
--- a/packages/prosody-ui/src/sortug.css
+++ /dev/null
@@ -1,248 +0,0 @@
-
-/* SORTUG CSS */
-/* variables */
-:root {
- --bai: rgba(255, 255, 255, 1);
- --baizi: rgba(230, 230, 230);
- --hui: rgba(130, 130, 130, 1);
- --hei: rgba(0, 0, 0, 1);
- --hong: rgb(141, 15, 15, 1);
- --huang: rgb(230, 180, 60, 1);
- --lan: rgb(30, 60, 80, 1);
-}
-
-[data-theme="dark"] {
- --bg: hei;
- --fg: baizi;
-}
-
-[data-theme="light"] {
- --bg: white;
- --fg: black;
-}
-
-* {
- box-sizing: border-box;
-}
-
-html,
-body,
-#root {
- height: 100%;
- min-height: 100%;
- overscroll-behavior: none;
- color: var(--fg);
- -webkit-font-smoothing: antialiased;
- margin: 0;
-}
-
-/* tailwindy classes */
-.card {
- padding: 1rem;
- max-width: max-content;
-}
-
-button,
-.button {
- max-width: max-content;
- padding: 0.5rem;
- border: 1px solid var(--fg);
-}
-
-/* borders */
-.nb {
- border: none;
-}
-
-/* widths */
-.hw {
- width: 50%;
-}
-
-.qw {
- width: 25%;
-}
-
-.tqw {
- width: 75%;
-}
-
-/* flex */
-.row {
- display: flex;
- align-items: center;
-}
-
-.sy {
- overflow-y: scroll;
-}
-
-.fsy {
- overflow-y: scroll;
- height: 100%;
-}
-
-.fxc {
- display: flex;
- justify-content: center;
- align-items: baseline;
-}
-
-/* flex spread */
-.fs {
- display: flex;
- justify-content: space-between;
-}
-
-.fsc {
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.g1 {
- gap: 0.5rem;
-}
-
-.g2 {
- gap: 1rem;
-}
-
-.address {
- font-family: "Courier New", Courier, monospace;
-}
-
-.spread {
- justify-content: space-between;
-}
-
-.even {
- justify-content: space-evenly;
-}
-
-.flexc {
- justify-content: center;
-}
-
-.cp {
- cursor: pointer;
-}
-
-/* centering */
-.gc {
- position: fixed;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
-}
-
-.agc {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
-}
-
-.ac {
- position: absolute;
- left: 50%;
- transform: translateX(-50%);
-}
-
-.xc {
- position: fixed;
- left: 50%;
- transform: translateX(-50%);
- z-index: 20;
-}
-
-.tc {
- text-align: center;
-}
-
-.bc {
- display: block;
- margin-left: auto;
- margin-right: auto;
-}
-
-.blocks {
- & * {
- display: block;
- }
-}
-
-.bold {
- font-weight: 700;
-}
-
-.weak {
- opacity: 0.7;
-}
-
-.all-c {
- & * {
- margin-left: auto;
- margin-right: auto;
- }
-}
-
-.mb-1 {
- margin-bottom: 1rem;
-}
-
-.error {
- color: red;
- text-align: center;
-}
-
-.tabs {
- display: flex;
- justify-content: space-evenly;
- align-items: center;
-
- & .tab {
- cursor: pointer;
- opacity: 0.5;
- }
-
- & .tab.active {
- opacity: 1;
- }
-}
-
-.disabled {
- opacity: 0.5;
-}
-
-.smol {
- font-size: 0.9rem;
-}
-
-/* The Modal (background) */
-#modal-bg {
- position: fixed;
- z-index: 1;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
- overflow: auto;
- background-color: rgba(0, 0, 0, 0.4);
- z-index: 998;
-}
-
-/* Modal Content */
-#modal-fg {
- background-color: var(--bg);
- position: fixed;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- padding: 20px;
- z-index: 999;
- max-height: 90vh;
- min-height: 20vh;
- max-width: 90vw;
- overflow: auto;
-}
diff --git a/packages/prosody-ui/src/styles/styles.css b/packages/prosody-ui/src/styles/styles.css
index 69351f1..e98d1f0 100644
--- a/packages/prosody-ui/src/styles/styles.css
+++ b/packages/prosody-ui/src/styles/styles.css
@@ -149,6 +149,7 @@
#word-modal {
position: relative;
+ height: 80vh;
& .font-icon {
position: absolute;
@@ -225,42 +226,6 @@ img {
justify-content: center;
}
-/* p { */
-/* position: absolute; */
-/* top: 50%; */
-/* left: 50%; */
-/* transform: translate(-50%, -50%); */
-/* color: white; */
-/* background-color: rgba(0, 0, 0, 0.5); */
-/* padding: 10px; */
-/* border-radius: 5px; */
-/* } */
-#modal-bg {
- height: 100vh;
- width: 100vw;
- background-color: rgb(0, 0, 0, 0.9);
- position: fixed;
- top: 0;
- left: 0;
- z-index: 100;
-}
-
-#modal-fg {
- position: fixed;
- top: 50%;
- left: 50%;
- width: 80%;
- z-index: 101;
- transform: translate(-50%, -50%);
- /* background-color: var(--background-color); */
- background-color: lightgrey;
- font-size: 1.2rem;
- padding: 1rem;
- max-height: 80%;
- overflow-y: scroll;
-}
-
-
.text-ipa {
font-size: 1.5rem;
}
diff --git a/packages/prosody-ui/src/thai/ThaiText.tsx b/packages/prosody-ui/src/thai/ThaiText.tsx
index fc1e1e6..794804a 100644
--- a/packages/prosody-ui/src/thai/ThaiText.tsx
+++ b/packages/prosody-ui/src/thai/ThaiText.tsx
@@ -1,49 +1,49 @@
import React, { useCallback, useEffect, useState } from "react";
import "../assets/fonts/Thai/style.css";
import { segmentateThai } from "./logic/thainlp";
-import type { AnalyzeRes } from "../logic/types";
+import type { AnalyzeRes, ColorTheme, LangToColor } from "../logic/types";
import { ColoredText } from "../components/Sentence";
import Word from "../components/Word";
export default function ThaiText({
text,
openWord,
+ theme,
}: {
text: string;
- openWord: (s: string) => void;
+ openWord: (s: AnalyzeRes) => void;
+ theme: ColorTheme;
}) {
useEffect(() => {
pythonseg();
}, [text]);
- const [data, setData] = useState<Record<string, AnalyzeRes>>({});
+ const [data, setData] = useState<Array<LangToColor<AnalyzeRes>>>([]);
const [modal, setModal] = useState<any>();
const pythonseg = useCallback(async () => {
const s2 = await segmentateThai(text.trim());
if ("ok" in s2) {
- const ob = s2.ok.reduce((acc, item) => {
- acc[item.word] = item;
- return acc;
- }, {} as any);
- setData(ob);
+ const ob = s2.ok.reduce(
+ (acc, item) => {
+ acc[item.word] = item;
+ return acc;
+ },
+ {} as Record<string, AnalyzeRes>,
+ );
+ const d = Object.values(ob).map((w) => ({
+ data: w,
+ colorBy: w.pos,
+ display: w.word,
+ }));
+ setData(d);
console.log(s2, "s2");
} else console.error(s2.error);
}, [text]);
- // function openWord(e: React.MouseEvent<any>) {
- // const s = e.currentTarget.innerText;
- // const d = data[s];
- // setModal(d);
- // // setModal(<WordModal data={d} lang={lang} />);
- // }
return (
<div className="thaitext">
- <ColoredText lang="tha" frags={Object.keys(data)} fn={openWord} />
+ <ColoredText lang="tha" theme={theme} frags={data} fn={openWord} />
{modal && <Word data={modal} lang={"tha"} />}
</div>
);
}
-
-function ThaiWord() {
- return <div />;
-}
diff --git a/packages/prosody-ui/src/thai/logic/thainlp.ts b/packages/prosody-ui/src/thai/logic/thainlp.ts
index 031bf4c..dc6ed23 100644
--- a/packages/prosody-ui/src/thai/logic/thainlp.ts
+++ b/packages/prosody-ui/src/thai/logic/thainlp.ts
@@ -1,4 +1,4 @@
-import type { AsyncRes } from "sortug";
+import type { AsyncRes } from "@sortug/lib";
import type { AnalyzeRes } from "../../logic/types";
const ENDPOINT = "http://192.168.1.110:8001";
@@ -24,7 +24,7 @@ export async function segmentateThai(sentence: string): AsyncRes<AnalyzeRes[]> {
return await call("/segmentate", { word: sentence });
}
-export const POSMAP: Record<string, string> = {
+export const POSMAP = {
ADJ: "Adjective",
ADP: "Adposition",
ADV: "Adverb",
@@ -87,4 +87,8 @@ export const POSMAP: Record<string, string> = {
EITT: "Ending for interrogative sentence",
NEG: "Negator",
PUNC: "Punctuation",
-};
+} as const;
+type POSTYPE = typeof POSMAP;
+
+export type POS_CODE = keyof POSTYPE;
+export type POS = POSTYPE[POS_CODE];
diff --git a/packages/prosody-ui/src/themes/ThemeSwitcher.tsx b/packages/prosody-ui/src/themes/ThemeSwitcher.tsx
new file mode 100644
index 0000000..bae617f
--- /dev/null
+++ b/packages/prosody-ui/src/themes/ThemeSwitcher.tsx
@@ -0,0 +1,130 @@
+import React, {
+ createContext,
+ useContext,
+ useEffect,
+ useState,
+ type ReactNode,
+} from "react";
+import { themes, type Theme, type ThemeName } from "./themes";
+
+interface ThemeContextType {
+ theme: Theme;
+ themeName: ThemeName;
+ setTheme: (name: ThemeName) => void;
+ availableThemes: ThemeName[];
+}
+
+const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
+
+interface ThemeProviderProps {
+ children: ReactNode;
+ defaultTheme?: ThemeName;
+}
+
+export const ThemeProvider: React.FC<ThemeProviderProps> = ({
+ children,
+ defaultTheme = "light",
+}) => {
+ const [themeName, setThemeName] = useState<ThemeName>(() => {
+ const savedTheme = localStorage.getItem("theme") as ThemeName;
+ if (savedTheme && themes[savedTheme]) {
+ return savedTheme;
+ }
+
+ if (
+ window.matchMedia &&
+ window.matchMedia("(prefers-color-scheme: dark)").matches
+ ) {
+ return "dark";
+ }
+
+ return defaultTheme;
+ });
+
+ const theme = themes[themeName];
+
+ useEffect(() => {
+ const root = document.documentElement;
+
+ root.setAttribute("data-theme", themeName);
+
+ // Set color variables
+ Object.entries(theme.colors).forEach(([key, value]) => {
+ const cssVarName = `--color-${key.replace(/([A-Z])/g, "-$1").toLowerCase()}`;
+ root.style.setProperty(cssVarName, value);
+ });
+
+ // Set typography variables
+ Object.entries(theme.typography).forEach(([key, value]) => {
+ const cssVarName = `--${key
+ .replace(/([A-Z])/g, "-$1")
+ .toLowerCase()
+ .replace("font-", "font-")
+ .replace("size", "")
+ .replace("weight", "")}`;
+ root.style.setProperty(cssVarName, value);
+ });
+
+ // Set spacing variables
+ Object.entries(theme.spacing).forEach(([key, value]) => {
+ const cssVarName = `--${key.replace(/([A-Z])/g, "-$1").toLowerCase()}`;
+ root.style.setProperty(cssVarName, value);
+ });
+
+ // Set radius variables
+ Object.entries(theme.radius).forEach(([key, value]) => {
+ const cssVarName = `--${key.replace(/([A-Z])/g, "-$1").toLowerCase()}`;
+ root.style.setProperty(cssVarName, value);
+ });
+
+ // Set transition variables
+ Object.entries(theme.transitions).forEach(([key, value]) => {
+ const cssVarName = `--${key.replace(/([A-Z])/g, "-$1").toLowerCase()}`;
+ root.style.setProperty(cssVarName, value);
+ });
+
+ // Legacy variables for backward compatibility
+ root.style.setProperty("--text-color", theme.colors.text);
+ root.style.setProperty("--background-color", theme.colors.background);
+
+ localStorage.setItem("theme", themeName);
+ }, [themeName, theme]);
+
+ useEffect(() => {
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
+ const handleChange = (e: MediaQueryListEvent) => {
+ const savedTheme = localStorage.getItem("theme");
+ if (!savedTheme) {
+ setThemeName(e.matches ? "dark" : "light");
+ }
+ };
+
+ mediaQuery.addEventListener("change", handleChange);
+ return () => mediaQuery.removeEventListener("change", handleChange);
+ }, []);
+
+ const setTheme = (name: ThemeName) => {
+ if (themes[name]) {
+ setThemeName(name);
+ }
+ };
+
+ const value: ThemeContextType = {
+ theme,
+ themeName,
+ setTheme,
+ availableThemes: Object.keys(themes) as ThemeName[],
+ };
+
+ return (
+ <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
+ );
+};
+
+export const useTheme = (): ThemeContextType => {
+ const context = useContext(ThemeContext);
+ if (context === undefined) {
+ throw new Error("useTheme must be used within a ThemeProvider");
+ }
+ return context;
+};
diff --git a/packages/prosody-ui/src/themes/themes.ts b/packages/prosody-ui/src/themes/themes.ts
new file mode 100644
index 0000000..5637c97
--- /dev/null
+++ b/packages/prosody-ui/src/themes/themes.ts
@@ -0,0 +1,321 @@
+export type ThemeName =
+ | "light"
+ | "dark"
+ | "sepia"
+ | "noir"
+ | "ocean"
+ | "forest"
+ | "gruvbox";
+
+export interface ThemeColors {
+ primary: string;
+ primaryHover: string;
+ secondary: string;
+ accent: string;
+ accentHover: string;
+ background: string;
+ surface: string;
+ surfaceHover: string;
+ text: string;
+ textSecondary: string;
+ textMuted: string;
+ border: string;
+ borderLight: string;
+ success: string;
+ warning: string;
+ error: string;
+ info: string;
+ link: string;
+ linkHover: string;
+ shadow: string;
+ overlay: string;
+}
+
+export interface ThemeTypography {
+ fontSizeXs: string;
+ fontSizeSm: string;
+ fontSizeMd: string;
+ fontSizeLg: string;
+ fontSizeXl: string;
+ fontWeightNormal: string;
+ fontWeightMedium: string;
+ fontWeightSemibold: string;
+ fontWeightBold: string;
+}
+
+export interface ThemeSpacing {
+ spacingXs: string;
+ spacingSm: string;
+ spacingMd: string;
+ spacingLg: string;
+ spacingXl: string;
+}
+
+export interface ThemeRadius {
+ radiusSm: string;
+ radiusMd: string;
+ radiusLg: string;
+ radiusFull: string;
+}
+
+export interface ThemeTransitions {
+ transitionFast: string;
+ transitionNormal: string;
+ transitionSlow: string;
+}
+
+export interface Theme {
+ name: ThemeName;
+ colors: ThemeColors;
+ typography: ThemeTypography;
+ spacing: ThemeSpacing;
+ radius: ThemeRadius;
+ transitions: ThemeTransitions;
+}
+
+// Common theme properties
+const commonTypography: ThemeTypography = {
+ fontSizeXs: "0.75rem",
+ fontSizeSm: "0.875rem",
+ fontSizeMd: "1rem",
+ fontSizeLg: "1.125rem",
+ fontSizeXl: "1.25rem",
+ fontWeightNormal: "400",
+ fontWeightMedium: "500",
+ fontWeightSemibold: "600",
+ fontWeightBold: "700",
+};
+
+const commonSpacing: ThemeSpacing = {
+ spacingXs: "0.25rem",
+ spacingSm: "0.5rem",
+ spacingMd: "1rem",
+ spacingLg: "1.5rem",
+ spacingXl: "2rem",
+};
+
+const commonRadius: ThemeRadius = {
+ radiusSm: "0.25rem",
+ radiusMd: "0.5rem",
+ radiusLg: "0.75rem",
+ radiusFull: "9999px",
+};
+
+const commonTransitions: ThemeTransitions = {
+ transitionFast: "150ms ease",
+ transitionNormal: "250ms ease",
+ transitionSlow: "350ms ease",
+};
+
+export const themes: Record<ThemeName, Theme> = {
+ light: {
+ name: "light",
+ colors: {
+ primary: "#543fd7",
+ primaryHover: "#4532b8",
+ secondary: "#f39c12",
+ accent: "#2a9d8f",
+ accentHover: "#238b7f",
+ background: "#ffffff",
+ surface: "#f8f9fa",
+ surfaceHover: "#e9ecef",
+ text: "#212529",
+ textSecondary: "#495057",
+ textMuted: "#6c757d",
+ border: "#dee2e6",
+ borderLight: "#e9ecef",
+ success: "#28a745",
+ warning: "#ffc107",
+ error: "#dc3545",
+ info: "#17a2b8",
+ link: "#543fd7",
+ linkHover: "#4532b8",
+ shadow: "rgba(0, 0, 0, 0.1)",
+ overlay: "rgba(0, 0, 0, 0.5)",
+ },
+ typography: commonTypography,
+ spacing: commonSpacing,
+ radius: commonRadius,
+ transitions: commonTransitions,
+ },
+ dark: {
+ name: "dark",
+ colors: {
+ primary: "#7c6ef7",
+ primaryHover: "#9085f9",
+ secondary: "#f39c12",
+ accent: "#2a9d8f",
+ accentHover: "#238b7f",
+ background: "#0d1117",
+ surface: "#161b22",
+ surfaceHover: "#21262d",
+ text: "#c9d1d9",
+ textSecondary: "#8b949e",
+ textMuted: "#6e7681",
+ border: "#30363d",
+ borderLight: "#21262d",
+ success: "#3fb950",
+ warning: "#d29922",
+ error: "#f85149",
+ info: "#58a6ff",
+ link: "#58a6ff",
+ linkHover: "#79b8ff",
+ shadow: "rgba(0, 0, 0, 0.3)",
+ overlay: "rgba(0, 0, 0, 0.7)",
+ },
+ typography: commonTypography,
+ spacing: commonSpacing,
+ radius: commonRadius,
+ transitions: commonTransitions,
+ },
+ sepia: {
+ name: "sepia",
+ colors: {
+ primary: "#8b4513",
+ primaryHover: "#6b3410",
+ secondary: "#d2691e",
+ accent: "#2a9d8f",
+ accentHover: "#238b7f",
+ background: "#f4e8d0",
+ surface: "#ede0c8",
+ surfaceHover: "#e6d9c0",
+ text: "#3e2723",
+ textSecondary: "#5d4037",
+ textMuted: "#6d4c41",
+ border: "#d7ccc8",
+ borderLight: "#e0d5d0",
+ success: "#689f38",
+ warning: "#ff9800",
+ error: "#d32f2f",
+ info: "#0288d1",
+ link: "#8b4513",
+ linkHover: "#6b3410",
+ shadow: "rgba(62, 39, 35, 0.1)",
+ overlay: "rgba(62, 39, 35, 0.5)",
+ },
+ typography: commonTypography,
+ spacing: commonSpacing,
+ radius: commonRadius,
+ transitions: commonTransitions,
+ },
+ noir: {
+ name: "noir",
+ colors: {
+ primary: "#ffffff",
+ primaryHover: "#e0e0e0",
+ secondary: "#808080",
+ accent: "#2a9d8f",
+ accentHover: "#238b7f",
+ background: "#000000",
+ surface: "#0a0a0a",
+ surfaceHover: "#1a1a1a",
+ text: "#ffffff",
+ textSecondary: "#b0b0b0",
+ textMuted: "#808080",
+ border: "#333333",
+ borderLight: "#1a1a1a",
+ success: "#4caf50",
+ warning: "#ff9800",
+ error: "#f44336",
+ info: "#2196f3",
+ link: "#b0b0b0",
+ linkHover: "#ffffff",
+ shadow: "rgba(255, 255, 255, 0.1)",
+ overlay: "rgba(0, 0, 0, 0.9)",
+ },
+ typography: commonTypography,
+ spacing: commonSpacing,
+ radius: commonRadius,
+ transitions: commonTransitions,
+ },
+ ocean: {
+ name: "ocean",
+ colors: {
+ primary: "#006994",
+ primaryHover: "#005577",
+ secondary: "#00acc1",
+ accent: "#2a9d8f",
+ accentHover: "#238b7f",
+ background: "#e1f5fe",
+ surface: "#b3e5fc",
+ surfaceHover: "#81d4fa",
+ text: "#01579b",
+ textSecondary: "#0277bd",
+ textMuted: "#4fc3f7",
+ border: "#81d4fa",
+ borderLight: "#b3e5fc",
+ success: "#00c853",
+ warning: "#ffab00",
+ error: "#d50000",
+ info: "#00b0ff",
+ link: "#0277bd",
+ linkHover: "#01579b",
+ shadow: "rgba(1, 87, 155, 0.1)",
+ overlay: "rgba(1, 87, 155, 0.5)",
+ },
+ typography: commonTypography,
+ spacing: commonSpacing,
+ radius: commonRadius,
+ transitions: commonTransitions,
+ },
+ forest: {
+ name: "forest",
+ colors: {
+ primary: "#2e7d32",
+ primaryHover: "#1b5e20",
+ secondary: "#689f38",
+ accent: "#2a9d8f",
+ accentHover: "#238b7f",
+ background: "#f1f8e9",
+ surface: "#dcedc8",
+ surfaceHover: "#c5e1a5",
+ text: "#1b5e20",
+ textSecondary: "#33691e",
+ textMuted: "#558b2f",
+ border: "#aed581",
+ borderLight: "#c5e1a5",
+ success: "#4caf50",
+ warning: "#ff9800",
+ error: "#f44336",
+ info: "#03a9f4",
+ link: "#388e3c",
+ linkHover: "#2e7d32",
+ shadow: "rgba(27, 94, 32, 0.1)",
+ overlay: "rgba(27, 94, 32, 0.5)",
+ },
+ typography: commonTypography,
+ spacing: commonSpacing,
+ radius: commonRadius,
+ transitions: commonTransitions,
+ },
+ gruvbox: {
+ name: "gruvbox",
+ colors: {
+ primary: "#fe8019",
+ primaryHover: "#d65d0e",
+ secondary: "#fabd2f",
+ accent: "#2a9d8f",
+ accentHover: "#238b7f",
+ background: "#282828",
+ surface: "#3c3836",
+ surfaceHover: "#504945",
+ text: "#ebdbb2",
+ textSecondary: "#d5c4a1",
+ textMuted: "#bdae93",
+ border: "#665c54",
+ borderLight: "#504945",
+ success: "#b8bb26",
+ warning: "#fabd2f",
+ error: "#fb4934",
+ info: "#83a598",
+ link: "#8ec07c",
+ linkHover: "#b8bb26",
+ shadow: "rgba(0, 0, 0, 0.3)",
+ overlay: "rgba(40, 40, 40, 0.8)",
+ },
+ typography: commonTypography,
+ spacing: commonSpacing,
+ radius: commonRadius,
+ transitions: commonTransitions,
+ },
+};
diff --git a/packages/prosody-ui/src/zoom/FullText.tsx b/packages/prosody-ui/src/zoom/FullText.tsx
index ec85f09..9b7fe63 100644
--- a/packages/prosody-ui/src/zoom/FullText.tsx
+++ b/packages/prosody-ui/src/zoom/FullText.tsx
@@ -3,7 +3,7 @@ import { motion, AnimatePresence } from "motion/react";
import Paragraph from "./Paragraph";
import { useZoom } from "./hooks/useZoom";
import { containerVariants, buttonVariants } from "./animations";
-import { NLP } from "sortug-ai";
+import { NLP } from "@sortug/ai";
interface TextFocusMorphProps {
text: string;
diff --git a/packages/prosody-ui/src/zoom/Paragraph.tsx b/packages/prosody-ui/src/zoom/Paragraph.tsx
index c26f806..b149468 100644
--- a/packages/prosody-ui/src/zoom/Paragraph.tsx
+++ b/packages/prosody-ui/src/zoom/Paragraph.tsx
@@ -1,7 +1,7 @@
import React, { memo, useCallback, useEffect, useState } from "react";
import { motion } from "motion/react";
import type { ViewProps, LoadingStatus } from "./logic/types";
-import { NLP } from "sortug-ai";
+import { NLP } from "@sortug/ai";
import Sentence from "./Sentence";
import { paragraphVariants, createHoverEffect } from "./animations";
import { useZoom } from "./hooks/useZoom";
diff --git a/packages/prosody-ui/src/zoom/Sentence.tsx b/packages/prosody-ui/src/zoom/Sentence.tsx
index 1d90346..fc75773 100644
--- a/packages/prosody-ui/src/zoom/Sentence.tsx
+++ b/packages/prosody-ui/src/zoom/Sentence.tsx
@@ -1,7 +1,7 @@
import React, { memo } from "react";
import { motion } from "motion/react";
import type { ViewProps, LoadingStatus } from "./logic/types";
-import { NLP } from "sortug-ai";
+import { NLP } from "@sortug/ai";
import SpacyClause from "./SpacyClause";
import { sentenceVariants, createHoverEffect } from "./animations";
import { useZoom } from "./hooks/useZoom";
diff --git a/packages/prosody-ui/src/zoom/SpacyClause.tsx b/packages/prosody-ui/src/zoom/SpacyClause.tsx
index 6b6f178..c08a291 100644
--- a/packages/prosody-ui/src/zoom/SpacyClause.tsx
+++ b/packages/prosody-ui/src/zoom/SpacyClause.tsx
@@ -1,7 +1,7 @@
import React, { memo, useState } from "react";
import { motion } from "motion/react";
import "./spacy.css";
-import { NLP } from "sortug-ai";
+import { NLP } from "@sortug/ai";
// import { clauseVariants, createHoverEffect } from "./animations";
// import { useZoom } from "./hooks/useZoom";
diff --git a/packages/prosody-ui/src/zoom/logic/types.ts b/packages/prosody-ui/src/zoom/logic/types.ts
index bea68ff..fd72601 100644
--- a/packages/prosody-ui/src/zoom/logic/types.ts
+++ b/packages/prosody-ui/src/zoom/logic/types.ts
@@ -1,4 +1,4 @@
-import type { NLP } from "sortug-ai";
+import type { NLP } from "@sortug/ai";
export type ViewLevel =
| "text"