feat: menyusun huruf kuis + mekanisme
This commit is contained in:
parent
8c38a0a1b1
commit
2069915578
|
@ -12,6 +12,7 @@
|
|||
"@mediapipe/tasks-vision": "^0.10.14",
|
||||
"@tensorflow/tfjs": "^4.20.0",
|
||||
"clsx": "^2.1.1",
|
||||
"motion": "^12.4.7",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.3.0",
|
||||
|
@ -2815,6 +2816,33 @@
|
|||
"url": "https://github.com/sponsors/rawify"
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "12.4.7",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.4.7.tgz",
|
||||
"integrity": "sha512-VhrcbtcAMXfxlrjeHPpWVu2+mkcoR31e02aNSR7OUS/hZAciKa8q6o3YN2mA1h+jjscRsSyKvX6E1CiY/7OLMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.4.5",
|
||||
"motion-utils": "^12.0.0",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
|
@ -3317,6 +3345,47 @@
|
|||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/motion": {
|
||||
"version": "12.4.7",
|
||||
"resolved": "https://registry.npmjs.org/motion/-/motion-12.4.7.tgz",
|
||||
"integrity": "sha512-mhegHAbf1r80fr+ytC6OkjKvIUegRNXKLWNPrCN2+GnixlNSPwT03FtKqp9oDny1kNcLWZvwbmEr+JqVryFrcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"framer-motion": "^12.4.7",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "12.4.5",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.4.5.tgz",
|
||||
"integrity": "sha512-Q2xmhuyYug1CGTo0jdsL05EQ4RhIYXlggFS/yPhQQRNzbrhjKQ1tbjThx5Plv68aX31LsUQRq4uIkuDxdO5vRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-utils": "^12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.0.0.tgz",
|
||||
"integrity": "sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
|
@ -4360,6 +4429,12 @@
|
|||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"@mediapipe/tasks-vision": "^0.10.14",
|
||||
"@tensorflow/tfjs": "^4.20.0",
|
||||
"clsx": "^2.1.1",
|
||||
"motion": "^12.4.7",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.3.0",
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 646 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
|
@ -0,0 +1,17 @@
|
|||
import { motion } from "framer-motion";
|
||||
|
||||
type ProgressBarProps = {
|
||||
progress: number; // nilai antara 0 - 100
|
||||
};
|
||||
|
||||
export default function ProgressBar({ progress }: ProgressBarProps) {
|
||||
return (
|
||||
<div className="w-full bg-gray-200 rounded-full h-4 overflow-hidden">
|
||||
<motion.div
|
||||
className="h-full bg-blue-500 rounded-full"
|
||||
style={{ width: `${progress}%` }}
|
||||
transition={{ ease: "easeInOut" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||
const MyLoading: React.FC = () => {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-screen">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-4 border-b-4 border-primary"></div>
|
||||
<div className="loader"></div>
|
||||
<p className="mt-4 text-lg text-gray-700">Loading...</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -8,4 +8,24 @@
|
|||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
@apply font-poppins;
|
||||
}
|
||||
}
|
||||
|
||||
.loader {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 3px solid black;
|
||||
border-bottom-color: transparent;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
animation: rotation 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
|
@ -37,6 +37,7 @@ const Home = () => {
|
|||
if (videoRef.current) {
|
||||
videoRef.current.srcObject = stream;
|
||||
}
|
||||
setLoadCamera(true);
|
||||
|
||||
// setLoadCamera(true);
|
||||
await initializeHandDetection();
|
||||
|
@ -145,7 +146,7 @@ const Home = () => {
|
|||
loadModel();
|
||||
startWebcam();
|
||||
|
||||
setLoadCamera(true);
|
||||
|
||||
|
||||
return () => {
|
||||
if (handLandmarker) {
|
||||
|
@ -180,7 +181,7 @@ const Home = () => {
|
|||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center flex-1">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-4 border-b-4 border-primary"></div>
|
||||
<div className="loader"></div>
|
||||
<p className="mt-4 text-lg text-gray-700">Loading...</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import LayoutPage from "@/components/templates/LayoutPage";
|
||||
import useNavbarStore from "@/stores/NavbarStore";
|
||||
import { useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const Kuis = () => {
|
||||
const store = useNavbarStore();
|
||||
|
@ -11,31 +12,39 @@ const Kuis = () => {
|
|||
return (
|
||||
<LayoutPage>
|
||||
<div className="flex flex-col flex-1 py-4">
|
||||
<h1 className="font-semibold text-3xl">Ayoo Kuiss</h1>
|
||||
<h1 className="font-semibold text-xl md:text-3xl">Ayoo Kuiss</h1>
|
||||
<p>Be the first!</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-24 md:gap-6 mt-24 md:mt-52 h-full mb-12">
|
||||
<div className="relative ">
|
||||
<img
|
||||
className="absolute w-24 -top-12 left-4 md:left-12"
|
||||
src="/assets/images/tebak-huruf.png"
|
||||
alt="Tebak Huruf"
|
||||
/>
|
||||
<div className="flex flex-col w-full bg-[#7FAFEF] hover:bg-[#6290cc] rounded-md px-4 md:px-12 py-6 pb-12 text-white pt-32 duration-300">
|
||||
<h1 className="font-semibold text-4xl">Tebak Huruf</h1>
|
||||
<p>Tebak huruf dan coba simulasikan</p>
|
||||
<Link to="/kuis/tebak-huruf">
|
||||
<div className="relative ">
|
||||
<img
|
||||
className="absolute w-24 -top-12 left-4 md:left-12"
|
||||
src="/assets/images/tebak-huruf.png"
|
||||
alt="Tebak Huruf"
|
||||
/>
|
||||
<div className="flex flex-col w-full bg-[#7FAFEF] hover:bg-[#6290cc] rounded-md px-4 md:px-12 py-6 pb-12 text-white pt-32 duration-300">
|
||||
<h1 className="font-semibold text-xl md:text-4xl">
|
||||
Tebak Huruf
|
||||
</h1>
|
||||
<p>Tebak huruf dan coba simulasikan</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative ">
|
||||
<img
|
||||
className="absolute w-42 -top-12 left-4 md:left-12"
|
||||
src="/assets/images/menyusun-huruf.png"
|
||||
alt="Menyusun Huruf"
|
||||
/>
|
||||
<div className="flex flex-col w-full bg-[#FF6884] hover:bg-[#d3546b] rounded-md px-4 md:px-12 py-6 pb-12 text-white pt-32 duration-300">
|
||||
<h1 className="font-semibold text-4xl">Menyusun Huruf</h1>
|
||||
<p>Susun huruf jadi kata yang tepat</p>
|
||||
</Link>
|
||||
<Link to="/kuis/menyusun-huruf">
|
||||
<div className="relative ">
|
||||
<img
|
||||
className="absolute w-42 -top-12 left-4 md:left-12"
|
||||
src="/assets/images/menyusun-huruf.png"
|
||||
alt="Menyusun Huruf"
|
||||
/>
|
||||
<div className="flex flex-col w-full bg-[#FF6884] hover:bg-[#d3546b] rounded-md px-4 md:px-12 py-6 pb-12 text-white pt-32 duration-300">
|
||||
<h1 className="font-semibold text-xl md:text-4xl">
|
||||
Menyusun Huruf
|
||||
</h1>
|
||||
<p>Susun huruf jadi kata yang tepat</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutPage>
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
import LayoutPage from "@/components/templates/LayoutPage";
|
||||
import useMenyusunHurufStore from "@/stores/MenyusunHurufStore";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const MenyusunHuruf = () => {
|
||||
const quizStore = useMenyusunHurufStore();
|
||||
|
||||
const shuffleArray = (array: any[]) => {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
return array;
|
||||
};
|
||||
|
||||
return (
|
||||
<LayoutPage>
|
||||
<div className="flex flex-col flex-1 py-4 relative">
|
||||
<ul className="flex gap-3 mt-4">
|
||||
<li className="hover:text-primary cursor-default">Home</li>
|
||||
<li>{">"}</li>
|
||||
<Link to="/kuis" className="hover:text-primary">
|
||||
Kuis
|
||||
</Link>
|
||||
<li>{">"}</li>
|
||||
<li className="font-medium text-gray-500">Menyusun Huruf</li>
|
||||
</ul>
|
||||
|
||||
<div className="flex-1 flex flex-col mt-12 md:mt-24 w-full md:w-[500px]">
|
||||
<h1 className="text-5xl font-semibold">
|
||||
Start Your <span className="text-primary">Quiz!</span>
|
||||
</h1>
|
||||
<div className="flex flex-col gap-3 w-full mt-24 md:mt-36">
|
||||
<h1 className="font-semibold text-xl">Masukkan Nama</h1>
|
||||
<div className="bg-[#F2F2F2] rounded-full pr-4 flex py-2 items-center">
|
||||
<input
|
||||
placeholder="Name.."
|
||||
type="text"
|
||||
className="bg-transparent outline-none p-2 px-8 flex-1 w-full"
|
||||
/>
|
||||
<Link to="/kuis/menyusun-huruf/app">
|
||||
<button
|
||||
onClick={() => {
|
||||
quizStore.setListSoal(shuffleArray(quizStore.listSoal));
|
||||
quizStore.setSession(true)
|
||||
}}
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white px-3 py-2 rounded-full whitespace-nowrap"
|
||||
>
|
||||
Mulai Kuis
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
<span className="text-primary">Lihat Ranking</span>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
className="absolute right-0 md:top-36 bottom-0 pointer-events-none"
|
||||
src="/assets/images/overlay-bg.png"
|
||||
alt="Overlay Background"
|
||||
/>
|
||||
</div>
|
||||
</LayoutPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenyusunHuruf;
|
|
@ -0,0 +1,327 @@
|
|||
import LayoutPage from "@/components/templates/LayoutPage";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import * as tf from "@tensorflow/tfjs";
|
||||
import { FilesetResolver, HandLandmarker } from "@mediapipe/tasks-vision";
|
||||
import calcLandmarkList from "@/utils/CalculateLandmark";
|
||||
import preProcessLandmark from "@/utils/PreProcessLandmark";
|
||||
import { abjads } from "@/utils/ConvertResult";
|
||||
import useNavbarStore from "@/stores/NavbarStore";
|
||||
import ProgressBar from "@/components/molecules/ProgressBar";
|
||||
import { MdOutlineQuiz } from "react-icons/md";
|
||||
import useMenyusunHurufStore from "@/stores/MenyusunHurufStore";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
// type PredictResult = {
|
||||
// abjad: String;
|
||||
// acc: String;
|
||||
// };
|
||||
|
||||
const Quiz = () => {
|
||||
const quizStore = useMenyusunHurufStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!quizStore.session) {
|
||||
window.location.href = "/kuis/menyusun-huruf";
|
||||
}
|
||||
}, []);
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const [loadCamera, setLoadCamera] = useState(false);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
// const [setResultPredict] = useState<PredictResult>({
|
||||
// abjad: "",
|
||||
// acc: "",
|
||||
// });
|
||||
|
||||
const [showAnswer, setShowAnswer] = useState(false);
|
||||
|
||||
let model: tf.LayersModel;
|
||||
let handLandmarker: HandLandmarker;
|
||||
|
||||
const [handPresence, setHandPresence] = useState(false);
|
||||
|
||||
const startWebcam = async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: true,
|
||||
});
|
||||
|
||||
if (videoRef.current) {
|
||||
videoRef.current.srcObject = stream;
|
||||
}
|
||||
|
||||
setLoadCamera(true);
|
||||
|
||||
// setLoadCamera(true);
|
||||
await initializeHandDetection();
|
||||
} catch (error) {
|
||||
console.error("Error accessing webcam:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadModel = async () => {
|
||||
setLoadCamera(false);
|
||||
try {
|
||||
const lm = await tf.loadLayersModel("/model/model.json");
|
||||
model = lm;
|
||||
|
||||
const emptyInput = tf.tensor2d([[0, 0]]);
|
||||
|
||||
model.predict(emptyInput) as tf.Tensor;
|
||||
|
||||
setLoadCamera(true);
|
||||
} catch (error) {
|
||||
// console.error("Error loading model:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const initializeHandDetection = async () => {
|
||||
try {
|
||||
const vision = await FilesetResolver.forVisionTasks(
|
||||
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm"
|
||||
);
|
||||
handLandmarker = await HandLandmarker.createFromOptions(vision, {
|
||||
baseOptions: {
|
||||
modelAssetPath: `https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task`,
|
||||
},
|
||||
numHands: 2,
|
||||
runningMode: "VIDEO",
|
||||
});
|
||||
|
||||
detectHands();
|
||||
} catch (error) {
|
||||
console.error("Error initializing hand detection:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [answer, setAnswer] = useState("");
|
||||
|
||||
let tempAnswer = "";
|
||||
let noSoal = 0;
|
||||
let startTimer: number = 0;
|
||||
let answerTime: number = 0;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [resultAnswer, setResultAnswer] = useState({
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
});
|
||||
|
||||
const makePrediction = async (finalResult: any) => {
|
||||
if (startTimer === 0) {
|
||||
startTimer = Date.now(); // Mulai waktu saat pertama kali prediksi
|
||||
}
|
||||
|
||||
const input = tf.tensor2d([finalResult]);
|
||||
|
||||
// Melakukan prediksi
|
||||
const prediction = model.predict(input) as tf.Tensor;
|
||||
|
||||
const result = prediction.dataSync();
|
||||
|
||||
const maxEntry = Object.entries(result).reduce((max, entry) => {
|
||||
const [, value] = entry;
|
||||
return value > max[1] ? entry : max;
|
||||
});
|
||||
|
||||
// maxEntry sekarang berisi [key, value] dengan nilai terbesar
|
||||
const [maxKey] = maxEntry;
|
||||
|
||||
// const percentageValue = (maxValue * 100).toFixed(2) + "%";
|
||||
|
||||
let currentResult = abjads[parseInt(maxKey)];
|
||||
|
||||
// Hapus tensor
|
||||
input.dispose();
|
||||
prediction.dispose();
|
||||
|
||||
if (tempAnswer.length == 0) {
|
||||
const firstChar = quizStore.listSoal[noSoal].charAt(0);
|
||||
console.log(noSoal);
|
||||
|
||||
if (currentResult === firstChar) {
|
||||
tempAnswer += currentResult;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
currentResult === quizStore.listSoal[noSoal].charAt(tempAnswer.length)
|
||||
) {
|
||||
tempAnswer += currentResult;
|
||||
}
|
||||
|
||||
setAnswer(tempAnswer);
|
||||
|
||||
if (tempAnswer === quizStore.listSoal[noSoal]) {
|
||||
setShowAnswer(true);
|
||||
|
||||
const elapsedTime = (Date.now() - startTimer) / 1000;
|
||||
|
||||
const minutes = Math.floor(elapsedTime / 60);
|
||||
const seconds = Math.floor(elapsedTime % 60);
|
||||
|
||||
setResultAnswer({
|
||||
minutes,
|
||||
seconds,
|
||||
});
|
||||
|
||||
startTimer = 0;
|
||||
tempAnswer = "";
|
||||
setAnswer("");
|
||||
|
||||
setTimeout(() => {
|
||||
setShowAnswer(false);
|
||||
setResultAnswer({
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
});
|
||||
}, 3000);
|
||||
|
||||
quizStore.setSoalIndex(noSoal + 1);
|
||||
|
||||
answerTime = answerTime += elapsedTime;
|
||||
|
||||
noSoal++;
|
||||
|
||||
if (noSoal === 10) {
|
||||
quizStore.setTime(answerTime);
|
||||
quizStore.setSession(false);
|
||||
|
||||
navigate("/kuis/menyusun-huruf/");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const detectHands = async () => {
|
||||
if (showAnswer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (videoRef.current && videoRef.current.readyState >= 2) {
|
||||
const detections = handLandmarker.detectForVideo(
|
||||
videoRef.current,
|
||||
performance.now()
|
||||
);
|
||||
|
||||
setHandPresence(detections.handedness.length > 0);
|
||||
// Assuming detections.landmarks is an array of landmark objects
|
||||
if (detections.landmarks) {
|
||||
if (detections.handednesses.length > 0) {
|
||||
// console.log(detections);
|
||||
|
||||
if (detections.handednesses[0][0].displayName === "Right") {
|
||||
const landm = detections.landmarks[0].map((landmark) => landmark);
|
||||
|
||||
const calt = calcLandmarkList(videoRef.current, landm);
|
||||
const finalResult = preProcessLandmark(calt);
|
||||
|
||||
makePrediction(finalResult);
|
||||
} else {
|
||||
setHandPresence(false);
|
||||
setProgress(0);
|
||||
}
|
||||
} else {
|
||||
setProgress(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(detectHands);
|
||||
};
|
||||
|
||||
const store = useNavbarStore();
|
||||
|
||||
useEffect(() => {
|
||||
store.setNavSelected("kuis");
|
||||
|
||||
loadModel();
|
||||
startWebcam();
|
||||
|
||||
return () => {
|
||||
if (handLandmarker) {
|
||||
handLandmarker.close();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LayoutPage>
|
||||
<div
|
||||
className={`fixed inset-0 w-screen h-screen bg-black/60 ${
|
||||
showAnswer ? "opacity-100" : "opacity-0"
|
||||
} z-[999] flex items-center justify-center pointer-events-none duration-300 ease-in-out`}
|
||||
>
|
||||
<div className="rounded-md px-3 py-2 text-white flex flex-col justify-center items-center gap-3">
|
||||
<img
|
||||
className="h-56"
|
||||
src="/assets/gif/betul.gif"
|
||||
alt="Jawaban Betul"
|
||||
/>
|
||||
<h1 className="text-2xl font-semibold text-center">
|
||||
Kamu berhasil menyusun dalam {resultAnswer.minutes} Menit{" "}
|
||||
{resultAnswer.seconds} Detik
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mt-4">
|
||||
<MdOutlineQuiz size={18} />
|
||||
<h1 className="text-xl font-semibold">
|
||||
Soal: {quizStore.soalIndex + 1} / 10
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col flex-1 py-4">
|
||||
{loadCamera ? (
|
||||
<div className="rounded-md overflow-hidden relative">
|
||||
{!showAnswer && (
|
||||
<div className="top-6 left-6 absolute flex flex-col gap-2">
|
||||
<div className="flex gap-2 items-center bg-white text-black rounded-md drop-shadow px-3 py-2">
|
||||
<h1 className="text-2xl font-semibold text-center">
|
||||
Susun huruf "{quizStore.listSoal[quizStore.soalIndex]}"
|
||||
</h1>
|
||||
</div>
|
||||
{answer.length > 0 && (
|
||||
<div className="flex gap-2 items-center bg-white text-black w-fit rounded-md drop-shadow px-3 py-2">
|
||||
<h1 className="text-2xl font-semibold text-center">
|
||||
{answer}
|
||||
</h1>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{handPresence && !showAnswer && (
|
||||
<div className="top-6 right-6 absolute flex gap-2 items-center bg-white text-black rounded-md drop-shadow px-3 py-2 w-fit">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="loader"></span>
|
||||
<h1>Tahan Tangan..</h1>
|
||||
</div>
|
||||
<ProgressBar progress={progress} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="absolute top-0 left-0 w-full h-full z-20"
|
||||
/>
|
||||
<video
|
||||
ref={videoRef}
|
||||
className="w-full max-h-[80svh] object-cover"
|
||||
autoPlay
|
||||
playsInline
|
||||
></video>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center flex-1">
|
||||
<div className="loader"></div>
|
||||
<p className="mt-4 text-lg text-gray-700">Loading...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</LayoutPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Quiz;
|
|
@ -0,0 +1,266 @@
|
|||
import LayoutPage from "@/components/templates/LayoutPage";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import * as tf from "@tensorflow/tfjs";
|
||||
import { FilesetResolver, HandLandmarker } from "@mediapipe/tasks-vision";
|
||||
import calcLandmarkList from "@/utils/CalculateLandmark";
|
||||
import preProcessLandmark from "@/utils/PreProcessLandmark";
|
||||
import ConvertResult from "@/utils/ConvertResult";
|
||||
import useNavbarStore from "@/stores/NavbarStore";
|
||||
import ProgressBar from "@/components/molecules/ProgressBar";
|
||||
import { MdOutlineQuiz } from "react-icons/md";
|
||||
|
||||
// type PredictResult = {
|
||||
// abjad: String;
|
||||
// acc: String;
|
||||
// };
|
||||
|
||||
const Quiz = () => {
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const [loadCamera, setLoadCamera] = useState(false);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
// const [setResultPredict] = useState<PredictResult>({
|
||||
// abjad: "",
|
||||
// acc: "",
|
||||
// });
|
||||
|
||||
const [showAnswer, setShowAnswer] = useState(false);
|
||||
|
||||
let model: tf.LayersModel;
|
||||
let handLandmarker: HandLandmarker;
|
||||
|
||||
const [handPresence, setHandPresence] = useState(false);
|
||||
|
||||
const startWebcam = async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: true,
|
||||
});
|
||||
|
||||
if (videoRef.current) {
|
||||
videoRef.current.srcObject = stream;
|
||||
}
|
||||
|
||||
setLoadCamera(true);
|
||||
|
||||
// setLoadCamera(true);
|
||||
await initializeHandDetection();
|
||||
} catch (error) {
|
||||
console.error("Error accessing webcam:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadModel = async () => {
|
||||
setLoadCamera(false);
|
||||
try {
|
||||
const lm = await tf.loadLayersModel("/model/model.json");
|
||||
model = lm;
|
||||
|
||||
const emptyInput = tf.tensor2d([[0, 0]]);
|
||||
|
||||
model.predict(emptyInput) as tf.Tensor;
|
||||
|
||||
setLoadCamera(true);
|
||||
} catch (error) {
|
||||
// console.error("Error loading model:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const initializeHandDetection = async () => {
|
||||
try {
|
||||
const vision = await FilesetResolver.forVisionTasks(
|
||||
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm"
|
||||
);
|
||||
handLandmarker = await HandLandmarker.createFromOptions(vision, {
|
||||
baseOptions: {
|
||||
modelAssetPath: `https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task`,
|
||||
},
|
||||
numHands: 2,
|
||||
runningMode: "VIDEO",
|
||||
});
|
||||
|
||||
detectHands();
|
||||
} catch (error) {
|
||||
console.error("Error initializing hand detection:", error);
|
||||
}
|
||||
};
|
||||
|
||||
let previousResult: string[] = [];
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const makePrediction = async (finalResult: any) => {
|
||||
const input = tf.tensor2d([finalResult]);
|
||||
|
||||
// Melakukan prediksi
|
||||
const prediction = model.predict(input) as tf.Tensor;
|
||||
|
||||
const result = prediction.dataSync();
|
||||
|
||||
const maxEntry = Object.entries(result).reduce((max, entry) => {
|
||||
const [, value] = entry;
|
||||
return value > max[1] ? entry : max;
|
||||
});
|
||||
|
||||
// maxEntry sekarang berisi [key, value] dengan nilai terbesar
|
||||
const [maxKey] = maxEntry;
|
||||
|
||||
// const percentageValue = (maxValue * 100).toFixed(2) + "%";
|
||||
|
||||
// setResultPredict({
|
||||
// abjad: ConvertResult(parseInt(maxKey)),
|
||||
// acc: percentageValue,
|
||||
// });
|
||||
|
||||
let currentResult = ConvertResult(parseInt(maxKey));
|
||||
|
||||
// Hapus tensor
|
||||
input.dispose();
|
||||
prediction.dispose();
|
||||
|
||||
if (
|
||||
previousResult.length > 0 &&
|
||||
previousResult[previousResult.length - 1] === currentResult
|
||||
) {
|
||||
previousResult.push(currentResult);
|
||||
setProgress((prev) => prev + 10);
|
||||
} else {
|
||||
previousResult = [currentResult];
|
||||
setProgress(10);
|
||||
}
|
||||
|
||||
if (previousResult.length == 11) {
|
||||
setShowAnswer(true);
|
||||
|
||||
previousResult = [];
|
||||
setProgress(0);
|
||||
|
||||
setTimeout(() => {
|
||||
setShowAnswer(false);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// console.log(previousResult);
|
||||
};
|
||||
|
||||
const detectHands = async () => {
|
||||
if (showAnswer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (videoRef.current && videoRef.current.readyState >= 2) {
|
||||
const detections = handLandmarker.detectForVideo(
|
||||
videoRef.current,
|
||||
performance.now()
|
||||
);
|
||||
|
||||
setHandPresence(detections.handedness.length > 0);
|
||||
// Assuming detections.landmarks is an array of landmark objects
|
||||
if (detections.landmarks) {
|
||||
if (detections.handednesses.length > 0) {
|
||||
// console.log(detections);
|
||||
|
||||
if (detections.handednesses[0][0].displayName === "Right") {
|
||||
const landm = detections.landmarks[0].map((landmark) => landmark);
|
||||
|
||||
const calt = calcLandmarkList(videoRef.current, landm);
|
||||
const finalResult = preProcessLandmark(calt);
|
||||
|
||||
makePrediction(finalResult);
|
||||
} else {
|
||||
setHandPresence(false);
|
||||
setProgress(0);
|
||||
previousResult = [];
|
||||
}
|
||||
} else {
|
||||
setProgress(0);
|
||||
previousResult = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(detectHands);
|
||||
};
|
||||
|
||||
const store = useNavbarStore();
|
||||
|
||||
useEffect(() => {
|
||||
store.setNavSelected("kuis");
|
||||
|
||||
loadModel();
|
||||
startWebcam();
|
||||
|
||||
return () => {
|
||||
if (handLandmarker) {
|
||||
handLandmarker.close();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LayoutPage>
|
||||
<div
|
||||
className={`fixed inset-0 w-screen h-screen bg-black/60 ${
|
||||
showAnswer ? "opacity-100" : "opacity-0"
|
||||
} z-[999] flex items-center justify-center pointer-events-none duration-300 ease-in-out`}
|
||||
>
|
||||
<div className="rounded-md px-3 py-2 text-white flex flex-col justify-center items-center gap-3">
|
||||
<img
|
||||
className="h-56"
|
||||
src="/assets/gif/salah.gif"
|
||||
alt="Jawaban Salah"
|
||||
/>
|
||||
<p className="text-center text-6xl font-bold">A</p>
|
||||
<h1 className="text-2xl font-semibold text-center">
|
||||
Jawaban kamu Salah
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mt-4">
|
||||
<MdOutlineQuiz size={18} />
|
||||
<h1 className="text-xl font-semibold">Soal: 1 / 10</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col flex-1 py-4">
|
||||
{loadCamera ? (
|
||||
<div className="rounded-md overflow-hidden relative">
|
||||
{!showAnswer && (
|
||||
<div className="top-6 left-6 absolute flex gap-2 items-center bg-white text-black rounded-md drop-shadow px-3 py-2">
|
||||
<h1 className="text-2xl font-semibold text-center">
|
||||
Tebak Huruf K
|
||||
</h1>
|
||||
</div>
|
||||
)}
|
||||
{handPresence && !showAnswer && (
|
||||
<div className="top-6 right-6 absolute flex gap-2 items-center bg-white text-black rounded-md drop-shadow px-3 py-2 w-fit">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="loader"></span>
|
||||
<h1>Tahan Tangan..</h1>
|
||||
</div>
|
||||
<ProgressBar progress={progress} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="absolute top-0 left-0 w-full h-full z-20"
|
||||
/>
|
||||
<video
|
||||
ref={videoRef}
|
||||
className="w-full max-h-[80svh] object-cover"
|
||||
autoPlay
|
||||
playsInline
|
||||
></video>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center flex-1">
|
||||
<div className="loader"></div>
|
||||
<p className="mt-4 text-lg text-gray-700">Loading...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</LayoutPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Quiz;
|
|
@ -0,0 +1,49 @@
|
|||
import LayoutPage from "@/components/templates/LayoutPage";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const TebakHuruf = () => {
|
||||
return (
|
||||
<LayoutPage>
|
||||
<div className="flex flex-col flex-1 py-4 relative">
|
||||
<ul className="flex gap-3 mt-4">
|
||||
<li className="hover:text-primary cursor-default">Home</li>
|
||||
<li>{">"}</li>
|
||||
<Link to="/kuis" className="hover:text-primary">
|
||||
Kuis
|
||||
</Link>
|
||||
<li>{">"}</li>
|
||||
<li className="font-medium text-gray-500">Tebak Huruf</li>
|
||||
</ul>
|
||||
|
||||
<div className="flex-1 flex flex-col mt-12 md:mt-24 w-full md:w-[500px]">
|
||||
<h1 className="text-5xl font-semibold">
|
||||
Start Your <span className="text-primary">Quiz!</span>
|
||||
</h1>
|
||||
<div className="flex flex-col gap-3 w-full mt-24 md:mt-36">
|
||||
<h1 className="font-semibold text-xl">Masukkan Nama</h1>
|
||||
<div className="bg-[#F2F2F2] rounded-full pr-4 flex py-2 items-center">
|
||||
<input
|
||||
placeholder="Name.."
|
||||
type="text"
|
||||
className="bg-transparent outline-none p-2 px-8 flex-1 w-full"
|
||||
/>
|
||||
<Link to="/kuis/tebak-huruf/app">
|
||||
<button className="bg-blue-500 hover:bg-blue-700 text-white px-3 py-2 rounded-full whitespace-nowrap">
|
||||
Mulai Kuis
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
<span className="text-primary">Lihat Ranking</span>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
className="absolute right-0 md:top-36 bottom-0 pointer-events-none"
|
||||
src="/assets/images/overlay-bg.png"
|
||||
alt="Overlay Background"
|
||||
/>
|
||||
</div>
|
||||
</LayoutPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default TebakHuruf;
|
|
@ -1,26 +1,41 @@
|
|||
import { lazy } from "react";
|
||||
|
||||
const Home = lazy(() => import("@/pages/Home"));
|
||||
const Kamus = lazy(() => import("@/pages/Kamus"));
|
||||
const Kuis = lazy(() => import("@/pages/Kuis"));
|
||||
|
||||
const myRoute = [
|
||||
{
|
||||
"title": "Home",
|
||||
"path": "/",
|
||||
"component": Home,
|
||||
},
|
||||
{
|
||||
"title": "Kamus",
|
||||
"path": "/kamus",
|
||||
"component": Kamus
|
||||
},
|
||||
{
|
||||
"title": "Kuis",
|
||||
"path": "/kuis",
|
||||
"component": Kuis
|
||||
},
|
||||
{
|
||||
title: "Home",
|
||||
path: "/",
|
||||
component: lazy(() => import("@/pages/Home")),
|
||||
},
|
||||
{
|
||||
title: "Kamus",
|
||||
path: "/kamus",
|
||||
component: lazy(() => import("@/pages/Kamus")),
|
||||
},
|
||||
{
|
||||
title: "Kuis",
|
||||
path: "/kuis",
|
||||
component: lazy(() => import("@/pages/Kuis")),
|
||||
},
|
||||
{
|
||||
title: "Tebak Huruf",
|
||||
path: "/kuis/tebak-huruf",
|
||||
component: lazy(() => import("@/pages/Kuis/TebakHuruf/TebakHuruf")),
|
||||
},
|
||||
{
|
||||
title: "Start Quiz",
|
||||
path: "/kuis/tebak-huruf/app",
|
||||
component: lazy(() => import("@/pages/Kuis/TebakHuruf/Quiz")),
|
||||
},
|
||||
{
|
||||
title: "Menyusun Huruf",
|
||||
path: "/kuis/menyusun-huruf",
|
||||
component: lazy(() => import("@/pages/Kuis/MenyusunHuruf/MenyusunHuruf")),
|
||||
},
|
||||
{
|
||||
title: "Start Quiz",
|
||||
path: "/kuis/menyusun-huruf/app",
|
||||
component: lazy(() => import("@/pages/Kuis/MenyusunHuruf/Quiz")),
|
||||
},
|
||||
];
|
||||
|
||||
]
|
||||
|
||||
export default myRoute;
|
||||
export default myRoute;
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import { create } from "zustand";
|
||||
|
||||
const soal = [
|
||||
"INDONESIA",
|
||||
"KUCING",
|
||||
"SIBI",
|
||||
"MAHASISWA",
|
||||
"KSULI",
|
||||
"INFORMATIKA",
|
||||
"CODING",
|
||||
"WHATSAPP",
|
||||
"INSTAGRAM",
|
||||
"TEMAN",
|
||||
];
|
||||
|
||||
type MenyusunHurufType = {
|
||||
listSoal: string[];
|
||||
setListSoal: (listSoal: any[]) => void;
|
||||
soalIndex: number;
|
||||
setSoalIndex: (index: number) => void;
|
||||
time: number;
|
||||
setTime: (time: number) => void;
|
||||
session: boolean;
|
||||
setSession: (session: boolean) => void;
|
||||
};
|
||||
|
||||
const useMenyusunHurufStore = create<MenyusunHurufType>((set) => ({
|
||||
listSoal: soal,
|
||||
setListSoal: (listSoal) => set({ listSoal: listSoal }),
|
||||
soalIndex: 0,
|
||||
setSoalIndex: (index) => set({ soalIndex: index }),
|
||||
time: 0,
|
||||
setTime: (time) => set({ time: time }),
|
||||
session: false,
|
||||
setSession: (session) => set({ session: session }),
|
||||
}));
|
||||
|
||||
export default useMenyusunHurufStore;
|
|
@ -0,0 +1,33 @@
|
|||
import { create } from "zustand";
|
||||
|
||||
type JawabanType = {
|
||||
jawaban: string;
|
||||
isCorrect: boolean;
|
||||
};
|
||||
|
||||
type TebakHurufType = {
|
||||
listSoal: any[];
|
||||
setListSoal: (listSoal: any[]) => void;
|
||||
soalIndex: number;
|
||||
setSoalIndex: (index: number) => void;
|
||||
score: number;
|
||||
setScore: (score: number) => void;
|
||||
jawaban: JawabanType[];
|
||||
setJawaban: (jawaban: JawabanType[]) => void;
|
||||
addJawaban: (jawaban: JawabanType) => void;
|
||||
};
|
||||
|
||||
const useTebakHurufStore = create<TebakHurufType>((set) => ({
|
||||
listSoal: [],
|
||||
setListSoal: (listSoal) => set({ listSoal: listSoal }),
|
||||
soalIndex: 0,
|
||||
setSoalIndex: (index) => set({ soalIndex: index }),
|
||||
score: 0,
|
||||
setScore: (score) => set({ score: score }),
|
||||
jawaban: [],
|
||||
setJawaban: (jawaban) => set({ jawaban: jawaban }),
|
||||
addJawaban: (jawaban) =>
|
||||
set((state) => ({ jawaban: [...state.jawaban, jawaban] })),
|
||||
}));
|
||||
|
||||
export default useTebakHurufStore;
|
Loading…
Reference in New Issue