From 85fe1b807fc7076d44f720a46cc8fe9814857227 Mon Sep 17 00:00:00 2001 From: Yue Fung Lee Date: Thu, 30 Nov 2023 15:24:14 -0500 Subject: [PATCH 1/4] fixed review styles --- frontend/src/pages/Review.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/Review.tsx b/frontend/src/pages/Review.tsx index fd7f972..b774047 100644 --- a/frontend/src/pages/Review.tsx +++ b/frontend/src/pages/Review.tsx @@ -56,9 +56,9 @@ export default function Review() { return (
-

Review Page

-

Written

-
+

Review Page

+

Written

+
{writtenReview.map(lesson => (
-

Verbal/Listening

-
+

Verbal/Listening

+
{verbalReview.map(lesson => (
From 514fba236c4fdd2ef8210059dd56f5caa5586f82 Mon Sep 17 00:00:00 2001 From: Yue Fung Lee Date: Fri, 1 Dec 2023 00:45:36 -0500 Subject: [PATCH 3/4] fixed layout and autoscroll --- frontend/src/index.sass | 14 ++--- frontend/src/index.tsx | 12 +++- frontend/src/logic/sdk.ts | 61 +++++++++++++++++++- frontend/src/pages/Character.tsx | 51 +++++++++++------ frontend/src/pages/CharacterSelection.tsx | 2 +- frontend/src/pages/CollabLearning.tsx | 70 +++++++---------------- frontend/src/pages/Lesson.tsx | 2 +- 7 files changed, 131 insertions(+), 81 deletions(-) diff --git a/frontend/src/index.sass b/frontend/src/index.sass index 447f76e..982985d 100644 --- a/frontend/src/index.sass +++ b/frontend/src/index.sass @@ -189,12 +189,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 @@ -209,7 +210,7 @@ h2 margin: 20px auto .message - margin: 10px + margin: 3px padding: 10px border-radius: 5px color: white @@ -223,8 +224,6 @@ h2 align-self: flex-start background-color: $c-green border-radius: 0 20px 20px 20px -<<<<<<< HEAD -======= .tags display: flex @@ -257,5 +256,4 @@ h2 color: white display: flex align-items: center - justify-content: center ->>>>>>> 199c0b0 (Added Creating Random Users) + justify-content: center \ No newline at end of file diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 2b3709e..9e2fadb 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -13,6 +13,8 @@ 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([ { @@ -54,7 +56,15 @@ const router = createBrowserRouter([ { path: '/lesson', element: - } + }, + { + path: '/fake-user-selection', + element: + }, + { + path: '/user-chat', + element: + }, ]) const root = ReactDOM.createRoot( diff --git a/frontend/src/logic/sdk.ts b/frontend/src/logic/sdk.ts index 26cbfdb..b962fd1 100644 --- a/frontend/src/logic/sdk.ts +++ b/frontend/src/logic/sdk.ts @@ -85,12 +85,12 @@ export interface CharacterChatCreationRequest language: string; } -export interface CharacterChatCreationResponse +export interface ChatCreationResponse { chat_id: string; } -export async function startFictionalChat(character: string): Promise { +export async function startFictionalChat(character: string): Promise { const currUser = getUsername(); const language = getLanguage(); @@ -117,6 +117,42 @@ export async function startFictionalChat(character: string): Promise { + + 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 { const formData = new FormData(); formData.append('audio_file', audioBlob, 'audio.wav'); @@ -156,6 +192,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 { const response = await fetch(`${backendUrl}/audio/${audioId}`); diff --git a/frontend/src/pages/Character.tsx b/frontend/src/pages/Character.tsx index bef6ccd..24769ff 100644 --- a/frontend/src/pages/Character.tsx +++ b/frontend/src/pages/Character.tsx @@ -16,6 +16,16 @@ export default function Character() { let chunks = [] as any; const mediaRecorder = useRef(null); + const messageEndRef = useRef(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 ( -
-
- navigate(-1)} /> -

Talk With...

- {}}/> -
- {messages.length === 0 ? ( -

Please record a message to start the conversation.

- ) : ( - messages.map((message, index) => ( -
-

{message.text}

-
- )) - )} +
+
+
+ navigate(-1)} /> +

Talk With...

+ {}}/> +
+ {messages.length === 0 ? ( +

Please record a message to start the conversation.

+ ) : ( + messages.map((message, index) => ( +
+

{message.text}

+
+ )) + )} +
+
+
+
+
-
) diff --git a/frontend/src/pages/CharacterSelection.tsx b/frontend/src/pages/CharacterSelection.tsx index 3d8800e..d8f4111 100644 --- a/frontend/src/pages/CharacterSelection.tsx +++ b/frontend/src/pages/CharacterSelection.tsx @@ -23,7 +23,7 @@ export default function CharacterSelection() { return (
-
+

Talk With...

{characters.map(character => ( diff --git a/frontend/src/pages/CollabLearning.tsx b/frontend/src/pages/CollabLearning.tsx index c61d3aa..96f0167 100644 --- a/frontend/src/pages/CollabLearning.tsx +++ b/frontend/src/pages/CollabLearning.tsx @@ -1,97 +1,67 @@ import NavBar from "../components/NavBar" import { useState } from 'react'; +import { generateFakeUsers } from '../logic/fakeUsers'; +import { useNavigate } from 'react-router-dom'; export default function CollabLearning() { - const target_interests = [ - "gaming", "cooking", "sci-fi", "sports", "music", "programming", "first-person shooters", "painting", "baking", "astronomy", "archery" - ] + const navigate = useNavigate(); - const target_names = [ - "John", "Mary", "Bob", "Alice", "Jane", "Frank", "Sally", "Jack", "Jill", "Joe", "Sue" - ] - - interface User { - name: string; - interests: string[]; - } - - const [tags, setTags] = useState([]); - const [otherUsers, setOtherUsers] = useState([]); - const [newTag, setNewTag] = useState("") + const [interests, setInterests] = useState([]); + const [newInterest, setNewInterest] = useState("") const [errorMessage, setErrorMessage] = useState("") const handleDelete = (tagToDelete: any) => { - setTags(tags.filter(tag => tag !== tagToDelete)); + setInterests(interests.filter(tag => tag !== tagToDelete)); } const handleAddTag = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { event.preventDefault(); - if (newTag && !tags.includes(newTag)) { - setTags([...tags, newTag]); - setNewTag(''); + if (newInterest && !interests.includes(newInterest)) { + setInterests([...interests, newInterest]); + setNewInterest(''); setErrorMessage(''); - } else if (tags.includes(newTag)) { + } else if (interests.includes(newInterest)) { setErrorMessage('You already entered this interest'); } } } const handleMatchClick = () => { - if (tags.length === 0) { + if (interests.length === 0) { setErrorMessage('Please enter at least one interest'); return; } - const randomNumber = Math.floor(Math.random() * target_names.length) + 1; - - console.log(randomNumber); - - let tempUsers = Array.from(otherUsers); - - 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 < randomNumberOfInterests; j++) { - const randomInterest = target_interests[Math.floor(Math.random() * target_interests.length)]; - if (!randomInterests.includes(randomInterest)) { - randomInterests.push(randomInterest); - } - } - const newUser = { name: randomName, interests: randomInterests }; - tempUsers.push(newUser); - } + const fakeUsers = generateFakeUsers(interests) - setOtherUsers(tempUsers); - - console.log(tempUsers); + navigate('/fake-user-selection', { state: { fakeUsers: fakeUsers, interests: interests } }); } return ( -
+

Collaborative Learning

Find people fluent in your taget language to Chat!

Help them learn a language you know!

Interests

- {tags.length === 0 ? ( + {interests.length === 0 ? (

Enter a new interest below and press "Enter"!

) : ( - tags.map((tag) => ( -
- {tag} - { e.stopPropagation(); handleDelete(tag); }}> X + interests.map((interest) => ( +
+ {interest} + { e.stopPropagation(); handleDelete(interest); }}> X
)) )}
{errorMessage &&

{errorMessage}

} - setNewTag(e.target.value)} onKeyDown={handleAddTag} placeholder="Enter your interest here!" /> + setNewInterest(e.target.value)} onKeyDown={handleAddTag} placeholder="Enter your interest here!" />
diff --git a/frontend/src/pages/Lesson.tsx b/frontend/src/pages/Lesson.tsx index d830698..1465f65 100644 --- a/frontend/src/pages/Lesson.tsx +++ b/frontend/src/pages/Lesson.tsx @@ -36,7 +36,7 @@ export default function Course() { }; return ( -
+
navigate(-1)} />

Course Page

From f89cf12ab34f8c348e25f8ef331692c21f5d6a47 Mon Sep 17 00:00:00 2001 From: Yue Fung Lee Date: Fri, 1 Dec 2023 00:46:24 -0500 Subject: [PATCH 4/4] Added Chatting between users --- frontend/src/logic/fakeUsers.ts | 40 ++++++++++++ frontend/src/pages/FakeUserSelection.sass | 29 +++++++++ frontend/src/pages/FakeUserSelection.tsx | 47 ++++++++++++++ frontend/src/pages/UserChat.tsx | 75 +++++++++++++++++++++++ 4 files changed, 191 insertions(+) create mode 100644 frontend/src/logic/fakeUsers.ts create mode 100644 frontend/src/pages/FakeUserSelection.sass create mode 100644 frontend/src/pages/FakeUserSelection.tsx create mode 100644 frontend/src/pages/UserChat.tsx diff --git a/frontend/src/logic/fakeUsers.ts b/frontend/src/logic/fakeUsers.ts new file mode 100644 index 0000000..343c6c9 --- /dev/null +++ b/frontend/src/logic/fakeUsers.ts @@ -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; +} \ No newline at end of file diff --git a/frontend/src/pages/FakeUserSelection.sass b/frontend/src/pages/FakeUserSelection.sass new file mode 100644 index 0000000..5c61a4a --- /dev/null +++ b/frontend/src/pages/FakeUserSelection.sass @@ -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 \ No newline at end of file diff --git a/frontend/src/pages/FakeUserSelection.tsx b/frontend/src/pages/FakeUserSelection.tsx new file mode 100644 index 0000000..f8cc007 --- /dev/null +++ b/frontend/src/pages/FakeUserSelection.tsx @@ -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 ( +
+ navigate(-1)} /> +
+

Learning Partners

+
+ {fakeUsers.map((user: any, index: any) => ( +
handleUserClick(user) + }> +
+
+ {user.name[0]} +
+

{user.name}

+
+
+

Interests

+
+ {user.interests.map((interest: any, i: any) => ( + {interest} + ))} +
+
+
+ ))} +
+ ) +} \ No newline at end of file diff --git a/frontend/src/pages/UserChat.tsx b/frontend/src/pages/UserChat.tsx new file mode 100644 index 0000000..19ac848 --- /dev/null +++ b/frontend/src/pages/UserChat.tsx @@ -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([]); + const [message, setMessage] = useState(''); + + const messageEndRef = useRef(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 ( +
+
+
+ navigate(-1)} /> +
+
+ {user.name[0]} +
+

{user.name}

+
+
+
+ {messages.length === 0 ? ( +

Say hi to your chat partner and introduce yourself!

+ ) : ( + messages.map((message, index) => ( +
+

{message.text}

+
+ )) + )} +
+
+
+
+ setMessage(e.target.value)} + placeholder='Type a message...' + /> + +
+
+ ) +} \ No newline at end of file