summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-05-21 15:28:03 +0700
committerpolwex <polwex@sortug.com>2025-05-21 15:28:03 +0700
commit9192e6c7747fd2d3f6a6c5c07d886a0982b53f11 (patch)
treec94a60a26732b697fa6734bff0fe898d690d2fd4 /src
parente839a5f61f0faa21ca8b4bd5767f7575d5e576ee (diff)
good tandem good tandem of ideas and implementation
Diffstat (limited to 'src')
-rw-r--r--src/actions/lang.ts7
-rw-r--r--src/components/Flashcard/Card.tsx16
-rw-r--r--src/components/Flashcard/Deck2.tsx205
-rw-r--r--src/components/Flashcard/ServerCard.tsx345
-rw-r--r--src/lib/db/schema.sql8
-rw-r--r--src/lib/utils.ts8
-rw-r--r--src/pages/api/nlp.ts38
-rw-r--r--src/pages/lesson/[slug].tsx9
8 files changed, 634 insertions, 2 deletions
diff --git a/src/actions/lang.ts b/src/actions/lang.ts
index fc798da..b38b542 100644
--- a/src/actions/lang.ts
+++ b/src/actions/lang.ts
@@ -2,6 +2,7 @@
import { AsyncRes } from "@/lib/types";
import { NLP } from "sortug-ai";
import ServerWord from "@/zoom/ServerWord";
+import { analyzeTHWord, segmentateThai } from "@/pages/api/nlp";
// import db from "../lib/db";
export async function wordAction(
@@ -12,6 +13,12 @@ export async function wordAction(
return ServerWord({ word: text, lang });
}
+export async function thaiAnalysis(text: string) {
+ const res = await segmentateThai(text);
+ const res2 = await analyzeTHWord(text);
+ console.log({ res, res2 });
+}
+
// export async function ocrAction(file: File): AsyncRes<string[]> {
// const res = await NLP.ocr(file);
// return res;
diff --git a/src/components/Flashcard/Card.tsx b/src/components/Flashcard/Card.tsx
index 7cada24..9eccdb5 100644
--- a/src/components/Flashcard/Card.tsx
+++ b/src/components/Flashcard/Card.tsx
@@ -1,6 +1,9 @@
"use client";
+import { thaiAnalysis } from "@/actions/lang";
import { CardResponse } from "@/lib/types/cards";
+import { useTransition } from "react";
+import { Spinner } from "../ui/spinner";
// export default function ({ user }: { user: { name: string; id: number } }) {
// const [state, formAction, isPending] = useActionState(postLogout, 0);
@@ -58,6 +61,13 @@ const Flashcard: React.FC<FlashcardProps> = ({
}
};
+ const [isPending, startTransition] = useTransition();
+ const handleClick = () => {
+ startTransition(async () => {
+ const res = await thaiAnalysis(data.expression.spelling);
+ });
+ };
+
return (
<div
className={`w-full max-w-md h-80 perspective group ${getAnimationClass()}`}
@@ -77,9 +87,13 @@ const Flashcard: React.FC<FlashcardProps> = ({
</span>
))}
</span>
- <p className="text-3xl md:text-4xl font-semibold text-slate-800 dark:text-slate-100 text-center">
+ <p
+ onClick={handleClick}
+ className="text-3xl cursor-pointer hover:text-blue-700 md:text-4xl font-semibold text-slate-800 dark:text-slate-100 text-center"
+ >
{data.expression.spelling}
</p>
+ {isPending && <Spinner />}
<div className="w-full h-6">
{" "}
{/* Placeholder for spacing, mimics bottom controls */}
diff --git a/src/components/Flashcard/Deck2.tsx b/src/components/Flashcard/Deck2.tsx
new file mode 100644
index 0000000..4fd8740
--- /dev/null
+++ b/src/components/Flashcard/Deck2.tsx
@@ -0,0 +1,205 @@
+"use client";
+
+import { CardResponse, DeckResponse } from "@/lib/types/cards";
+import React, { ReactNode, useCallback, useEffect, useState } from "react";
+import { Button } from "../ui/button";
+import { ChevronLeftIcon, ChevronRightIcon, RotateCcwIcon } from "lucide-react";
+import "./cards.css";
+import Flashcard from "./Card";
+
+type CardData = {
+ id: number;
+ front: ReactNode;
+ back: ReactNode;
+};
+// --- Main App Component ---
+function Deck({ data, cards }: { data: DeckResponse; cards: CardData[] }) {
+ const [currentPage, setCurrentPage] = useState<number>(0);
+ const [currentIndex, setCurrentIndex] = useState<number>(0);
+ const [isFlipped, setIsFlipped] = useState<boolean>(false);
+ const [animationDirection, setAnimationDirection] = useState<
+ "enter-left" | "enter-right" | "exit-left" | "exit-right" | "none"
+ >("none");
+ const [isAnimating, setIsAnimating] = useState<boolean>(false);
+
+ const handleFlip = () => {
+ if (isAnimating) return;
+ setIsFlipped(!isFlipped);
+ };
+
+ const handleNext = useCallback(() => {
+ if (isAnimating || currentIndex >= cards.length - 1) return;
+ setIsAnimating(true);
+ setIsFlipped(false); // Flip back to front before changing card
+ setAnimationDirection("exit-left");
+
+ setTimeout(() => {
+ setCurrentIndex((prevIndex) => Math.min(prevIndex + 1, cards.length - 1));
+ setAnimationDirection("enter-right");
+ setTimeout(() => {
+ setAnimationDirection("none");
+ setIsAnimating(false);
+ }, 500); // Duration of enter animation
+ }, 500); // Duration of exit animation
+ }, [currentIndex, cards.length, isAnimating]);
+
+ const handlePrev = useCallback(() => {
+ if (isAnimating || currentIndex <= 0) return;
+ setIsAnimating(true);
+ setIsFlipped(false); // Flip back to front
+ setAnimationDirection("exit-right");
+
+ setTimeout(() => {
+ setCurrentIndex((prevIndex) => Math.max(prevIndex - 1, 0));
+ setAnimationDirection("enter-left");
+ setTimeout(() => {
+ setAnimationDirection("none");
+ setIsAnimating(false);
+ }, 500); // Duration of enter animation
+ }, 500); // Duration of exit animation
+ }, [currentIndex, isAnimating]);
+
+ // Keyboard navigation
+ useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (isAnimating) return;
+ if (event.key === "ArrowRight") {
+ handleNext();
+ } else if (event.key === "ArrowLeft") {
+ handlePrev();
+ } else if (event.key === " " || event.key === "Enter") {
+ // Space or Enter to flip
+ event.preventDefault(); // Prevent scrolling if space is pressed
+ handleFlip();
+ }
+ };
+
+ window.addEventListener("keydown", handleKeyDown);
+ return () => {
+ window.removeEventListener("keydown", handleKeyDown);
+ };
+ }, [handleNext, handlePrev, isAnimating]);
+
+ if (cards.length === 0) {
+ return (
+ <div className="min-h-screen bg-slate-50 dark:bg-slate-900 flex flex-col items-center justify-center p-4 font-inter text-slate-800 dark:text-slate-200">
+ <p>No flashcards available.</p>
+ </div>
+ );
+ }
+
+ const currentCard = cards[currentIndex];
+ if (!currentCard) return <p>wtf</p>;
+
+ return (
+ <div className="min-h-screen bg-slate-100 dark:bg-slate-900 flex flex-col items-center justify-center p-4 font-inter transition-colors duration-300">
+ <div className="w-full max-w-md mb-8 relative">
+ {/* This div is for positioning the card and managing overflow during animations */}
+ <div className="relative h-80">
+ <FlashCard
+ key={currentCard.id} // Important for re-rendering on card change with animation
+ isFlipped={isFlipped}
+ onFlip={handleFlip}
+ animationDirection={animationDirection}
+ front={currentCard.front}
+ back={currentCard.back}
+ />
+ </div>
+ </div>
+
+ <div className="flex items-center justify-between w-full max-w-md mb-6">
+ <Button
+ onClick={handlePrev}
+ disabled={currentIndex === 0 || isAnimating}
+ variant="outline"
+ size="icon"
+ aria-label="Previous card"
+ >
+ <ChevronLeftIcon />
+ </Button>
+ <div className="text-center">
+ <p className="text-sm text-slate-600 dark:text-slate-400">
+ Card {currentIndex + 1} of {cards.length}
+ </p>
+ <Button
+ onClick={handleFlip}
+ variant="ghost"
+ size="sm"
+ className="mt-1 text-slate-600 dark:text-slate-400"
+ disabled={isAnimating}
+ >
+ <RotateCcwIcon className="w-4 h-4 mr-2" /> Flip Card
+ </Button>
+ </div>
+ <Button
+ onClick={handleNext}
+ disabled={currentIndex === cards.length - 1 || isAnimating}
+ variant="outline"
+ size="icon"
+ aria-label="Next card"
+ >
+ <ChevronRightIcon />
+ </Button>
+ </div>
+
+ <div className="text-xs text-slate-500 dark:text-slate-400 mt-8">
+ Use Arrow Keys (← →) to navigate, Space/Enter to flip.
+ </div>
+ </div>
+ );
+}
+
+export default Deck;
+
+interface FlashcardProps {
+ isFlipped: boolean;
+ onFlip: () => void;
+ animationDirection:
+ | "enter-left"
+ | "enter-right"
+ | "exit-left"
+ | "exit-right"
+ | "none";
+}
+interface ServerCards {
+ front: ReactNode;
+ back: ReactNode;
+}
+
+function FlashCard({
+ isFlipped,
+ onFlip,
+ animationDirection,
+ front,
+ back,
+}: FlashcardProps & ServerCards) {
+ const getAnimationClass = () => {
+ switch (animationDirection) {
+ case "enter-right":
+ return "animate-slide-in-right";
+ case "enter-left":
+ return "animate-slide-in-left";
+ case "exit-right":
+ return "animate-slide-out-right";
+ case "exit-left":
+ return "animate-slide-out-left";
+ default:
+ return "";
+ }
+ };
+ return (
+ <div
+ className={`w-full max-w-md h-80 perspective group ${getAnimationClass()}`}
+ onClick={onFlip}
+ >
+ <div
+ className={`relative w-full h-full rounded-xl shadow-xl transition-transform duration-700 ease-in-out transform-style-preserve-3d cursor-pointer ${
+ isFlipped ? "rotate-y-180" : ""
+ }`}
+ >
+ {front}
+ {back}
+ </div>
+ </div>
+ );
+}
diff --git a/src/components/Flashcard/ServerCard.tsx b/src/components/Flashcard/ServerCard.tsx
new file mode 100644
index 0000000..75442b4
--- /dev/null
+++ b/src/components/Flashcard/ServerCard.tsx
@@ -0,0 +1,345 @@
+// 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 } from "@/lib/types/cards";
+import { thaiData } from "@/pages/api/nlp";
+import { getRandomHexColor } from "@/lib/utils";
+
+export async function CardFront({ data }: { data: CardResponse }) {
+ // const extraData = data.expression.lang
+ const extraData = await thaiData(data.expression.spelling);
+ console.log({ extraData });
+
+ 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.expression.spelling}
+ </p>
+ }
+ >
+ <p className="text-5xl cursor-pointer font-semibold text-slate-800 dark:text-slate-100 text-center">
+ {extraData[0]?.syllables.map((syl) => (
+ <span
+ style={{ color: getRandomHexColor() }}
+ className="m-1 hover:text-6l"
+ >
+ {syl}
+ </span>
+ ))}
+ </p>
+ </Suspense>
+ <IpaDisplay ipaEntries={data.expression.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: CardResponse }) {
+ // <BookmarkIcon onClick={handleClick} className="absolute top-5 right-10" />
+ return (
+ <div className="absolute 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">
+ <BookmarkIcon className="absolute top-5 right-10" />
+ <span className="text-lg text-slate-500 dark:text-slate-400 self-start">
+ {data.expression.senses.map((ss, i) => (
+ <div key={`ss${i}`}>
+ {ss.senses.map((sss, i) => (
+ <div key={`sss${i}`}>
+ {sss.glosses.map((ssss, i) => (
+ <p key={ssss + i}>{ssss}</p>
+ ))}
+ </div>
+ ))}
+ </div>
+ ))}
+ </span>
+ <p className="text-3xl md:text-4xl font-semibold text-slate-800 dark:text-slate-100 text-center">
+ {data.note}
+ </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>
+ );
+};
diff --git a/src/lib/db/schema.sql b/src/lib/db/schema.sql
index 8d1b288..4506619 100644
--- a/src/lib/db/schema.sql
+++ b/src/lib/db/schema.sql
@@ -57,6 +57,14 @@ CREATE INDEX IF NOT EXISTS idx_words_pos ON senses(pos);
CREATE INDEX IF NOT EXISTS idx_senses_parent ON senses(parent_id);
+CREATE TABLE IF NOT EXISTS bookmarks(
+ word_id INTEGER PRIMARY KEY,
+ notes TEXT,
+ created INTEGER NOT NULL,
+ FOREIGN KEY (word_id) REFERENCES expressions(id)
+);
+CREATE INDEX IF NOT EXISTS idx_bookmarks ON bookmarks(word_id);
+
-- Categories table (for noun and verb categories)
CREATE TABLE IF NOT EXISTS categories (
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index d3fdf9c..9bc74b8 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -49,3 +49,11 @@ export function handlePromise<T>(
if (settlement.status === "fulfilled") return settlement.value;
else return `${settlement.reason}`;
}
+
+export function getRandomHexColor() {
+ // Generate a random number and convert it to a hexadecimal string
+ const randomColor = Math.floor(Math.random() * 16777215).toString(16);
+
+ // Ensure the color code is always 6 digits by padding with zeros if needed
+ return "#" + randomColor.padStart(6, "0");
+}
diff --git a/src/pages/api/nlp.ts b/src/pages/api/nlp.ts
index 0e5eacb..27c330d 100644
--- a/src/pages/api/nlp.ts
+++ b/src/pages/api/nlp.ts
@@ -30,3 +30,41 @@ export const POST = async (request: Request): Promise<Response> => {
return Response.json({ message: "Failure" }, { status: 500 });
}
};
+
+type AnalyzeRes = {
+ word: string;
+ syllables: string[];
+ ipa: string;
+ pos: string;
+};
+
+export async function thaiData(word: string): Promise<AnalyzeRes[]> {
+ const [head, tail] = await Promise.all([
+ analyzeTHWord(word),
+ segmentateThai(word),
+ ]);
+ return [head, ...tail];
+}
+
+export async function analyzeTHWord(word: string): Promise<AnalyzeRes> {
+ const opts = {
+ method: "POST",
+ headers: { "Content-type": "application/json" },
+ body: JSON.stringify({ word }),
+ };
+ const r1 = await fetch("http://localhost:8001" + "/analyze", opts);
+ // const r2 = await fetch(`http://192.168.1.110:8000/analyze`, opts);
+ const jj = await r1.json();
+ return jj;
+}
+export async function segmentateThai(sentence: string): Promise<AnalyzeRes[]> {
+ const opts = {
+ method: "POST",
+ headers: { "Content-type": "application/json" },
+ body: JSON.stringify({ word: sentence }),
+ };
+ // const r1 = await fetch(`http://localhost:8000/segmentate`, opts);
+ const r2 = await fetch("http://localhost:8001" + `/segmentate`, opts);
+ const jj = await r2.json();
+ return jj;
+}
diff --git a/src/pages/lesson/[slug].tsx b/src/pages/lesson/[slug].tsx
index 6632838..9e6e6cc 100644
--- a/src/pages/lesson/[slug].tsx
+++ b/src/pages/lesson/[slug].tsx
@@ -8,6 +8,8 @@ import type { PageProps } from "waku/router";
import db from "@/lib/db";
import { useCookies } from "@/lib/server/cookiebridge";
import Deck from "@/components/Flashcard/Deck";
+import Deck2 from "@/components/Flashcard/Deck2";
+import { CardFront, CardBack } from "@/components/Flashcard/ServerCard";
const flags: Record<string, string> = {
th: "🇹🇭",
@@ -34,12 +36,17 @@ export default async function HomePage(props: PageProps<"/lesson/[slug]">) {
const data = await getData(Number(props.slug), user.id);
if ("error" in data) return <p>Error</p>;
// console.log({ data });
+ const cardComponents = data.ok.cards.map((card) => ({
+ id: card.id,
+ front: <CardFront data={card} />,
+ back: <CardBack data={card} />,
+ }));
return (
<>
<section>
<h2 className="text-lg">Thai!</h2>
- <Deck data={data.ok} />
+ <Deck2 data={data.ok} cards={cardComponents} />
</section>
</>
);