diff options
Diffstat (limited to 'src/components/tones/ToneSelectorClient.tsx')
-rw-r--r-- | src/components/tones/ToneSelectorClient.tsx | 405 |
1 files changed, 262 insertions, 143 deletions
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 <p className="text-gray-500">No prosody data</p>; +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<AudioBufferSourceNode>(null); + const audioRef = useRef<HTMLAudioElement>(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 ( <div className="flex flex-col items-center mb-4"> - <h1 className="text-6xl font-bold text-blue-600 mb-2">{wordData.spelling}</h1> - <div className="flex space-x-4"> - {wordData.prosody.map((p, index) => ( - <div key={index} className="text-center"> - <p className="text-sm text-gray-500">Syllable {index + 1}</p> - <p className="text-5xl font-semibold text-indigo-500">{p.tone ?? '?'}</p> - </div> + <h1 className="text-6xl font-bold mb-2"> + {syls.map((syl: string, idx: number) => ( + <span + key={syl + idx} + onClick={() => mutateWord(idx)} + style={{ color: getColorByTone(tones[idx]!) }} + className="cursor-pointer hover:text-gray-700" + > + {syl} + </span> ))} + </h1> + <div className="mt-4 space-x-4"> + <p className="ipa text-xl text-gray-700 mt-2">{word.ipa}</p> + <button + className="p-1 text-blue-500 hover:text-blue-700 transition-colors" + title="Pronounce" + onClick={playAudio} + > + <Volume2 size={20} /> + </button> + {isPending && <Loader2 />} + <audio ref={audioRef} /> + <p className="ipa text-xl text-gray-700 mt-2">{word.frequency}</p> + <p className="ipa text-xl text-gray-700 mt-2">{word.word_id}</p> </div> - {wordData.ipa && wordData.ipa.length > 0 && ( - <p className="text-xl text-gray-700 mt-2"> - {wordData.ipa.map(i => i.ipa).join(' / ')} - </p> - )} </div> ); }; +export default function ToneSelectorClient({ + initialData, + initialTones, +}: { + initialData: any[]; + initialTones: ToneQuery; +}) { + const [data, setData] = useState<any[]>(initialData); + const [currentIdx, setCurrentIdx] = useState(0); + const [isLoading, startTransition] = useTransition(); + const [selectedTones, setTones] = useState<ToneQuery>(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<WordData | null>(initialWord); - const [syllableCount, setSyllableCount] = useState<number>(initialWord?.syllables || 1); - const [selectedTones, setSelectedTones] = useState<(number | null)[]>( - initialWord?.prosody?.map(p => p.tone ?? null) || [null] + return ( + <div className="container mx-auto p-4 max-w-2xl"> + <ToneForm + isLoading={isLoading} + handleFetch={handleFetch} + selectedTones={selectedTones} + setTones={setTones} + /> + + <Inner + isLoading={isLoading} + currentWord={data[currentIdx]} + goPrev={goPrev} + goNext={goNext} + /> + </div> ); - const [isLoading, startTransition] = useTransition(); +} +type IProps = { + isLoading: boolean; + currentWord: any; + goPrev: () => void; + goNext: () => void; +}; +function Inner({ isLoading, currentWord, goPrev, goNext }: IProps) { + return isLoading ? ( + <Card> + <CardHeader> + <Skeleton className="h-12 w-3/4" /> + </CardHeader> + <CardContent className="space-y-4"> + <Skeleton className="h-8 w-1/2" /> + <Skeleton className="h-20 w-full" /> + <Skeleton className="h-6 w-full" /> + </CardContent> + </Card> + ) : currentWord ? ( + <Card> + <CardHeader> + <CardTitle className="text-center">Current Word</CardTitle> + </CardHeader> + <CardContent> + <ProminentToneDisplay word={currentWord} /> + {/* You can add more details from WordData here if needed, like definitions */} + </CardContent> + <CardFooter className="justify-between"> + <ArrowLeft onClick={goPrev} /> + <ArrowRight onClick={goNext} /> + </CardFooter> + </Card> + ) : ( + <Card> + <CardHeader> + <CardTitle className="text-center">No Word Found</CardTitle> + </CardHeader> + <CardContent> + <p className="text-center text-gray-600"> + Could not find a Thai word matching your criteria. Try different + selections. + </p> + </CardContent> + </Card> + ); +} + +type ToneFormProps = { + isLoading: boolean; + handleFetch: (tones: ToneQuery) => void; + selectedTones: ToneQuery; + setTones: React.Dispatch<React.SetStateAction<ToneQuery>>; +}; +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<number>(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 ( - <div className="container mx-auto p-4 max-w-2xl"> - <Card className="mb-6"> - <CardHeader> - <CardTitle>Thai Tone Explorer</CardTitle> - <CardDescription>Select syllable count and tones to find Thai words.</CardDescription> - </CardHeader> - <CardContent className="space-y-6"> - <div> - <Label htmlFor="syllable-count" className="text-lg font-medium">Number of Syllables</Label> - <Select - value={syllableCount.toString()} - onValueChange={handleSyllableCountChange} - > - <SelectTrigger id="syllable-count" className="w-full md:w-1/2 mt-1"> - <SelectValue placeholder="Select number of syllables" /> - </SelectTrigger> - <SelectContent> - {[1, 2, 3, 4, 5].map(num => ( - <SelectItem key={num} value={num.toString()}> - {num} Syllable{num > 1 ? 's' : ''} - </SelectItem> - ))} - </SelectContent> - </Select> - </div> - + <Card className="mb-6"> + <CardHeader> + <CardTitle>Thai Tone Explorer</CardTitle> + <CardDescription> + Select syllable count and tones to find Thai words. + </CardDescription> + </CardHeader> + <CardContent className="space-y-6"> + <div className="flex gap-10 justify-center"> {Array.from({ length: syllableCount }).map((_, index) => ( - <div key={index}> - <Label htmlFor={`tone-select-${index}`} className="text-lg font-medium"> - Tone for Syllable {index + 1} - </Label> + <div key={index} className="w-fit"> <Select - value={selectedTones[index]?.toString() || 'any'} + value={selectedTones[index]?.toString() || "any"} onValueChange={(value) => handleToneChange(index, value)} > - <SelectTrigger id={`tone-select-${index}`} className="w-full md:w-1/2 mt-1"> - <SelectValue placeholder={`Select tone for syllable ${index + 1}`} /> + <SelectTrigger + id={`tone-select-${index}`} + className="w-full md:w-full mt-1" + > + <SelectValue + className="w-full" + placeholder={`Select tone for syllable ${index + 1}`} + /> </SelectTrigger> - <SelectContent> + <SelectContent className="lolol md:w-full bg-white w-full"> <SelectItem value="any">Any Tone</SelectItem> - {thaiTones.map(tone => ( + {thaiTones.map((tone) => ( <SelectItem key={tone.value} value={tone.value}> {tone.label} </SelectItem> @@ -136,64 +296,23 @@ export default function ToneSelectorClient({ initialWord }: { initialWord: WordD </Select> </div> ))} - </CardContent> - <CardFooter> - <Button onClick={handleFetchWord} disabled={isLoading} className="w-full md:w-auto"> - {isLoading ? 'Searching...' : 'Find Word'} - </Button> - </CardFooter> - </Card> - - {isLoading && !currentWord && ( - <Card> - <CardHeader><Skeleton className="h-12 w-3/4" /></CardHeader> - <CardContent className="space-y-4"> - <Skeleton className="h-8 w-1/2" /> - <Skeleton className="h-20 w-full" /> - <Skeleton className="h-6 w-full" /> - </CardContent> - </Card> - )} - - {!isLoading && currentWord && ( - <Card> - <CardHeader> - <CardTitle className="text-center">Current Word</CardTitle> - </CardHeader> - <CardContent> - <ProminentToneDisplay wordData={currentWord} /> - {/* You can add more details from WordData here if needed, like definitions */} - {currentWord.senses && currentWord.senses.length > 0 && ( - <div className="mt-4 pt-4 border-t"> - <h3 className="text-lg font-semibold mb-2">Meanings:</h3> - {currentWord.senses.map((sense, sIdx) => ( - <div key={sIdx} className="mb-2 p-2 border rounded bg-gray-50"> - <p className="font-medium text-indigo-600">{sense.pos}</p> - {sense.senses && Array.isArray(sense.senses) && sense.senses.map((subSense, ssIdx) => ( - subSense.glosses && Array.isArray(subSense.glosses) && subSense.glosses.map((gloss: string, gIdx: number) => ( - <p key={`${ssIdx}-${gIdx}`} className="text-sm text-gray-700 ml-2">- {gloss}</p> - )) - ))} - </div> - ))} - </div> - )} - </CardContent> - </Card> - )} - - {!isLoading && !currentWord && ( - <Card> - <CardHeader> - <CardTitle className="text-center">No Word Found</CardTitle> - </CardHeader> - <CardContent> - <p className="text-center text-gray-600"> - Could not find a Thai word matching your criteria. Try different selections. - </p> - </CardContent> - </Card> - )} - </div> + </div> + </CardContent> + <CardFooter className="justify-center gap-18"> + <Button className="" onClick={decrSyl}> + - + </Button> + <Button + onClick={() => handleFetch(selectedTones)} + disabled={isLoading} + className="w-full md:w-auto" + > + {isLoading ? "Searching..." : "Fetch"} + </Button> + <Button className="" onClick={incrSyl}> + + + </Button> + </CardFooter> + </Card> ); } |