From 175ddca375cef765cec8ca5bbc527a205c40bf25 Mon Sep 17 00:00:00 2001 From: polwex Date: Tue, 3 Jun 2025 15:41:31 +0700 Subject: preeeeettty much done FUCK YES --- src/components/tones/ToneSelectorClient.tsx | 405 ++++++++++++++++++---------- src/components/ui/select.tsx | 42 +-- 2 files changed, 283 insertions(+), 164 deletions(-) (limited to 'src/components') diff --git a/src/components/tones/ToneSelectorClient.tsx b/src/components/tones/ToneSelectorClient.tsx index 0ee9433..8a0327c 100644 --- a/src/components/tones/ToneSelectorClient.tsx +++ b/src/components/tones/ToneSelectorClient.tsx @@ -1,52 +1,240 @@ -'use client'; +"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 +import { useState, useEffect, useTransition, useRef } from "react"; +import { WordData } from "@/zoom/logic/types"; +import { + fetchWordsByToneAndSyllables, + mutateToneSelection, +} 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 +import { MutationOrder, ToneQuery } from "@/lib/types/phonetics"; +import { ProsodySyllable } from "@/lib/types/cards"; +import { ArrowLeft, ArrowRight, Loader2, Volume2 } from "lucide-react"; +function getColorByTone(tone: string): string { + if (tone === "mid") return "blue"; + if (tone === "low") return "green"; + if (tone === "falling") return "gold"; + if (tone === "high") return "purple"; + if (tone === "rising") return "black"; + else return "black"; +} // Helper to display tones prominently -const ProminentToneDisplay = ({ wordData }: { wordData: WordData }) => { - if (!wordData.prosody || !Array.isArray(wordData.prosody)) { - return

No prosody data

; +const ProminentToneDisplay = ({ word }: { word: any }) => { + const tones: string[] = word.tone_sequence.split(","); + const syls: string[] = word.syl_seq.split(","); + const [isPending, startTransition] = useTransition(); + function mutateWord(idx: number) { + console.log("changing", idx); + const mutationOrder: MutationOrder = syls.map((s, i) => { + if (idx === i) return { change: tones[idx]! }; + else return { keep: syls[i]! }; + }); + console.log("hey hey", word); + startTransition(async () => { + const words = await mutateToneSelection(mutationOrder); + console.log({ words }); + // setCurrentWord(word); + }); + } + // playing audio + // const sourceRef = useRef(null); + const audioRef = useRef(null); + + async function playAudio() { + // setLoading(true); + // const audioContext = new (window.AudioContext || + // (window as any).webkitAudioContext)(); + // const response = await fetch(audioUrl); + // const arrayBuffer = await response.arrayBuffer(); + // const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); + // if (audioContext && audioBuffer) { + // setLoading(false); + // const source = audioContext.createBufferSource(); + // source.buffer = audioBuffer; + // source.connect(audioContext.destination); + // source.start(); + // sourceRef.current = source; + // } + const res = await fetch(`/api/tts?word=${word.spelling}&lang=thai`); + const audioBlob = await res.blob(); + const audioURL = URL.createObjectURL(audioBlob); + if (audioRef.current) { + audioRef.current.src = audioURL; + audioRef.current.play(); + } } return (
-

{wordData.spelling}

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

Syllable {index + 1}

-

{p.tone ?? '?'}

-
+

+ {syls.map((syl: string, idx: number) => ( + mutateWord(idx)} + style={{ color: getColorByTone(tones[idx]!) }} + className="cursor-pointer hover:text-gray-700" + > + {syl} + ))} +

+
+

{word.ipa}

+ + {isPending && } +
- {wordData.ipa && wordData.ipa.length > 0 && ( -

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

- )}
); }; +export default function ToneSelectorClient({ + initialData, + initialTones, +}: { + initialData: any[]; + initialTones: ToneQuery; +}) { + const [data, setData] = useState(initialData); + const [currentIdx, setCurrentIdx] = useState(0); + const [isLoading, startTransition] = useTransition(); + const [selectedTones, setTones] = useState(initialTones); + + function goPrev() { + setCurrentIdx((i) => (i === 0 ? 0 : i - 1)); + } + function goNext() { + setCurrentIdx((i) => (i === data.length - 1 ? data.length - 1 : i + 1)); + } + + const handleFetch = () => { + startTransition(async () => { + const words = await fetchWordsByToneAndSyllables(selectedTones); + setData(words); + }); + }; -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] + return ( +
+ + + +
); - const [isLoading, startTransition] = useTransition(); +} +type IProps = { + isLoading: boolean; + currentWord: any; + goPrev: () => void; + goNext: () => void; +}; +function Inner({ isLoading, currentWord, goPrev, goNext }: IProps) { + return isLoading ? ( + + + + + + + + + + + ) : currentWord ? ( + + + Current Word + + + + {/* You can add more details from WordData here if needed, like definitions */} + + + + + + + ) : ( + + + No Word Found + + +

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

+
+
+ ); +} + +type ToneFormProps = { + isLoading: boolean; + handleFetch: (tones: ToneQuery) => void; + selectedTones: ToneQuery; + setTones: React.Dispatch>; +}; +function ToneForm({ + selectedTones, + setTones, + isLoading, + handleFetch, +}: ToneFormProps) { + const thaiTones = [ + { value: "mid", label: "1 (Mid)" }, + { value: "low", label: "2 (Low)" }, + { value: "falling", label: "3 (Falling)" }, + { value: "high", label: "4 (High)" }, + { value: "rising", label: "5 (Rising)" }, + ]; + const [syllableCount, setSyllableCount] = useState(2); + function decrSyl() { + setSyllableCount((s) => (s <= 1 ? 1 : s - 1)); + } + function incrSyl() { + setSyllableCount((s) => (s >= 5 ? 5 : s + 1)); + } useEffect(() => { // Adjust selectedTones array length when syllableCount changes - setSelectedTones(prevTones => { + setTones((prevTones) => { const newTones = Array(syllableCount).fill(null); for (let i = 0; i < Math.min(prevTones.length, syllableCount); i++) { newTones[i] = prevTones[i]; @@ -55,79 +243,51 @@ export default function ToneSelectorClient({ initialWord }: { initialWord: WordD }); }, [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 + 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 tone = value === "any" ? null : value; + setTones((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. - - -
- - -
- + + + 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/select.tsx b/src/components/ui/select.tsx index b624a5b..23e7161 100644 --- a/src/components/ui/select.tsx +++ b/src/components/ui/select.tsx @@ -1,25 +1,25 @@ -import * as React from "react" -import * as SelectPrimitive from "@radix-ui/react-select" -import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Select({ ...props }: React.ComponentProps) { - return + return ; } function SelectGroup({ ...props }: React.ComponentProps) { - return + return ; } function SelectValue({ ...props }: React.ComponentProps) { - return + return ; } function SelectTrigger({ @@ -32,7 +32,7 @@ function SelectTrigger({ data-slot="select-trigger" className={cn( "border-input data-[placeholder]:text-muted-foreground aria-invalid:border-destructive ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex h-9 w-full items-center justify-between rounded-md border bg-transparent px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:focus-visible:ring-0 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&>span]:line-clamp-1", - className + className, )} {...props} > @@ -41,7 +41,7 @@ function SelectTrigger({ - ) + ); } function SelectContent({ @@ -58,7 +58,7 @@ function SelectContent({ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border shadow-md", position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", - className + className, )} position={position} {...props} @@ -68,7 +68,7 @@ function SelectContent({ className={cn( "p-1", position === "popper" && - "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1" + "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1", )} > {children} @@ -76,7 +76,7 @@ function SelectContent({ - ) + ); } function SelectLabel({ @@ -89,7 +89,7 @@ function SelectLabel({ className={cn("px-2 py-1.5 text-sm font-semibold", className)} {...props} /> - ) + ); } function SelectItem({ @@ -102,7 +102,7 @@ function SelectItem({ data-slot="select-item" className={cn( "focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", - className + className, )} {...props} > @@ -113,7 +113,7 @@ function SelectItem({ {children} - ) + ); } function SelectSeparator({ @@ -126,7 +126,7 @@ function SelectSeparator({ className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)} {...props} /> - ) + ); } function SelectScrollUpButton({ @@ -138,13 +138,13 @@ function SelectScrollUpButton({ data-slot="select-scroll-up-button" className={cn( "flex cursor-default items-center justify-center py-1", - className + className, )} {...props} > - ) + ); } function SelectScrollDownButton({ @@ -156,13 +156,13 @@ function SelectScrollDownButton({ data-slot="select-scroll-down-button" className={cn( "flex cursor-default items-center justify-center py-1", - className + className, )} {...props} > - ) + ); } export { @@ -176,4 +176,4 @@ export { SelectSeparator, SelectTrigger, SelectValue, -} +}; -- cgit v1.2.3