6 Commits

Author SHA1 Message Date
azalea 4076776d7d Merge branch 'master' of https://github.com/hykilpikonna/CSC318
Deploy to GH Pages / build-and-deploy (push) Has been cancelled
2023-12-06 06:47:13 -05:00
azalea 2da4bb213e [+] Finalize 2023-12-06 06:46:40 -05:00
Azalea 966c6a271b Merge pull request #9 from hykilpikonna/message-loading
[+] Message loading indicator
2023-12-06 13:59:13 +09:00
Leuxll 354cb6f322 Delete frontend/package-lock.json 2023-12-05 18:40:58 -05:00
Yue Fung Lee eb54b0470d Added message loading indicator 2023-12-05 18:29:08 -05:00
Juan Pablo Acosta a5ecfde9db Merge pull request #8 from hykilpikonna/page-improvements
Page improvements
2023-12-02 07:00:34 -05:00
17 changed files with 168 additions and 201 deletions
+1 -6
View File
@@ -51,13 +51,8 @@ def ai_mark(request: AIMarkRequest):
marking_system_prompt = f"""
You are a marking system for a language learning app.
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.
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.
Output in the following JSON format: {{"correct": bool, "reason": str}}
"""
user_prompt = f"""
The question is: {request.question}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -17,6 +17,7 @@ export default function VerbalPronunciationExercise({q, chapter, onSubmit}: Verb
const [loading, setLoading] = useState(false);
const [correct, setCorrect] = useState("");
const [reason, setReason] = useState("");
const [userAnswer, setUserAnswer] = useState("");
const handleSubmit = () => {
if (answered) {
@@ -46,7 +47,8 @@ export default function VerbalPronunciationExercise({q, chapter, onSubmit}: Verb
const audioFile = new File([blob], "audio.wav", { type: 'audio/wav' });
const text = await speechToText(audioFile);
const aiMark = await getAIMarking(q.question, text.toLowerCase(), "", chapter, language);
setUserAnswer(text);
const aiMark = await getAIMarking(`Please pronounce the following: ${q.question}`, text.toLowerCase(), "", chapter, language);
setAnswered(true);
setCorrect(aiMark.correct);
setReason(aiMark.reason);
@@ -73,14 +75,14 @@ export default function VerbalPronunciationExercise({q, chapter, onSubmit}: Verb
<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 ?
{isRecording ? 'Stop Recording'
: loading ?
<ClipLoader
color="white"
loading={loading}
aria-label="Loading Spinner"
data-testid="loader"
/> :
/> :
'Record'}
</button>
</div>
@@ -88,24 +90,25 @@ export default function VerbalPronunciationExercise({q, chapter, onSubmit}: Verb
)
} 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>
<div className='flex flex-wrap w-full gap-5'>
<div className="font-bold">{correct ? "Correct!" : "Incorrect"}</div>
<div>{reason}</div>
<button className='green w-full' onClick={() => handleSubmit()}>{!answered ? "Submit" : "Continue"}</button>
{answered && !correct && <button className='red w-full' onClick={() => handleSubmit()}>I was right</button>}
</div>
)
}
}
return (
<div>
<div className="v-layout page-pad 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'>
{q.question}
</div>
{ResponseSection(correct, reason)}
</div>
return <div className="v-layout flex justify-center h-full">
<div className="font-bold">Say the following</div>
<div className='box'>
{q.question}
</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,41 +52,40 @@ export default function VerbalQuestionsExercise({q, chapter, onSubmit}: VerbalQu
}
const ResponseSection = (correct: string | null, reason: string | null) => {
if (!answered) {
return (
<div className='w-full h-32'>
<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)}>
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={() => 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>
))}
</div>
<button className='green' onClick={() => 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>
)
}
)
} else {
return <div className='flex flex-wrap w-full gap-5'>
<div className="font-bold">{correct ? "Correct!" : "Incorrect"}</div>
<div>{reason}</div>
<button className='green w-full' onClick={() => handleSubmit()}>{!answered ? "Submit" : "Continue"}</button>
{answered && !correct && <button className='red w-full' onClick={() => handleSubmit()}>I was right</button>}
</div>
}
}
return (
<div className='v-layout space-y-8 items-center w-full'>
+7 -5
View File
@@ -43,11 +43,12 @@ export default function VideoExercise({q, chapter, onSubmit}: VideoQuestionProps
}
return (
<div className='v-layout space-y-8 items-center w-full'>
<div className='v-layout w-full h-full non-center'>
<div className="box">
{q.question}
</div>
<video src={q.clipUrl} controls className='video-player'></video>
<div className="flex-1"></div>
<div className='flex-col w-full'>
{!answered ?
<div className="v-layout">
@@ -57,10 +58,11 @@ export default function VideoExercise({q, chapter, onSubmit}: VideoQuestionProps
{loading ? <ClipLoader color="white" loading={loading} /> : "Submit"}
</button>
</div> :
<div className='flex-col w-full'>
<h3>{correct}</h3>
<p>{reason}</p>
<button className='green w-full' onClick={() => handleSubmit()}>Continue</button>
<div className='flex flex-wrap w-full gap-5'>
<div className="font-bold">{correct ? "Correct!" : "Incorrect"}</div>
<div>{reason}</div>
<button className='green w-full' onClick={() => handleSubmit()}>{!answered ? "Submit" : "Continue"}</button>
{answered && !correct && <button className='red w-full' onClick={() => handleSubmit()}>I was right</button>}
</div>
}
</div>
@@ -80,13 +80,12 @@ export default function WrittenQuestionExercise({q, chapter, onSubmit}: WrittenQ
)
} 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={() => handleSubmit()}>{!answered ? "Submit" : "Continue"}</button>
</div>
)
return <div className='flex flex-wrap w-full gap-5'>
<div className="font-bold">{correct ? "Correct!" : "Incorrect"}</div>
<div>{reason}</div>
<button className='green w-full' onClick={() => handleSubmit()}>{!answered ? "Submit" : "Continue"}</button>
{answered && !correct && <button className='red w-full' onClick={() => handleSubmit()}>I was right</button>}
</div>
}
}
@@ -1,5 +1,6 @@
import { useState } from 'react';
import {VocabularyQuestion} from "../logic/CourseData";
import {Icon} from "@iconify/react";
interface WrittenVocabularyProps {
q: VocabularyQuestion
@@ -19,24 +20,30 @@ export default function WrittenQuestionExercise({q, onSubmit}: WrittenVocabulary
}
return (
<div className='v-layout space-y-8 items-center w-full'>
<h1>Vocabulary: Do you know this word?</h1>
<div className='round box h-min no-shadow relative min-h-[60px] flex items-center justify-center mx-5'>
<div className='v-layout gap-5 h-full'>
<div className="font-bold">Recall the following word</div>
<div className='box text-center'>
{q.question}
</div>
<div className=' flex-col flex-wrap w-full'>
{answered ?
<div className='flex-col w-full'>
<h3>{q.pronunciation}</h3>
<p>{q.description}</p>
<p>{q.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 className="flex-1 flex justify-center items-center 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 ?
<>
<button className='green my-4' onClick={(e) => handleSubmit()}>I got it!</button>
<button className='red' onClick={(e) => handleSubmit()}>I forgot</button>
</> :
<>
<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='white' onClick={(e) => handleSubmit()}>Show Meaning</button>
</>
}
</div>
</div>
+15 -96
View File
@@ -59,14 +59,6 @@ export const chapters_jp: Chapter[] = [
name: 'Order food',
steps: [{
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: すしをください',
wordBank: ['I', 'sushi', 'cookies', 'want', 'please', 'give', 'rice', 'some', 'yesterday'],
@@ -89,10 +81,17 @@ export const chapters_jp: Chapter[] = [
type: 'verbal-question',
},
{
question: 'Please say: すしをたくさんあります',
question: 'すしをたくさんあります',
translation: 'There is a lot of sushi',
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',
}
]
},
{
@@ -101,7 +100,7 @@ export const chapters_jp: Chapter[] = [
// question: "Translate this sentence: Water please",
// wordBank: ['水', 'を', 'ください', 'おいしい', 'おもい', 'すし', '中', 'です'],
question: 'Translate this sentence: 水をください',
wordBank: ['I', 'sushi', 'cookies', 'want', 'please', 'give', 'Water', 'some', 'yesterday'],
wordBank: ['I', 'sushi', 'cookies', 'want', 'please', 'give', 'rice', 'some', 'yesterday'],
// expected: '水をください',
expected: 'Water please',
type: "written-question"
@@ -122,98 +121,18 @@ export const chapters_jp: Chapter[] = [
type: 'verbal-question',
},
{
question: 'Please say: 刺身おください',
question: '刺身おください',
translation: 'Please give me sashimi',
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',
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',
}
},
]
}]
}
]
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',
}
]
}]
}
]
+3 -6
View File
@@ -1,16 +1,14 @@
import MandarinChinese from '../assets/img/lang/zh.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 {Chapter, chapters_jp, chapters_es} from "./CourseData";
import {Chapter, chapters_jp} from "./CourseData";
// db.users: Signup table map<username, password>
// db.user: Current logged-in user
const db = localStorage
// const backendUrl = 'https://318-bk.hydev.org'
const backendUrl = "https://127.0.0.1:8000"
const backendUrl = 'https://318-bk.hydev.org'
export interface Lang {
name: string
@@ -22,7 +20,6 @@ export interface Lang {
export const possibleLangs: Lang[] = [
// {name: 'Mandarin Chinese', code: 'zh', icon: MandarinChinese, data: []},
{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: []},
]
@@ -33,7 +30,7 @@ export function signup(username: string, password: string, language: string)
const users = JSON.parse(db.users)
users[username] = {password, language, "experience": 10, "completed_modules": [], "day_streak": 0, "speaking": 0}
users[username] = {password, language}
db.users = JSON.stringify(users)
db.user = username
+35 -4
View File
@@ -3,6 +3,7 @@ import { Icon } from '@iconify/react';
import CharacterBadge from '../components/CharacterBadge';
import { useState, useEffect, useRef, useCallback } from 'react';
import { speechToText, characterChatMessage, getAudio } from '../logic/sdk';
import { SyncLoader } from 'react-spinners';
export default function Character() {
const location = useLocation();
@@ -12,6 +13,8 @@ export default function Character() {
type Message = { text: string, sender: string };
const [messages, setMessages] = useState<Message[]>([]);
const [isRecording, setIsRecording] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isOtherLoading, setIsOtherLoading] = useState(false);
let chunks = [] as any;
const mediaRecorder = useRef<MediaRecorder | null>(null);
@@ -24,7 +27,7 @@ export default function Character() {
useEffect(() => {
scrollToBottom()
}, [messages]);
}, [messages, isLoading]);
useEffect(() => {
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
@@ -35,12 +38,17 @@ export default function Character() {
mediaRecorder.current.onstop = async (e) => {
setIsRecording(false);
setIsLoading(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);
setMessages(prevMessages => [...prevMessages, { text: text, sender: 'me' }]);
setIsLoading(false);
setIsOtherLoading(true);
const response = await characterChatMessage(sessionId, text);
const { msg, audio_id } = response;
const audioBlob = await getAudio(audio_id);
@@ -48,7 +56,8 @@ export default function Character() {
const audioUrl = URL.createObjectURL(audioFileResponse);
const audio = new Audio(audioUrl);
audio.play();
setMessages(prevMessages => [...prevMessages, { text: text, sender: 'me' }, { text: msg, sender: 'other' }]);
setMessages(prevMessages => [...prevMessages, { text: msg, sender: 'other' }]);
setIsOtherLoading(false);
}
});
}, []);
@@ -74,8 +83,8 @@ export default function Character() {
<h1 className="text-center">Talk With...</h1>
<CharacterBadge name={name} image={image} onClick={() => {}}/>
<div className="chat-area pb-10">
{messages.length === 0 ? (
<p className='subtext'>Please record a message to start the conversation.</p>
{messages.length === 0 && !isLoading ? (
<p className="text-center text-gray-400">Please record a message to start the conversation.</p>
) : (
messages.map((message, index) => (
<div key={index} className={`message ${message.sender}`}>
@@ -83,6 +92,28 @@ export default function Character() {
</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 ref={messageEndRef} />
</div>
+2 -1
View File
@@ -2,6 +2,7 @@ import NavBar from "../components/NavBar"
import React, { useState } from 'react';
import { generateFakeUsers } from '../logic/fakeUsers';
import { useNavigate } from 'react-router-dom';
import {getLanguage} from "../logic/sdk";
export default function CollabLearning() {
@@ -45,7 +46,7 @@ export default function CollabLearning() {
<div className="v-layout p-6">
<div className="flex flex-col flex-1">
<h1>Chat</h1>
<p className="subtext">Find people fluent in your taget language to Chat!</p>
<p className="subtext">Find people fluent in {getLanguage().name} to Chat!</p>
<p className="subtext">Help them learn a language you know!</p>
<p className="font-bold pt-10">Interests</p>
<div className="tags">
+2 -4
View File
@@ -55,10 +55,8 @@ export default function Lesson()
return (
<div className="v-layout page-pad non-center">
<Progress percent={currQuestion / questions.length * 100} back={handleNavigateBack}/>
<div className="p-5">
<div className="flex flex-col flex-1 mb-8 items-center">
{renderQuestion(currQuestion)}
</div>
<div className="p-5 h-full">
{renderQuestion(currQuestion)}
</div>
</div>
)
+2 -2
View File
@@ -12,12 +12,12 @@ export default function Review() {
const handleReviewLessonClick = (reviewType: string, lesson: string) => {
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 (
<div className="layout-v page-pad">
<h1>Review Page</h1>
<h1>Daily Review</h1>
<h2>Written</h2>
<div className="flex flex-col flex-1 mb-8 gap-3">
{writtenReview.map(lesson => (
+20 -4
View File
@@ -2,6 +2,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
import { Icon } from '@iconify/react';
import { useState, useRef, useEffect } from 'react';
import { humanChatMessage } from '../logic/sdk';
import { SyncLoader } from 'react-spinners';
export default function Character() {
const location = useLocation();
@@ -11,6 +12,7 @@ export default function Character() {
type Message = { text: string, sender: string };
const [messages, setMessages] = useState<Message[]>([]);
const [message, setMessage] = useState('');
const [isOtherLoading, setIsOtherLoading] = useState(false);
const messageEndRef = useRef<HTMLDivElement>(null);
@@ -22,13 +24,16 @@ export default function Character() {
scrollToBottom()
}, [messages]);
const handleSendClick = () => {
const handleSendClick = async () => {
if (message !== '') {
setMessages(prevMessages => [...prevMessages, { text: message, sender: 'me' }]);
setMessage('');
humanChatMessage(sessionId, message).then((response) => {
setMessages(prevMessages => [...prevMessages, { text: response.msg, sender: 'other' }]);
})
setIsOtherLoading(true);
const response = await humanChatMessage(sessionId, message);
const { msg } = response;
setMessages(prevMessages => [...prevMessages, { text: msg, sender: 'other' }]);
setIsOtherLoading(false);
}
}
@@ -55,6 +60,17 @@ export default function Character() {
</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>
</div>