diff options
Diffstat (limited to 'src/components/prosody')
-rw-r--r-- | src/components/prosody/ClientCard.tsx | 64 | ||||
-rw-r--r-- | src/components/prosody/ServerCard.tsx | 343 |
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> + ); +}; |