45 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
juanpabloacosta 61fa2495e4 Generalized completion message. 2023-12-02 06:58:13 -05:00
juanpabloacosta 9f65f9e935 Added lesson complete page after completing a module. 2023-12-02 06:55:43 -05:00
juanpabloacosta 82460da4e5 Fixed re-rendering of question components and context to vocabulary question. 2023-12-02 06:36:11 -05:00
azalea 1574fcce0c [+] Complete course 2023-12-01 08:31:15 -05:00
azalea 2145b08e16 [+] Prononciations 2023-12-01 07:42:43 -05:00
azalea 1397e50912 [F] Wrong course, F 2023-12-01 05:39:13 -05:00
azalea e49e9b2c8e Merge branch 'master' of https://github.com/hykilpikonna/CSC318 2023-12-01 05:35:59 -05:00
azalea 7935b78ce4 [O] Use course data 2023-12-01 05:35:55 -05:00
azalea b2f0746ec4 [+] Auto redirect 2023-12-01 05:22:05 -05:00
azalea 28eae67f9e [O] Cleanup 2023-12-01 05:21:52 -05:00
azalea 177139f9f5 [+] Course data 2023-12-01 05:21:23 -05:00
azalea d7dee43d81 [+] Deployed backend 2023-12-01 05:20:53 -05:00
Azalea 3f5751f59f [F] Fix build 2023-12-01 00:34:50 -08:00
Azalea 83b89ea942 Merge pull request #7 from hykilpikonna/collab-learning 2023-12-01 17:28:40 +09:00
Leuxll f3cbca4b73 Merge branch 'master' into collab-learning 2023-12-01 00:49:46 -05:00
Yue Fung Lee f89cf12ab3 Added Chatting between users 2023-12-01 00:46:24 -05:00
Yue Fung Lee 514fba236c fixed layout and autoscroll 2023-12-01 00:45:36 -05:00
Azalea 5c6fc7a123 Merge pull request #6 from hykilpikonna/review-lesson-page 2023-12-01 13:45:34 +09:00
juanpabloacosta 43fa157687 Laid out the verbal question exercise, but incomplete (needs text to speech). 2023-11-30 23:33:35 -05:00
juanpabloacosta f0efdb556f Fully implemented the verbal pronunciation exercise. 2023-11-30 23:33:11 -05:00
juanpabloacosta b88b0fd1f4 Added paths to verbal questions. 2023-11-30 23:32:57 -05:00
juanpabloacosta 4c293e463e Added ability to remember where you started lesson (ie. course or review) 2023-11-30 20:11:45 -05:00
juanpabloacosta 9c7f8576b8 Fixed styling and appearance of questions. 2023-11-30 19:59:47 -05:00
juanpabloacosta 56d51ca32f Included rendering of WrittenVocabularyExercise in lesson. 2023-11-30 19:42:24 -05:00
juanpabloacosta 6dcab68366 Created WrittenVocabulary exercise question. 2023-11-30 19:42:06 -05:00
juanpabloacosta 3963f9c37a Added vocabular question path. 2023-11-30 19:41:32 -05:00
Yue Fung Lee 45d9e4bcb7 Added Creating Random Users 2023-11-30 19:05:15 -05:00
Yue Fung Lee 85fe1b807f fixed review styles 2023-11-30 19:00:45 -05:00
juanpabloacosta f3588e4831 Added spinner and functional marking of user's response. 2023-11-30 17:58:56 -05:00
juanpabloacosta 72db21192f Added progress bar and forced re-rendering of question component. 2023-11-30 17:58:31 -05:00
juanpabloacosta f737889799 Added written questions for mandarin. 2023-11-30 17:58:03 -05:00
juanpabloacosta 492589fe49 Added package for spinners. 2023-11-30 17:57:32 -05:00
azalea a5a8b13a6b [+] Profile page 2023-11-30 17:30:00 -05:00
azalea 6a21716ec6 [+] Course page 2023-11-30 17:03:39 -05:00
azalea 8f94bdf73a [S] styling fixes 2023-11-30 16:11:19 -05:00
Azalea d89c91b643 Merge pull request #5 from jpablo2002/review-lessons-page 2023-12-01 02:43:18 +09:00
juanpabloacosta 2eab4e97af Wrote component for a WrittenQuestion type exercise. 2023-11-30 07:22:13 -05:00
juanpabloacosta 4e8a4acaae Wrote lesson page (currently only one type of question). 2023-11-30 07:21:52 -05:00
juanpabloacosta a34671434b Wrote page for Review with one type of review and questions for 1 language. 2023-11-30 07:21:09 -05:00
juanpabloacosta 498d188925 Added interfaces and helper function to send request for AI marking. 2023-11-30 07:20:37 -05:00
juanpabloacosta 1ff4c1cf1b Added path to a lesson (either from Review or Course since similar). 2023-11-30 07:05:06 -05:00
39 changed files with 1549 additions and 90 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
- name: Install and build
run: |
yarn install
yarn build
CI=false yarn build
- name: Upload build artifact
uses: actions/upload-pages-artifact@v1
+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}
+1
View File
@@ -14,6 +14,7 @@
"react-dom": "^18.2.0",
"react-router-dom": "^6.20.0",
"react-scripts": "^5.0.1",
"react-spinners": "^0.13.8",
"sass": "^1.69.5",
"typescript": "^4.4.2",
"web-vitals": "^2.1.0"
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,26 @@
import DuoSplash from "../assets/img/duo-splash.png";
import { useNavigate } from "react-router-dom";
interface LessonCompleteProps {
home: string;
}
export default function LessonComplete({ home } : LessonCompleteProps)
{
const navigate = useNavigate();
return <div className="flex flex-col justify-center">
<div className="flex flex-col p-5 gap-5 items-center">
<img src={DuoSplash} alt="Duolingo Logo"></img>
<h1>Well done!</h1>
<p className="white">
Experience Gained: +100XP
</p>
<button className="green" onClick={() => navigate(home)}>
Continue
</button>
</div>
</div>
}
+3 -3
View File
@@ -1,7 +1,7 @@
// NavBar.tsx
import React, { useState, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { NavItem } from '../components/NavItem';
import { NavItem } from './NavItem';
export default function NavBar() {
const navigate = useNavigate();
@@ -18,7 +18,7 @@ export default function NavBar() {
const navItems = [
{ icon: "mdi:book", label: "Courses", path: "/courses" },
{ icon: "mdi:earth", label: "Collab Learning", path: "/collab-learning" },
{ icon: "mdi:earth", label: "Chat", path: "/collab-learning" },
{ icon: "mdi:clipboard-text-clock", label: "Review", path: "/review" },
{ icon: "mdi:microphone-message", label: "Speaking", path: "/speaking" },
{ icon: "mdi:account", label: "Profile", path: "/profile" },
@@ -38,4 +38,4 @@ export default function NavBar() {
))}
</nav>
)
}
}
@@ -0,0 +1,111 @@
import { useLocation, useNavigate } from 'react-router-dom';
import { Icon } from '@iconify/react';
import { useState, useEffect, useRef, useCallback } from 'react';
import {speechToText, getAIMarking, getLanguage} from '../logic/sdk';
import ClipLoader from "react-spinners/ClipLoader";
import {VerbalPronunciation} from "../logic/CourseData";
interface VerbalPronunciationProps {
q: VerbalPronunciation;
chapter: string;
onSubmit: Function;
}
export default function VerbalPronunciationExercise({q, chapter, onSubmit}: VerbalPronunciationProps) {
const language = getLanguage().name;
const [answered, setAnswered] = useState(false);
const [loading, setLoading] = useState(false);
const [correct, setCorrect] = useState("");
const [reason, setReason] = useState("");
const handleSubmit = () => {
if (answered) {
setAnswered(false);
onSubmit();
}
}
const [isRecording, setIsRecording] = useState(false);
let chunks = [] as any;
const mediaRecorder = useRef<MediaRecorder | null>(null);
useEffect(() => {
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
mediaRecorder.current = new MediaRecorder(stream);
mediaRecorder.current.ondataavailable = (e) => {
chunks.push(e.data);
}
mediaRecorder.current.onstop = async (e) => {
setIsRecording(false);
setLoading(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);
const aiMark = await getAIMarking(q.question, text.toLowerCase(), "", chapter, language);
setAnswered(true);
setCorrect(aiMark.correct);
setReason(aiMark.reason);
}
});
}, []);
const handleRecord = useCallback(() => {
if (!isRecording) {
if (mediaRecorder.current) {
mediaRecorder.current.start();
}
setIsRecording(true);
} else {
if (mediaRecorder.current) {
mediaRecorder.current.stop();
}
}
}, [isRecording]);
const ResponseSection = (correct: string | null, reason: string | null) => {
if (!answered) {
return (
<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 ?
<ClipLoader
color="white"
loading={loading}
aria-label="Loading Spinner"
data-testid="loader"
/> :
'Record'}
</button>
</div>
)
} 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>
)
}
}
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>
)
}
@@ -0,0 +1,107 @@
import { useState } from 'react';
import {getAIMarking, getLanguage} from '../logic/sdk';
import ClipLoader from "react-spinners/ClipLoader";
import { Icon } from '@iconify/react';
import {VerbalQuestion} from "../logic/CourseData";
interface VerbalQuestionProps {
q: VerbalQuestion
chapter: string;
onSubmit: Function;
}
export default function VerbalQuestionsExercise({q, chapter, onSubmit}: VerbalQuestionProps) {
const language = getLanguage().name;
const [selectedWords, setSelectedWords] = useState<string[]>([]);
const [remainingWords, setRemainingWords] = useState(q.wordBank);
const [answered, setAnswered] = useState(false);
const [correct, setCorrect] = useState("");
const [reason, setReason] = useState("");
const [loading, setLoading] = useState(false);
const handleWordBankClick = (word: string) => {
setSelectedWords([...selectedWords, word]);
setRemainingWords(remainingWords.filter((w) => w !== word));
}
const handleSelectedWordClick = (word: string) => {
setRemainingWords([...remainingWords, word]);
setSelectedWords(selectedWords.filter((w) => w !== word));
}
const handleSubmit = () => {
const userAnswer = selectedWords.join("");
if (answered) {
setAnswered(false);
onSubmit();
} else {
setLoading(true);
getAIMarking(
q.question,
userAnswer.toLowerCase(),
q.expected.toLowerCase(),
chapter,
language
).then((res) => {
setLoading(false);
setAnswered(true);
setCorrect(res.correct);
setReason(res.reason);
}).catch((e) => console.error(e));
}
}
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={(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-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'>
<h2>What do you hear?</h2>
<audio src={q.url} controls className='audio-player'></audio>
<div className='flex-row flex-wrap border-b-4 w-full h-32'>
{selectedWords.map((word, index) => (
<span
key={index}
className="border-gray-300 border-b-2 border-2 m-1 p-1 px-3 rounded-xl inline-block cursor-pointer"
onClick={(e) => handleSelectedWordClick(word)}>
{word}
</span>
))}
</div>
{ResponseSection(correct, reason)}
</div>
)}
+69
View File
@@ -0,0 +1,69 @@
import { useState } from 'react';
import { getAIMarking, getLanguage } from '../logic/sdk';
import ClipLoader from "react-spinners/ClipLoader";
import { VideoQuestion } from "../logic/CourseData";
interface VideoQuestionProps {
q: VideoQuestion;
chapter: string;
onSubmit: Function;
}
export default function VideoExercise({q, chapter, onSubmit}: VideoQuestionProps) {
const language = getLanguage();
const [userAnswer, setUserAnswer] = useState("");
const [answered, setAnswered] = useState(false);
const [correct, setCorrect] = useState("");
const [reason, setReason] = useState("");
const [loading, setLoading] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setUserAnswer(e.target.value);
}
const handleSubmit = () => {
if (answered) {
setAnswered(false);
onSubmit();
} else {
setLoading(true);
getAIMarking(
`Please watch the video and answer the question: "${q.question}".`,
userAnswer.toLowerCase(),
`${q.expected} (For the grader, here is the video description: ${q.description}. And please accept any reasonable answer in both English and ${language.name}).`,
chapter,
language.name
).then((res) => {
setLoading(false);
setAnswered(true);
setCorrect(res.correct);
setReason(res.reason);
}).catch((e) => console.error(e));
}
}
return (
<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-col w-full'>
{!answered ?
<div className="v-layout">
<input type="text" value={userAnswer} onChange={handleChange}
placeholder="Type your answer here"/>
<button className='green' onClick={() => handleSubmit()}>
{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>
}
</div>
</div>
);
}
@@ -0,0 +1,110 @@
import { useState } from 'react';
import {getAIMarking, getLanguage} from '../logic/sdk';
import ClipLoader from "react-spinners/ClipLoader";
import {WrittenQuestion} from "../logic/CourseData";
interface WrittenQuestionProps {
q: WrittenQuestion
chapter: string;
onSubmit: Function;
}
export default function WrittenQuestionExercise({q, chapter, onSubmit}: WrittenQuestionProps) {
const language = getLanguage();
const [selectedWords, setSelectedWords] = useState<string[]>([]);
const [remainingWords, setRemainingWords] = useState(q.wordBank);
const [answered, setAnswered] = useState(false);
const [correct, setCorrect] = useState("");
const [reason, setReason] = useState("");
const [loading, setLoading] = useState(false);
const handleWordBankClick = (word: string) => {
setSelectedWords([...selectedWords, word]);
setRemainingWords(remainingWords.filter((w) => w !== word));
}
const handleSelectedWordClick = (word: string) => {
setRemainingWords([...remainingWords, word]);
setSelectedWords(selectedWords.filter((w) => w !== word));
}
const handleSubmit = () => {
// TODO: This space join isn't correct.
// When the word bank is Japanese or Chinese, the words are not separated by spaces.
// When the langauge is English, the words are separated by spaces.
// We need to figure out how to handle this.
const userAnswer = selectedWords.join(" ");
if (answered) {
setAnswered(false);
onSubmit();
} else {
setLoading(true);
getAIMarking(
q.question,
userAnswer.toLowerCase(),
q.expected.toLowerCase(),
chapter,
language.name
).then((res) => {
setLoading(false);
setAnswered(true);
setCorrect(res.correct);
setReason(res.reason);
}).catch((e) => console.error(e));
}
}
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={() => 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={() => handleSubmit()}>{!answered ? "Submit" : "Continue"}</button>
</div>
)
}
}
return (
<div className='v-layout space-y-8 items-center w-full'>
<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-row flex-wrap border-b-4 w-full h-36'>
{selectedWords.map((word, index) => (
<span
key={index}
className="border-gray-300 border-b-2 border-2 m-1 p-1 px-3 rounded-xl inline-block cursor-pointer"
onClick={() => handleSelectedWordClick(word)}>
{word}
</span>
))}
</div>
{ResponseSection(correct, reason)}
</div>
)}
@@ -0,0 +1,43 @@
import { useState } from 'react';
import {VocabularyQuestion} from "../logic/CourseData";
interface WrittenVocabularyProps {
q: VocabularyQuestion
onSubmit: Function;
}
export default function WrittenQuestionExercise({q, onSubmit}: WrittenVocabularyProps) {
const [answered, setAnswered] = useState(false);
const handleSubmit = () => {
if (answered) {
setAnswered(false);
onSubmit();
} else {
setAnswered(true);
}
}
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'>
{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>
</div>
)}
+64 -6
View File
@@ -1,6 +1,7 @@
@tailwind base
@tailwind components
@tailwind utilities
@yaireo/tagify/src/tagify.scss
$c-default-text: #222
$c-default-icon: #888
@@ -10,7 +11,13 @@ $c-green: rgb(88, 204, 2)
$c-green-shadow: rgb(88, 167, 0)
$c-red: rgb(255, 69, 58)
$c-red-shadow: darken($c-red, 10%)
$c-gold: rgb(255, 200, 0)
$c-gold-flare: rgb(251, 233, 2)
$c-gold-shadow: rgb(221, 164, 11)
$c-gold-text: rgb(195, 126, 11)
$c-white-shadow: rgb(222, 222, 222)
$c-gray: rgb(229, 229, 229)
$c-gray-shadow: rgb(175, 175, 175)
$border-radius: 16px
$shadow-width: 0 4px 0
@@ -78,6 +85,12 @@ button.white
&.no-shadow
border-width: 2px
.box.green
border: none
background-color: $c-green
box-shadow: $shadow-width $c-green-shadow
color: white
input
@extend .box
transition: all 0.2s ease-in-out
@@ -101,6 +114,11 @@ h2
font-size: 1.5em
margin-bottom: 0.5em
text-align: center
.subtext
text-align: center
color: rgb(75, 85, 99)
opacity: 0.8
.v-layout
display: flex
@@ -109,13 +127,19 @@ h2
gap: 1em
width: 100%
height: 100%
margin-bottom: 60px
&.non-center
justify-content: flex-start
&.gap2
gap: 2em
.page-pad
padding: 2em 1em
height: 100%
@extend .non-center
.navbar
position: fixed
@@ -168,12 +192,13 @@ h2
color: #a0aec0
font-size: 1.5rem
.record-btn
.record-container
background-color: white !important
position: fixed
bottom: 20px
bottom: 0px
left: 50%
transform: translateX(-50%)
width: calc(100% - 40px)
width: 100%
padding: 10px 16px
.record-btn.red
@@ -188,7 +213,7 @@ h2
margin: 20px auto
.message
margin: 10px
margin: 3px
padding: 10px
border-radius: 5px
color: white
@@ -201,4 +226,37 @@ h2
.message.other
align-self: flex-start
background-color: $c-green
border-radius: 0 20px 20px 20px
border-radius: 0 20px 20px 20px
.tags
display: flex
flex-wrap: wrap
border: solid $c-white-shadow
padding: 10px
border-radius: 20px
height: 200px
overflow-y: scroll
align-content: flex-start
gap: 10px 5px
margin-top: 3px
.tag
background-color: $c-blue
color: white
border-radius: 20px
padding: 5px 10px
margin: 0
display: flex
align-items: center
justify-content: space-between
height: 30px
.delete-tag
margin-left: 6px
font-weight: 600
width: 20px
height: 20px
color: white
display: flex
align-items: center
justify-content: center
+16 -1
View File
@@ -12,6 +12,9 @@ import CollabLearning from './pages/CollabLearning';
import Review from './pages/Review';
import CharacterSelection from './pages/CharacterSelection';
import Character from './pages/Character';
import Lesson from './pages/Lesson';
import FakeUserSelection from './pages/FakeUserSelection';
import UserChat from './pages/UserChat';
const router = createBrowserRouter([
{
@@ -49,7 +52,19 @@ const router = createBrowserRouter([
{
path: '/character',
element: <Character/>
}
},
{
path: '/lesson',
element: <Lesson/>
},
{
path: '/fake-user-selection',
element: <FakeUserSelection/>
},
{
path: '/user-chat',
element: <UserChat/>
},
])
const root = ReactDOM.createRoot(
+219
View File
@@ -0,0 +1,219 @@
export interface Chapter
{
name: string
steps: Step[]
}
export interface Step
{
questions: Question[]
}
export interface _Question
{
question: string
type: string
}
export type Question = WrittenQuestion | VocabularyQuestion | VerbalQuestion | VerbalPronunciation | VideoQuestion
export interface WrittenQuestion extends _Question
{
wordBank: string[]
expected: string
type: 'written-question'
}
export interface VocabularyQuestion extends _Question
{
pronunciation: string
description: string
example: string
type: 'written-vocabulary'
}
export interface VerbalQuestion extends _Question
{
wordBank: string[]
expected: string
translation: string
url: string
type: 'verbal-question'
}
export interface VerbalPronunciation extends _Question
{
translation: string
type: 'verbal-pronunciation'
}
export interface VideoQuestion extends _Question
{
clipUrl: string
description: string
expected: string
type: 'video'
}
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'],
expected: 'Sushi please / I want sushi',
type: "written-question"
},
{
question: '刺身',
pronunciation: 'さしみ (sashimi)',
description: 'Sashimi is a Japanese delicacy consisting of fresh raw fish or meat sliced into thin pieces and often eaten with soy sauce.',
example: '刺身を好きです。 (I like sashimi)',
type: 'written-vocabulary',
},
{
question: 'What do you hear?',
wordBank: ['刺身', 'ケーキ', 'の', 'は', 'おいしい', 'おもい', 'すし', '中', 'です'],
expected: 'ケーキはおいしいです',
translation: 'Cake is delicious',
url: window.location.origin + '/audio/jp_1_1_3.mp3',
type: 'verbal-question',
},
{
question: 'Please say: すしをたくさんあります',
translation: 'There is a lot of sushi',
type: 'verbal-pronunciation',
},
]
},
{
questions: [
{
// question: "Translate this sentence: Water please",
// wordBank: ['水', 'を', 'ください', 'おいしい', 'おもい', 'すし', '中', 'です'],
question: 'Translate this sentence: 水をください',
wordBank: ['I', 'sushi', 'cookies', 'want', 'please', 'give', 'Water', 'some', 'yesterday'],
// expected: '水をください',
expected: 'Water please',
type: "written-question"
},
{
question: '団子',
pronunciation: 'だんご (dango)',
description: 'Dango is a kind of mochi, Japanese dumpling and sweet made from mochiko (rice flour).',
example: '大きいな団子です。(It is a big dango)',
type: 'written-vocabulary',
},
{
question: 'What do you hear?',
wordBank: ['刺身', '水', 'は', 'おいしい', 'おもい', 'すし', '中', 'です'],
expected: '水です',
translation: 'It is water',
url: window.location.origin + '/audio/jp_1_2_3.mp3',
type: 'verbal-question',
},
{
question: 'Please say: 刺身おください',
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',
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',
}
]
}]
}
]
+40
View File
@@ -0,0 +1,40 @@
export const target_interests = [
"gaming", "cooking", "sci-fi", "sports", "music", "programming", "first-person shooters", "painting", "baking", "astronomy", "archery"
]
export const target_names = [
"John", "Mary", "Bob", "Alice", "Jane", "Frank", "Sally", "Jack", "Jill", "Joe", "Sue"
]
interface User {
name: string;
interests: string[];
}
export function generateFakeUsers(interests: string[]): User[] {
let fakeUsers = [];
const randomNumber = Math.floor(Math.random() * target_names.length) + 1;
for (let i = 0; i < randomNumber; i++) {
const randomName = target_names[Math.floor(Math.random() * target_names.length)];
const randomNumberOfInterests = Math.floor(Math.random() * target_interests.length) + 1;
const randomInterests: string[] = [];
for (let j = 0; j < 3; j++) {
const randomInterest = target_interests[Math.floor(Math.random() * target_interests.length)];
if (!randomInterests.includes(randomInterest)) {
randomInterests.push(randomInterest);
}
}
// add a random interest from the user's interests
const randomInterest = interests[Math.floor(Math.random() * interests.length)];
if (!randomInterests.includes(randomInterest)) {
randomInterests.push(randomInterest);
}
const newUser = { name: randomName, interests: randomInterests };
fakeUsers.push(newUser);
}
return fakeUsers;
}
+157 -17
View File
@@ -1,24 +1,29 @@
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";
// db.users: Signup table map<username, password>
// db.user: Current logged-in user
const db = localStorage
const backendUrl = 'https://localhost:8000'
// const backendUrl = 'https://318-bk.hydev.org'
const backendUrl = "https://127.0.0.1:8000"
export interface Lang {
name: string
code: string
icon: string
data: Chapter[]
}
export const possibleLangs = [
{name: 'Mandarin Chinese', code: 'zh', icon: MandarinChinese},
{name: 'Japanese', code: 'ja', icon: Japanese},
{name: 'English', code: 'en', icon: English},
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: []},
]
export function signup(username: string, password: string, language: string)
@@ -28,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
@@ -53,17 +58,54 @@ export function isLoggedIn()
return !!db.user
}
export function getLanguage(username: string)
{
const users = JSON.parse(db.users)
return users[username].language
}
export function getUsername()
{
return db.user
}
export function getXp()
{
if (!db.completed) return 0
const completed = JSON.parse(db.completed)
return completed.length * 20
}
export function isStepCompleted(chapter: string, step: number)
{
if (!db.completed) return false
const completed = JSON.parse(db.completed)
return completed.includes(`${chapter}-${step}`)
}
export function setStepCompleted(chapter: string, step: number)
{
if (!db.completed)
db.completed = JSON.stringify([])
const completed = JSON.parse(db.completed)
completed.push(`${chapter}-${step}`)
db.completed = JSON.stringify(completed)
}
export function getLanguage(): Lang
{
const users = JSON.parse(db.users)
const lang = users[getUsername()].language
if (!lang)
{
alert('No language set, logging out')
logout()
window.location.href = '/'
}
const lang_obj = possibleLangs.find(l => l.name === lang)!
if (!lang_obj)
{
alert(`Invalid language set: ${lang}, logging out`)
logout()
window.location.href = '/'
}
return lang_obj
}
export interface CharacterChatCreationRequest
{
character: string;
@@ -71,19 +113,19 @@ export interface CharacterChatCreationRequest
language: string;
}
export interface CharacterChatCreationResponse
export interface ChatCreationResponse
{
chat_id: string;
}
export async function startFictionalChat(character: string): Promise<CharacterChatCreationResponse> {
export async function startFictionalChat(character: string): Promise<ChatCreationResponse> {
const currUser = getUsername();
const language = getLanguage(currUser);
const language = getLanguage();
const request: CharacterChatCreationRequest = {
character: character,
user_name: currUser,
language: language
language: language.name
};
const response = await fetch(`${backendUrl}/character-chat`, {
@@ -103,6 +145,42 @@ export async function startFictionalChat(character: string): Promise<CharacterCh
return json.session_id;
}
export interface HumanChatCreationRequest
{
user_name: string;
user_hobbies: string[];
target_name: string;
target_hobbies: string[];
language: string;
}
export async function startHumanChat(user_hobbies: string[], target_name: string, target_hobbies: string[]): Promise<ChatCreationResponse> {
const request: HumanChatCreationRequest = {
user_name: getUsername(),
user_hobbies: user_hobbies,
target_name: target_name,
target_hobbies: target_hobbies,
language: getLanguage().name
};
const response = await fetch(`${backendUrl}/human-chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(request),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.message}`);
}
const json = await response.json();
return json.session_id;
}
export async function speechToText(audioBlob: Blob): Promise<string> {
const formData = new FormData();
formData.append('audio_file', audioBlob, 'audio.wav');
@@ -142,6 +220,27 @@ export async function characterChatMessage(sessionId: string, message: string):
return json;
}
export async function humanChatMessage(sessionId: string, message: string): Promise<{ msg: string, audio_id: string }> {
const request = {msg : message};
const response = await fetch(`${backendUrl}/human-chat/${sessionId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(request),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.message}`);
}
const json = await response.json();
return json;
}
export async function getAudio(audioId: string): Promise<Blob> {
const response = await fetch(`${backendUrl}/audio/${audioId}`);
@@ -151,4 +250,45 @@ export async function getAudio(audioId: string): Promise<Blob> {
const blob = await response.blob();
return blob;
}
}
export interface UserQuestionRequest
{
question: string;
user_answer: string;
expected: string;
chapter: string;
language: string;
}
export interface UserQuestionFeedbackResponse
{
correct: string;
reason: string;
}
export async function getAIMarking(question: string, user_answer: string, expected: string, chapter: string, language: string): Promise<UserQuestionFeedbackResponse> {
const request: UserQuestionRequest = {
question: question,
user_answer: user_answer,
expected: expected,
chapter: chapter,
language: language
};
const response = await fetch(`${backendUrl}/ai-mark`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(request),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.message}`);
}
const json = await response.json();
return json;
}
+33 -18
View File
@@ -16,6 +16,16 @@ export default function Character() {
let chunks = [] as any;
const mediaRecorder = useRef<MediaRecorder | null>(null);
const messageEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messageEndRef.current?.scrollIntoView({ behavior: "smooth" })
}
useEffect(() => {
scrollToBottom()
}, [messages]);
useEffect(() => {
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
mediaRecorder.current = new MediaRecorder(stream);
@@ -57,25 +67,30 @@ export default function Character() {
}, [isRecording]);
return (
<div>
<div className="v-layout p-10">
<Icon icon="mdi:arrow-left" className="back-button" onClick={() => navigate(-1)} />
<h1 className="text-center">Talk With...</h1>
<CharacterBadge name={name} image={image} onClick={() => {}}/>
<div className="chat-area">
{messages.length === 0 ? (
<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}`}>
<p>{message.text}</p>
</div>
))
)}
<div className='flex flex-col h-screen'>
<div className="flex-grow overflow-y-auto p-6">
<div>
<Icon icon="mdi:arrow-left" className="back-button" onClick={() => navigate(-1)} />
<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.map((message, index) => (
<div key={index} className={`message ${message.sender}`}>
<p>{message.text}</p>
</div>
))
)}
</div>
<div ref={messageEndRef} />
</div>
<div className='record-container'>
<button className={`${isRecording ? 'red' : ''}`} onClick={handleRecord}>
{isRecording ? 'Stop Recording' : 'Record'}
</button>
</div>
<button className={`record-btn ${isRecording ? 'red' : ''}`} onClick={handleRecord}>
{isRecording ? 'Stop Recording' : 'Record'}
</button>
</div>
</div>
)
+1 -1
View File
@@ -23,7 +23,7 @@ export default function CharacterSelection() {
return (
<div className="flex flex-col items-center justify-center">
<div className="v-layout p-10">
<div className="v-layout p-6">
<h1>Talk With...</h1>
<div className="flex flex-wrap justify-center gap-3">
{characters.map(character => (
+60 -3
View File
@@ -1,14 +1,71 @@
import NavBar from "../components/NavBar"
import React, { useState } from 'react';
import { generateFakeUsers } from '../logic/fakeUsers';
import { useNavigate } from 'react-router-dom';
export default function CollabLearning() {
const navigate = useNavigate();
const [interests, setInterests] = useState<string[]>([]);
const [newInterest, setNewInterest] = useState("")
const [errorMessage, setErrorMessage] = useState("")
const handleDelete = (tagToDelete: any) => {
setInterests(interests.filter(tag => tag !== tagToDelete));
}
const handleAddTag = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
event.preventDefault();
if (newInterest && !interests.includes(newInterest)) {
setInterests([...interests, newInterest]);
setNewInterest('');
setErrorMessage('');
} else if (interests.includes(newInterest)) {
setErrorMessage('You already entered this interest');
}
}
}
const handleMatchClick = () => {
if (interests.length === 0) {
setErrorMessage('Please enter at least one interest');
return;
}
const fakeUsers = generateFakeUsers(interests)
navigate('/fake-user-selection', { state: { fakeUsers: fakeUsers, interests: interests } });
}
return (
<div className="v-layout p-10">
<div className="v-layout p-6">
<div className="flex flex-col flex-1">
<h1>Collaborative Learning</h1>
<h1>Chat</h1>
<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">
{interests.length === 0 ? (
<p className="subtext">Enter a new interest below and press "Enter"!</p>
) : (
interests.map((interest) => (
<div key={interest} className="tag">
{interest}
<span className="delete-tag" onClick={(e) => { e.stopPropagation(); handleDelete(interest); }}> X</span>
</div>
))
)}
</div>
{errorMessage && <p className="text-red-600 font-bold mt-5">{errorMessage}</p>}
<input className="mt-5" type="text" value={newInterest} onChange={(e) => setNewInterest(e.target.value)} onKeyDown={handleAddTag} placeholder="Enter your interest here!" />
<button className="mt-5 green" onClick={() => handleMatchClick()}>Find Chat Partners!</button>
</div>
<NavBar />
</div>
)
}
}
+29
View File
@@ -0,0 +1,29 @@
@import "../index"
.course-button
@extend button
@extend .green
box-shadow: 0 8px 0 $c-green-shadow
width: 70px
height: 60px
border-radius: 50%
display: flex
justify-content: center
align-items: center
font-size: 5em
overflow: hidden
&.gray
background: $c-gray
color: $c-gray-shadow
box-shadow: 0 8px 0 $c-gray-shadow
&.gold
background: $c-gold
color: $c-gold-text
box-shadow: 0 8px 0 $c-gold-shadow
+70 -9
View File
@@ -1,14 +1,75 @@
import NavBar from "../components/NavBar"
import {Icon} from "@iconify/react";
import {getLanguage, isLoggedIn, isStepCompleted} from "../logic/sdk";
import React from "react";
import "./Course.sass"
import {Step} from "../logic/CourseData";
import {useLocation, useNavigate} from "react-router-dom";
export default function Course() {
function CourseButton(props: {state: 'active' | 'locked' | 'completed', index: number})
{
let cs = 'course-button ' + {active: 'green', locked: 'gray', completed: 'gold'}[props.state];
return (
<div className="v-layout p-10">
<div className="flex flex-col flex-1">
<h1>Course Page</h1>
</div>
<NavBar />
const icon = {active: 'solar:star-bold', locked: 'solar:lock-bold', completed: 'mingcute:check-fill'}[props.state];
// Parameters for the sin wave
const amplitude = 50; // Change this value to adjust the wave's height
const frequency = 1; // Change this value to adjust the wave's frequency
// Calculate the horizontal translation
const translateXValue = amplitude * Math.sin(props.index * frequency);
return <div className={cs} style={{transform: `translateX(${translateXValue}px)`}}>
<Icon icon={icon}/>
</div>
}
export default function Course()
{
const location = useLocation();
const navigate = useNavigate();
if (!isLoggedIn())
{
window.location.href = '/login';
return <div></div>;
}
// Get language
const lang = getLanguage();
function click(step: Step)
{
navigate('/lesson', {state: {questions: step.questions, home: location.pathname}})
}
return (
<div className="v-layout page-pad non-center">
<div className="flex items-center justify-between font-bold">
<img src={lang.icon} alt={lang.name} className="max-w-[50px]"/>
<div className="flex items-center text-red-500 text-lg gap-2">
<Icon icon="bi:fire" className="text-xl"/>
<span>1</span>
</div>
)
</div>
}
{lang.data.map((chapter, i) => <div key={i}>
<div className="box green mb-10">
<div>Chapter 1, Section 1</div>
<div className="font-bold">{chapter.name}</div>
</div>
<div className="v-layout non-center items-center gap2">
{chapter.steps.map((step, i) => <div key={i} onClick={() => click(step)}>
<CourseButton state={isStepCompleted(chapter.name, i) ? 'completed' : i == 0 || isStepCompleted(chapter.name, i - 1) ? 'active' : 'locked'}
index={i}/></div>)}
{[...Array(5)].map((_, i) => <CourseButton state={'locked'} index={i + chapter.steps.length} key={i}/>)}
</div>
<NavBar/>
</div>)}
</div>
)
}
+29
View File
@@ -0,0 +1,29 @@
@import "../index"
.user-card
display: flex
align-items: center
justify-content: space-between
margin: 10px
border: solid $c-green
border-radius: 20px
padding: 10px
.user-interests
display: flex
flex-direction: row
flex-wrap: wrap
gap: 5px
padding: 5px
.user-interest
background-color: $c-green
color: white
border-radius: 20px
padding: 5px 10px
margin: 0
display: flex
align-items: center
justify-content: space-between
height: 30px
+47
View File
@@ -0,0 +1,47 @@
import { useNavigate, useLocation } from "react-router-dom"
import { Icon } from '@iconify/react';
import "./FakeUserSelection.sass";
import { startHumanChat } from "../logic/sdk";
export default function FakeUserSelection() {
const navigate = useNavigate();
const location = useLocation();
const { fakeUsers, interests } = location.state;
const handleUserClick = (user: any) => {
startHumanChat(interests, user.name, user.interests).then((sessionId) => {
console.log(sessionId);
navigate('/user-chat', { state: { user: user, sessionId: sessionId } });
})
}
return (
<div className="v-layout p-6">
<Icon icon="mdi:arrow-left" className="back-button" onClick={() => navigate(-1)} />
<div className="flex flex-col flex-1">
<h1>Learning Partners</h1>
</div>
{fakeUsers.map((user: any, index: any) => (
<div className="user-card" key={index} onClick={
() => handleUserClick(user)
}>
<div>
<div className="w-20 h-20 mx-auto mb-2 p-2 rounded-full border-2 border-dashed border-gray-300 relative">
<span className="text-2xl uppercase font-semibold text-gray-400 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">{user.name[0]}</span>
</div>
<h2>{user.name}</h2>
</div>
<div className="flex flex-col">
<p className="font-bold ml-2">Interests</p>
<div className="user-interests">
{user.interests.map((interest: any, i: any) => (
<span className="user-interest" key={i}>{interest}</span>
))}
</div>
</div>
</div>
))}
</div>
)
}
+65
View File
@@ -0,0 +1,65 @@
import {useLocation, useNavigate} from 'react-router-dom';
import React, {useState} from 'react';
import WrittenQuestionExercise from "../components/WrittenQuestionExercise"
import WrittenVocabularyExercise from "../components/WrittenVocabularyExercise"
import VerbalQuestionsExercise from "../components/VerbalQuestionsExercise"
import Progress from '../components/Progress';
import VerbalPronunciationExercise from '../components/VerbalPronunciationExercise';
import LessonComplete from '../components/LessonComplete';
import {_Question, chapters_jp, Question} from "../logic/CourseData";
import VideoExercise from "../components/VideoExercise";
export default function Lesson()
{
const location = useLocation();
const navigate = useNavigate();
const {questions, home} = location.state;
const [currQuestion, setCurrQuestion] = useState<number>(0);
console.log(questions)
const handleNavigateBack = () =>
{
navigate(-1);
};
const onSubmit = () =>
{
setCurrQuestion(currQuestion + 1);
}
const renderQuestion = (currIndex: number) =>
{
if (currIndex >= questions.length) {
return <LessonComplete home={home}/>;
}
const chapter = 'Ordering food';
const question: Question = questions[currQuestion];
switch (question.type)
{
case 'written-question':
return <WrittenQuestionExercise key={currQuestion} q={question} chapter={chapter} onSubmit={onSubmit}/>;
case 'written-vocabulary':
return <WrittenVocabularyExercise key={currQuestion} q={question} onSubmit={onSubmit}/>;
case 'verbal-question':
return <VerbalQuestionsExercise key={currQuestion} q={question} chapter={chapter} onSubmit={onSubmit}/>;
case 'verbal-pronunciation':
return <VerbalPronunciationExercise key={currQuestion} q={question} chapter={chapter} onSubmit={onSubmit}/>;
case 'video':
return <VideoExercise key={currQuestion} q={question} chapter={chapter} onSubmit={onSubmit}/>;
default:
return null;
}
};
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>
</div>
)
}
+8 -2
View File
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import React, {useEffect, useState} from 'react';
import { useNavigate } from "react-router-dom";
import { login } from '../logic/sdk';
import {isLoggedIn, login} from '../logic/sdk';
export default function Login() {
@@ -9,6 +9,12 @@ export default function Login() {
const [password, setPassword] = useState("");
const [err, setErr] = useState("")
useEffect(() => {
if (isLoggedIn()) {
navigate("/courses");
}
}, []);
function submitLogin() {
try {
login(username, password)
+21
View File
@@ -0,0 +1,21 @@
@import "../index"
.profile-stats > div
@extend .box
display: flex
align-items: center
gap: 10px
*
white-space: nowrap
svg
font-size: 2em
max-width: 25px
> div
> div:nth-child(2)
font-size: 0.9em
+84 -21
View File
@@ -1,28 +1,91 @@
import NavBar from "../components/NavBar"
import { getUsername, getLanguage, logout } from "../logic/sdk"
import { useNavigate } from "react-router-dom"
import {getUsername, getLanguage, logout} from "../logic/sdk"
import {useNavigate} from "react-router-dom"
import {Icon} from "@iconify/react";
import React from "react";
import "./Profile.sass"
export default function Profile() {
export default function Profile()
{
const username = getUsername();
const navigate = useNavigate();
const username = getUsername();
const lang = getLanguage();
const navigate = useNavigate();
function handleLougout() {
logout();
console.log("Logged out");
navigate('/')
}
function handleLougout()
{
logout();
console.log("Logged out");
navigate('/')
}
return (
<div className="v-layout p-10">
<div className="flex flex-col flex-1 h-full">
<h1>Profile</h1>
<h2>Username: {username}</h2>
<h2>Currently Learning: {getLanguage(username)}</h2>
<button className="red mt-auto mb-12" onClick={() => handleLougout()}>Logout</button>
</div>
<NavBar />
return (
<div className="v-layout page-pad non-center">
<div className="text-center">
<div className="mb-6">
<div className="w-20 h-20 mx-auto mb-2 p-2 rounded-full border-2 border-dashed border-gray-300 relative">
<span className="text-2xl uppercase font-semibold text-gray-400 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">{username[0]}</span>
</div>
<h2 className="text-4xl font-semibold">{username}</h2>
</div>
)
}
<div className="flex flex-col mb-4 text-gray-500">
<div className="flex items-center gap-3">
<Icon icon="fa-solid:clock" />
<span>Joined November 2023</span>
</div>
<div className="flex items-center gap-3">
<Icon icon="fa-solid:user-friends" />
<span>0 Friends</span>
</div>
<div className="flex items-center gap-3">
<Icon icon="fa-solid:language" />
<span>{getLanguage().name}</span>
</div>
</div>
</div>
<hr className="mb-4"/>
<h3 className="text-xl font-semibold mb-4">
Statistics
</h3>
<div className="grid grid-cols-2 gap-4 profile-stats">
<div>
<Icon icon="fa-solid:fire" className="text-red-400"/>
<div>
<div className="font-bold">0</div>
<div>Day streak</div>
</div>
</div>
<div>
<Icon icon="ph:lightning-fill" className="text-yellow-400"/>
<div>
<div className="font-bold">10</div>
<div>Total XP</div>
</div>
</div>
<div>
<Icon icon="fa-solid:check" className="text-green-400"/>
<div>
<div className="font-bold">0</div>
<div>Courses</div>
</div>
</div>
<div>
<Icon icon="mdi:microphone-message" className="text-blue-400"/>
<div>
<div className="font-bold">0</div>
<div>Speaking</div>
</div>
</div>
</div>
<NavBar/>
</div>
)
}
+33 -5
View File
@@ -1,14 +1,42 @@
import { useLocation, useNavigate } from "react-router-dom"
import NavBar from "../components/NavBar"
import { getLanguage, getUsername } from "../logic/sdk";
import {_Question, WrittenQuestion} from "../logic/CourseData";
export default function Review() {
const navigate = useNavigate();
const location = useLocation();
const writtenReview = ["Question", "Vocabulary"];
const verbalReview = ["Question", "Pronunciation"];
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 } });
}
return (
<div className="layout-v p-10">
<div className="flex flex-col flex-1">
<h1>Review Page</h1>
<div className="layout-v page-pad">
<h1>Review Page</h1>
<h2>Written</h2>
<div className="flex flex-col flex-1 mb-8 gap-3">
{writtenReview.map(lesson => (
<button className="white" key={lesson}
onClick={() => handleReviewLessonClick("written", lesson.toLowerCase())}>
{lesson}
</button>
))}
</div>
<h2>Verbal/Listening</h2>
<div className="flex flex-col flex-1 gap-3">
{verbalReview.map(lesson => (
<button className="white" key={lesson}
onClick={() => handleReviewLessonClick("verbal", lesson.toLowerCase())}>
{lesson}
</button>
))}
</div>
<NavBar />
</div>
)
}
}
+8 -2
View File
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import React, {useEffect, useState} from 'react';
import { useNavigate } from "react-router-dom";
import {possibleLangs, signup} from '../logic/sdk';
import {isLoggedIn, possibleLangs, signup} from '../logic/sdk';
import Progress from "../components/Progress";
import DuoWriting from "../assets/img/duo-writing.png";
import ChatBox from "../components/ChatBox";
@@ -21,6 +21,12 @@ export default function Signup() {
_setStage(stage)
}
useEffect(() => {
if (isLoggedIn()) {
navigate("/courses");
}
}, []);
function submitSignup() {
try {
if (password !== confirmPassword) {
+75
View File
@@ -0,0 +1,75 @@
import { useLocation, useNavigate } from 'react-router-dom';
import { Icon } from '@iconify/react';
import { useState, useRef, useEffect } from 'react';
import { humanChatMessage } from '../logic/sdk';
export default function Character() {
const location = useLocation();
const navigate = useNavigate();
const { user, sessionId } = location.state;
type Message = { text: string, sender: string };
const [messages, setMessages] = useState<Message[]>([]);
const [message, setMessage] = useState('');
const messageEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messageEndRef.current?.scrollIntoView({ behavior: "smooth" })
}
useEffect(() => {
scrollToBottom()
}, [messages]);
const handleSendClick = () => {
if (message !== '') {
setMessages(prevMessages => [...prevMessages, { text: message, sender: 'me' }]);
setMessage('');
humanChatMessage(sessionId, message).then((response) => {
setMessages(prevMessages => [...prevMessages, { text: response.msg, sender: 'other' }]);
})
}
}
return (
<div className="flex flex-col h-screen">
<div className="flex-grow overflow-y-auto p-6">
<div className="top-part">
<Icon icon="mdi:arrow-left" className="back-button" onClick={() => navigate(-1)} />
<div>
<div className="w-40 h-40 mx-auto mb-2 p-4 rounded-full border-2 border-dashed border-gray-300 relative">
<span className="text-4xl uppercase font-semibold text-gray-400 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">{user.name[0]}</span>
</div>
<h2 className='font-bold'>{user.name}</h2>
</div>
</div>
<div className="chat-area">
{messages.length === 0 ? (
<p className='subtext'>Say hi to your chat partner and introduce yourself!</p>
) : (
messages.map((message, index) => (
<div key={index} className={`message ${message.sender}`}>
<p>{message.text}</p>
</div>
))
)}
<div ref={messageEndRef} />
</div>
</div>
<div className="flex justify-between p-3 bg-white">
<input
className='flex-1 mr-3'
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder='Type a message...'
/>
<button className="w-auto" onClick={handleSendClick}>
<Icon icon="mdi:send" />
</button>
</div>
</div>
)
}
+8
View File
@@ -1,10 +1,18 @@
import DuoSplash from "../assets/img/duo-splash.png";
import { useNavigate } from "react-router-dom";
import {isLoggedIn} from "../logic/sdk";
import {useEffect} from "react";
export default function Welcome()
{
const navigate = useNavigate();
useEffect(() => {
if (isLoggedIn()) {
navigate("/courses");
}
}, []);
return <div className="flex flex-col h-screen justify-center">
<div className="flex flex-col p-5 gap-5 items-center">
<img src={DuoSplash} alt="Duolingo Logo"></img>
+5
View File
@@ -7905,6 +7905,11 @@ react-scripts@^5.0.1:
optionalDependencies:
fsevents "^2.3.2"
react-spinners@^0.13.8:
version "0.13.8"
resolved "https://registry.yarnpkg.com/react-spinners/-/react-spinners-0.13.8.tgz#5262571be0f745d86bbd49a1e6b49f9f9cb19acc"
integrity sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==
react@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"