summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-08-16 13:18:51 +0700
committerpolwex <polwex@sortug.com>2025-08-16 13:18:51 +0700
commit9a1e8af707acec9bfabd4c5be9ba6595d7f14c3e (patch)
treeba345111027c359cf76501781236b4d8f1581ade
parentf6f8e791fc95d5efb585177071ba0328d3c3b17f (diff)
pretty fast gotta say
-rw-r--r--src/components/Main.tsx6
-rw-r--r--src/components/tones/ToneSelectorClient.tsx65
-rw-r--r--src/pages/tones.tsx1
-rw-r--r--src/picker/App.tsx5
-rw-r--r--src/styles/globals.css32
-rw-r--r--src/zoom/App.tsx4
-rw-r--r--src/zoom/zoom.css32
7 files changed, 95 insertions, 50 deletions
diff --git a/src/components/Main.tsx b/src/components/Main.tsx
index 3e6f3e7..6be61a3 100644
--- a/src/components/Main.tsx
+++ b/src/components/Main.tsx
@@ -18,8 +18,9 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
-import { Loader2 } from "lucide-react"; // Loading spinner
import { useRouter } from "waku";
+import { Spinner } from "./ui/spinner";
+import { Loader2 } from "lucide-react";
const SorlangPage: React.FC = () => {
const [textValue, setTextValue] = useState<string>("");
@@ -192,7 +193,8 @@ const SorlangPage: React.FC = () => {
>
{isExtracting ? (
<>
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+ {/*<Loader2 className="mr-2 h-4 w-4 animate-spin" />*/}
+ <Spinner />
Extracting...
</>
) : (
diff --git a/src/components/tones/ToneSelectorClient.tsx b/src/components/tones/ToneSelectorClient.tsx
index 8a0327c..48580a4 100644
--- a/src/components/tones/ToneSelectorClient.tsx
+++ b/src/components/tones/ToneSelectorClient.tsx
@@ -1,6 +1,7 @@
"use client";
-import { useState, useEffect, useTransition, useRef } from "react";
+import { Spinner } from "@/components/ui/spinner";
+import { useState, useEffect, useTransition, useRef, useCallback } from "react";
import { WordData } from "@/zoom/logic/types";
import {
fetchWordsByToneAndSyllables,
@@ -26,7 +27,7 @@ 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";
+import { ArrowLeft, ArrowRight, Volume2 } from "lucide-react";
function getColorByTone(tone: string): string {
if (tone === "mid") return "blue";
@@ -38,6 +39,7 @@ function getColorByTone(tone: string): string {
}
// Helper to display tones prominently
const ProminentToneDisplay = ({ word }: { word: any }) => {
+ const [isLoading, setLoading] = useState(false);
const tones: string[] = word.tone_sequence.split(",");
const syls: string[] = word.syl_seq.split(",");
const [isPending, startTransition] = useTransition();
@@ -59,7 +61,7 @@ const ProminentToneDisplay = ({ word }: { word: any }) => {
const audioRef = useRef<HTMLAudioElement>(null);
async function playAudio() {
- // setLoading(true);
+ setLoading(true);
// const audioContext = new (window.AudioContext ||
// (window as any).webkitAudioContext)();
// const response = await fetch(audioUrl);
@@ -76,11 +78,22 @@ const ProminentToneDisplay = ({ word }: { word: any }) => {
const res = await fetch(`/api/tts?word=${word.spelling}&lang=thai`);
const audioBlob = await res.blob();
const audioURL = URL.createObjectURL(audioBlob);
+ setLoading(false);
if (audioRef.current) {
audioRef.current.src = audioURL;
audioRef.current.play();
}
}
+ useEffect(() => {
+ const onKeyDown = (e: KeyboardEvent) => {
+ if (e.key === " ") {
+ e.preventDefault();
+ playAudio();
+ }
+ };
+ window.addEventListener("keydown", onKeyDown);
+ return () => window.removeEventListener("keydown", onKeyDown);
+ }, [playAudio]);
return (
<div className="flex flex-col items-center mb-4">
@@ -105,7 +118,7 @@ const ProminentToneDisplay = ({ word }: { word: any }) => {
>
<Volume2 size={20} />
</button>
- {isPending && <Loader2 />}
+ {(isPending || isLoading) && <Spinner />}
<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>
@@ -126,12 +139,26 @@ export default function ToneSelectorClient({
const [isLoading, startTransition] = useTransition();
const [selectedTones, setTones] = useState<ToneQuery>(initialTones);
- function goPrev() {
+ const goPrev = useCallback(() => {
setCurrentIdx((i) => (i === 0 ? 0 : i - 1));
- }
- function goNext() {
+ }, []);
+ const goNext = useCallback(() => {
setCurrentIdx((i) => (i === data.length - 1 ? data.length - 1 : i + 1));
- }
+ }, [data.length]);
+
+ useEffect(() => {
+ const onKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "ArrowLeft") {
+ e.preventDefault();
+ goPrev();
+ } else if (e.key === "ArrowRight") {
+ e.preventDefault();
+ goNext();
+ }
+ };
+ window.addEventListener("keydown", onKeyDown);
+ return () => window.removeEventListener("keydown", onKeyDown);
+ }, [goPrev, goNext]);
const handleFetch = () => {
startTransition(async () => {
@@ -225,12 +252,12 @@ function ToneForm({
{ value: "rising", label: "5 (Rising)" },
];
const [syllableCount, setSyllableCount] = useState<number>(2);
- function decrSyl() {
+ const decrSyl = useCallback(() => {
setSyllableCount((s) => (s <= 1 ? 1 : s - 1));
- }
- function incrSyl() {
+ }, []);
+ const incrSyl = useCallback(() => {
setSyllableCount((s) => (s >= 5 ? 5 : s + 1));
- }
+ }, []);
useEffect(() => {
// Adjust selectedTones array length when syllableCount changes
@@ -251,6 +278,20 @@ function ToneForm({
}
};
+ useEffect(() => {
+ const onKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "ArrowUp") {
+ e.preventDefault();
+ incrSyl();
+ } else if (e.key === "ArrowDown") {
+ e.preventDefault();
+ decrSyl();
+ }
+ };
+ window.addEventListener("keydown", onKeyDown);
+ return () => window.removeEventListener("keydown", onKeyDown);
+ }, [incrSyl, decrSyl]);
+
const handleToneChange = (syllableIndex: number, value: string) => {
const tone = value === "any" ? null : value;
setTones((prevTones) => {
diff --git a/src/pages/tones.tsx b/src/pages/tones.tsx
index 732ebd1..8658401 100644
--- a/src/pages/tones.tsx
+++ b/src/pages/tones.tsx
@@ -1,3 +1,4 @@
+import "@/styles/globals.css";
import { Suspense } from "react";
import { fetchWordsByToneAndSyllables } from "@/actions/tones";
import ToneSelectorClient from "@/components/tones/ToneSelectorClient";
diff --git a/src/picker/App.tsx b/src/picker/App.tsx
index 0b4d46f..4b17a1e 100644
--- a/src/picker/App.tsx
+++ b/src/picker/App.tsx
@@ -10,12 +10,13 @@ import React, {
startTransition,
useTransition,
} from "react";
-import { Brain, Zap, Loader2 } from "lucide-react";
+import { Brain, Zap } from "lucide-react";
import { NLP } from "sortug-ai";
import GranularityMenu, { GranularityId } from "./LevelPicker";
import TextViewer from "./TextViewer";
import { wordAction } from "@/actions/lang";
import Modal from "@/components/Modal";
+import { Spinner } from "@/components/ui/spinner";
// --- Granularity Definition ---
type AnalysisEngine = "spacy" | "stanza";
@@ -171,7 +172,7 @@ export default function NlpTextAnalysisScreen({
onElementSelect={handleElementSelect}
/>
) : (
- <Loader2 />
+ <Spinner />
)}
</main>
</div>
diff --git a/src/styles/globals.css b/src/styles/globals.css
index 4bc2c75..d22c133 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -121,4 +121,36 @@
body {
@apply bg-background text-foreground;
}
+}
+
+/* Loaders */
+.spinner {
+ border: 4px solid rgba(0, 0, 0, 0.1);
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ border-left-color: #09f;
+ margin: 5px auto;
+ animation: spin 1s ease infinite;
+}
+
+.mini-spinner {
+ display: inline-block;
+ border: 2px solid rgba(0, 0, 0, 0.1);
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ border-left-color: #09f;
+ margin: 0 3px;
+ animation: spin 1s ease infinite;
+}
+
+@keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ transform: rotate(360deg);
+ }
} \ No newline at end of file
diff --git a/src/zoom/App.tsx b/src/zoom/App.tsx
index d41dd7f..8ea3007 100644
--- a/src/zoom/App.tsx
+++ b/src/zoom/App.tsx
@@ -5,7 +5,7 @@ import FullText from "./FullText";
import "./zoom.css";
import { useZoom, ZoomProvider } from "./hooks/useZoom";
import { NLP } from "sortug-ai";
-import { Loader2 } from "lucide-react";
+import { Spinner } from "@/components/ui/spinner";
const App: React.FC = () => {
const { viewState } = useZoom();
@@ -70,7 +70,7 @@ const App: React.FC = () => {
<main>
{isLoading ? (
- <Loader2 />
+ <Spinner />
) : spacy ? (
<FullText text={spacy.input} doc={spacy} stanza={stanza} />
) : (
diff --git a/src/zoom/zoom.css b/src/zoom/zoom.css
index 2c743bd..2887ca2 100644
--- a/src/zoom/zoom.css
+++ b/src/zoom/zoom.css
@@ -409,38 +409,6 @@ main {
background-color: #e0e0e0;
}
-/* Loaders */
-.spinner {
- border: 4px solid rgba(0, 0, 0, 0.1);
- width: 24px;
- height: 24px;
- border-radius: 50%;
- border-left-color: #09f;
- margin: 5px auto;
- animation: spin 1s ease infinite;
-}
-
-.mini-spinner {
- display: inline-block;
- border: 2px solid rgba(0, 0, 0, 0.1);
- width: 12px;
- height: 12px;
- border-radius: 50%;
- border-left-color: #09f;
- margin: 0 3px;
- animation: spin 1s ease infinite;
-}
-
-@keyframes spin {
- 0% {
- transform: rotate(0deg);
- }
-
- 100% {
- transform: rotate(360deg);
- }
-}
-
/* Responsive adjustments */
@media (max-width: 768px) {