4 Commits

Author SHA1 Message Date
juanpabloacosta 60267f23d2 Linked correct video to question. 2023-12-06 03:39:30 -05:00
juanpabloacosta 3d3af9a412 Removed duplicate word. 2023-12-06 03:29:11 -05:00
juanpabloacosta d0227f7f43 Added questions and related resources for Spanish lesson. 2023-12-06 03:25:47 -05:00
juanpabloacosta 78d5c841ea Tweaked AI prompt to provide more useful feedback, and more forgiving. 2023-12-06 03:25:14 -05:00
17 changed files with 201 additions and 168 deletions
+6 -1
View File
@@ -51,8 +51,13 @@ def ai_mark(request: AIMarkRequest):
marking_system_prompt = f""" marking_system_prompt = f"""
You are a marking system for a language learning app. You are a marking system for a language learning app.
You are marking a question from a chapter on {request.chapter} in {request.language}. You are marking a question from a chapter on {request.chapter} in {request.language}.
Please mark the user's answer as correct or incorrect and give a reason for your marking. Please mark the user's answer as correct or incorrect and give a reason for your marking.
Output in the following JSON format: {{"correct": bool, "reason": str}} Your marking is primarily based on the user's answer being semantically equivalent, in the given scenario,
to the expected answer, rather than identical.
Ignore incorrect spacing and capitalisation in your grading.
Output in the following JSON format: {{"correct": bool, "reason": str}}.
"reason" should always state the expected answer and the user's answer, along with an explanation of the mistake.
""" """
user_prompt = f""" user_prompt = f"""
The question is: {request.question} The question is: {request.question}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -17,7 +17,6 @@ export default function VerbalPronunciationExercise({q, chapter, onSubmit}: Verb
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [correct, setCorrect] = useState(""); const [correct, setCorrect] = useState("");
const [reason, setReason] = useState(""); const [reason, setReason] = useState("");
const [userAnswer, setUserAnswer] = useState("");
const handleSubmit = () => { const handleSubmit = () => {
if (answered) { if (answered) {
@@ -47,8 +46,7 @@ export default function VerbalPronunciationExercise({q, chapter, onSubmit}: Verb
const audioFile = new File([blob], "audio.wav", { type: 'audio/wav' }); const audioFile = new File([blob], "audio.wav", { type: 'audio/wav' });
const text = await speechToText(audioFile); const text = await speechToText(audioFile);
setUserAnswer(text); const aiMark = await getAIMarking(q.question, text.toLowerCase(), "", chapter, language);
const aiMark = await getAIMarking(`Please pronounce the following: ${q.question}`, text.toLowerCase(), "", chapter, language);
setAnswered(true); setAnswered(true);
setCorrect(aiMark.correct); setCorrect(aiMark.correct);
setReason(aiMark.reason); setReason(aiMark.reason);
@@ -90,25 +88,24 @@ export default function VerbalPronunciationExercise({q, chapter, onSubmit}: Verb
) )
} else { } else {
return ( return (
<div className='flex flex-wrap w-full gap-5'> <div className=' flex-row flex-wrap w-full'>
<div className="font-bold">{correct ? "Correct!" : "Incorrect"}</div> <h3>{correct}</h3>
<div>{reason}</div> <p>{reason}</p>
<button className='green w-full' onClick={() => handleSubmit()}>{!answered ? "Submit" : "Continue"}</button> <button className='record-btn w-full bottom-0 relative' onClick={(e) => handleSubmit()}>Continue</button>
{answered && !correct && <button className='red w-full' onClick={() => handleSubmit()}>I was right</button>} </div>
</div>
) )
} }
} }
return <div className="v-layout flex justify-center h-full"> return (
<div className="font-bold">Say the following</div> <div>
<div className='box'> <div className="v-layout page-pad flex justify-center">
{q.question} <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'>
{q.question}
</div>
{ResponseSection(correct, reason)}
</div>
</div> </div>
{userAnswer && <div className='flex items-center gap-3'> )
<Icon icon="mdi:microphone"/> {userAnswer}
</div>}
<div className="flex-1"></div>
{ResponseSection(correct, reason)}
</div>
} }
@@ -52,40 +52,41 @@ export default function VerbalQuestionsExercise({q, chapter, onSubmit}: VerbalQu
} }
const ResponseSection = (correct: string | null, reason: string | null) => { const ResponseSection = (correct: string | null, reason: string | null) => {
if (!answered) { if (!answered) {
return ( return (
<div className='w-full h-36'> <div className='w-full h-32'>
<div className=' flex-row flex-wrap w-full h-full'> <div className=' flex-row flex-wrap w-full h-full'>
{remainingWords.map((word, index) => ( {remainingWords.map((word, index) => (
<span <span
key={index} key={index}
className="border-gray-300 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={() => handleWordBankClick(word)}> onClick={(event) => handleWordBankClick(word)}>
{word} {word}
</span> </span>
))} ))}
</div> </div>
<button className='green' onClick={() => handleSubmit()}> <button className='green' onClick={(e) => handleSubmit()}>
{loading ? <ClipLoader {loading ? <ClipLoader
color="white" color="white"
loading={loading} loading={loading}
aria-label="Loading Spinner" aria-label="Loading Spinner"
data-testid="loader" data-testid="loader"
/> : !answered ? "Submit" : "Continue"} /> : !answered ? "Submit" : "Continue"}
</button> </button>
</div> </div>
) )
} else { } else {
return <div className='flex flex-wrap w-full gap-5'> return (
<div className="font-bold">{correct ? "Correct!" : "Incorrect"}</div> <div className=' flex-row flex-wrap border-b-4 w-full h-36'>
<div>{reason}</div> <h3>{correct}</h3>
<button className='green w-full' onClick={() => handleSubmit()}>{!answered ? "Submit" : "Continue"}</button> <p>{reason}</p>
{answered && !correct && <button className='red w-full' onClick={() => handleSubmit()}>I was right</button>} <button className='green w-full' onClick={(e) => handleSubmit()}>{!answered ? "Submit" : "Continue"}</button>
</div> </div>
)
}
} }
}
return ( return (
<div className='v-layout space-y-8 items-center w-full'> <div className='v-layout space-y-8 items-center w-full'>
+5 -7
View File
@@ -43,12 +43,11 @@ export default function VideoExercise({q, chapter, onSubmit}: VideoQuestionProps
} }
return ( return (
<div className='v-layout w-full h-full non-center'> <div className='v-layout space-y-8 items-center w-full'>
<div className="box"> <div className="box">
{q.question} {q.question}
</div> </div>
<video src={q.clipUrl} controls className='video-player'></video> <video src={q.clipUrl} controls className='video-player'></video>
<div className="flex-1"></div>
<div className='flex-col w-full'> <div className='flex-col w-full'>
{!answered ? {!answered ?
<div className="v-layout"> <div className="v-layout">
@@ -58,11 +57,10 @@ export default function VideoExercise({q, chapter, onSubmit}: VideoQuestionProps
{loading ? <ClipLoader color="white" loading={loading} /> : "Submit"} {loading ? <ClipLoader color="white" loading={loading} /> : "Submit"}
</button> </button>
</div> : </div> :
<div className='flex flex-wrap w-full gap-5'> <div className='flex-col w-full'>
<div className="font-bold">{correct ? "Correct!" : "Incorrect"}</div> <h3>{correct}</h3>
<div>{reason}</div> <p>{reason}</p>
<button className='green w-full' onClick={() => handleSubmit()}>{!answered ? "Submit" : "Continue"}</button> <button className='green w-full' onClick={() => handleSubmit()}>Continue</button>
{answered && !correct && <button className='red w-full' onClick={() => handleSubmit()}>I was right</button>}
</div> </div>
} }
</div> </div>
@@ -80,12 +80,13 @@ export default function WrittenQuestionExercise({q, chapter, onSubmit}: WrittenQ
) )
} else { } else {
return <div className='flex flex-wrap w-full gap-5'> return (
<div className="font-bold">{correct ? "Correct!" : "Incorrect"}</div> <div className=' flex-row flex-wrap border-b-4 w-full h-36'>
<div>{reason}</div> <h3>{correct}</h3>
<button className='green w-full' onClick={() => handleSubmit()}>{!answered ? "Submit" : "Continue"}</button> <p>{reason}</p>
{answered && !correct && <button className='red w-full' onClick={() => handleSubmit()}>I was right</button>} <button className='green w-full' onClick={() => handleSubmit()}>{!answered ? "Submit" : "Continue"}</button>
</div> </div>
)
} }
} }
@@ -1,6 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import {VocabularyQuestion} from "../logic/CourseData"; import {VocabularyQuestion} from "../logic/CourseData";
import {Icon} from "@iconify/react";
interface WrittenVocabularyProps { interface WrittenVocabularyProps {
q: VocabularyQuestion q: VocabularyQuestion
@@ -20,30 +19,24 @@ export default function WrittenQuestionExercise({q, onSubmit}: WrittenVocabulary
} }
return ( return (
<div className='v-layout gap-5 h-full'> <div className='v-layout space-y-8 items-center w-full'>
<div className="font-bold">Recall the following word</div> <h1>Vocabulary: Do you know this word?</h1>
<div className='box text-center'> <div className='round box h-min no-shadow relative min-h-[60px] flex items-center justify-center mx-5'>
{q.question} {q.question}
</div> </div>
<div className="flex-1 flex justify-center items-center w-full"> <div className=' flex-col flex-wrap w-full'>
{answered &&
<div className='flex flex-col gap-3 w-full'>
<div className="font-bold">{q.pronunciation}</div>
<div>{q.description}</div>
<div className="text-yellow-600 flex items-center gap-3"><Icon icon="fa:star"/>{q.example}</div>
</div>
}
</div>
<div className="mb-5">
{answered ? {answered ?
<> <div className='flex-col w-full'>
<button className='green my-4' onClick={(e) => handleSubmit()}>I got it!</button> <h3>{q.pronunciation}</h3>
<button className='red' onClick={(e) => handleSubmit()}>I forgot</button> <p>{q.description}</p>
</> : <p>{q.example}</p>
<> <div className='flex-row'>
<div className="text-gray-400 text-sm mb-5">Please click "show meaning" after you have tried to recall the meaning of the word</div> <button className='green my-4' onClick={(e) => handleSubmit()}>I got it!</button>
<button className='white' onClick={(e) => handleSubmit()}>Show Meaning</button> <button className='red' onClick={(e) => handleSubmit()}>I forgot</button>
</> </div>
</div>
:
<button className='white' onClick={(e) => handleSubmit()}>Show Meaning</button>
} }
</div> </div>
</div> </div>
+96 -15
View File
@@ -59,6 +59,14 @@ export const chapters_jp: Chapter[] = [
name: 'Order food', name: 'Order food',
steps: [{ steps: [{
questions: [ questions: [
{
question: 'What did Yui come to the club meeting for?',
clipUrl: window.location.origin + "/video/cake.mp4",
description: "This is a clip from the anime 'K-On'. Yui is a member of the light music club. She came to the club meeting to eat cake.",
expected: 'ケーキ or cake',
type: 'video',
},
{ {
question: 'Translate this sentence: すしをください', question: 'Translate this sentence: すしをください',
wordBank: ['I', 'sushi', 'cookies', 'want', 'please', 'give', 'rice', 'some', 'yesterday'], wordBank: ['I', 'sushi', 'cookies', 'want', 'please', 'give', 'rice', 'some', 'yesterday'],
@@ -81,17 +89,10 @@ export const chapters_jp: Chapter[] = [
type: 'verbal-question', type: 'verbal-question',
}, },
{ {
question: 'すしをたくさんあります', question: 'Please say: すしをたくさんあります',
translation: 'There is a lot of sushi', translation: 'There is a lot of sushi',
type: 'verbal-pronunciation', type: 'verbal-pronunciation',
}, },
{
question: 'What is the following song about?',
clipUrl: window.location.origin + "/video/dango.mp4",
description: "This is the song 'Dango Daikazoku' from the anime 'Clannad'. It is about a family of dango.",
expected: '団子 or dango',
type: 'video',
}
] ]
}, },
{ {
@@ -100,7 +101,7 @@ export const chapters_jp: Chapter[] = [
// question: "Translate this sentence: Water please", // question: "Translate this sentence: Water please",
// wordBank: ['水', 'を', 'ください', 'おいしい', 'おもい', 'すし', '中', 'です'], // wordBank: ['水', 'を', 'ください', 'おいしい', 'おもい', 'すし', '中', 'です'],
question: 'Translate this sentence: 水をください', question: 'Translate this sentence: 水をください',
wordBank: ['I', 'sushi', 'cookies', 'want', 'please', 'give', 'rice', 'some', 'yesterday'], wordBank: ['I', 'sushi', 'cookies', 'want', 'please', 'give', 'Water', 'some', 'yesterday'],
// expected: '水をください', // expected: '水をください',
expected: 'Water please', expected: 'Water please',
type: "written-question" type: "written-question"
@@ -121,17 +122,97 @@ export const chapters_jp: Chapter[] = [
type: 'verbal-question', type: 'verbal-question',
}, },
{ {
question: '刺身おください', question: 'Please say: 刺身おください',
translation: 'Please give me sashimi', translation: 'Please give me sashimi',
type: 'verbal-pronunciation', type: 'verbal-pronunciation',
}, },
{ {
question: 'What did Yui come to the club meeting for?', question: 'What is the following song about?',
clipUrl: window.location.origin + "/video/cake.mp4", clipUrl: window.location.origin + "/video/dango.mp4",
description: "This is a clip from the anime 'K-On'. Yui is a member of the light music club. She came to the club meeting to eat cake.", description: "This is the song 'Dango Daikazoku' from the anime 'Clannad'. It is about a family of dango.",
expected: 'ケーキ or cake', expected: '団子 or dango',
type: 'video', type: 'video',
}, }
]
}]
}
]
export const chapters_es: Chapter[] = [
{
name: 'Order food',
steps: [{
questions: [
{
question: "Translate this phrase: Me provoca un helado",
wordBank: ['I', 'want', 'ice cream', 'an', 'urgent', 'am', 'craving', 'milkshake'],
expected: 'I want an ice cream',
type: "written-question"
},
{
question: 'Pastel',
pronunciation: 'Pastel (pahs-tehl)',
description: 'Pastel is the Spanish word for "cake". It is a popular dessert in many Spanish-speaking countries, also known as torta.',
example: 'Vamos a celebrar con un pastel! (Let\'s celebrate with a cake!)',
type: 'written-vocabulary',
},
{
question: 'What do you hear?',
wordBank: ['favor', 'ver', 'el', 'menú', 'por', 'Quiero', 'libro', 'risas', 'oler'],
expected: 'Quiero ver el menú por favor',
translation: 'I want to see the menu, please',
url: window.location.origin + '/audio/es_1_1_3.mp3',
type: 'verbal-question',
},
{
question: 'Please say: Este postre es exquisito',
translation: 'This dessert is exquisite',
type: 'verbal-pronunciation',
},
{
question: 'What is the filling of the tequeño?',
clipUrl: window.location.origin + "/video/tequeños.mp4",
description: "Tequeños are a popular Venezuelan appetizer. They are made of cheese wrapped in dough and fried.",
expected: 'Cheese (queso)',
type: 'video',
}
]
},
{
questions: [
{
question: "Translate this phrase: Yo quiero agua por favor",
wordBank: ['I', 'juice', 'water', 'want', 'please', 'give', 'some', 'now'],
expected: 'I want water please',
type: "written-question"
},
{
question: 'Jugo',
pronunciation: 'Jugo (hoo-goh)',
description: 'Jugo is the Spanish word for "juice". In some parts of the world, juice is also called zumo.',
example: 'Me gusta tomar jugo de naranja! (I like to drink orange juice!)',
type: 'written-vocabulary',
},
{
question: 'What do you hear?',
wordBank: ['Que', 'Quien', 'tomar', 'yo', 'tú', 'quieres', 'comer', 'de'],
expected: 'Que quieres de comer',
translation: 'What do you want to eat?',
url: window.location.origin + '/audio/es_1_2_3.mp3',
type: 'verbal-question',
},
{
question: 'Please say: La comida está deliciosa!',
translation: 'The food is delicious!',
type: 'verbal-pronunciation',
},
{
question: 'What is the boy going to eat?',
clipUrl: window.location.origin + "/video/paella.mp4",
description: "The boy is about to eat paella, a traditional Spanish dish. It is made of rice, seafood, and vegetables.",
expected: 'Paella',
type: 'video',
}
] ]
}] }]
} }
+6 -3
View File
@@ -1,14 +1,16 @@
import MandarinChinese from '../assets/img/lang/zh.svg' import MandarinChinese from '../assets/img/lang/zh.svg'
import Japanese from '../assets/img/lang/ja.svg' import Japanese from '../assets/img/lang/ja.svg'
import Spanish from '../assets/img/lang/es.svg'
import English from '../assets/img/lang/en.svg' import English from '../assets/img/lang/en.svg'
import {Chapter, chapters_jp} from "./CourseData"; import {Chapter, chapters_jp, chapters_es} from "./CourseData";
// db.users: Signup table map<username, password> // db.users: Signup table map<username, password>
// db.user: Current logged-in user // db.user: Current logged-in user
const db = localStorage const db = localStorage
const backendUrl = 'https://318-bk.hydev.org' // const backendUrl = 'https://318-bk.hydev.org'
const backendUrl = "https://127.0.0.1:8000"
export interface Lang { export interface Lang {
name: string name: string
@@ -20,6 +22,7 @@ export interface Lang {
export const possibleLangs: Lang[] = [ export const possibleLangs: Lang[] = [
// {name: 'Mandarin Chinese', code: 'zh', icon: MandarinChinese, data: []}, // {name: 'Mandarin Chinese', code: 'zh', icon: MandarinChinese, data: []},
{name: 'Japanese', code: 'ja', icon: Japanese, data: chapters_jp}, {name: 'Japanese', code: 'ja', icon: Japanese, data: chapters_jp},
{name: 'Spanish', code: 'es', icon: Spanish, data: chapters_es},
// {name: 'English', code: 'en', icon: English, data: []}, // {name: 'English', code: 'en', icon: English, data: []},
] ]
@@ -30,7 +33,7 @@ export function signup(username: string, password: string, language: string)
const users = JSON.parse(db.users) const users = JSON.parse(db.users)
users[username] = {password, language} users[username] = {password, language, "experience": 10, "completed_modules": [], "day_streak": 0, "speaking": 0}
db.users = JSON.stringify(users) db.users = JSON.stringify(users)
db.user = username db.user = username
+4 -35
View File
@@ -3,7 +3,6 @@ import { Icon } from '@iconify/react';
import CharacterBadge from '../components/CharacterBadge'; import CharacterBadge from '../components/CharacterBadge';
import { useState, useEffect, useRef, useCallback } from 'react'; import { useState, useEffect, useRef, useCallback } from 'react';
import { speechToText, characterChatMessage, getAudio } from '../logic/sdk'; import { speechToText, characterChatMessage, getAudio } from '../logic/sdk';
import { SyncLoader } from 'react-spinners';
export default function Character() { export default function Character() {
const location = useLocation(); const location = useLocation();
@@ -13,8 +12,6 @@ export default function Character() {
type Message = { text: string, sender: string }; type Message = { text: string, sender: string };
const [messages, setMessages] = useState<Message[]>([]); const [messages, setMessages] = useState<Message[]>([]);
const [isRecording, setIsRecording] = useState(false); const [isRecording, setIsRecording] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isOtherLoading, setIsOtherLoading] = useState(false);
let chunks = [] as any; let chunks = [] as any;
const mediaRecorder = useRef<MediaRecorder | null>(null); const mediaRecorder = useRef<MediaRecorder | null>(null);
@@ -27,7 +24,7 @@ export default function Character() {
useEffect(() => { useEffect(() => {
scrollToBottom() scrollToBottom()
}, [messages, isLoading]); }, [messages]);
useEffect(() => { useEffect(() => {
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => { navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
@@ -38,17 +35,12 @@ export default function Character() {
mediaRecorder.current.onstop = async (e) => { mediaRecorder.current.onstop = async (e) => {
setIsRecording(false); setIsRecording(false);
setIsLoading(true);
const blob = new Blob(chunks, { type: 'audio/wav' }); const blob = new Blob(chunks, { type: 'audio/wav' });
chunks = []; chunks = [];
const audioFile = new File([blob], "audio.wav", { type: 'audio/wav' }); const audioFile = new File([blob], "audio.wav", { type: 'audio/wav' });
const text = await speechToText(audioFile); const text = await speechToText(audioFile);
setMessages(prevMessages => [...prevMessages, { text: text, sender: 'me' }]);
setIsLoading(false);
setIsOtherLoading(true);
const response = await characterChatMessage(sessionId, text); const response = await characterChatMessage(sessionId, text);
const { msg, audio_id } = response; const { msg, audio_id } = response;
const audioBlob = await getAudio(audio_id); const audioBlob = await getAudio(audio_id);
@@ -56,8 +48,7 @@ export default function Character() {
const audioUrl = URL.createObjectURL(audioFileResponse); const audioUrl = URL.createObjectURL(audioFileResponse);
const audio = new Audio(audioUrl); const audio = new Audio(audioUrl);
audio.play(); audio.play();
setMessages(prevMessages => [...prevMessages, { text: msg, sender: 'other' }]); setMessages(prevMessages => [...prevMessages, { text: text, sender: 'me' }, { text: msg, sender: 'other' }]);
setIsOtherLoading(false);
} }
}); });
}, []); }, []);
@@ -83,8 +74,8 @@ export default function Character() {
<h1 className="text-center">Talk With...</h1> <h1 className="text-center">Talk With...</h1>
<CharacterBadge name={name} image={image} onClick={() => {}}/> <CharacterBadge name={name} image={image} onClick={() => {}}/>
<div className="chat-area pb-10"> <div className="chat-area pb-10">
{messages.length === 0 && !isLoading ? ( {messages.length === 0 ? (
<p className="text-center text-gray-400">Please record a message to start the conversation.</p> <p className='subtext'>Please record a message to start the conversation.</p>
) : ( ) : (
messages.map((message, index) => ( messages.map((message, index) => (
<div key={index} className={`message ${message.sender}`}> <div key={index} className={`message ${message.sender}`}>
@@ -92,28 +83,6 @@ export default function Character() {
</div> </div>
)) ))
)} )}
{isLoading && (
<div className="message me">
<SyncLoader
color="white"
loading={isLoading}
aria-label="Loading Spinner"
data-testid="loader"
size={10}
/>
</div>
)}
{isOtherLoading && (
<div className="message other">
<SyncLoader
color="white"
loading={isOtherLoading}
aria-label="Loading Spinner"
data-testid="loader"
size={10}
/>
</div>
)}
</div> </div>
<div ref={messageEndRef} /> <div ref={messageEndRef} />
</div> </div>
+1 -2
View File
@@ -2,7 +2,6 @@ import NavBar from "../components/NavBar"
import React, { useState } from 'react'; import React, { useState } from 'react';
import { generateFakeUsers } from '../logic/fakeUsers'; import { generateFakeUsers } from '../logic/fakeUsers';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import {getLanguage} from "../logic/sdk";
export default function CollabLearning() { export default function CollabLearning() {
@@ -46,7 +45,7 @@ export default function CollabLearning() {
<div className="v-layout p-6"> <div className="v-layout p-6">
<div className="flex flex-col flex-1"> <div className="flex flex-col flex-1">
<h1>Chat</h1> <h1>Chat</h1>
<p className="subtext">Find people fluent in {getLanguage().name} to Chat!</p> <p className="subtext">Find people fluent in your taget language to Chat!</p>
<p className="subtext">Help them learn a language you know!</p> <p className="subtext">Help them learn a language you know!</p>
<p className="font-bold pt-10">Interests</p> <p className="font-bold pt-10">Interests</p>
<div className="tags"> <div className="tags">
+4 -2
View File
@@ -55,8 +55,10 @@ export default function Lesson()
return ( return (
<div className="v-layout page-pad non-center"> <div className="v-layout page-pad non-center">
<Progress percent={currQuestion / questions.length * 100} back={handleNavigateBack}/> <Progress percent={currQuestion / questions.length * 100} back={handleNavigateBack}/>
<div className="p-5 h-full"> <div className="p-5">
{renderQuestion(currQuestion)} <div className="flex flex-col flex-1 mb-8 items-center">
{renderQuestion(currQuestion)}
</div>
</div> </div>
</div> </div>
) )
+2 -2
View File
@@ -12,12 +12,12 @@ export default function Review() {
const handleReviewLessonClick = (reviewType: string, lesson: string) => { const handleReviewLessonClick = (reviewType: string, lesson: string) => {
const lessons: _Question[] = getLanguage().data.flatMap(chapter => chapter.steps).flatMap(step => step.questions); const lessons: _Question[] = getLanguage().data.flatMap(chapter => chapter.steps).flatMap(step => step.questions);
navigate('/lesson', { state: { questions: lessons.filter(it => it.type == `${reviewType}-${lesson}`), home: location.pathname } }); navigate('/lesson', { state: { questions: lessons.filter(it => it.type === `${reviewType}-${lesson}`), home: location.pathname } });
} }
return ( return (
<div className="layout-v page-pad"> <div className="layout-v page-pad">
<h1>Daily Review</h1> <h1>Review Page</h1>
<h2>Written</h2> <h2>Written</h2>
<div className="flex flex-col flex-1 mb-8 gap-3"> <div className="flex flex-col flex-1 mb-8 gap-3">
{writtenReview.map(lesson => ( {writtenReview.map(lesson => (
+4 -20
View File
@@ -2,7 +2,6 @@ import { useLocation, useNavigate } from 'react-router-dom';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { humanChatMessage } from '../logic/sdk'; import { humanChatMessage } from '../logic/sdk';
import { SyncLoader } from 'react-spinners';
export default function Character() { export default function Character() {
const location = useLocation(); const location = useLocation();
@@ -12,7 +11,6 @@ export default function Character() {
type Message = { text: string, sender: string }; type Message = { text: string, sender: string };
const [messages, setMessages] = useState<Message[]>([]); const [messages, setMessages] = useState<Message[]>([]);
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const [isOtherLoading, setIsOtherLoading] = useState(false);
const messageEndRef = useRef<HTMLDivElement>(null); const messageEndRef = useRef<HTMLDivElement>(null);
@@ -24,16 +22,13 @@ export default function Character() {
scrollToBottom() scrollToBottom()
}, [messages]); }, [messages]);
const handleSendClick = async () => { const handleSendClick = () => {
if (message !== '') { if (message !== '') {
setMessages(prevMessages => [...prevMessages, { text: message, sender: 'me' }]); setMessages(prevMessages => [...prevMessages, { text: message, sender: 'me' }]);
setMessage(''); setMessage('');
humanChatMessage(sessionId, message).then((response) => {
setIsOtherLoading(true); setMessages(prevMessages => [...prevMessages, { text: response.msg, sender: 'other' }]);
const response = await humanChatMessage(sessionId, message); })
const { msg } = response;
setMessages(prevMessages => [...prevMessages, { text: msg, sender: 'other' }]);
setIsOtherLoading(false);
} }
} }
@@ -60,17 +55,6 @@ export default function Character() {
</div> </div>
)) ))
)} )}
{isOtherLoading && (
<div className="message other">
<SyncLoader
color="white"
loading={isOtherLoading}
aria-label="Loading Spinner"
data-testid="loader"
size={10}
/>
</div>
)}
<div ref={messageEndRef} /> <div ref={messageEndRef} />
</div> </div>
</div> </div>