Merge pull request #6 from hykilpikonna/review-lesson-page

This commit is contained in:
Azalea
2023-12-01 13:45:34 +09:00
committed by GitHub
8 changed files with 504 additions and 99 deletions
+1
View File
@@ -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"
@@ -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<MediaRecorder | null>(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 (
<div className='flex backdrop:flex-row justify-center w-full'>
<Icon icon="mdi:microphone" className="microphone h-20 w-20 mx-auto"/>
<button className={`record-btn mx-auto ${isRecording ? 'red' : ''}`} onClick={handleRecord}>
{isRecording ? 'Stop Recording'
: loading ?
<ClipLoader
color="white"
loading={loading}
aria-label="Loading Spinner"
data-testid="loader"
/> :
'Record'}
</button>
</div>
)
} else {
return (
<div className=' flex-row flex-wrap w-full'>
<h3>{correct}</h3>
<p>{reason}</p>
<button className='record-btn w-full bottom-0 relative' onClick={(e) => handleSubmit()}>Continue</button>
</div>
)
}
}
return (
<div>
<div className="v-layout p-10 flex justify-center">
<h1 className="text-center">Say the following</h1>
<div className='round box h-min no-shadow relative min-h-[60px] flex items-center justify-center'>
{question}
</div>
{ResponseSection(correct, reason)}
</div>
</div>
)
}
@@ -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<string[]>([]);
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 (
<div className='w-full h-36'>
<div className=' flex-row flex-wrap w-full h-full'>
{remainingWords.map((word, index) => (
<span
key={index}
className="border-gray-300 border-2 m-1 p-1 px-3 rounded-xl inline-block cursor-pointer"
onClick={(event) => handleWordBankClick(word)}>
{word}
</span>
))}
</div>
<button className='green' onClick={(e) => handleSubmit()}>
{loading ? <ClipLoader
color="white"
loading={loading}
aria-label="Loading Spinner"
data-testid="loader"
/> : !answered ? "Submit" : "Continue"}
</button>
</div>
)
} else {
return (
<div className=' flex-row flex-wrap border-b-4 w-full h-36'>
<h3>{correct}</h3>
<p>{reason}</p>
<button className='green w-full' onClick={(e) => handleSubmit()}>{!answered ? "Submit" : "Continue"}</button>
</div>
)
}
}
const handleListenToQuestion = () => {
// TODO: Play static file that holds the audio for the current
console.log("Heard you");
}
return (
<div className='v-layout space-y-8 items-center w-full'>
<h1>What do you hear?</h1>
<div className='round box h-min no-shadow relative min-h-[60px] flex items-center justify-center mx-5'>
<Icon icon="mdi:volume-high" className="volume-high h-16 w-16" onClick={handleListenToQuestion} />
</div>
<div className='flex-row flex-wrap border-b-4 w-full h-36'>
{selectedWords.map((word, index) => (
<span
key={index}
className="border-gray-300 border-b-2 border-2 m-1 p-1 px-3 rounded-xl inline-block cursor-pointer"
onClick={(e) => handleSelectedWordClick(word)}>
{word}
</span>
))}
</div>
{ResponseSection(correct, reason)}
</div>
)}
@@ -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<string[]>([]);
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 (
<div>
<div className=' flex-row flex-wrap border-b-4 w-full'>
<div className='w-full h-36'>
<div className=' flex-row flex-wrap w-full h-full'>
{remainingWords.map((word, index) => (
<span
key={index}
className="border-gray-300 border-b-2 border-2 m-1 p-1 px-3 rounded-xl inline-block cursor-pointer"
className="border-gray-300 border-2 m-1 p-1 px-3 rounded-xl inline-block cursor-pointer"
onClick={(event) => handleWordBankClick(word)}>
{word}
</span>
))}
</div>
<button className='green' onClick={(e) => handleSubmit()}>Submit</button>
<button className='green' onClick={(e) => handleSubmit()}>
{loading ? <ClipLoader
color="white"
loading={loading}
aria-label="Loading Spinner"
data-testid="loader"
/> : !answered ? "Submit" : "Continue"}
</button>
</div>
)
} else {
return (
<div>
<div className=' flex-row flex-wrap border-b-4 w-full h-36'>
<h3>{correct}</h3>
<p>{reason}</p>
<button className='green w-full' onClick={(e) => handleSubmit()}>{!answered ? "Submit" : "Continue"}</button>
</div>
)
}
@@ -77,7 +94,7 @@ export default function WrittenQuestionExercise({question, wordBank, expected, c
<div className='round box h-min no-shadow relative min-h-[60px] flex items-center justify-center mx-5'>
{question}
</div>
<div className='flex-row flex-wrap border-b-4 w-full'>
<div className='flex-row flex-wrap border-b-4 w-full h-36'>
{selectedWords.map((word, index) => (
<span
key={index}
@@ -88,16 +105,5 @@ export default function WrittenQuestionExercise({question, wordBank, expected, c
))}
</div>
{ResponseSection(correct, reason)}
{/* <div className=' flex-row flex-wrap border-b-4 w-full'>
{remainingWords.map((word, index) => (
<span
key={index}
className="border-gray-300 border-b-2 border-2 m-1 p-1 px-3 rounded-xl inline-block cursor-pointer"
onClick={(event) => handleWordBankClick(word)}>
{word}
</span>
))}
</div>
<button className='green' onClick={(e) => handleSubmit()}>Submit</button> */}
</div>
)}
@@ -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 (
<div className='v-layout space-y-8 items-center w-full'>
<div className='round box h-min no-shadow relative min-h-[60px] flex items-center justify-center mx-5'>
{question}
</div>
<div className=' flex-col flex-wrap w-full'>
{answered ?
<div className='flex-col w-full'>
<h3>{pronunciation}</h3>
<p>{definition}</p>
<p>{example}</p>
<div className='flex-row'>
<button className='green my-4' onClick={(e) => handleSubmit()}>I got it!</button>
<button className='red' onClick={(e) => handleSubmit()}>I forgot</button>
</div>
</div>
:
<button className='white' onClick={(e) => handleSubmit()}>Show Meaning</button>
}
</div>
</div>
)}
+69 -25
View File
@@ -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<number>(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 <WrittenQuestionExercise question={question} wordBank={wordBank} expected={expected} chapter={"Travel"} language={"Japanese"} setCurrQuestion={setCurrQuestion}/>;
// case 'vocabulary':
// return <WrittenVocabularyLesson />;
return <WrittenQuestionExercise
key={currQuestion}
question={question.question}
wordBank={question.wordBank}
expected={question.expected}
chapter={"Travel"}
language={"Japanese"}
onQuestionSubmit={handleQuestionSubmit}
/>;
case 'vocabulary':
return <WrittenVocabularyExercise
key={currQuestion}
question={question.question}
pronunciation={question.pronunciation}
definition={question.definition}
example={question.example}
onQuestionSubmit={handleQuestionSubmit}
/>;
default:
return null;
}
// case 'verbal-listening':
// switch (exercise) {
// case 'questions':
// return <VerbalQuestionsLesson />;
// case 'pronunciation':
// return <VerbalPronunciationLesson />;
// default:
// return null;
// }
// default:
// return null;
case 'verbal':
switch (question.exercise) {
case 'questions':
return <VerbalQuestionsExercise
key={currQuestion}
question={question.question}
wordBank={question.wordBank}
expected={question.expected}
chapter={"Travel"}
language={"Japanese"}
onQuestionSubmit={handleQuestionSubmit}
/>;
case 'pronunciation':
return <VerbalPronunciationExercise
key={currQuestion}
question={question.question}
expected={question.expected}
chapter={"Travel"}
language={"Japanese"}
onQuestionSubmit={handleQuestionSubmit}
/>;
default:
return null;
}
default:
return null;
}
};
return (
<div className="v-layout p-10">
<Icon icon="mdi:arrow-left" className="back-button" onClick={() => navigate(-1)} />
<h1 className="text-center">Course Page</h1>
<Progress percent={currQuestion / questions.length * 100} back={handleNavigateBack}/>
<div className="flex flex-col flex-1 mb-8 items-center">
{renderLessonContent()}
{renderQuestion(currQuestion)}
</div>
</div>
)
+115 -43
View File
@@ -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 => (
<button
className="white"
key={"verbal-listening-" + lesson}
onClick={() => handleReviewLessonClick("verbal-listening", lesson.toLowerCase())}
key={"verbal" + lesson}
onClick={() => handleReviewLessonClick("verbal", lesson.toLowerCase())}
>
{lesson}
</button>
+5
View File
@@ -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"