From 492589fe49c4a1dbb22ace26d49b08033b701dc0 Mon Sep 17 00:00:00 2001 From: juanpabloacosta Date: Thu, 30 Nov 2023 17:57:32 -0500 Subject: [PATCH 01/12] Added package for spinners. --- frontend/package.json | 1 + frontend/yarn.lock | 5 +++++ 2 files changed, 6 insertions(+) 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/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" From f737889799afc25f004f98d4a89f2dc6a9854079 Mon Sep 17 00:00:00 2001 From: juanpabloacosta Date: Thu, 30 Nov 2023 17:58:03 -0500 Subject: [PATCH 02/12] Added written questions for mandarin. --- frontend/src/pages/Review.tsx | 48 +++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/Review.tsx b/frontend/src/pages/Review.tsx index e982cad..3570b68 100644 --- a/frontend/src/pages/Review.tsx +++ b/frontend/src/pages/Review.tsx @@ -15,41 +15,79 @@ export default function Review() { if (language === "Japanese") { reviewQuestions = [ { - question: 'Translate this sentence: 新幹線で東京に行きます。', + 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: 空港で荷物を受け取ります。', + 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: ホテルでチェックインします。', + 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: 観光名所を訪れます。', + 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: 美味しい地元の食べ物を試します。', + 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 }, ] + } else if (language === "Mandarin Chinese") { + reviewQuestions = [ + { + question: 'Translate this sentence: 新幹線で東京に行きます.', + wordBank: ['探索', '列車', '意志', '都市', '私', '行く', '弾丸', 'へ', '東京', 'に', '山'], + expected: 'I will go to Tokyo by bullet train.', + type: reviewType, + exercise: lesson + }, + { + question: 'Translate this sentence: 空港で荷物を受け取ります.', + wordBank: ['夕日', '意志', '海洋', 'で', '冒険', '空港', 'の', '私', '受け取る', '荷物'], + expected: 'I will receive luggage at the airport.', + type: reviewType, + exercise: lesson + }, + { + question: 'Translate this sentence: ホテルでチェックインします.', + wordBank: ['文化', 'で', 'ジャングル', '意志', 'の', 'チェックイン', 'ホテル', '私', '歴史的'], + expected: 'I will check in at the hotel.', + type: reviewType, + exercise: lesson + }, + { + question: 'Translate this sentence: 観光名所を訪れます.', + wordBank: ['砂漠', '山', 'ビーチ', '訪れる', '名所', '観光客', '私', '意志'], + expected: 'I will visit tourist attractions.', + type: reviewType, + exercise: lesson + }, + { + question: 'Translate this sentence: 美味しい地元の食べ物を試します.', + wordBank: ['試す', '市場', '美味しい', '私', '食べ物', '意志', '川', '地元', '公園'], + expected: 'I will try delicious local food.', + type: reviewType, + exercise: lesson + }, + ] } navigate('/lesson', { state: { questions: reviewQuestions } }); } From 72db21192faa91fc8d11fd87b5b7be59b803aeb6 Mon Sep 17 00:00:00 2001 From: juanpabloacosta Date: Thu, 30 Nov 2023 17:58:31 -0500 Subject: [PATCH 03/12] Added progress bar and forced re-rendering of question component. --- frontend/src/pages/Lesson.tsx | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/Lesson.tsx b/frontend/src/pages/Lesson.tsx index d830698..225a8c4 100644 --- a/frontend/src/pages/Lesson.tsx +++ b/frontend/src/pages/Lesson.tsx @@ -1,21 +1,41 @@ import { useLocation, useNavigate } from 'react-router-dom'; -import { Icon } from '@iconify/react'; import { useState } from 'react'; import WrittenQuestionExercise from "../components/WrittenQuestionExercise" +import Progress from '../components/Progress'; export default function Course() { const location = useLocation(); const navigate = useNavigate(); const { questions } = location.state; - const [currQuestion, setCurrQuestion] = useState(0); + const [currQuestion, setCurrQuestion] = useState(0); - const renderLessonContent = () => { + const handleNavigateBack = () => { + navigate(-1); + }; + + const handleQuestionSubmit = () => { + if (currQuestion < questions.length - 1) { + setCurrQuestion(currQuestion + 1); + } else { + navigate("/review"); + } + } + + const renderQuestion = (questionIndex: number) => { const {question, wordBank, expected, type, exercise} = questions[currQuestion]; switch (type) { case 'written': switch (exercise) { case 'questions': - return ; + return ; // case 'vocabulary': // return ; default: @@ -37,10 +57,10 @@ export default function Course() { return (
- navigate(-1)} /> +

Course Page

- {renderLessonContent()} + {renderQuestion(currQuestion)}
) From f3588e48316431dce44b86335dd060c170a663eb Mon Sep 17 00:00:00 2001 From: juanpabloacosta Date: Thu, 30 Nov 2023 17:58:56 -0500 Subject: [PATCH 04/12] Added spinner and functional marking of user's response. --- .../components/WrittenQuestionExercise.tsx | 64 +++++++++++-------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/frontend/src/components/WrittenQuestionExercise.tsx b/frontend/src/components/WrittenQuestionExercise.tsx index 7a910d2..6ffc8e9 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,17 +32,25 @@ 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) => { @@ -58,15 +67,25 @@ export default function WrittenQuestionExercise({question, wordBank, expected, c ))} - + ) } else { return (
-

{correct}

-

{reason}

+
+

{correct}

+

{reason}

+ +
) } @@ -88,16 +107,5 @@ export default function WrittenQuestionExercise({question, wordBank, expected, c ))} {ResponseSection(correct, reason)} - {/*
- {remainingWords.map((word, index) => ( - handleWordBankClick(word)}> - {word} - - ))} -
- */} )} From 3963f9c37ac9beacd11bfc3d55157e71afb548f4 Mon Sep 17 00:00:00 2001 From: juanpabloacosta Date: Thu, 30 Nov 2023 19:41:32 -0500 Subject: [PATCH 05/12] Added vocabular question path. --- frontend/src/pages/Review.tsx | 154 ++++++++++++++++------------------ 1 file changed, 74 insertions(+), 80 deletions(-) diff --git a/frontend/src/pages/Review.tsx b/frontend/src/pages/Review.tsx index 3570b68..f269e56 100644 --- a/frontend/src/pages/Review.tsx +++ b/frontend/src/pages/Review.tsx @@ -10,86 +10,80 @@ export default function Review() { const language = getLanguage(getUsername()); 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 }; 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 - }, - ] - } else if (language === "Mandarin Chinese") { - reviewQuestions = [ - { - question: 'Translate this sentence: 新幹線で東京に行きます.', - wordBank: ['探索', '列車', '意志', '都市', '私', '行く', '弾丸', 'へ', '東京', 'に', '山'], - expected: 'I will go to Tokyo by bullet train.', - type: reviewType, - exercise: lesson - }, - { - question: 'Translate this sentence: 空港で荷物を受け取ります.', - wordBank: ['夕日', '意志', '海洋', 'で', '冒険', '空港', 'の', '私', '受け取る', '荷物'], - expected: 'I will receive luggage at the airport.', - type: reviewType, - exercise: lesson - }, - { - question: 'Translate this sentence: ホテルでチェックインします.', - wordBank: ['文化', 'で', 'ジャングル', '意志', 'の', 'チェックイン', 'ホテル', '私', '歴史的'], - expected: 'I will check in at the hotel.', - type: reviewType, - exercise: lesson - }, - { - question: 'Translate this sentence: 観光名所を訪れます.', - wordBank: ['砂漠', '山', 'ビーチ', '訪れる', '名所', '観光客', '私', '意志'], - expected: 'I will visit tourist attractions.', - type: reviewType, - exercise: lesson - }, - { - question: 'Translate this sentence: 美味しい地元の食べ物を試します.', - wordBank: ['試す', '市場', '美味しい', '私', '食べ物', '意志', '川', '地元', '公園'], - 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 + }, + { + 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 + }, + ] + 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 } }); + } } - navigate('/lesson', { state: { questions: reviewQuestions } }); } return ( @@ -112,8 +106,8 @@ export default function Review() { {verbalReview.map(lesson => ( From 6dcab6836615c8078286b583be6cc513ed7eb7f2 Mon Sep 17 00:00:00 2001 From: juanpabloacosta Date: Thu, 30 Nov 2023 19:42:06 -0500 Subject: [PATCH 06/12] Created WrittenVocabulary exercise question. --- .../components/WrittenVocabularyExercise.tsx | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 frontend/src/components/WrittenVocabularyExercise.tsx diff --git a/frontend/src/components/WrittenVocabularyExercise.tsx b/frontend/src/components/WrittenVocabularyExercise.tsx new file mode 100644 index 0000000..c746252 --- /dev/null +++ b/frontend/src/components/WrittenVocabularyExercise.tsx @@ -0,0 +1,46 @@ +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 ( +
+

Recall Meaning

+
+ {question} +
+
+ {answered ? +
+

{pronunciation}

+

{definition}

+

{example}

+
+ + +
+
+ : + + } +
+
+ )} From 56d51ca32f65efef00f2001311aa8e55e499061f Mon Sep 17 00:00:00 2001 From: juanpabloacosta Date: Thu, 30 Nov 2023 19:42:24 -0500 Subject: [PATCH 07/12] Included rendering of WrittenVocabularyExercise in lesson. --- frontend/src/pages/Lesson.tsx | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/frontend/src/pages/Lesson.tsx b/frontend/src/pages/Lesson.tsx index 225a8c4..23405d5 100644 --- a/frontend/src/pages/Lesson.tsx +++ b/frontend/src/pages/Lesson.tsx @@ -1,6 +1,7 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { useState } from 'react'; import WrittenQuestionExercise from "../components/WrittenQuestionExercise" +import WrittenVocabularyExercise from "../components/WrittenVocabularyExercise" import Progress from '../components/Progress'; export default function Course() { @@ -22,22 +23,29 @@ export default function Course() { } const renderQuestion = (questionIndex: number) => { - const {question, wordBank, expected, type, exercise} = questions[currQuestion]; - switch (type) { + const question = questions[currQuestion]; + switch (question.type) { case 'written': - switch (exercise) { + switch (question.exercise) { case 'questions': return ; - // case 'vocabulary': - // return ; + case 'vocabulary': + return ; default: return null; } From 9c7f8576b89b7805e3e52d8fa6a3e1a21da398be Mon Sep 17 00:00:00 2001 From: juanpabloacosta Date: Thu, 30 Nov 2023 19:59:47 -0500 Subject: [PATCH 08/12] Fixed styling and appearance of questions. --- .../src/components/WrittenQuestionExercise.tsx | 18 ++++++++---------- .../components/WrittenVocabularyExercise.tsx | 1 - frontend/src/pages/Lesson.tsx | 1 - 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/WrittenQuestionExercise.tsx b/frontend/src/components/WrittenQuestionExercise.tsx index 6ffc8e9..ed3803d 100644 --- a/frontend/src/components/WrittenQuestionExercise.tsx +++ b/frontend/src/components/WrittenQuestionExercise.tsx @@ -56,12 +56,12 @@ export default function WrittenQuestionExercise({question, wordBank, expected, c const ResponseSection = (correct: string | null, reason: string | null) => { if (!answered) { return ( -
-
+
+
{remainingWords.map((word, index) => ( handleWordBankClick(word)}> {word} @@ -80,12 +80,10 @@ export default function WrittenQuestionExercise({question, wordBank, expected, c ) } else { return ( -
-
-

{correct}

-

{reason}

- -
+
+

{correct}

+

{reason}

+
) } @@ -96,7 +94,7 @@ export default function WrittenQuestionExercise({question, wordBank, expected, c
{question}
-
+
{selectedWords.map((word, index) => ( -

Recall Meaning

{question}
diff --git a/frontend/src/pages/Lesson.tsx b/frontend/src/pages/Lesson.tsx index 23405d5..f055db5 100644 --- a/frontend/src/pages/Lesson.tsx +++ b/frontend/src/pages/Lesson.tsx @@ -66,7 +66,6 @@ export default function Course() { return (
-

Course Page

{renderQuestion(currQuestion)}
From 4c293e463ecd1cc5e753f6bf68a49ed5419d10f9 Mon Sep 17 00:00:00 2001 From: juanpabloacosta Date: Thu, 30 Nov 2023 20:11:45 -0500 Subject: [PATCH 09/12] Added ability to remember where you started lesson (ie. course or review) --- frontend/src/pages/Lesson.tsx | 35 ++++++++++++++++++++++------------- frontend/src/pages/Review.tsx | 5 +++-- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/frontend/src/pages/Lesson.tsx b/frontend/src/pages/Lesson.tsx index f055db5..e275625 100644 --- a/frontend/src/pages/Lesson.tsx +++ b/frontend/src/pages/Lesson.tsx @@ -2,12 +2,13 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { useState } from 'react'; import WrittenQuestionExercise from "../components/WrittenQuestionExercise" import WrittenVocabularyExercise from "../components/WrittenVocabularyExercise" +import VerbalQuestionsExercise from "../components/VerbalQuestionsExercise" import Progress from '../components/Progress'; -export default function Course() { +export default function Lesson() { const location = useLocation(); const navigate = useNavigate(); - const { questions } = location.state; + const { questions, home } = location.state; const [currQuestion, setCurrQuestion] = useState(0); const handleNavigateBack = () => { @@ -18,11 +19,11 @@ export default function Course() { if (currQuestion < questions.length - 1) { setCurrQuestion(currQuestion + 1); } else { - navigate("/review"); + navigate(home); } } - const renderQuestion = (questionIndex: number) => { + const renderQuestion = (currIndex: number) => { const question = questions[currQuestion]; switch (question.type) { case 'written': @@ -49,17 +50,25 @@ export default function Course() { default: return null; } - // case 'verbal-listening': - // switch (exercise) { - // case 'questions': - // return ; + case 'verbal-listening': + switch (question.exercise) { + case 'questions': + return ; // case 'pronunciation': // return ; - // default: - // return null; - // } - // default: - // return null; + default: + return null; + } + default: + return null; } }; diff --git a/frontend/src/pages/Review.tsx b/frontend/src/pages/Review.tsx index f269e56..7d5577b 100644 --- a/frontend/src/pages/Review.tsx +++ b/frontend/src/pages/Review.tsx @@ -1,9 +1,10 @@ -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"]; @@ -81,7 +82,7 @@ export default function Review() { exercise: 'vocabulary', }, ]; - navigate('/lesson', { state: { questions: reviewQuestions } }); + navigate('/lesson', { state: { questions: reviewQuestions, home: location.pathname } }); } } } From b88b0fd1f49bcc5b80b0b940f6a233bceb9de9fb Mon Sep 17 00:00:00 2001 From: juanpabloacosta Date: Thu, 30 Nov 2023 23:32:57 -0500 Subject: [PATCH 10/12] Added paths to verbal questions. --- frontend/src/pages/Lesson.tsx | 14 +++++-- frontend/src/pages/Review.tsx | 69 +++++++++++++++++++++++++++-------- 2 files changed, 65 insertions(+), 18 deletions(-) diff --git a/frontend/src/pages/Lesson.tsx b/frontend/src/pages/Lesson.tsx index e275625..7f32fb5 100644 --- a/frontend/src/pages/Lesson.tsx +++ b/frontend/src/pages/Lesson.tsx @@ -4,6 +4,7 @@ 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 Lesson() { const location = useLocation(); @@ -50,7 +51,7 @@ export default function Lesson() { default: return null; } - case 'verbal-listening': + case 'verbal': switch (question.exercise) { case 'questions': return ; - // case 'pronunciation': - // return ; + case 'pronunciation': + return ; default: return null; } diff --git a/frontend/src/pages/Review.tsx b/frontend/src/pages/Review.tsx index 7d5577b..ec4b26c 100644 --- a/frontend/src/pages/Review.tsx +++ b/frontend/src/pages/Review.tsx @@ -13,6 +13,7 @@ export default function Review() { const handleReviewLessonClick = (reviewType: string, lesson: string) => { 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") { if (reviewType === "written" && lesson === "questions") { let reviewQuestions: WrittenQuestion[] = []; @@ -37,21 +38,7 @@ export default function Review() { 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 - }, + } ] navigate('/lesson', { state: { questions: reviewQuestions } }); } else if (reviewType === "written" && lesson === "vocabulary") { @@ -83,6 +70,58 @@ export default function Review() { }, ]; 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 } }); } } } From f0efdb556fb01b76349bae27daa5083ca6704a84 Mon Sep 17 00:00:00 2001 From: juanpabloacosta Date: Thu, 30 Nov 2023 23:33:11 -0500 Subject: [PATCH 11/12] Fully implemented the verbal pronunciation exercise. --- .../VerbalPronunciationExercise.tsx | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 frontend/src/components/VerbalPronunciationExercise.tsx 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 From 43fa15768754889a14268065aec02d7a1df82270 Mon Sep 17 00:00:00 2001 From: juanpabloacosta Date: Thu, 30 Nov 2023 23:33:35 -0500 Subject: [PATCH 12/12] Laid out the verbal question exercise, but incomplete (needs text to speech). --- .../components/VerbalQuestionsExercise.tsx | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 frontend/src/components/VerbalQuestionsExercise.tsx 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)} +
+ )}