diff --git a/frontend/package.json b/frontend/package.json index 24fc840..d185d73 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "react-dom": "^18.2.0", "react-router-dom": "^6.20.0", "react-scripts": "^5.0.1", + "react-spinners": "^0.13.8", "sass": "^1.69.5", "typescript": "^4.4.2", "web-vitals": "^2.1.0" diff --git a/frontend/src/components/VerbalPronunciationExercise.tsx b/frontend/src/components/VerbalPronunciationExercise.tsx new file mode 100644 index 0000000..fe1699a --- /dev/null +++ b/frontend/src/components/VerbalPronunciationExercise.tsx @@ -0,0 +1,115 @@ +import { useLocation, useNavigate } from 'react-router-dom'; +import { Icon } from '@iconify/react'; +import CharacterBadge from '../components/CharacterBadge'; +import { useState, useEffect, useRef, useCallback } from 'react'; +import { speechToText, getAIMarking } from '../logic/sdk'; +import ClipLoader from "react-spinners/ClipLoader"; + +interface VerbalPronunciationProps { + question: string; + expected: string; + chapter: string; + language: string; + onQuestionSubmit: Function; + } + +export default function VerbalPronunciationExercise({question, expected, chapter, language, onQuestionSubmit}: VerbalPronunciationProps) { + const location = useLocation(); + const navigate = useNavigate(); + const [answered, setAnswered] = useState(false); + const [loading, setLoading] = useState(false); + const [correct, setCorrect] = useState(""); + const [reason, setReason] = useState(""); + + const handleSubmit = () => { + if (answered) { + setAnswered(false); + onQuestionSubmit(); + } + } + + type Message = { text: string, sender: string }; + const [isRecording, setIsRecording] = useState(false); + + let chunks = [] as any; + const mediaRecorder = useRef(null); + + useEffect(() => { + navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => { + mediaRecorder.current = new MediaRecorder(stream); + mediaRecorder.current.ondataavailable = (e) => { + chunks.push(e.data); + } + + mediaRecorder.current.onstop = async (e) => { + setIsRecording(false); + setLoading(true); + const blob = new Blob(chunks, { type: 'audio/wav' }); + chunks = []; + + const audioFile = new File([blob], "audio.wav", { type: 'audio/wav' }); + + const text = await speechToText(audioFile); + const aiMark = await getAIMarking(question, text.toLowerCase(), expected.toLowerCase(), chapter, language); + setAnswered(true); + setCorrect(aiMark.correct); + setReason(aiMark.reason); + } + }); + }, []); + + const handleRecord = useCallback(() => { + if (!isRecording) { + if (mediaRecorder.current) { + mediaRecorder.current.start(); + } + setIsRecording(true); + } else { + if (mediaRecorder.current) { + mediaRecorder.current.stop(); + } + } + }, [isRecording]); + + const ResponseSection = (correct: string | null, reason: string | null) => { + if (!answered) { + return ( +
+ + +
+ + ) + } else { + return ( +
+

{correct}

+

{reason}

+ +
+ ) + } + } + + return ( +
+
+

Say the following

+
+ {question} +
+ {ResponseSection(correct, reason)} +
+
+ ) +} \ No newline at end of file diff --git a/frontend/src/components/VerbalQuestionsExercise.tsx b/frontend/src/components/VerbalQuestionsExercise.tsx new file mode 100644 index 0000000..1d8d01f --- /dev/null +++ b/frontend/src/components/VerbalQuestionsExercise.tsx @@ -0,0 +1,117 @@ +import { useState } from 'react'; +import { getAIMarking } from '../logic/sdk'; +import ClipLoader from "react-spinners/ClipLoader"; +import { Icon } from '@iconify/react'; + +interface VerbalQuestionProps { + question: string; + wordBank: string[]; + expected: string; + chapter: string; + language: string; + onQuestionSubmit: Function; +} + +export default function VerbalQuestionsExercise({question, wordBank, expected, chapter, language, onQuestionSubmit}: VerbalQuestionProps) { + const [selectedWords, setSelectedWords] = useState([]); + const [remainingWords, setRemainingWords] = useState(wordBank); + const lastPunctuation = question[question.length - 1]; + const [answered, setAnswered] = useState(false); + const [correct, setCorrect] = useState(""); + const [reason, setReason] = useState(""); + const [loading, setLoading] = useState(false); + const [isListening, setListening] = useState(false); + + const handleWordBankClick = (word: string) => { + setSelectedWords([...selectedWords, word]); + setRemainingWords(remainingWords.filter((w) => w !== word)); + } + + const handleSelectedWordClick = (word: string) => { + setRemainingWords([...remainingWords, word]); + setSelectedWords(selectedWords.filter((w) => w !== word)); + } + + const handleSubmit = () => { + const userAnswer = selectedWords.join(" ") + lastPunctuation; + if (answered) { + setAnswered(false); + onQuestionSubmit(); + } else { + setLoading(true); + getAIMarking( + question, + userAnswer.toLowerCase(), + expected.toLowerCase(), + chapter, + language + ).then((res) => { + setLoading(false); + setAnswered(true); + setCorrect(res.correct); + setReason(res.reason); + }).catch((e) => console.error(e)); + } + + } + + const ResponseSection = (correct: string | null, reason: string | null) => { + if (!answered) { + return ( +
+
+ {remainingWords.map((word, index) => ( + handleWordBankClick(word)}> + {word} + + ))} +
+ +
+ + ) + } else { + return ( +
+

{correct}

+

{reason}

+ +
+ ) + } + } + + const handleListenToQuestion = () => { + // TODO: Play static file that holds the audio for the current + console.log("Heard you"); + } + + return ( +
+

What do you hear?

+
+ +
+
+ {selectedWords.map((word, index) => ( + handleSelectedWordClick(word)}> + {word} + + ))} +
+ {ResponseSection(correct, reason)} +
+ )} diff --git a/frontend/src/components/WrittenQuestionExercise.tsx b/frontend/src/components/WrittenQuestionExercise.tsx index 7a910d2..ed3803d 100644 --- a/frontend/src/components/WrittenQuestionExercise.tsx +++ b/frontend/src/components/WrittenQuestionExercise.tsx @@ -1,6 +1,6 @@ -import ChatBoxBorder from '../assets/img/chatbox-border.svg'; import { useState } from 'react'; import { getAIMarking } from '../logic/sdk'; +import ClipLoader from "react-spinners/ClipLoader"; interface WrittenQuestionProps { question: string; @@ -8,16 +8,17 @@ interface WrittenQuestionProps { expected: string; chapter: string; language: string; - setCurrQuestion: Function; + onQuestionSubmit: Function; } -export default function WrittenQuestionExercise({question, wordBank, expected, chapter, language}: WrittenQuestionProps) { +export default function WrittenQuestionExercise({question, wordBank, expected, chapter, language, onQuestionSubmit}: WrittenQuestionProps) { const [selectedWords, setSelectedWords] = useState([]); const [remainingWords, setRemainingWords] = useState(wordBank); const lastPunctuation = question[question.length - 1]; const [answered, setAnswered] = useState(false); const [correct, setCorrect] = useState(""); const [reason, setReason] = useState(""); + const [loading, setLoading] = useState(false); const handleWordBankClick = (word: string) => { setSelectedWords([...selectedWords, word]); @@ -31,42 +32,58 @@ export default function WrittenQuestionExercise({question, wordBank, expected, c const handleSubmit = () => { const userAnswer = selectedWords.join(" ") + lastPunctuation; - getAIMarking( - question, - userAnswer, - expected, - chapter, - language - ).then((res) => { - setAnswered(true); - setCorrect(res.correct); - setReason(res.reason); - }) + if (answered) { + setAnswered(false); + onQuestionSubmit(); + } else { + setLoading(true); + getAIMarking( + question, + userAnswer.toLowerCase(), + expected.toLowerCase(), + chapter, + language + ).then((res) => { + setLoading(false); + setAnswered(true); + setCorrect(res.correct); + setReason(res.reason); + }).catch((e) => console.error(e)); + } + } const ResponseSection = (correct: string | null, reason: string | null) => { if (!answered) { return ( -
-
+
+
{remainingWords.map((word, index) => ( handleWordBankClick(word)}> {word} ))}
- +
) } else { return ( -
+

{correct}

{reason}

+
) } @@ -77,7 +94,7 @@ export default function WrittenQuestionExercise({question, wordBank, expected, c
{question}
-
+
{selectedWords.map((word, index) => ( {ResponseSection(correct, reason)} - {/*
- {remainingWords.map((word, index) => ( - handleWordBankClick(word)}> - {word} - - ))} -
- */}
)} diff --git a/frontend/src/components/WrittenVocabularyExercise.tsx b/frontend/src/components/WrittenVocabularyExercise.tsx new file mode 100644 index 0000000..c1916c4 --- /dev/null +++ b/frontend/src/components/WrittenVocabularyExercise.tsx @@ -0,0 +1,45 @@ +import { useState } from 'react'; + +interface WrittenVocabularyProps { + question: string; + pronunciation: string; + definition: string; + example: string; + onQuestionSubmit: Function; +} + +export default function WrittenQuestionExercise({question, pronunciation, definition, example, onQuestionSubmit}: WrittenVocabularyProps) { + const [answered, setAnswered] = useState(false); + + const handleSubmit = () => { + if (answered) { + setAnswered(false); + onQuestionSubmit(); + } else { + setAnswered(true); + } + + } + + return ( +
+
+ {question} +
+
+ {answered ? +
+

{pronunciation}

+

{definition}

+

{example}

+
+ + +
+
+ : + + } +
+
+ )} diff --git a/frontend/src/pages/Lesson.tsx b/frontend/src/pages/Lesson.tsx index d830698..7f32fb5 100644 --- a/frontend/src/pages/Lesson.tsx +++ b/frontend/src/pages/Lesson.tsx @@ -1,46 +1,90 @@ import { useLocation, useNavigate } from 'react-router-dom'; -import { Icon } from '@iconify/react'; import { useState } from 'react'; import WrittenQuestionExercise from "../components/WrittenQuestionExercise" +import WrittenVocabularyExercise from "../components/WrittenVocabularyExercise" +import VerbalQuestionsExercise from "../components/VerbalQuestionsExercise" +import Progress from '../components/Progress'; +import VerbalPronunciationExercise from '../components/VerbalPronunciationExercise'; -export default function Course() { +export default function Lesson() { const location = useLocation(); const navigate = useNavigate(); - const { questions } = location.state; - const [currQuestion, setCurrQuestion] = useState(0); + const { questions, home } = location.state; + const [currQuestion, setCurrQuestion] = useState(0); - const renderLessonContent = () => { - const {question, wordBank, expected, type, exercise} = questions[currQuestion]; - switch (type) { + const handleNavigateBack = () => { + navigate(-1); + }; + + const handleQuestionSubmit = () => { + if (currQuestion < questions.length - 1) { + setCurrQuestion(currQuestion + 1); + } else { + navigate(home); + } + } + + const renderQuestion = (currIndex: number) => { + const question = questions[currQuestion]; + switch (question.type) { case 'written': - switch (exercise) { + switch (question.exercise) { case 'questions': - return ; - // case 'vocabulary': - // return ; + return ; + case 'vocabulary': + return ; default: return null; } - // case 'verbal-listening': - // switch (exercise) { - // case 'questions': - // return ; - // case 'pronunciation': - // return ; - // default: - // return null; - // } - // default: - // return null; + case 'verbal': + switch (question.exercise) { + case 'questions': + return ; + case 'pronunciation': + return ; + default: + return null; + } + default: + return null; } }; return (
- navigate(-1)} /> -

Course Page

+
- {renderLessonContent()} + {renderQuestion(currQuestion)}
) diff --git a/frontend/src/pages/Review.tsx b/frontend/src/pages/Review.tsx index fd7f972..ea8b467 100644 --- a/frontend/src/pages/Review.tsx +++ b/frontend/src/pages/Review.tsx @@ -1,57 +1,129 @@ -import { useNavigate } from "react-router-dom" +import { useLocation, useNavigate } from "react-router-dom" import NavBar from "../components/NavBar" import { getLanguage, getUsername } from "../logic/sdk"; export default function Review() { const navigate = useNavigate(); + const location = useLocation(); const writtenReview = ["Questions", "Vocabulary"]; const verbalReview = ["Questions", "Pronunciation"]; const language = getLanguage().name; const handleReviewLessonClick = (reviewType: string, lesson: string) => { - type Question = { question: string, wordBank: string[], expected: string, type: string, exercise: string }; - let reviewQuestions: Question[] = []; + type WrittenQuestion = { question: string, wordBank: string[], expected: string, type: string, exercise: string }; + type VocabularyQuestion = { question: string, pronunciation: string, definition: string, example: string, type: string, exercise: string }; + type VerbalQuestion = { question: string, wordBank: string[], expected: string, type: string, exercise: string }; if (language === "Japanese") { - reviewQuestions = [ - { - question: 'Translate this sentence: 新幹線で東京に行きます。', - wordBank: ['Explore', 'Train', 'Will', 'City', 'I', 'Go', 'Bullet', 'To', 'Tokyo', 'By', 'Mountain'], - expected: 'I will go to Tokyo by bullet train.', - type: reviewType, - exercise: lesson - }, - { - question: 'Translate this sentence: 空港で荷物を受け取ります。', - wordBank: ['Sunset', 'Will', 'Ocean', 'At', 'Adventure', 'Airport', 'The', 'I', 'Receive', 'Luggage'], - expected: 'I will receive luggage at the airport.', - type: reviewType, - exercise: lesson - }, - { - question: 'Translate this sentence: ホテルでチェックインします。', - wordBank: ['Culture', 'At', 'Jungle', 'Will', 'The', 'Check-In', 'Hotel', 'I', 'Historic'], - expected: 'I will check in at the hotel.', - type: reviewType, - exercise: lesson - }, - { - question: 'Translate this sentence: 観光名所を訪れます。', - wordBank: ['Desert', 'Mountain', 'Beach', 'Visit', 'Attractions', 'Tourist', 'I', 'Will'], - expected: 'I will visit tourist attractions.', - type: reviewType, - exercise: lesson - }, - { - question: 'Translate this sentence: 美味しい地元の食べ物を試します。', - wordBank: ['Try', 'Market', 'Delicious', 'I', 'Food', 'Will', 'River', 'Local', 'Park'], - expected: 'I will try delicious local food.', - type: reviewType, - exercise: lesson - }, - ] + if (reviewType === "written" && lesson === "questions") { + let reviewQuestions: WrittenQuestion[] = []; + reviewQuestions = [ + { + question: 'Translate this sentence: 新幹線で東京に行きます.', + wordBank: ['Explore', 'Train', 'Will', 'City', 'I', 'Go', 'Bullet', 'To', 'Tokyo', 'By', 'Mountain'], + expected: 'I will go to Tokyo by bullet train.', + type: reviewType, + exercise: lesson + }, + { + question: 'Translate this sentence: 空港で荷物を受け取ります.', + wordBank: ['Sunset', 'Will', 'Ocean', 'At', 'Adventure', 'Airport', 'The', 'I', 'Receive', 'Luggage'], + expected: 'I will receive luggage at the airport.', + type: reviewType, + exercise: lesson + }, + { + question: 'Translate this sentence: ホテルでチェックインします.', + wordBank: ['Culture', 'At', 'Jungle', 'Will', 'The', 'Check-In', 'Hotel', 'I', 'Historic'], + expected: 'I will check in at the hotel.', + type: reviewType, + exercise: lesson + } + ] + navigate('/lesson', { state: { questions: reviewQuestions } }); + } else if (reviewType === "written" && lesson === "vocabulary") { + let reviewQuestions: VocabularyQuestion[] = []; + reviewQuestions = [ + { + question: '飛行機', + pronunciation: 'ひこうき (Hikouki)', + definition: '飛行機 (Hikouki) is the Japanese word for airplane. It refers to a powered flying vehicle with fixed wings and a weight greater than that of the air it displaces.', + example: '飛行機で旅行します。 (I will travel by airplane.)', + type: 'written', + exercise: 'vocabulary', + }, + { + question: '観光', + pronunciation: 'かんこう (Kankou)', + definition: '観光 (Kankou) is the Japanese word for sightseeing. It refers to the activity of visiting places of interest in a particular location, typically as part of a vacation.', + example: '観光地を訪れます。 (I will visit tourist attractions.)', + type: 'written', + exercise: 'vocabulary', + }, + { + question: '予約', + pronunciation: 'よやく (Yoyaku)', + definition: '予約 (Yoyaku) is the Japanese word for reservation. It refers to an arrangement for a seat, room, etc. to be kept for a customer at a restaurant, hotel, or other place.', + example: 'ホテルを予約します。 (I will make a hotel reservation.)', + type: 'written', + exercise: 'vocabulary', + }, + ]; + navigate('/lesson', { state: { questions: reviewQuestions, home: location.pathname } }); + } else if (reviewType === "verbal" && lesson === "questions") { + let reviewQuestions: VerbalQuestion[] = []; + reviewQuestions = [ + { + question: '新幹線で東京に行きます.', + wordBank: ['新', '幹', '線', 'で', '東', '京', 'に', '行', 'き', 'ま', 'す'], + expected: '新幹線で東京に行きます.', + type: reviewType, + exercise: lesson + }, + { + question: '空港で荷物を受け取ります.', + wordBank: ['空', '港', 'で', '荷', '物', 'を', '受', 'け', '取', 'り', 'ます', '.'], + expected: '空港で荷物を受け取ります.', + type: reviewType, + exercise: lesson + }, + { + question: 'ホテルでチェックインします.', + wordBank: ['ホ', 'テ', 'ル', 'で', 'チ', 'ェ', 'ッ', 'ク', 'イ', 'ン', 'し', 'ま', 'す', '.'], + expected: 'ホテルでチェックインします.', + type: reviewType, + exercise: lesson + } + ]; + navigate('/lesson', { state: { questions: reviewQuestions, home: location.pathname } }); + } else if (reviewType === "verbal" && lesson === "pronunciation") { + let reviewQuestions: VerbalQuestion[] = []; + reviewQuestions = [ + { + question: '新幹線で東京に行きます.', + wordBank: ['新', '幹', '線', 'で', '東', '京', 'に', '行', 'き', 'ま', 'す'], + expected: '新幹線で東京に行きます.', + type: reviewType, + exercise: lesson + }, + { + question: '空港で荷物を受け取ります.', + wordBank: ['空', '港', 'で', '荷', '物', 'を', '受', 'け', '取', 'り', 'ます', '.'], + expected: '空港で荷物を受け取ります.', + type: reviewType, + exercise: lesson + }, + { + question: 'ホテルでチェックインします.', + wordBank: ['ホ', 'テ', 'ル', 'で', 'チ', 'ェ', 'ッ', 'ク', 'イ', 'ン', 'し', 'ま', 'す', '.'], + expected: 'ホテルでチェックインします.', + type: reviewType, + exercise: lesson + } + ]; + navigate('/lesson', { state: { questions: reviewQuestions, home: location.pathname } }); + } } - navigate('/lesson', { state: { questions: reviewQuestions } }); } return ( @@ -74,8 +146,8 @@ export default function Review() { {verbalReview.map(lesson => ( diff --git a/frontend/yarn.lock b/frontend/yarn.lock index d37bb5a..2654aab 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -7905,6 +7905,11 @@ react-scripts@^5.0.1: optionalDependencies: fsevents "^2.3.2" +react-spinners@^0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/react-spinners/-/react-spinners-0.13.8.tgz#5262571be0f745d86bbd49a1e6b49f9f9cb19acc" + integrity sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA== + react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"