Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60267f23d2 | |||
| 3d3af9a412 | |||
| d0227f7f43 | |||
| 78d5c841ea | |||
| 61fa2495e4 | |||
| 9f65f9e935 | |||
| 82460da4e5 | |||
| 1574fcce0c | |||
| 2145b08e16 | |||
| 1397e50912 | |||
| e49e9b2c8e | |||
| 7935b78ce4 | |||
| b2f0746ec4 | |||
| 28eae67f9e | |||
| 177139f9f5 | |||
| d7dee43d81 | |||
| 3f5751f59f | |||
| 83b89ea942 | |||
| f3cbca4b73 | |||
| f89cf12ab3 | |||
| 514fba236c | |||
| 5c6fc7a123 | |||
| 43fa157687 | |||
| f0efdb556f | |||
| b88b0fd1f4 | |||
| 4c293e463e | |||
| 9c7f8576b8 | |||
| 56d51ca32f | |||
| 6dcab68366 | |||
| 3963f9c37a | |||
| 45d9e4bcb7 | |||
| 85fe1b807f | |||
| f3588e4831 | |||
| 72db21192f | |||
| f737889799 | |||
| 492589fe49 | |||
| a5a8b13a6b | |||
| 6a21716ec6 | |||
| 8f94bdf73a | |||
| d89c91b643 | |||
| 2eab4e97af | |||
| 4e8a4acaae | |||
| a34671434b | |||
| 498d188925 | |||
| 1ff4c1cf1b |
@@ -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
@@ -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}
|
||||
|
||||
@@ -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.
Executable
BIN
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>
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
]
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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 => (
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user