From a3f24ea79b14394b24c4b60a010651eb29eeb872 Mon Sep 17 00:00:00 2001 From: polwex Date: Thu, 29 May 2025 12:10:22 +0700 Subject: glorious new db --- src/components/Flashcard/ServerCard.tsx | 43 +++-- src/components/Flashcard/Syllable.tsx | 44 +++++ src/components/Flashcard/SyllableModal.tsx | 110 ++++++++++++ src/components/Flashcard/SyllableSpan.tsx | 45 +++++ src/components/lang/ThaiPhonology.tsx | 250 ++++++++++++++++++++++++++++ src/components/tones/ToneSelectorClient.tsx | 199 ++++++++++++++++++++++ src/components/ui/skeleton.tsx | 13 ++ 7 files changed, 692 insertions(+), 12 deletions(-) create mode 100644 src/components/Flashcard/Syllable.tsx create mode 100644 src/components/Flashcard/SyllableModal.tsx create mode 100644 src/components/Flashcard/SyllableSpan.tsx create mode 100644 src/components/lang/ThaiPhonology.tsx create mode 100644 src/components/tones/ToneSelectorClient.tsx create mode 100644 src/components/ui/skeleton.tsx (limited to 'src/components') diff --git a/src/components/Flashcard/ServerCard.tsx b/src/components/Flashcard/ServerCard.tsx index d377dce..df37ba8 100644 --- a/src/components/Flashcard/ServerCard.tsx +++ b/src/components/Flashcard/ServerCard.tsx @@ -23,13 +23,21 @@ import { WordData, } from "@/zoom/logic/types"; import { CardResponse } from "@/lib/types/cards"; -import { thaiData } from "@/pages/api/nlp"; +import { thaiData } from "@/lib/calls/nlp"; import { getRandomHexColor } from "@/lib/utils"; import { BookmarkIconito } from "./BookmarkButton"; +import SyllableSpan from "./SyllableSpan"; +import SyllableCard from "./Syllable"; -export async function CardFront({ data }: { data: CardResponse }) { +export async function CardFront({ + data, + needFetch = true, +}: { + data: CardResponse; + needFetch?: boolean; +}) { // const extraData = data.expression.lang - const extraData = await thaiData(data.expression.spelling); + const extraData = needFetch ? await thaiData(data.expression.spelling) : []; // console.log({ extraData }); return ( @@ -42,15 +50,26 @@ export async function CardFront({ data }: { data: CardResponse }) { } >

- {extraData[0]?.syllables.map((syl, i) => ( - - {syl} - - ))} + {needFetch ? ( + extraData[0]?.syllables.map((syl, i) => ( + // + // {syl} + // + + )) + ) : ( +

+ {data.expression.spelling} +

+ )}

diff --git a/src/components/Flashcard/Syllable.tsx b/src/components/Flashcard/Syllable.tsx new file mode 100644 index 0000000..e470a4b --- /dev/null +++ b/src/components/Flashcard/Syllable.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { syllableAction, thaiAnalysis } from "@/actions/lang"; +import { CardResponse } from "@/lib/types/cards"; +import { ReactNode, useState, useTransition } from "react"; +import { Spinner } from "../ui/spinner"; +import Modal from "@/components/Modal"; +import { getRandomHexColor } from "@/lib/utils"; + +const SyllableCard: React.FC<{ data: CardResponse }> = ({ data }) => { + return ( +
+

+ {data.expression.spelling} +

+ +
+ ); +}; + +export default SyllableCard; + +const IpaDisplay = ({ + ipaEntries, +}: { + ipaEntries: Array<{ ipa: string; tags?: string[] }>; +}) => { + if (!ipaEntries || ipaEntries.length === 0) return null; + return ( +
+ {ipaEntries.map((entry, index) => { + const tags = entry.tags ? entry.tags : []; + return ( + + {entry.ipa}{" "} + {tags.length > 0 && ( + ({tags.join(", ")}) + )} + + ); + })} +
+ ); +}; diff --git a/src/components/Flashcard/SyllableModal.tsx b/src/components/Flashcard/SyllableModal.tsx new file mode 100644 index 0000000..a00fd10 --- /dev/null +++ b/src/components/Flashcard/SyllableModal.tsx @@ -0,0 +1,110 @@ +// This is a Server Component +import React from "react"; +import db from "@/lib/db"; +import { + Card, + CardHeader, + CardDescription, + CardContent, + CardFooter, + CardTitle, +} from "@/components/ui/card"; +import { NLP } from "sortug-ai"; +import { + BookOpen, + Volume2, + Link as LinkIcon, + ChevronDown, + ChevronUp, + Search, + Info, + MessageSquareQuote, + Tags, + ListTree, + Lightbulb, +} from "lucide-react"; +import { + Example, + SubSense, + RelatedEntry, + Sense, + WordData, +} from "@/zoom/logic/types"; +import { isTonal } from "@/lib/lang/utils"; + +type WordProps = { text: string; lang: string }; +export default async function (props: WordProps) { + const { text, lang } = props; + const data = db.fetchWordBySpelling(text, lang); + + if (!data) return

oh...

; + console.log(data.senses[0]); + return ( + + + +

{text}

+
+ + + +
+ + {isTonal(text) ? : } + + +
+ ); + // return ( + //
+ //

{word}

+ //

${word.}

+ //

{word}

+ //

+ // Content rendered on the server at: {new Date().toLocaleTimeString()} + //

+ //
+ // ); +} + +// Helper component for IPA display +const IpaDisplay = ({ + ipaEntries, +}: { + ipaEntries: Array<{ ipa: string; tags?: string[] }>; +}) => { + if (!ipaEntries || ipaEntries.length === 0) return null; + return ( +
+ {ipaEntries.map((entry, index) => { + const tags = entry.tags ? entry.tags : []; + return ( + + {entry.ipa}{" "} + {tags.length > 0 && ( + ({tags.join(", ")}) + )} + + ); + })} + +
+ ); +}; + +function Tones({ text, lang }: WordProps) { + return
; +} +function NotTones({ text, lang }: WordProps) { + return
; +} diff --git a/src/components/Flashcard/SyllableSpan.tsx b/src/components/Flashcard/SyllableSpan.tsx new file mode 100644 index 0000000..445895e --- /dev/null +++ b/src/components/Flashcard/SyllableSpan.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { syllableAction, thaiAnalysis } from "@/actions/lang"; +import { CardResponse } from "@/lib/types/cards"; +import { ReactNode, useState, useTransition } from "react"; +import { Spinner } from "../ui/spinner"; +import Modal from "@/components/Modal"; +import { getRandomHexColor } from "@/lib/utils"; + +const SyllableSpan: React.FC<{ spelling: string; lang: string }> = ({ + spelling, + lang, +}) => { + const [modalContent, setModalContent] = useState(null); + + const closeModal = () => setModalContent(null); + + const [isPending, startTransition] = useTransition(); + const handleClick = (e: React.MouseEvent) => { + e.stopPropagation(); + startTransition(async () => { + const modal = await syllableAction(spelling, lang); + setModalContent(modal); + }); + }; + + return ( + <> + + {spelling} + + {modalContent && ( + + {modalContent} + + )} + + ); +}; + +export default SyllableSpan; diff --git a/src/components/lang/ThaiPhonology.tsx b/src/components/lang/ThaiPhonology.tsx new file mode 100644 index 0000000..199d0b8 --- /dev/null +++ b/src/components/lang/ThaiPhonology.tsx @@ -0,0 +1,250 @@ +// import React from "react"; + +// /** +// * ThaiConsonantGrid – a visual table of Thai consonants modelled after the +// * traditional Sanskrit‑style chart. Each cell shows the Thai glyph and its +// * IPA. Rows are places of articulation, columns are manners. Colours follow +// * the pedagogical convention from the reference screenshot. +// */ +// export default function ThaiConsonantGrid() { +// /** Column headers in display order */ +// const cols = [ +// "stopped", +// "aspirated", +// "voiced", +// "voiced‑aspirated", +// "nasal", +// "semiVowel", +// "sibilant", +// "H‑aspirate", +// "throatBase", +// "others", +// ] as const; + +// /** Row headers in display order */ +// const rows = [ +// "Deep Throat", +// "guttural", +// "palatal", +// "cerebral", +// "dental", +// "labial", +// ] as const; +// type ArticulationPoint = +// | "bilabial" +// | "labiodental" +// | "dental" +// | "alveolar" +// | "postalveolar" +// | "retroflex" +// | "palatal" +// | "velar" +// | "uvular" +// | "pharyngeal" +// | "glottal"; +// type ArticulationMode = +// | "plosive" +// | "nasal" +// | "trill" +// | "flap" +// | "fricative" +// | "affricate" +// | "lateral fricative" +// | "approximant" +// | "lateral approximant"; +// type Voicing = "unvoiced" | "voiced" | "aspirated" | "voiced aspirated"; +// type VowelHeight = "high" | "close-mid" | "open-mid" | "open"; +// type VowelFront = "front" | "central" | "back"; +// type VowelRound = "rounded" | "unrounded"; + +// /** +// * Minimal description for each consonant we want to render. Position is +// * given by its (rowIdx, colIdx). The colour is a Tailwind background class +// * so you can tweak the palette in one place. +// */ +// interface Cell { +// row: number; // 0‑based index into rows +// col: number; // 0‑based index into cols +// glyph: string; +// ipa: string; +// colour: string; // Tailwind bg‑* class +// highlight?: boolean; // optional neon border +// class: "high" | "mid" | "low"; +// } + +// const cells: Cell[] = [ +// // ───────────────────── guttural row (index 1) ────────────────────── +// { +// row: 1, +// col: 0, +// glyph: "ก", +// class: "high", +// ipa: "/k/", +// colour: "bg-sky-500", +// }, +// { +// row: 1, +// col: 1, +// glyph: "ข", +// class: "high", +// ipa: "/kʰ/", +// colour: "bg-sky-500", +// }, +// { +// row: 1, +// col: 1, +// glyph: "ฃ", +// class: "high", +// ipa: "/kʰ/", +// colour: "bg-sky-500", +// }, +// { +// row: 1, +// col: 2, +// glyph: "ค", +// class: "high", +// ipa: "/kʰ/", +// colour: "bg-sky-500", +// }, +// { +// row: 1, +// col: 2, +// glyph: "ฅ", +// class: "high", +// ipa: "/kʰ/", +// colour: "bg-sky-500", +// }, +// { +// row: 1, +// col: 2, +// glyph: "ฆ", +// class: "high", +// ipa: "/kʰ/", +// colour: "bg-sky-500", +// }, +// { +// row: 1, +// col: 4, +// glyph: "ง", +// ipa: "/ŋ/", +// colour: "bg-sky-500", +// highlight: true, +// }, + +// // ───────────────────── palatal row (index 2) ─────────────────────── +// { row: 2, col: 0, glyph: "จ", ipa: "/tɕ/", colour: "bg-pink-500" }, +// { row: 2, col: 1, glyph: "ฉ", ipa: "/tɕʰ/", colour: "bg-pink-500" }, +// { row: 2, col: 2, glyph: "ช", ipa: "/tɕʰ/", colour: "bg-pink-500" }, +// { row: 2, col: 2, glyph: "ซ", ipa: "/s/", colour: "bg-pink-500" }, +// { row: 2, col: 3, glyph: "ฌ", ipa: "/tɕʰ/", colour: "bg-pink-500" }, +// { row: 2, col: 5, glyph: "ญ", ipa: "/j/", colour: "bg-pink-500" }, + +// // ───────────────────── cerebral row (index 3) ────────────────────── +// { row: 3, col: 0, glyph: "ฎ", ipa: "/d/", colour: "bg-emerald-700" }, +// { row: 3, col: 0, glyph: "ฐ", ipa: "/t/", colour: "bg-emerald-700" }, +// { row: 3, col: 1, glyph: "ฏ", ipa: "/tʰ/", colour: "bg-emerald-700" }, +// { +// row: 3, +// col: 4, +// glyph: "ฑ", +// ipa: "/tʰ or d/", +// colour: "bg-emerald-700", +// highlight: true, +// }, +// { row: 3, col: 3, glyph: "ฒ", ipa: "/tʰ/", colour: "bg-emerald-700" }, +// { row: 3, col: 4, glyph: "ณ", ipa: "/n/", colour: "bg-emerald-700" }, +// { row: 3, col: 5, glyph: "ศ", ipa: "/s/", colour: "bg-emerald-700" }, +// { row: 3, col: 5, glyph: "ษ", ipa: "/s/", colour: "bg-emerald-700" }, + +// // ───────────────────── dental row (index 4) ──────────────────────── +// { row: 4, col: 0, glyph: "ต", ipa: "/d/", colour: "bg-emerald-600" }, +// { row: 4, col: 0, glyph: "ถ", ipa: "/t/", colour: "bg-emerald-600" }, +// { row: 4, col: 1, glyph: "ท", ipa: "/tʰ/", colour: "bg-emerald-600" }, +// { row: 4, col: 2, glyph: "ธ", ipa: "/tʰ/", colour: "bg-emerald-600" }, +// { row: 4, col: 4, glyph: "น", ipa: "/n/", colour: "bg-emerald-600" }, +// { row: 4, col: 6, glyph: "ส", ipa: "/s/", colour: "bg-emerald-600" }, + +// // ───────────────────── labial row (index 5) ──────────────────────── +// { row: 5, col: 0, glyph: "บ", ipa: "/b/", colour: "bg-orange-500" }, +// { row: 5, col: 0, glyph: "ป", ipa: "/p/", colour: "bg-orange-500" }, +// { row: 5, col: 1, glyph: "ผ", ipa: "/pʰ/", colour: "bg-orange-500" }, +// { row: 5, col: 2, glyph: "พ", ipa: "/pʰ/", colour: "bg-orange-500" }, +// { row: 5, col: 2, glyph: "ฟ", ipa: "/f/", colour: "bg-orange-500" }, +// { row: 5, col: 3, glyph: "ภ", ipa: "/pʰ/", colour: "bg-orange-500" }, +// { row: 5, col: 4, glyph: "ม", ipa: "/m/", colour: "bg-orange-500" }, +// { +// row: 5, +// col: 9, +// glyph: "ฟฬ", +// ipa: "/l/", +// colour: "bg-emerald-600", +// highlight: true, +// }, + +// // ───────────────────── extra column (index^?) – throat + others ───── +// { row: 1, col: 7, glyph: "ห", ipa: "/h/", colour: "bg-gray-400" }, +// { row: 1, col: 8, glyph: "อ", ipa: "/ʔ/", colour: "bg-gray-400" }, +// ]; + +// return ( +//
+// {/* Column header */} +//
+// {/* top‑left empty cell */} +//
+// {cols.map((c) => ( +//
+// {c} +//
+// ))} + +// {/* rows */} +// {rows.map((rowLabel, ri) => ( +// +// {/* row header */} +//
+// {rowLabel} +//
+// {/* cells within the row */} +// {cols.map((_, ci) => { +// // We may have multiple consonants per slot; gather them. +// const here = cells.filter((c) => c.row === ri && c.col === ci); +// if (here.length === 0) +// return
; + +// return ( +//
c.highlight) +// ? "ring-2 ring-green-400" +// : "", +// ].join(" ")} +// > +// {here.map((c, i) => ( +// +// +// {c.glyph} +// +// {c.ipa} +// +// ))} +//
+// ); +// })} +// +// ))} +//
+//
+// ); +// } diff --git a/src/components/tones/ToneSelectorClient.tsx b/src/components/tones/ToneSelectorClient.tsx new file mode 100644 index 0000000..0ee9433 --- /dev/null +++ b/src/components/tones/ToneSelectorClient.tsx @@ -0,0 +1,199 @@ +'use client'; + +import { useState, useEffect, useTransition } from 'react'; +import { WordData } from '@/zoom/logic/types'; +import { fetchWordsByToneAndSyllables } from '@/actions/tones'; +import { Button } from '@/components/ui/button'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Label } from '@/components/ui/label'; +import { Skeleton } from '@/components/ui/skeleton'; // For loading state + +// Helper to display tones prominently +const ProminentToneDisplay = ({ wordData }: { wordData: WordData }) => { + if (!wordData.prosody || !Array.isArray(wordData.prosody)) { + return

No prosody data

; + } + + return ( +
+

{wordData.spelling}

+
+ {wordData.prosody.map((p, index) => ( +
+

Syllable {index + 1}

+

{p.tone ?? '?'}

+
+ ))} +
+ {wordData.ipa && wordData.ipa.length > 0 && ( +

+ {wordData.ipa.map(i => i.ipa).join(' / ')} +

+ )} +
+ ); +}; + + +export default function ToneSelectorClient({ initialWord }: { initialWord: WordData | null }) { + const [currentWord, setCurrentWord] = useState(initialWord); + const [syllableCount, setSyllableCount] = useState(initialWord?.syllables || 1); + const [selectedTones, setSelectedTones] = useState<(number | null)[]>( + initialWord?.prosody?.map(p => p.tone ?? null) || [null] + ); + const [isLoading, startTransition] = useTransition(); + + useEffect(() => { + // Adjust selectedTones array length when syllableCount changes + setSelectedTones(prevTones => { + const newTones = Array(syllableCount).fill(null); + for (let i = 0; i < Math.min(prevTones.length, syllableCount); i++) { + newTones[i] = prevTones[i]; + } + return newTones; + }); + }, [syllableCount]); + + const handleFetchWord = () => { + startTransition(async () => { + const word = await fetchWordsByToneAndSyllables(syllableCount, selectedTones); + setCurrentWord(word); + }); + }; + + const handleSyllableCountChange = (value: string) => { + const count = parseInt(value, 10); + if (!isNaN(count) && count > 0 && count <= 5) { // Max 5 syllables for simplicity + setSyllableCount(count); + } + }; + + const handleToneChange = (syllableIndex: number, value: string) => { + const tone = value === 'any' ? null : parseInt(value, 10); + setSelectedTones(prevTones => { + const newTones = [...prevTones]; + newTones[syllableIndex] = tone; + return newTones; + }); + }; + + const thaiTones = [ + { value: '1', label: '1 (Mid)' }, + { value: '2', label: '2 (Low)' }, + { value: '3', label: '3 (Falling)' }, + { value: '4', label: '4 (High)' }, + { value: '5', label: '5 (Rising)' }, + ]; + + return ( +
+ + + Thai Tone Explorer + Select syllable count and tones to find Thai words. + + +
+ + +
+ + {Array.from({ length: syllableCount }).map((_, index) => ( +
+ + +
+ ))} +
+ + + +
+ + {isLoading && !currentWord && ( + + + + + + + + + )} + + {!isLoading && currentWord && ( + + + Current Word + + + + {/* You can add more details from WordData here if needed, like definitions */} + {currentWord.senses && currentWord.senses.length > 0 && ( +
+

Meanings:

+ {currentWord.senses.map((sense, sIdx) => ( +
+

{sense.pos}

+ {sense.senses && Array.isArray(sense.senses) && sense.senses.map((subSense, ssIdx) => ( + subSense.glosses && Array.isArray(subSense.glosses) && subSense.glosses.map((gloss: string, gIdx: number) => ( +

- {gloss}

+ )) + ))} +
+ ))} +
+ )} +
+
+ )} + + {!isLoading && !currentWord && ( + + + No Word Found + + +

+ Could not find a Thai word matching your criteria. Try different selections. +

+
+
+ )} +
+ ); +} diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..32ea0ef --- /dev/null +++ b/src/components/ui/skeleton.tsx @@ -0,0 +1,13 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Skeleton } -- cgit v1.2.3