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"""
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.
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"""
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 [correct, setCorrect] = useState("");
const [reason, setReason] = useState("");
const [userAnswer, setUserAnswer] = useState("");
const handleSubmit = () => {
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 text = await speechToText(audioFile);
setUserAnswer(text);
const aiMark = await getAIMarking(`Please pronounce the following: ${q.question}`, text.toLowerCase(), "", chapter, language);
const aiMark = await getAIMarking(q.question, text.toLowerCase(), "", chapter, language);
setAnswered(true);
setCorrect(aiMark.correct);
setReason(aiMark.reason);
@@ -75,14 +73,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>
@@ -90,25 +88,24 @@ export default function VerbalPronunciationExercise({q, chapter, onSubmit}: Verb
)
} 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>
<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 className="v-layout flex justify-center h-full">
<div className="font-bold">Say the following</div>
<div className='box'>
{q.question}
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>
</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) => {
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)}>
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)}>
{word}
</span>
))}
</div>
<button className='green' onClick={() => handleSubmit()}>
{loading ? <ClipLoader
color="white"
loading={loading}
aria-label="Loading Spinner"
data-testid="loader"
/> : !answered ? "Submit" : "Continue"}
</button>
</div>
))}
</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 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>
)
} 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>
)
}
}
}
return (
<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 (
<div className='v-layout w-full h-full non-center'>
<div className='v-layout space-y-8 items-center w-full'>
<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">
@@ -58,11 +57,10 @@ export default function VideoExercise({q, chapter, onSubmit}: VideoQuestionProps
{loading ? <ClipLoader color="white" loading={loading} /> : "Submit"}
</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 className='flex-col w-full'>
<h3>{correct}</h3>
<p>{reason}</p>
<button className='green w-full' onClick={() => handleSubmit()}>Continue</button>
</div>
}
</div>
@@ -80,12 +80,13 @@ export default function WrittenQuestionExercise({q, chapter, onSubmit}: WrittenQ
)
} 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=' 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>
)
}
}
@@ -1,6 +1,5 @@
import { useState } from 'react';
import {VocabularyQuestion} from "../logic/CourseData";
import {Icon} from "@iconify/react";
interface WrittenVocabularyProps {
q: VocabularyQuestion
@@ -20,30 +19,24 @@ export default function WrittenQuestionExercise({q, onSubmit}: WrittenVocabulary
}
return (
<div className='v-layout gap-5 h-full'>
<div className="font-bold">Recall the following word</div>
<div className='box text-center'>
<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'>
{q.question}
</div>
<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 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>
</div>
+96 -15
View File
@@ -59,6 +59,14 @@ 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'],
@@ -81,17 +89,10 @@ export const chapters_jp: Chapter[] = [
type: 'verbal-question',
},
{
question: 'すしをたくさんあります',
question: 'Please say: すしをたくさんあります',
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',
}
]
},
{
@@ -100,7 +101,7 @@ export const chapters_jp: Chapter[] = [
// question: "Translate this sentence: Water please",
// wordBank: ['水', 'を', 'ください', 'おいしい', 'おもい', 'すし', '中', 'です'],
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: 'Water please',
type: "written-question"
@@ -121,18 +122,98 @@ export const chapters_jp: Chapter[] = [
type: 'verbal-question',
},
{
question: '刺身おください',
question: 'Please say: 刺身おください',
translation: 'Please give me sashimi',
type: 'verbal-pronunciation',
},
{
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',
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',
},
}
]
}]
}
]
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 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} from "./CourseData";
import {Chapter, chapters_jp, chapters_es} 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://318-bk.hydev.org'
const backendUrl = "https://127.0.0.1:8000"
export interface Lang {
name: string
@@ -20,6 +22,7 @@ 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: []},
]
@@ -30,7 +33,7 @@ export function signup(username: string, password: string, language: string)
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.user = username
+4 -35
View File
@@ -3,7 +3,6 @@ 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();
@@ -13,8 +12,6 @@ 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);
@@ -27,7 +24,7 @@ export default function Character() {
useEffect(() => {
scrollToBottom()
}, [messages, isLoading]);
}, [messages]);
useEffect(() => {
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
@@ -38,17 +35,12 @@ 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);
@@ -56,8 +48,7 @@ export default function Character() {
const audioUrl = URL.createObjectURL(audioFileResponse);
const audio = new Audio(audioUrl);
audio.play();
setMessages(prevMessages => [...prevMessages, { text: msg, sender: 'other' }]);
setIsOtherLoading(false);
setMessages(prevMessages => [...prevMessages, { text: text, sender: 'me' }, { text: msg, sender: 'other' }]);
}
});
}, []);
@@ -83,8 +74,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 && !isLoading ? (
<p className="text-center text-gray-400">Please record a message to start the conversation.</p>
{messages.length === 0 ? (
<p className='subtext'>Please record a message to start the conversation.</p>
) : (
messages.map((message, index) => (
<div key={index} className={`message ${message.sender}`}>
@@ -92,28 +83,6 @@ 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>
+1 -2
View File
@@ -2,7 +2,6 @@ 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() {
@@ -46,7 +45,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 {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="font-bold pt-10">Interests</p>
<div className="tags">
+4 -2
View File
@@ -55,8 +55,10 @@ 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 h-full">
{renderQuestion(currQuestion)}
<div className="p-5">
<div className="flex flex-col flex-1 mb-8 items-center">
{renderQuestion(currQuestion)}
</div>
</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>Daily Review</h1>
<h1>Review Page</h1>
<h2>Written</h2>
<div className="flex flex-col flex-1 mb-8 gap-3">
{writtenReview.map(lesson => (
+4 -20
View File
@@ -2,7 +2,6 @@ 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();
@@ -12,7 +11,6 @@ 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);
@@ -24,16 +22,13 @@ export default function Character() {
scrollToBottom()
}, [messages]);
const handleSendClick = async () => {
const handleSendClick = () => {
if (message !== '') {
setMessages(prevMessages => [...prevMessages, { text: message, sender: 'me' }]);
setMessage('');
setIsOtherLoading(true);
const response = await humanChatMessage(sessionId, message);
const { msg } = response;
setMessages(prevMessages => [...prevMessages, { text: msg, sender: 'other' }]);
setIsOtherLoading(false);
humanChatMessage(sessionId, message).then((response) => {
setMessages(prevMessages => [...prevMessages, { text: response.msg, sender: 'other' }]);
})
}
}
@@ -60,17 +55,6 @@ 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>