summaryrefslogtreecommitdiff
path: root/src/components/prosody
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-06-03 09:34:29 +0700
committerpolwex <polwex@sortug.com>2025-06-03 09:34:29 +0700
commit2401217a4019938d1c1cc61b6e33ccb233eb6e74 (patch)
tree06118284965be5cfd6b417dca86d46db5758217b /src/components/prosody
parent2b80f7950df34f2a160135d7e20220a9b2ec3352 (diff)
this is golden thanks claude
Diffstat (limited to 'src/components/prosody')
-rw-r--r--src/components/prosody/ClientCard.tsx64
-rw-r--r--src/components/prosody/ServerCard.tsx343
2 files changed, 407 insertions, 0 deletions
diff --git a/src/components/prosody/ClientCard.tsx b/src/components/prosody/ClientCard.tsx
new file mode 100644
index 0000000..795e3bf
--- /dev/null
+++ b/src/components/prosody/ClientCard.tsx
@@ -0,0 +1,64 @@
+"use client";
+import { getOnsets } from "@/actions/prosody";
+import type { ProsodySyllable, ProsodyWord } from "@/lib/types/cards";
+import { useTransition, useState } from "react";
+
+export async function SyllableBreakdown({ syl }: { syl: ProsodySyllable }) {
+ const [isPending, startTransition] = useTransition();
+ const [data, setData] = useState<any[]>([]);
+ function showOnset(e: React.MouseEvent) {
+ e.stopPropagation();
+ console.log(syl);
+ startTransition(async () => {
+ const data = await getOnsets(syl.onset);
+ setData(data);
+ });
+ }
+ console.log({ isPending });
+ return (
+ <div>
+ <div>whole: {syl.ipa}</div>
+ <div
+ onClick={showOnset}
+ className="text-blue-500 hover:text-blue-700 hover:underline"
+ >
+ onset: {syl.onset}
+ </div>
+ <div
+ onClick={showOnset}
+ className="text-blue-500 hover:text-blue-700 hover:underline"
+ >
+ nucleus: {syl.nucleus}
+ </div>
+ <div
+ onClick={showOnset}
+ className="text-blue-500 hover:text-blue-700 hover:underline"
+ >
+ coda: {syl.coda}
+ </div>
+ <div
+ onClick={showOnset}
+ className="text-blue-500 hover:text-blue-700 hover:underline"
+ >
+ tone: {syl.tone}
+ </div>
+ <div
+ onClick={showOnset}
+ className="text-blue-500 hover:text-blue-700 hover:underline"
+ >
+ tonename: {syl.tonen}
+ </div>
+ {data.length > 0 && (
+ <div>
+ {data.map((d) => (
+ <div key={d.id}>
+ <div>{d.spelling}</div>
+ </div>
+ ))}
+ </div>
+ )}
+ </div>
+ );
+}
+
+// Component for displaying examples
diff --git a/src/components/prosody/ServerCard.tsx b/src/components/prosody/ServerCard.tsx
new file mode 100644
index 0000000..4867721
--- /dev/null
+++ b/src/components/prosody/ServerCard.tsx
@@ -0,0 +1,343 @@
+// This is a Server Component
+import React, { Suspense, useTransition } from "react";
+import { NLP } from "sortug-ai";
+import {
+ BookOpen,
+ Volume2,
+ Link as LinkIcon,
+ ChevronDown,
+ ChevronUp,
+ Search,
+ Info,
+ MessageSquareQuote,
+ Tags,
+ ListTree,
+ Lightbulb,
+ BookmarkIcon,
+} from "lucide-react";
+import {
+ Example,
+ SubSense,
+ RelatedEntry,
+ Sense,
+ WordData,
+} from "@/zoom/logic/types";
+import { CardResponse, ProsodyWord } from "@/lib/types/cards";
+import { thaiData } from "@/lib/calls/nlp";
+import { getRandomHexColor } from "@/lib/utils";
+import { BookmarkIconito } from "../Flashcard/BookmarkButton";
+import SyllableCard from "../Flashcard/Syllable";
+import SyllableSpan from "../Flashcard/SyllableSpan";
+import { SyllableBreakdown } from "./ClientCard";
+
+export async function CardFront({ data }: { data: ProsodyWord }) {
+ console.log("cardfront", data);
+ return (
+ <div className="absolute w-full h-full bg-white dark:bg-slate-800 rounded-xl backface-hidden flex flex-col justify-center gap-8 items-center p-6">
+ <Suspense
+ fallback={
+ <p className="text-5xl cursor-pointer hover:text-blue-700 font-semibold text-slate-800 dark:text-slate-100 text-center">
+ {data.spelling}
+ </p>
+ }
+ >
+ <p className="text-5xl cursor-pointer font-semibold text-slate-800 dark:text-slate-100 text-center">
+ {data.syllables.map((syl, i) => (
+ // <span
+ // key={syl + i}
+ // style={{ color: getRandomHexColor() }}
+ // className="m-1 hover:text-6xl"
+ // >
+ // {syl}
+ // </span>
+ <SyllableSpan
+ key={syl.spelling + i}
+ spelling={syl.spelling}
+ lang={data.lang}
+ />
+ ))}
+ </p>
+ </Suspense>
+ <p>Word: {data.id}</p>
+ <IpaDisplay ipaEntries={[{ ipa: data.ipa }]} />
+ </div>
+ );
+}
+
+// Helper component for IPA display
+export const IpaDisplay = ({
+ ipaEntries,
+}: {
+ ipaEntries: Array<{ ipa: string; tags?: string[] }>;
+}) => {
+ if (!ipaEntries || ipaEntries.length === 0) return null;
+ return (
+ <div className="flex items-center space-x-2 flex-wrap">
+ {ipaEntries.map((entry, index) => {
+ const tags = entry.tags ? entry.tags : [];
+ return (
+ <span key={index} className="text-lg text-blue-600 font-serif">
+ {entry.ipa}{" "}
+ {tags.length > 0 && (
+ <span className="text-xs text-gray-500">({tags.join(", ")})</span>
+ )}
+ </span>
+ );
+ })}
+ <button
+ className="p-1 text-blue-500 hover:text-blue-700 transition-colors"
+ title="Pronounce"
+ // onClick={() => {
+ // /* Pronunciation logic would be client-side or a server roundtrip for audio file. */ alert(
+ // "Pronunciation feature not implemented for server component.",
+ // );
+ // }}
+ >
+ <Volume2 size={20} />
+ </button>
+ </div>
+ );
+};
+
+export async function CardBack({ data }: { data: ProsodyWord }) {
+ return (
+ <div className="w-full h-full bg-slate-50 dark:bg-slate-700 rounded-xl backface-hidden rotate-y-180 flex flex-col justify-between items-center p-6 relative">
+ <span className="text-lg text-slate-500 dark:text-slate-400 self-start">
+ {data.syllables.map((syl, i) => (
+ <SyllableBreakdown syl={syl} key={syl.spelling + i} />
+ ))}
+ </span>
+ <p className="text-3xl md:text-4xl font-semibold text-slate-800 dark:text-slate-100 text-center">
+ {data.notes || ""}
+ </p>
+ </div>
+ );
+}
+
+// Component for displaying examples
+const ExampleDisplay = ({ examples }: { examples: Example[] }) => {
+ if (!examples || examples.length === 0) return null;
+ return (
+ <div className="mt-2">
+ <h5 className="text-xs font-semibold text-gray-600 mb-1 flex items-center">
+ <MessageSquareQuote size={14} className="mr-1 text-gray-500" />
+ Examples:
+ </h5>
+ <ul className="list-disc list-inside pl-2 space-y-1">
+ {examples.map((ex, idx) => (
+ <li key={idx} className="text-xs text-gray-600">
+ <span className="italic">"{ex.text}"</span>
+ {ex.ref && (
+ <span className="text-gray-400 text-xs"> ({ex.ref})</span>
+ )}
+ {ex.type !== "quote" && (
+ <span className="ml-1 text-xs bg-sky-100 text-sky-700 px-1 rounded-sm">
+ {ex.type}
+ </span>
+ )}
+ </li>
+ ))}
+ </ul>
+ </div>
+ );
+};
+
+// Component for displaying related terms (synonyms, antonyms, etc.)
+const RelatedTermsDisplay = ({
+ terms,
+ type,
+}: {
+ terms: RelatedEntry[] | undefined;
+ type: string;
+}) => {
+ if (!terms || terms.length === 0) return null;
+ return (
+ <div className="mt-1">
+ <span className="text-xs font-semibold text-gray-500 capitalize">
+ {type}:{" "}
+ </span>
+ {terms.map((term, idx) => (
+ <React.Fragment key={idx}>
+ <a
+ href={`/search?q=${encodeURIComponent(term.word)}`}
+ className="text-xs text-blue-500 hover:text-blue-700 hover:underline"
+ >
+ {term.word}
+ </a>
+ {/*term.source && (
+ <span className="text-xs text-gray-400"> ({term.source})</span>
+ )*/}
+ {idx < terms.length - 1 && ", "}
+ </React.Fragment>
+ ))}
+ </div>
+ );
+};
+
+// Component for displaying a SubSense
+const SubSenseDisplay = ({
+ subSense,
+ subSenseNumber,
+}: {
+ subSense: SubSense;
+ subSenseNumber: number;
+}) => {
+ return (
+ <div className="mb-3 pl-4 border-l-2 border-indigo-200">
+ {subSense.glosses.map((gloss, glossIdx) => (
+ <p key={glossIdx} className="text-gray-700 mb-1">
+ <span className="font-semibold">
+ {subSenseNumber}.{glossIdx + 1}
+ </span>{" "}
+ {gloss}
+ </p>
+ ))}
+ {subSense.raw_glosses &&
+ subSense.raw_glosses.length > 0 &&
+ subSense.raw_glosses.join("") !== subSense.glosses.join("") && (
+ <p className="text-xs text-gray-500 italic mb-1">
+ (Raw: {subSense.raw_glosses.join("; ")})
+ </p>
+ )}
+
+ {subSense.categories && subSense.categories.length > 0 && (
+ <div className="mt-1 mb-2">
+ <h5 className="text-xs font-semibold text-gray-600 mb-0.5 flex items-center">
+ <ListTree size={14} className="mr-1 text-gray-500" />
+ Categories:
+ </h5>
+ <div className="flex flex-wrap gap-1">
+ {subSense.categories.map((cat, idx) => (
+ <span
+ key={idx}
+ className="text-xs bg-gray-100 text-gray-700 px-1.5 py-0.5 rounded-full"
+ >
+ {cat}
+ </span>
+ ))}
+ </div>
+ </div>
+ )}
+
+ <ExampleDisplay examples={subSense.examples || []} />
+ <RelatedTermsDisplay terms={subSense.synonyms} type="Synonyms" />
+
+ {subSense.tags && subSense.tags.length > 0 && (
+ <div className="mt-2">
+ <h5 className="text-xs font-semibold text-gray-600 mb-0.5 flex items-center">
+ <Tags size={14} className="mr-1 text-gray-500" />
+ Tags:
+ </h5>
+ <div className="flex flex-wrap gap-1">
+ {subSense.tags.map((tag, idx) => (
+ <span
+ key={idx}
+ className="text-xs bg-purple-100 text-purple-700 px-1.5 py-0.5 rounded-full"
+ >
+ {tag}
+ </span>
+ ))}
+ </div>
+ </div>
+ )}
+
+ {subSense.links && subSense.links.length > 0 && (
+ <div className="mt-2">
+ {subSense.links.map(([type, target], linkIdx) => (
+ <a
+ key={linkIdx}
+ href={target} // Assuming target is a full URL or a path
+ target="_blank"
+ rel="noopener noreferrer"
+ className="text-xs text-blue-500 hover:text-blue-700 hover:underline mr-2 inline-flex items-center"
+ >
+ <LinkIcon size={12} className="mr-1" /> {type}
+ </a>
+ ))}
+ </div>
+ )}
+ </div>
+ );
+};
+
+// Component for individual sense
+const SenseCard = ({
+ senseData,
+ senseNumber,
+}: {
+ senseData: Sense;
+ senseNumber: number;
+}) => {
+ return (
+ <div className="mb-6 p-4 border border-gray-200 rounded-lg shadow-sm bg-white">
+ <div className="flex justify-between items-center mb-2">
+ <h3 className="text-xl font-semibold text-indigo-700">
+ {senseNumber}. {NLP.unpackPos(senseData.pos)}
+ </h3>
+ </div>
+
+ {senseData.etymology && (
+ <details className="mb-3 group">
+ <summary className="cursor-pointer flex items-center text-sm text-gray-600 hover:text-indigo-600 transition-colors list-none">
+ Etymology
+ <ChevronDown size={16} className="ml-1 group-open:hidden" />
+ <ChevronUp size={16} className="ml-1 hidden group-open:inline" />
+ </summary>
+ <p className="mt-1 text-xs text-gray-500 italic bg-gray-50 p-2 rounded">
+ {senseData.etymology}
+ </p>
+ </details>
+ )}
+
+ {senseData.forms && senseData.forms.length > 0 && (
+ <div className="mb-3">
+ <h4 className="text-sm font-medium text-gray-700">Forms:</h4>
+ <div className="flex flex-wrap gap-2 mt-1">
+ {senseData.forms.map((form, idx) => (
+ <span
+ key={idx}
+ className="text-xs bg-gray-100 text-gray-700 px-2 py-1 rounded-full"
+ >
+ {form.form}{" "}
+ {form.tags.length > 0 && `(${form.tags.join(", ")})`}
+ </span>
+ ))}
+ </div>
+ </div>
+ )}
+
+ {senseData.senses.map((subSense, idx) => (
+ <SubSenseDisplay
+ key={idx}
+ subSense={subSense}
+ subSenseNumber={senseNumber}
+ />
+ ))}
+
+ {senseData.related && (
+ <div className="mt-3 pt-3 border-t border-gray-100">
+ <h4 className="text-sm font-medium text-gray-700 mb-1 flex items-center">
+ <Lightbulb size={16} className="mr-1 text-gray-500" />
+ Related Terms:
+ </h4>
+ <RelatedTermsDisplay
+ terms={senseData.related.related}
+ type="Related"
+ />
+ <RelatedTermsDisplay
+ terms={senseData.related.synonyms}
+ type="Synonyms (POS)"
+ />
+ <RelatedTermsDisplay
+ terms={senseData.related.antonyms}
+ type="Antonyms (POS)"
+ />
+ <RelatedTermsDisplay
+ terms={senseData.related.derived}
+ type="Derived"
+ />
+ </div>
+ )}
+ </div>
+ );
+};