Merge pull request #7 from hykilpikonna/collab-learning
This commit is contained in:
+44
-4
@@ -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
|
||||
+11
-1
@@ -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: <Lesson/>
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/fake-user-selection',
|
||||
element: <FakeUserSelection/>
|
||||
},
|
||||
{
|
||||
path: '/user-chat',
|
||||
element: <UserChat/>
|
||||
},
|
||||
])
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<CharacterChatCreationResponse> {
|
||||
export async function startFictionalChat(character: string): Promise<ChatCreationResponse> {
|
||||
const currUser = getUsername();
|
||||
const language = getLanguage();
|
||||
|
||||
@@ -117,6 +117,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');
|
||||
@@ -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<Blob> {
|
||||
const response = await fetch(`${backendUrl}/audio/${audioId}`);
|
||||
|
||||
|
||||
@@ -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,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<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>
|
||||
<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"
|
||||
|
||||
.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>
|
||||
)
|
||||
}
|
||||
@@ -128,9 +128,9 @@ export default function Review() {
|
||||
|
||||
return (
|
||||
<div className="layout-v p-10">
|
||||
<h1 className="text-center">Review Page</h1>
|
||||
<h2 className="text-center">Written</h2>
|
||||
<div className="flex flex-col flex-1 mb-10 gap-5">
|
||||
<h1>Review Page</h1>
|
||||
<h2>Written</h2>
|
||||
<div className="flex flex-col flex-1 mb-8 gap-3">
|
||||
{writtenReview.map(lesson => (
|
||||
<button
|
||||
className="white"
|
||||
@@ -141,8 +141,8 @@ export default function Review() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<h2 className="text-center">Verbal/Listening</h2>
|
||||
<div className="flex flex-col flex-1 gap-5">
|
||||
<h2>Verbal/Listening</h2>
|
||||
<div className="flex flex-col flex-1 gap-3">
|
||||
{verbalReview.map(lesson => (
|
||||
<button
|
||||
className="white"
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user