Merge branch 'master' of https://github.com/hykilpikonna/CSC318
Deploy to GH Pages / build-and-deploy (push) Has been cancelled

This commit is contained in:
2023-12-06 06:47:13 -05:00
4 changed files with 91 additions and 16 deletions
@@ -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>
}
+35 -4
View File
@@ -3,6 +3,7 @@ import { Icon } from '@iconify/react';
import CharacterBadge from '../components/CharacterBadge'; import CharacterBadge from '../components/CharacterBadge';
import { useState, useEffect, useRef, useCallback } from 'react'; import { useState, useEffect, useRef, useCallback } from 'react';
import { speechToText, characterChatMessage, getAudio } from '../logic/sdk'; import { speechToText, characterChatMessage, getAudio } from '../logic/sdk';
import { SyncLoader } from 'react-spinners';
export default function Character() { export default function Character() {
const location = useLocation(); const location = useLocation();
@@ -12,6 +13,8 @@ export default function Character() {
type Message = { text: string, sender: string }; type Message = { text: string, sender: string };
const [messages, setMessages] = useState<Message[]>([]); const [messages, setMessages] = useState<Message[]>([]);
const [isRecording, setIsRecording] = useState(false); const [isRecording, setIsRecording] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isOtherLoading, setIsOtherLoading] = useState(false);
let chunks = [] as any; let chunks = [] as any;
const mediaRecorder = useRef<MediaRecorder | null>(null); const mediaRecorder = useRef<MediaRecorder | null>(null);
@@ -24,7 +27,7 @@ export default function Character() {
useEffect(() => { useEffect(() => {
scrollToBottom() scrollToBottom()
}, [messages]); }, [messages, isLoading]);
useEffect(() => { useEffect(() => {
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => { navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
@@ -35,12 +38,17 @@ export default function Character() {
mediaRecorder.current.onstop = async (e) => { mediaRecorder.current.onstop = async (e) => {
setIsRecording(false); setIsRecording(false);
setIsLoading(true);
const blob = new Blob(chunks, { type: 'audio/wav' }); const blob = new Blob(chunks, { type: 'audio/wav' });
chunks = []; chunks = [];
const audioFile = new File([blob], "audio.wav", { type: 'audio/wav' }); const audioFile = new File([blob], "audio.wav", { type: 'audio/wav' });
const text = await speechToText(audioFile); const text = await speechToText(audioFile);
setMessages(prevMessages => [...prevMessages, { text: text, sender: 'me' }]);
setIsLoading(false);
setIsOtherLoading(true);
const response = await characterChatMessage(sessionId, text); const response = await characterChatMessage(sessionId, text);
const { msg, audio_id } = response; const { msg, audio_id } = response;
const audioBlob = await getAudio(audio_id); const audioBlob = await getAudio(audio_id);
@@ -48,7 +56,8 @@ export default function Character() {
const audioUrl = URL.createObjectURL(audioFileResponse); const audioUrl = URL.createObjectURL(audioFileResponse);
const audio = new Audio(audioUrl); const audio = new Audio(audioUrl);
audio.play(); audio.play();
setMessages(prevMessages => [...prevMessages, { text: text, sender: 'me' }, { text: msg, sender: 'other' }]); setMessages(prevMessages => [...prevMessages, { text: msg, sender: 'other' }]);
setIsOtherLoading(false);
} }
}); });
}, []); }, []);
@@ -74,8 +83,8 @@ export default function Character() {
<h1 className="text-center">Talk With...</h1> <h1 className="text-center">Talk With...</h1>
<CharacterBadge name={name} image={image} onClick={() => {}}/> <CharacterBadge name={name} image={image} onClick={() => {}}/>
<div className="chat-area pb-10"> <div className="chat-area pb-10">
{messages.length === 0 ? ( {messages.length === 0 && !isLoading ? (
<p className='subtext'>Please record a message to start the conversation.</p> <p className="text-center text-gray-400">Please record a message to start the conversation.</p>
) : ( ) : (
messages.map((message, index) => ( messages.map((message, index) => (
<div key={index} className={`message ${message.sender}`}> <div key={index} className={`message ${message.sender}`}>
@@ -83,6 +92,28 @@ export default function Character() {
</div> </div>
)) ))
)} )}
{isLoading && (
<div className="message me">
<SyncLoader
color="white"
loading={isLoading}
aria-label="Loading Spinner"
data-testid="loader"
size={10}
/>
</div>
)}
{isOtherLoading && (
<div className="message other">
<SyncLoader
color="white"
loading={isOtherLoading}
aria-label="Loading Spinner"
data-testid="loader"
size={10}
/>
</div>
)}
</div> </div>
<div ref={messageEndRef} /> <div ref={messageEndRef} />
</div> </div>
+9 -7
View File
@@ -5,6 +5,7 @@ import WrittenVocabularyExercise from "../components/WrittenVocabularyExercise"
import VerbalQuestionsExercise from "../components/VerbalQuestionsExercise" import VerbalQuestionsExercise from "../components/VerbalQuestionsExercise"
import Progress from '../components/Progress'; import Progress from '../components/Progress';
import VerbalPronunciationExercise from '../components/VerbalPronunciationExercise'; import VerbalPronunciationExercise from '../components/VerbalPronunciationExercise';
import LessonComplete from '../components/LessonComplete';
import {_Question, chapters_jp, Question} from "../logic/CourseData"; import {_Question, chapters_jp, Question} from "../logic/CourseData";
import VideoExercise from "../components/VideoExercise"; import VideoExercise from "../components/VideoExercise";
@@ -24,27 +25,28 @@ export default function Lesson()
const onSubmit = () => const onSubmit = () =>
{ {
if (currQuestion < questions.length - 1)
setCurrQuestion(currQuestion + 1); setCurrQuestion(currQuestion + 1);
else navigate(home);
} }
const renderQuestion = (currIndex: number) => const renderQuestion = (currIndex: number) =>
{ {
if (currIndex >= questions.length) {
return <LessonComplete home={home}/>;
}
const chapter = 'Ordering food'; const chapter = 'Ordering food';
const question: Question = questions[currQuestion]; const question: Question = questions[currQuestion];
switch (question.type) switch (question.type)
{ {
case 'written-question': case 'written-question':
return <WrittenQuestionExercise q={question} chapter={chapter} onSubmit={onSubmit}/>; return <WrittenQuestionExercise key={currQuestion} q={question} chapter={chapter} onSubmit={onSubmit}/>;
case 'written-vocabulary': case 'written-vocabulary':
return <WrittenVocabularyExercise q={question} onSubmit={onSubmit}/>; return <WrittenVocabularyExercise key={currQuestion} q={question} onSubmit={onSubmit}/>;
case 'verbal-question': case 'verbal-question':
return <VerbalQuestionsExercise q={question} chapter={chapter} onSubmit={onSubmit}/>; return <VerbalQuestionsExercise key={currQuestion} q={question} chapter={chapter} onSubmit={onSubmit}/>;
case 'verbal-pronunciation': case 'verbal-pronunciation':
return <VerbalPronunciationExercise q={question} chapter={chapter} onSubmit={onSubmit}/>; return <VerbalPronunciationExercise key={currQuestion} q={question} chapter={chapter} onSubmit={onSubmit}/>;
case 'video': case 'video':
return <VideoExercise q={question} chapter={chapter} onSubmit={onSubmit}/>; return <VideoExercise key={currQuestion} q={question} chapter={chapter} onSubmit={onSubmit}/>;
default: default:
return null; return null;
} }
+20 -4
View File
@@ -2,6 +2,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { humanChatMessage } from '../logic/sdk'; import { humanChatMessage } from '../logic/sdk';
import { SyncLoader } from 'react-spinners';
export default function Character() { export default function Character() {
const location = useLocation(); const location = useLocation();
@@ -11,6 +12,7 @@ export default function Character() {
type Message = { text: string, sender: string }; type Message = { text: string, sender: string };
const [messages, setMessages] = useState<Message[]>([]); const [messages, setMessages] = useState<Message[]>([]);
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const [isOtherLoading, setIsOtherLoading] = useState(false);
const messageEndRef = useRef<HTMLDivElement>(null); const messageEndRef = useRef<HTMLDivElement>(null);
@@ -22,13 +24,16 @@ export default function Character() {
scrollToBottom() scrollToBottom()
}, [messages]); }, [messages]);
const handleSendClick = () => { const handleSendClick = async () => {
if (message !== '') { if (message !== '') {
setMessages(prevMessages => [...prevMessages, { text: message, sender: 'me' }]); setMessages(prevMessages => [...prevMessages, { text: message, sender: 'me' }]);
setMessage(''); setMessage('');
humanChatMessage(sessionId, message).then((response) => {
setMessages(prevMessages => [...prevMessages, { text: response.msg, sender: 'other' }]); setIsOtherLoading(true);
}) const response = await humanChatMessage(sessionId, message);
const { msg } = response;
setMessages(prevMessages => [...prevMessages, { text: msg, sender: 'other' }]);
setIsOtherLoading(false);
} }
} }
@@ -55,6 +60,17 @@ export default function Character() {
</div> </div>
)) ))
)} )}
{isOtherLoading && (
<div className="message other">
<SyncLoader
color="white"
loading={isOtherLoading}
aria-label="Loading Spinner"
data-testid="loader"
size={10}
/>
</div>
)}
<div ref={messageEndRef} /> <div ref={messageEndRef} />
</div> </div>
</div> </div>