summaryrefslogtreecommitdiff
path: root/src/components/tones/ToneSelectorClient.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/tones/ToneSelectorClient.tsx')
-rw-r--r--src/components/tones/ToneSelectorClient.tsx405
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>
);
}