3 Commits

Author SHA1 Message Date
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
10 changed files with 61 additions and 102 deletions
+1 -6
View File
@@ -51,13 +51,8 @@ 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.
Your marking is primarily based on the user's answer being semantically equivalent, in the given scenario, Output in the following JSON format: {{"correct": bool, "reason": str}}
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.
+1 -81
View File
@@ -101,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', 'Water', 'some', 'yesterday'], wordBank: ['I', 'sushi', 'cookies', 'want', 'please', 'give', 'rice', 'some', 'yesterday'],
// expected: '水をください', // expected: '水をください',
expected: 'Water please', expected: 'Water please',
type: "written-question" type: "written-question"
@@ -137,83 +137,3 @@ export const chapters_jp: Chapter[] = [
}] }]
} }
] ]
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 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, chapters_es} from "./CourseData"; import {Chapter, chapters_jp} 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
@@ -22,7 +20,6 @@ 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: []},
] ]
@@ -33,7 +30,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, "experience": 10, "completed_modules": [], "day_streak": 0, "speaking": 0} users[username] = {password, language}
db.users = JSON.stringify(users) db.users = JSON.stringify(users)
db.user = username db.user = username
+35 -4
View File
@@ -3,6 +3,7 @@ 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();
@@ -12,6 +13,8 @@ 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);
@@ -24,7 +27,7 @@ export default function Character() {
useEffect(() => { useEffect(() => {
scrollToBottom() scrollToBottom()
}, [messages]); }, [messages, isLoading]);
useEffect(() => { useEffect(() => {
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => { navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
@@ -35,12 +38,17 @@ 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);
@@ -48,7 +56,8 @@ 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: 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> <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 ? ( {messages.length === 0 && !isLoading ? (
<p className='subtext'>Please record a message to start the conversation.</p> <p className="text-center text-gray-400">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}`}>
@@ -83,6 +92,28 @@ 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 -1
View File
@@ -12,7 +12,7 @@ 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 (
+20 -4
View File
@@ -2,6 +2,7 @@ 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();
@@ -11,6 +12,7 @@ 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);
@@ -22,13 +24,16 @@ export default function Character() {
scrollToBottom() scrollToBottom()
}, [messages]); }, [messages]);
const handleSendClick = () => { const handleSendClick = async () => {
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) => {
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> </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>