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