Merge pull request #5 from jpablo2002/review-lessons-page

This commit is contained in:
Azalea
2023-12-01 02:43:18 +09:00
committed by GitHub
14 changed files with 507 additions and 36 deletions
+17
View File
@@ -13,12 +13,25 @@ import json
from dataclasses import dataclass
import torch
from transformers import pipeline
from fastapi.middleware.cors import CORSMiddleware
load_dotenv()
app = FastAPI()
openai_client = OpenAI()
origins = [
"http://localhost:3000"
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class AIMarkRequest(BaseModel):
question: str
@@ -146,6 +159,10 @@ def get_character_prompt(character: str, user_name: str, language: str):
They are a language learner trying to learn {language}. Please help them learn by chatting with them in the language
but do not act like a robot. Try to be as human as possible.
"""
elif character == "c3po_starwars":
return f"""
You are embodying the role of C-3PO from Star Wars, a fluent speaker in over six million forms of communication. You are engaging in a conversation with a person named {user_name}, who is eager to learn {language}. Your task is to assist them in their language learning journey by conversing with them in the desired language. Despite your robotic nature, strive to communicate in a manner as human-like as possible, showcasing the rich diversity of your language skills.
"""
raise HTTPException(status_code=404, detail="Character not found")
+1 -1
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'; // adjust the path as needed
import { NavItem } from '../components/NavItem';
export default function NavBar() {
const navigate = useNavigate();
@@ -0,0 +1,103 @@
import ChatBoxBorder from '../assets/img/chatbox-border.svg';
import { useState } from 'react';
import { getAIMarking } from '../logic/sdk';
interface WrittenQuestionProps {
question: string;
wordBank: string[];
expected: string;
chapter: string;
language: string;
setCurrQuestion: Function;
}
export default function WrittenQuestionExercise({question, wordBank, expected, chapter, language}: WrittenQuestionProps) {
const [selectedWords, setSelectedWords] = useState<string[]>([]);
const [remainingWords, setRemainingWords] = useState(wordBank);
const lastPunctuation = question[question.length - 1];
const [answered, setAnswered] = useState(false);
const [correct, setCorrect] = useState("");
const [reason, setReason] = useState("");
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(" ") + lastPunctuation;
getAIMarking(
question,
userAnswer,
expected,
chapter,
language
).then((res) => {
setAnswered(true);
setCorrect(res.correct);
setReason(res.reason);
})
}
const ResponseSection = (correct: string | null, reason: string | null) => {
if (!answered) {
return (
<div>
<div className=' flex-row flex-wrap border-b-4 w-full'>
{remainingWords.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={(event) => handleWordBankClick(word)}>
{word}
</span>
))}
</div>
<button className='green' onClick={(e) => handleSubmit()}>Submit</button>
</div>
)
} else {
return (
<div>
<h3>{correct}</h3>
<p>{reason}</p>
</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'>
{question}
</div>
<div className='flex-row flex-wrap border-b-4 w-full'>
{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 className=' flex-row flex-wrap border-b-4 w-full'>
{remainingWords.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={(event) => handleWordBankClick(word)}>
{word}
</span>
))}
</div>
<button className='green' onClick={(e) => handleSubmit()}>Submit</button> */}
</div>
)}
+17
View File
@@ -8,6 +8,8 @@ $c-blue: rgb(28, 176, 246)
$c-blue-shadow: rgb(24, 153, 214)
$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-white-shadow: rgb(222, 222, 222)
$border-radius: 16px
$shadow-width: 0 4px 0
@@ -55,6 +57,10 @@ button.green
background-color: $c-green
box-shadow: $shadow-width $c-green-shadow
button.red
background-color: $c-red
box-shadow: $shadow-width $c-red-shadow
button.white
background-color: white
color: $c-default-text
@@ -89,6 +95,12 @@ label
h1
font-size: 2em
margin-bottom: 0.5em
text-align: center
h2
font-size: 1.5em
margin-bottom: 0.5em
text-align: center
.v-layout
display: flex
@@ -116,6 +128,7 @@ h1
justify-content: space-around
padding: 10px
color: $c-default-icon
background-color: white
.navbar-item
text-decoration: none
@@ -163,6 +176,10 @@ h1
width: calc(100% - 40px)
padding: 10px 16px
.record-btn.red
background-color: $c-red
box-shadow: $shadow-width $c-red-shadow
.chat-area
display: flex
flex-direction: column
+5
View File
@@ -12,6 +12,7 @@ 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';
const router = createBrowserRouter([
{
@@ -49,6 +50,10 @@ const router = createBrowserRouter([
{
path: '/character',
element: <Character/>
},
{
path: '/lesson',
element: <Lesson/>
}
])
+147
View File
@@ -1,11 +1,14 @@
import MandarinChinese from '../assets/img/lang/zh.svg'
import Japanese from '../assets/img/lang/ja.svg'
import English from '../assets/img/lang/en.svg'
// db.users: Signup table map<username, password>
// db.user: Current logged-in user
const db = localStorage
const backendUrl = 'https://localhost:8000'
export interface Lang {
name: string
code: string
@@ -15,6 +18,7 @@ export interface Lang {
export const possibleLangs = [
{name: 'Mandarin Chinese', code: 'zh', icon: MandarinChinese},
{name: 'Japanese', code: 'ja', icon: Japanese},
{name: 'English', code: 'en', icon: English},
]
export function signup(username: string, password: string, language: string)
@@ -26,6 +30,8 @@ export function signup(username: string, password: string, language: string)
users[username] = {password, language}
db.users = JSON.stringify(users)
db.user = username
}
export function login(username: string, password: string)
@@ -46,3 +52,144 @@ 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 interface CharacterChatCreationRequest
{
character: string;
user_name: string;
language: string;
}
export interface CharacterChatCreationResponse
{
chat_id: string;
}
export async function startFictionalChat(character: string): Promise<CharacterChatCreationResponse> {
const currUser = getUsername();
const language = getLanguage(currUser);
const request: CharacterChatCreationRequest = {
character: character,
user_name: currUser,
language: language
};
const response = await fetch(`${backendUrl}/character-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');
const response = await fetch(`${backendUrl}/recognize`, {
method: 'POST',
body: formData,
});
const json = await response.json();
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}, message: ${json.message}`);
}
return json.text;
}
export async function characterChatMessage(sessionId: string, message: string): Promise<{ msg: string, audio_id: string }> {
const request = {msg : message};
const response = await fetch(`${backendUrl}/character-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}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
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;
}
+60 -16
View File
@@ -1,22 +1,60 @@
import { useLocation, useNavigate } from 'react-router-dom';
import { Icon } from '@iconify/react';
import CharacterBadge from '../components/CharacterBadge';
import { useState } from 'react';
import { useState, useEffect, useRef, useCallback } from 'react';
import { speechToText, characterChatMessage, getAudio } from '../logic/sdk';
export default function Character() {
const location = useLocation();
const navigate = useNavigate();
const { name, image } = location.state;
const { name, image , sessionId } = location.state;
const [messages, setMessages] = useState([
{ text: 'Hello!', sender: 'me' },
{ text: 'Hi!', sender: 'other' },
// Add more messages here
]);
type Message = { text: string, sender: string };
const [messages, setMessages] = useState<Message[]>([]);
const [isRecording, setIsRecording] = useState(false);
function handleRecord() {
// handle the recording
}
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);
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 response = await characterChatMessage(sessionId, text);
const { msg, audio_id } = response;
const audioBlob = await getAudio(audio_id);
const audioFileResponse = new File([audioBlob], "audio.wav", { type: 'audio/wav' });
const audioUrl = URL.createObjectURL(audioFileResponse);
const audio = new Audio(audioUrl);
audio.play();
setMessages(prevMessages => [...prevMessages, { text: text, sender: 'me' }, { text: msg, sender: 'other' }]);
}
});
}, []);
const handleRecord = useCallback(() => {
if (!isRecording) {
if (mediaRecorder.current) {
mediaRecorder.current.start();
}
setIsRecording(true);
} else {
if (mediaRecorder.current) {
mediaRecorder.current.stop();
}
}
}, [isRecording]);
return (
<div>
@@ -25,13 +63,19 @@ export default function Character() {
<h1 className="text-center">Talk With...</h1>
<CharacterBadge name={name} image={image} onClick={() => {}}/>
<div className="chat-area">
{messages.map((message, index) => (
<div key={index} className={`message ${message.sender}`}>
<p>{message.text}</p>
</div>
))}
{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>
<button className="record-btn" onClick={handleRecord}>Record</button>
<button className={`record-btn ${isRecording ? 'red' : ''}`} onClick={handleRecord}>
{isRecording ? 'Stop Recording' : 'Record'}
</button>
</div>
</div>
)
+12 -8
View File
@@ -3,31 +3,35 @@ import Ash from "../assets/img/ash.png"
import C3PO from "../assets/img/c3po.png"
import { useNavigate } from "react-router-dom"
import CharacterBadge from "../components/CharacterBadge"
import { startFictionalChat } from "../logic/sdk"
export default function CharacterSelection() {
const navigate = useNavigate();
const characters = [
{ name: 'Ash', image: Ash },
{ name: 'C3PO', image: C3PO },
{ name: 'Ash', image: Ash, alias: 'ash_pokemon'},
{ name: 'C3PO', image: C3PO, alias: 'c3po_starwars'},
// Add more characters here
];
const handleCharacterClick = (characterName: string, characterImage: string) => {
navigate('/character', { state: { name: characterName, image: characterImage } });
const handleCharacterClick = (characterName: string, characterImage: string, alias: string) => {
startFictionalChat(alias).then((sessionId) => {
console.log(sessionId);
navigate('/character', { state: { name: characterName, image: characterImage, sessionId: sessionId } });
})
}
return (
<div >
<div className="flex flex-col items-center justify-center">
<div className="v-layout p-10">
<h1 className="text-center">Talk With...</h1>
<div className="grid grid-cols-2 gap-3">
<h1>Talk With...</h1>
<div className="flex flex-wrap justify-center gap-3">
{characters.map(character => (
<CharacterBadge
key={character.name}
name={character.name}
image={character.image}
onClick={() => handleCharacterClick(character.name, character.image)}
onClick={() => handleCharacterClick(character.name, character.image, character.alias)}
/>
))}
</div>
+2 -2
View File
@@ -3,9 +3,9 @@ import NavBar from "../components/NavBar"
export default function CollabLearning() {
return (
<div className="flex flex-col h-screen">
<div className="v-layout p-10">
<div className="flex flex-col flex-1">
<h1>Collaborative Learning Page</h1>
<h1>Collaborative Learning</h1>
</div>
<NavBar />
</div>
+1 -1
View File
@@ -3,7 +3,7 @@ import NavBar from "../components/NavBar"
export default function Course() {
return (
<div className="flex flex-col h-screen">
<div className="v-layout p-10">
<div className="flex flex-col flex-1">
<h1>Course Page</h1>
</div>
+47
View File
@@ -0,0 +1,47 @@
import { useLocation, useNavigate } from 'react-router-dom';
import { Icon } from '@iconify/react';
import { useState } from 'react';
import WrittenQuestionExercise from "../components/WrittenQuestionExercise"
export default function Course() {
const location = useLocation();
const navigate = useNavigate();
const { questions } = location.state;
const [currQuestion, setCurrQuestion] = useState(0);
const renderLessonContent = () => {
const {question, wordBank, expected, type, exercise} = questions[currQuestion];
switch (type) {
case 'written':
switch (exercise) {
case 'questions':
return <WrittenQuestionExercise question={question} wordBank={wordBank} expected={expected} chapter={"Travel"} language={"Japanese"} setCurrQuestion={setCurrQuestion}/>;
// case 'vocabulary':
// return <WrittenVocabularyLesson />;
default:
return null;
}
// case 'verbal-listening':
// switch (exercise) {
// case 'questions':
// return <VerbalQuestionsLesson />;
// case 'pronunciation':
// return <VerbalPronunciationLesson />;
// default:
// return null;
// }
// default:
// return null;
}
};
return (
<div className="v-layout p-10">
<Icon icon="mdi:arrow-left" className="back-button" onClick={() => navigate(-1)} />
<h1 className="text-center">Course Page</h1>
<div className="flex flex-col flex-1 mb-8 items-center">
{renderLessonContent()}
</div>
</div>
)
}
+17 -3
View File
@@ -1,11 +1,25 @@
import NavBar from "../components/NavBar"
import { getUsername, getLanguage, logout } from "../logic/sdk"
import { useNavigate } from "react-router-dom"
export default function Profile() {
const username = getUsername();
const navigate = useNavigate();
function handleLougout() {
logout();
console.log("Logged out");
navigate('/')
}
return (
<div className="flex flex-col h-screen">
<div className="flex flex-col flex-1">
<h1>Profile Page</h1>
<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 />
</div>
+76 -3
View File
@@ -1,14 +1,87 @@
import { useNavigate } from "react-router-dom"
import NavBar from "../components/NavBar"
import { getLanguage, getUsername } from "../logic/sdk";
export default function Review() {
const navigate = useNavigate();
const writtenReview = ["Questions", "Vocabulary"];
const verbalReview = ["Questions", "Pronunciation"];
const language = getLanguage(getUsername());
const handleReviewLessonClick = (reviewType: string, lesson: string) => {
type Question = { question: string, wordBank: string[], expected: string, type: string, exercise: string };
let reviewQuestions: Question[] = [];
if (language === "Japanese") {
reviewQuestions = [
{
question: 'Translate this sentence: 新幹線で東京に行きます。',
wordBank: ['Explore', 'Train', 'Will', 'City', 'I', 'Go', 'Bullet', 'To', 'Tokyo', 'By', 'Mountain'],
expected: 'I will go to Tokyo by bullet train.',
type: reviewType,
exercise: lesson
},
{
question: 'Translate this sentence: 空港で荷物を受け取ります。',
wordBank: ['Sunset', 'Will', 'Ocean', 'At', 'Adventure', 'Airport', 'The', 'I', 'Receive', 'Luggage'],
expected: 'I will receive luggage at the airport.',
type: reviewType,
exercise: lesson
},
{
question: 'Translate this sentence: ホテルでチェックインします。',
wordBank: ['Culture', 'At', 'Jungle', 'Will', 'The', 'Check-In', 'Hotel', 'I', 'Historic'],
expected: 'I will check in at the hotel.',
type: reviewType,
exercise: lesson
},
{
question: 'Translate this sentence: 観光名所を訪れます。',
wordBank: ['Desert', 'Mountain', 'Beach', 'Visit', 'Attractions', 'Tourist', 'I', 'Will'],
expected: 'I will visit tourist attractions.',
type: reviewType,
exercise: lesson
},
{
question: 'Translate this sentence: 美味しい地元の食べ物を試します。',
wordBank: ['Try', 'Market', 'Delicious', 'I', 'Food', 'Will', 'River', 'Local', 'Park'],
expected: 'I will try delicious local food.',
type: reviewType,
exercise: lesson
},
]
}
navigate('/lesson', { state: { questions: reviewQuestions } });
}
return (
<div className="flex flex-col h-screen">
<div className="layout-v p-10">
<h1 className="text-center p-10">Review Page</h1>
<h2 className="text-center">Written</h2>
<div className="flex flex-col flex-1 mb-8">
{writtenReview.map(lesson => (
<button
className="white"
key={"written-" + lesson}
onClick={() => handleReviewLessonClick("written", lesson.toLowerCase())}
>
{lesson}
</button>
))}
</div>
<h2 className="text-center">Verbal/Listening</h2>
<div className="flex flex-col flex-1">
<h1>Review Page</h1>
{verbalReview.map(lesson => (
<button
className="white"
key={"verbal-listening-" + lesson}
onClick={() => handleReviewLessonClick("verbal-listening", lesson.toLowerCase())}
>
{lesson}
</button>
))}
</div>
<NavBar />
</div>
)
}
+2 -2
View File
@@ -31,7 +31,7 @@ export default function Signup() {
console.log(confirmPassword)
return
} else {
signup(username, password, "")
signup(username, password, lang)
navigate('/courses')
}
}
@@ -58,7 +58,7 @@ export default function Signup() {
{err && <label className="text-red-600">{err}</label>}
{stage === 0 && <>
{possibleLangs.map((lang, i) => <button className="white flex items-center gap-5" onClick={() => setStage(1)} key={i}>
{possibleLangs.map((lang, i) => <button className="white flex items-center gap-5" onClick={() => {setLang(lang.name); setStage(1);}} key={i}>
<img src={lang.icon} alt={lang.name} className="max-w-[50px]"/>
<span>{lang.name}</span>
</button>)}