Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60267f23d2 | |||
| 3d3af9a412 | |||
| d0227f7f43 | |||
| 78d5c841ea |
+6
-1
@@ -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'>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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 => (
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user