fixed layout and autoscroll

This commit is contained in:
Yue Fung Lee
2023-12-01 00:45:36 -05:00
parent 45d9e4bcb7
commit 514fba236c
7 changed files with 131 additions and 81 deletions
+5 -7
View File
@@ -189,12 +189,13 @@ h2
color: #a0aec0 color: #a0aec0
font-size: 1.5rem font-size: 1.5rem
.record-btn .record-container
background-color: white !important
position: fixed position: fixed
bottom: 20px bottom: 0px
left: 50% left: 50%
transform: translateX(-50%) transform: translateX(-50%)
width: calc(100% - 40px) width: 100%
padding: 10px 16px padding: 10px 16px
.record-btn.red .record-btn.red
@@ -209,7 +210,7 @@ h2
margin: 20px auto margin: 20px auto
.message .message
margin: 10px margin: 3px
padding: 10px padding: 10px
border-radius: 5px border-radius: 5px
color: white color: white
@@ -223,8 +224,6 @@ h2
align-self: flex-start align-self: flex-start
background-color: $c-green background-color: $c-green
border-radius: 0 20px 20px 20px border-radius: 0 20px 20px 20px
<<<<<<< HEAD
=======
.tags .tags
display: flex display: flex
@@ -258,4 +257,3 @@ h2
display: flex display: flex
align-items: center align-items: center
justify-content: center justify-content: center
>>>>>>> 199c0b0 (Added Creating Random Users)
+11 -1
View File
@@ -13,6 +13,8 @@ import Review from './pages/Review';
import CharacterSelection from './pages/CharacterSelection'; import CharacterSelection from './pages/CharacterSelection';
import Character from './pages/Character'; import Character from './pages/Character';
import Lesson from './pages/Lesson'; import Lesson from './pages/Lesson';
import FakeUserSelection from './pages/FakeUserSelection';
import UserChat from './pages/UserChat';
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@@ -54,7 +56,15 @@ const router = createBrowserRouter([
{ {
path: '/lesson', path: '/lesson',
element: <Lesson/> element: <Lesson/>
} },
{
path: '/fake-user-selection',
element: <FakeUserSelection/>
},
{
path: '/user-chat',
element: <UserChat/>
},
]) ])
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
+59 -2
View File
@@ -85,12 +85,12 @@ export interface CharacterChatCreationRequest
language: string; language: string;
} }
export interface CharacterChatCreationResponse export interface ChatCreationResponse
{ {
chat_id: string; chat_id: string;
} }
export async function startFictionalChat(character: string): Promise<CharacterChatCreationResponse> { export async function startFictionalChat(character: string): Promise<ChatCreationResponse> {
const currUser = getUsername(); const currUser = getUsername();
const language = getLanguage(); const language = getLanguage();
@@ -117,6 +117,42 @@ export async function startFictionalChat(character: string): Promise<CharacterCh
return json.session_id; 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> { export async function speechToText(audioBlob: Blob): Promise<string> {
const formData = new FormData(); const formData = new FormData();
formData.append('audio_file', audioBlob, 'audio.wav'); formData.append('audio_file', audioBlob, 'audio.wav');
@@ -156,6 +192,27 @@ export async function characterChatMessage(sessionId: string, message: string):
return json; 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> { export async function getAudio(audioId: string): Promise<Blob> {
const response = await fetch(`${backendUrl}/audio/${audioId}`); const response = await fetch(`${backendUrl}/audio/${audioId}`);
+33 -18
View File
@@ -16,6 +16,16 @@ export default function Character() {
let chunks = [] as any; let chunks = [] as any;
const mediaRecorder = useRef<MediaRecorder | null>(null); const mediaRecorder = useRef<MediaRecorder | null>(null);
const messageEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messageEndRef.current?.scrollIntoView({ behavior: "smooth" })
}
useEffect(() => {
scrollToBottom()
}, [messages]);
useEffect(() => { useEffect(() => {
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => { navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
mediaRecorder.current = new MediaRecorder(stream); mediaRecorder.current = new MediaRecorder(stream);
@@ -57,25 +67,30 @@ export default function Character() {
}, [isRecording]); }, [isRecording]);
return ( return (
<div> <div className='flex flex-col h-screen'>
<div className="v-layout p-10"> <div className="flex-grow overflow-y-auto p-6">
<Icon icon="mdi:arrow-left" className="back-button" onClick={() => navigate(-1)} /> <div>
<h1 className="text-center">Talk With...</h1> <Icon icon="mdi:arrow-left" className="back-button" onClick={() => navigate(-1)} />
<CharacterBadge name={name} image={image} onClick={() => {}}/> <h1 className="text-center">Talk With...</h1>
<div className="chat-area"> <CharacterBadge name={name} image={image} onClick={() => {}}/>
{messages.length === 0 ? ( <div className="chat-area pb-10">
<p className='subtext'>Please record a message to start the conversation.</p> {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}`}> messages.map((message, index) => (
<p>{message.text}</p> <div key={index} className={`message ${message.sender}`}>
</div> <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> </div>
<button className={`record-btn ${isRecording ? 'red' : ''}`} onClick={handleRecord}>
{isRecording ? 'Stop Recording' : 'Record'}
</button>
</div> </div>
</div> </div>
) )
+1 -1
View File
@@ -23,7 +23,7 @@ export default function CharacterSelection() {
return ( return (
<div className="flex flex-col items-center justify-center"> <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> <h1>Talk With...</h1>
<div className="flex flex-wrap justify-center gap-3"> <div className="flex flex-wrap justify-center gap-3">
{characters.map(character => ( {characters.map(character => (
+20 -50
View File
@@ -1,97 +1,67 @@
import NavBar from "../components/NavBar" import NavBar from "../components/NavBar"
import { useState } from 'react'; import { useState } from 'react';
import { generateFakeUsers } from '../logic/fakeUsers';
import { useNavigate } from 'react-router-dom';
export default function CollabLearning() { export default function CollabLearning() {
const target_interests = [ const navigate = useNavigate();
"gaming", "cooking", "sci-fi", "sports", "music", "programming", "first-person shooters", "painting", "baking", "astronomy", "archery"
]
const target_names = [ const [interests, setInterests] = useState<string[]>([]);
"John", "Mary", "Bob", "Alice", "Jane", "Frank", "Sally", "Jack", "Jill", "Joe", "Sue" const [newInterest, setNewInterest] = useState("")
]
interface User {
name: string;
interests: string[];
}
const [tags, setTags] = useState<string[]>([]);
const [otherUsers, setOtherUsers] = useState<User[]>([]);
const [newTag, setNewTag] = useState("")
const [errorMessage, setErrorMessage] = useState("") const [errorMessage, setErrorMessage] = useState("")
const handleDelete = (tagToDelete: any) => { const handleDelete = (tagToDelete: any) => {
setTags(tags.filter(tag => tag !== tagToDelete)); setInterests(interests.filter(tag => tag !== tagToDelete));
} }
const handleAddTag = (event: React.KeyboardEvent<HTMLInputElement>) => { const handleAddTag = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
event.preventDefault(); event.preventDefault();
if (newTag && !tags.includes(newTag)) { if (newInterest && !interests.includes(newInterest)) {
setTags([...tags, newTag]); setInterests([...interests, newInterest]);
setNewTag(''); setNewInterest('');
setErrorMessage(''); setErrorMessage('');
} else if (tags.includes(newTag)) { } else if (interests.includes(newInterest)) {
setErrorMessage('You already entered this interest'); setErrorMessage('You already entered this interest');
} }
} }
} }
const handleMatchClick = () => { const handleMatchClick = () => {
if (tags.length === 0) { if (interests.length === 0) {
setErrorMessage('Please enter at least one interest'); setErrorMessage('Please enter at least one interest');
return; return;
} }
const randomNumber = Math.floor(Math.random() * target_names.length) + 1; const fakeUsers = generateFakeUsers(interests)
console.log(randomNumber); navigate('/fake-user-selection', { state: { fakeUsers: fakeUsers, interests: interests } });
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);
}
setOtherUsers(tempUsers);
console.log(tempUsers);
} }
return ( return (
<div className="v-layout p-10"> <div className="v-layout p-6">
<div className="flex flex-col flex-1"> <div className="flex flex-col flex-1">
<h1>Collaborative Learning</h1> <h1>Collaborative Learning</h1>
<p className="subtext">Find people fluent in your taget language to Chat!</p> <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="subtext">Help them learn a language you know!</p>
<p className="font-bold pt-10">Interests</p> <p className="font-bold pt-10">Interests</p>
<div className="tags"> <div className="tags">
{tags.length === 0 ? ( {interests.length === 0 ? (
<p className="subtext">Enter a new interest below and press "Enter"!</p> <p className="subtext">Enter a new interest below and press "Enter"!</p>
) : ( ) : (
tags.map((tag) => ( interests.map((interest) => (
<div key={tag} className="tag"> <div key={interest} className="tag">
{tag} {interest}
<span className="delete-tag" onClick={(e) => { e.stopPropagation(); handleDelete(tag); }}> X</span> <span className="delete-tag" onClick={(e) => { e.stopPropagation(); handleDelete(interest); }}> X</span>
</div> </div>
)) ))
)} )}
</div> </div>
{errorMessage && <p className="text-red-600 font-bold mt-5">{errorMessage}</p>} {errorMessage && <p className="text-red-600 font-bold mt-5">{errorMessage}</p>}
<input className="mt-5" type="text" value={newTag} onChange={(e) => setNewTag(e.target.value)} onKeyDown={handleAddTag} placeholder="Enter your interest here!" /> <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> <button className="mt-5 green" onClick={() => handleMatchClick()}>Find Chat Partners!</button>
</div> </div>
<NavBar /> <NavBar />
+1 -1
View File
@@ -36,7 +36,7 @@ export default function Course() {
}; };
return ( return (
<div className="v-layout p-10"> <div className="v-layout p-6">
<Icon icon="mdi:arrow-left" className="back-button" onClick={() => navigate(-1)} /> <Icon icon="mdi:arrow-left" className="back-button" onClick={() => navigate(-1)} />
<h1 className="text-center">Course Page</h1> <h1 className="text-center">Course Page</h1>
<div className="flex flex-col flex-1 mb-8 items-center"> <div className="flex flex-col flex-1 mb-8 items-center">