diff --git a/FE/pages/latihan.tsx b/FE/pages/latihan.tsx index 21713e5..426c4b0 100644 --- a/FE/pages/latihan.tsx +++ b/FE/pages/latihan.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useRef, useEffect } from 'react'; import Head from 'next/head'; import { useRouter } from 'next/router'; import styles from '../styles/Latihan.module.css'; @@ -22,13 +22,67 @@ export default function Latihan() { const router = useRouter(); const [view, setView] = useState<'landing' | 'learning' | 'quiz'>('landing'); const [stage, setStage] = useState(1); + const [stage3View, setStage3View] = useState<'learning' | 'camera' | 'analyzing' | 'result'>('learning'); + const [capturedImage, setCapturedImage] = useState(null); const [selectedOption, setSelectedOption] = useState(null); const [isCorrect, setIsCorrect] = useState(null); + const videoRef = useRef(null); const handleLanjut = () => { setView('learning'); }; + const startCamera = async () => { + setStage3View('camera'); + try { + const stream = await navigator.mediaDevices.getUserMedia({ + video: { facingMode: 'environment' } + }); + if (videoRef.current) { + videoRef.current.srcObject = stream; + } + return stream; + } catch (err) { + console.error("Camera error:", err); + } + }; + + useEffect(() => { + let activeStream: MediaStream | undefined; + + if (view === 'quiz' && stage === 3 && (stage3View === 'learning' || stage3View === 'camera')) { + startCamera().then(stream => { + activeStream = stream; + }); + } + + return () => { + if (activeStream) { + activeStream.getTracks().forEach(track => track.stop()); + } + }; + }, [view, stage]); + + const handleCapture = () => { + if (!videoRef.current) return; + + setStage3View('analyzing'); + + const canvas = document.createElement('canvas'); + canvas.width = videoRef.current.videoWidth; + canvas.height = videoRef.current.videoHeight; + const ctx = canvas.getContext('2d'); + if (ctx) { + ctx.drawImage(videoRef.current, 0, 0); + setCapturedImage(canvas.toDataURL('image/jpeg')); + } + + // Simulasi analisis + setTimeout(() => { + setStage3View('result'); + }, 2000); + }; + const handleStartQuiz = () => { setView('quiz'); }; @@ -49,13 +103,29 @@ export default function Latihan() { setView('learning'); setSelectedOption(null); setIsCorrect(null); + } else if (stage === 2 && isCorrect) { + setStage(3); + setView('learning'); + setSelectedOption(null); + setIsCorrect(null); + } else if (stage === 3) { + // Handle completion or next level + router.push('/result'); } else { - // Reset or continue setSelectedOption(null); setIsCorrect(null); } }; + const handleBack = () => { + if (stage > 1) { + setStage(stage - 1); + setView('learning'); + } else { + setView('landing'); + } + }; + if (view === 'learning') { return (
@@ -64,29 +134,58 @@ export default function Latihan() {
-
-
- - Level 1 + {stage === 3 ? ( +
+ +
+
Level 1
+

Huruf & Bunyi Dasar

+
-

Huruf & Bunyi Dasar

-
Tahap {stage}
-
-
+ ) : ( +
+
+ + Level 1 +
+

Huruf & Bunyi Dasar

+
Tahap {stage}
+
+
+
-
+ )} -
{stage === 1 ? 'Huruf Vokal' : 'Huruf Mirip'}
+ {stage === 3 && ( +
+ Duck Mascot +

Dengarkan lalu tulis dalam kertas

+
+ )} + +
+ {stage === 1 ? 'Huruf Vokal' : stage === 2 ? 'Huruf Mirip' : ''} +
-
- - - -
+ {stage !== 3 && ( +
+ + + +
+ )} -

{stage === 1 ? 'Aa' : 'Bb'}

-

{stage === 1 ? 'seperti bunyi “ayam”' : 'seperti bunyi “bola”'}

+ {stage !== 3 ? ( + <> +

{stage === 1 ? 'Aa' : 'Bb'}

+

{stage === 1 ? 'seperti bunyi “ayam”' : 'seperti bunyi “bola”'}

+ + ) : null}
+ {stage === 3 &&

Tekan tombol untuk mendengar!

} +
@@ -116,50 +217,109 @@ export default function Latihan() {
-
-
- - Level 1 -
-

Huruf & Bunyi Dasar

-
Tahap {stage}
-
-
-
-
+ {stage === 3 ? ( + <> +
+ +
+
Level 1
+

Huruf & Bunyi Dasar

+
+
-
Latihan
+ {stage3View === 'camera' || stage3View === 'analyzing' ? ( +
+
+ ) : ( +
+
+ {capturedImage ? ( + Captured + ) : ( +
+ )} +
M
+
-
Pilih huruf yang kamu dengar
+
+ Mascot +
+

Hebat! Kamu menulis huruf dengan benar. Teruskan ya!

+
+
-
- {(stage === 1 ? ['A', 'I', 'U', 'E', 'O'] : ['B', 'D', 'P', 'Q']).map((char) => ( - - ))} -
+
+ +
+
+ )} + + ) : ( + <> +
+
+ + Level 1 +
+

Huruf & Bunyi Dasar

+
Tahap {stage}
+
+
+
+
+ +
Latihan
+ +
+

+ {stage === 1 ? 'Huruf vokal apakah aku?' : 'Huruf apakah aku?'} +

+ +
+ +
Pilih huruf yang kamu dengar
+ +
+ {(stage === 1 ? ['A', 'I', 'U', 'E', 'O'] : ['B', 'D', 'P', 'Q']).map((char) => ( + + ))} +
+ + )}
- {isCorrect !== null && ( + {isCorrect !== null && stage !== 3 && (
diff --git a/FE/pages/result.tsx b/FE/pages/result.tsx index 9dbfbe3..ced1a18 100644 --- a/FE/pages/result.tsx +++ b/FE/pages/result.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import Head from 'next/head'; import { useRouter } from 'next/router'; import styles from '../styles/Result.module.css'; @@ -34,55 +34,102 @@ export default function Result() { return (
+ + Ringkasan Hasil Tes - Arcana + +
-

Hasil Analisis PANDAI

-
-
+ +

Ringkasan Hasil Tes

+
+ +
+
+
+ Inversi b/d + 80% Sering +
+
+ Urutan huruf terbalik + 40% Kadang +
+
+ Huruf hilang/tambah + 20% Jarang +
+
+ Inversi p/q + 0% Tidak ada +
-
- Duck Mascot -

Kerja Bagus! 🎉

- -
-
- Tingkat Presisi Tulisan -
- {100 - result.risk_score}% +
+

Detail Hasil per Kata

+
+
+
+ + + + +
+
+ Batu + Ditulis batu
- -

- {result.feedback} +

+
+ + + + +
+
+ Buku + Ditulis duku +
+
+
+
+ +
+

Indikasi Awal Berdasarkan Pola Error

+
+

+ Pola kesalahan konsisten dengan gejala disleksia fonologis dan visual, khususnya kesulitan membedakan huruf yang mirip secara spasial (b/d) dan mempertahankan urutan huruf dalam kata. Pola ini bukan karena kurang belajar atau tidak teliti, melainkan cara otak memproses simbol tertulis yang berbeda.

+
-
- Pola yang Dideteksi AI: -
    - {result.detected_errors.length > 0 ? ( - result.detected_errors.map((err, idx) => ( -
  • - ⚠️ {err} -
  • - )) - ) : ( -
  • - ✅ Semua ejaan dan rotasi huruf terlihat sempurna. -
  • - )} -
+
+

Rekomendasi Tindak Lanjut

+
+
+ Lanjutkan latihan harian di Leksa level 1 pengenalan huruf dasar +
+
+ Konsultasikan ke psikolog atau dokter anak tumbuh kembang +
+
+ Informasikan ke guru kelas
+
+ +
+

+ ⚠️ Disclaimer penting: Laporan ini adalah hasil skrining awal berbasis pola tulisan tangan menggunakan computer vision — BUKAN diagnosis klinis. Leksa bukan pengganti psikolog atau dokter. Jika pola error ini konsisten selama 2+ minggu, sangat disarankan konsultasi ke profesional untuk assessment formal. Hasil laporan ini dapat dicetak atau dikirim sebagai bahan diskusi dengan tenaga ahli. +

+
-
diff --git a/FE/styles/Latihan.module.css b/FE/styles/Latihan.module.css index d006b52..346774b 100644 --- a/FE/styles/Latihan.module.css +++ b/FE/styles/Latihan.module.css @@ -505,6 +505,190 @@ padding-bottom: 40px; } +.backButton { + width: 56px; + height: 56px; + border-radius: 50%; + border: 1.5px solid #E2E4E9; + background: #ffffff; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05); +} + +.stage3Header { + display: flex; + align-items: center; + gap: 20px; + padding: 20px 0; +} + +.stage3HeaderText { + display: flex; + flex-direction: column; + gap: 4px; +} + +.instructionSection { + display: flex; + align-items: center; + gap: 16px; + margin-top: 20px; +} + +.mascotIcon { + width: 48px; + height: 48px; +} + +.instructionTitle { + font-size: 24px; + font-weight: 800; + color: #7D57C1; + margin: 0; + line-height: 1.2; +} + +.hearInstruction { + text-align: center; + font-size: 16px; + font-weight: 800; + color: #333333; + margin-top: 12px; +} + +/* Camera View */ +.cameraViewfinder { + flex: 1; + background: #232323; + border-radius: 24px; + margin: 20px 0; + position: relative; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: center; + padding-bottom: 40px; + overflow: hidden; +} + +.ambilFotoBtn { + background: #7D57C1; + border: none; + color: #ffffff; + padding: 12px 40px; + border-radius: 14px; + font-size: 18px; + font-weight: 800; + cursor: pointer; + box-shadow: 0 4px 0 #5E3FA3; +} + +/* Result Step 3 Styles */ +.resultImageContainer { + width: 100%; + height: 240px; + background: #f0f0f0; + border-radius: 24px; + overflow: hidden; + position: relative; + margin-bottom: 30px; + border: 1px solid #E2E4E9; +} + +.resultImage { + width: 100%; + height: 100%; + object-fit: cover; + filter: brightness(0.8); +} + +.resultOverlay { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: #ffffff; + font-size: 64px; + font-weight: 800; + text-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); +} + +.feedbackSection { + display: flex; + align-items: flex-start; + gap: 12px; + margin-bottom: 40px; +} + +.speechBubble { + background: #ffffff; + border: 1.5px solid #E2E4E9; + border-radius: 20px; + padding: 16px 20px; + position: relative; + flex: 1; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05); +} + +.speechBubble::before { + content: ''; + position: absolute; + left: -10px; + top: 20px; + width: 20px; + height: 20px; + background: #ffffff; + border-left: 1.5px solid #E2E4E9; + border-bottom: 1.5px solid #E2E4E9; + transform: rotate(45deg); +} + +.speechText { + margin: 0; + font-size: 16px; + font-weight: 700; + color: #333333; + line-height: 1.4; +} + +.cameraVideo { + width: 100%; + height: 100%; + object-fit: cover; +} + +.loadingOverlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(125, 87, 193, 0.9); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: #ffffff; + gap: 16px; + z-index: 10; +} + +.spinner { + width: 40px; + height: 40px; + border: 4px solid rgba(255, 255, 255, 0.3); + border-top-color: #ffffff; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + .nextButtonPurple { background: #7D57C1; border: none; diff --git a/FE/styles/Result.module.css b/FE/styles/Result.module.css index e3b8048..530049d 100644 --- a/FE/styles/Result.module.css +++ b/FE/styles/Result.module.css @@ -1,145 +1,191 @@ .container { + max-width: 480px; + margin: 0 auto; min-height: 100vh; + background-color: #ffffff; + padding: 24px; display: flex; flex-direction: column; - align-items: center; - background: #ffffff; - padding: 0; font-family: 'Inter', sans-serif; } .header { - width: 100%; - padding: 60px 24px 20px 24px; display: flex; - flex-direction: column; - gap: 12px; + align-items: center; + gap: 20px; + margin-bottom: 30px; +} + +.backButton { + width: 56px; + height: 56px; + border-radius: 50%; + border: 1.5px solid #E2E4E9; + background: #ffffff; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; } .headerTitle { - font-size: 1.1rem; - font-weight: bold; + font-size: 24px; + font-weight: 800; color: #333333; margin: 0; } -.progressBar { - width: 100%; - height: 12px; - background: #eef0f3; - border-radius: 1000px; - overflow: hidden; +.section { + margin-bottom: 32px; } -.progress { - width: 100%; - height: 100%; - background: #6F45BA; - border-radius: 1000px; +.sectionTitle { + font-size: 22px; + font-weight: 800; + color: #333333; + margin-bottom: 16px; } -.content { - width: 100%; - padding: 40px 24px; +/* Summary List */ +.summaryList { display: flex; flex-direction: column; + gap: 16px; +} + +.summaryItem { + display: flex; + justify-content: space-between; align-items: center; - text-align: center; } -.duck { - width: 100px; - margin-bottom: 20px; - animation: bounce 2s infinite ease-in-out; +.summaryLabel { + font-size: 16px; + font-weight: 700; + color: #333333; } -@keyframes bounce { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-10px); } +.summaryValue { + font-size: 14px; + font-weight: 700; + color: #7D57C1; } -.title { - color: #6F45BA; - font-size: 1.8rem; - font-weight: 800; - margin-bottom: 24px; - line-height: 1.3; +/* Detail Cards */ +.detailCards { + display: flex; + flex-direction: column; + gap: 12px; } -.card { - background: rgba(255, 255, 255, 0.7); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border: 1px solid rgba(111, 69, 186, 0.2); - border-radius: 20px; - padding: 30px; - width: 100%; - max-width: 400px; - box-shadow: 0 10px 40px rgba(111, 69, 186, 0.1); +.detailCard { + border: 1.5px solid #E2E4E9; + border-radius: 16px; + padding: 16px; + display: flex; + align-items: center; + gap: 12px; +} + +.statusIcon { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; +} + +.cardContent { display: flex; flex-direction: column; - gap: 16px; - margin-bottom: 40px; } -.scoreLabel { - font-size: 1rem; - color: #555555; +.word { + font-size: 18px; + font-weight: 800; + color: #333333; +} + +.subWord { + font-size: 14px; + color: #888888; font-weight: 500; } -.scoreValue { - font-size: 3rem; - font-weight: 900; - color: #6F45BA; +/* Indication Box */ +.indicationBox { + background: #ffffff; + border: 1.5px solid #E2E4E9; + border-radius: 16px; + padding: 20px; } -.analysisList { - list-style: none; - padding: 0; +.indicationText { + font-size: 14px; + color: #333333; + line-height: 1.6; + font-weight: 500; margin: 0; +} + +/* Recommendation List */ +.recommendationList { display: flex; flex-direction: column; - gap: 10px; - text-align: left; + gap: 12px; } -.analysisItem { - background: #f7f4fc; - padding: 12px 16px; - border-radius: 10px; - font-size: 0.95rem; +.recommendationCard { + border: 1.5px solid #E2E4E9; + border-radius: 16px; + padding: 16px; + font-size: 14px; + font-weight: 700; color: #333333; + line-height: 1.4; +} + +/* Disclaimer Box */ +.disclaimerBox { + background: #F8F5FF; + border: 1.5px solid #7D57C1; + border-radius: 16px; + padding: 20px; + margin-bottom: 40px; +} + +.disclaimerText { + font-size: 13px; + color: #000000; + line-height: 1.5; + font-weight: 600; + margin: 0; display: flex; - align-items: center; + flex-direction: column; gap: 10px; - border-left: 4px solid #6F45BA; + text-align: left; } -.analysisItemError { - border-left-color: #ff4757; +/* Footer */ +.footer { + margin-top: auto; + padding-bottom: 20px; } -.buttonPrimary { +.closeButton { width: 100%; - max-width: 320px; - background: #7d57c1; - color: #ffffff; - font-size: 1.2rem; - font-weight: bold; + background: #7D57C1; border: none; - border-radius: 1000px; - padding: 16px 0; + color: #ffffff; + padding: 18px; + border-radius: 18px; + font-size: 18px; + font-weight: 800; cursor: pointer; - box-shadow: 0 4px 0 #5a3d91; - transition: transform 0.1s, box-shadow 0.1s; - display: flex; - align-items: center; - justify-content: center; - gap: 10px; + box-shadow: 0 4px 0 #5E3FA3; } -.buttonPrimary:active { - transform: translateY(4px); - box-shadow: none; +.closeButton:active { + transform: translateY(2px); + box-shadow: 0 2px 0 #5E3FA3; }