diff --git a/frontend/src/index.sass b/frontend/src/index.sass index 65b42ec..982985d 100644 --- a/frontend/src/index.sass +++ b/frontend/src/index.sass @@ -1,6 +1,7 @@ @tailwind base @tailwind components @tailwind utilities +@yaireo/tagify/src/tagify.scss $c-default-text: #222 $c-default-icon: #888 @@ -113,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 @@ -183,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 @@ -203,7 +210,7 @@ h2 margin: 20px auto .message - margin: 10px + margin: 3px padding: 10px border-radius: 5px color: white @@ -217,3 +224,36 @@ h2 align-self: flex-start background-color: $c-green 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 \ 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/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/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 216c6cd..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 ab77287..96f0167 100644 --- a/frontend/src/pages/CollabLearning.tsx +++ b/frontend/src/pages/CollabLearning.tsx @@ -1,11 +1,68 @@ 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 navigate = useNavigate(); + + const [interests, setInterests] = useState([]); + const [newInterest, setNewInterest] = useState("") + const [errorMessage, setErrorMessage] = useState("") + + const handleDelete = (tagToDelete: any) => { + setInterests(interests.filter(tag => tag !== tagToDelete)); + } + + const handleAddTag = (event: React.KeyboardEvent) => { + 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 ( -
+

Collaborative Learning

+

Find people fluent in your taget language to Chat!

+

Help them learn a language you know!

+

Interests

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

Enter a new interest below and press "Enter"!

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

{errorMessage}

} + setNewInterest(e.target.value)} onKeyDown={handleAddTag} placeholder="Enter your interest here!" /> +
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/Review.tsx b/frontend/src/pages/Review.tsx index ea8b467..b03cb60 100644 --- a/frontend/src/pages/Review.tsx +++ b/frontend/src/pages/Review.tsx @@ -128,9 +128,9 @@ export default function Review() { return (
-

Review Page

-

Written

-
+

Review Page

+

Written

+
{writtenReview.map(lesson => (
-

Verbal/Listening

-
+

Verbal/Listening

+
{verbalReview.map(lesson => ( +
+
+ ) +} \ No newline at end of file