Krish-05's picture
Update frontend/src/App.jsx
38a896a verified
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate, Link, useNavigate } from 'react-router-dom';
import { useReactMediaRecorder } from "react-media-recorder";
import './App.css'; // Your existing App.css
import chatbotLogo from './assets/chatbot.png'; // Import the logo
// Mock user object for demonstration. In your real app, this comes from Firebase.
const mockUser = {
isLoggedIn: true,
};
// =================================================================================
// Chat Components (ChatPage and ChatInput)
// =================================================================================
const ChatInput = ({ onSendMessage, isLoading, setInputText, inputText }) => {
const { status, startRecording, stopRecording, mediaBlobUrl } = useReactMediaRecorder({
audio: true,
mimeType: "audio/webm", // Explicitly set mimetype
});
const [isTranscribing, setIsTranscribing] = useState(false);
const isRecording = status === "recording";
// This effect handles the recorded audio blob
useEffect(() => {
// When a blob URL is available and recording has stopped
if (mediaBlobUrl && status === 'stopped') {
const transcribeAudio = async () => {
setIsTranscribing(true);
try {
// Fetch the audio blob from its URL
const audioBlob = await fetch(mediaBlobUrl).then((res) => res.blob());
// Create a FormData object to send the file
const formData = new FormData();
formData.append("audio_file", audioBlob, "recording.webm");
// Send the audio to the backend transcription endpoint
const response = await fetch("/api/transcribe-audio", {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error("Transcription failed");
}
const result = await response.json();
setInputText(result.transcription); // Set the input field with the transcribed text
} catch (error) {
console.error("Error transcribing audio:", error);
alert("Could not transcribe audio. Please try again.");
} finally {
setIsTranscribing(false);
}
};
transcribeAudio();
}
}, [mediaBlobUrl, status, setInputText]);
const handleTextChange = (e) => {
setInputText(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
if (inputText.trim()) {
onSendMessage(inputText);
setInputText('');
}
};
const getPlaceholderText = () => {
if (isRecording) return "Recording...";
if (isTranscribing) return "Transcribing...";
return "Type or hold the mic to talk...";
};
return (
<div className="chat-input-area">
<form onSubmit={handleSubmit} className="chat-form">
<input
type="text"
value={inputText}
onChange={handleTextChange}
placeholder={getPlaceholderText()}
disabled={isLoading || isRecording || isTranscribing}
/>
<div className="button-container">
{/* Voice Input Button */}
<button
type="button"
className={`voice-button ${isRecording ? 'recording' : ''}`}
onMouseDown={startRecording}
onMouseUp={stopRecording}
onTouchStart={startRecording}
onTouchEnd={stopRecording}
disabled={isLoading || isTranscribing}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M8.25 4.5a3.75 3.75 0 1 1 7.5 0v8.25a3.75 3.75 0 1 1-7.5 0V4.5Z" />
<path d="M6 10.5a.75.75 0 0 1 .75.75v1.5a5.25 5.25 0 1 0 10.5 0v-1.5a.75.75 0 0 1 1.5 0v1.5a6.75 6.75 0 1 1-13.5 0v-1.5A.75.75 0 0 1 6 10.5Z" />
</svg>
</button>
{/* Text Send Button */}
<button
type="submit"
className="send-button"
disabled={isLoading || !inputText.trim()}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M3.478 2.404a.75.75 0 0 0-.926.941l2.432 7.905H13.5a.75.75 0 0 1 0 1.5H4.984l-2.432 7.905a.75.75 0 0 0 .926.94 60.519 60.519 0 0 0 18.445-8.986.75.75 0 0 0 0-1.218A60.517 60.517 0 0 0 3.478 2.404Z" />
</svg>
</button>
</div>
</form>
</div>
);
};
const ChatPage = () => {
const [chatHistory, setChatHistory] = useState([]);
const [isSending, setIsSending] = useState(false);
const [inputText, setInputText] = useState(""); // State for the input field
const chatEndRef = useRef(null);
useEffect(() => {
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [chatHistory]);
const handleSendMessage = useCallback(async (prompt) => {
if (!prompt.trim() || isSending) return;
const userMessage = { role: 'user', message: prompt };
// Add user message and a temporary placeholder for the bot's response
setChatHistory(prev => [...prev, userMessage, { role: 'assistant', message: '...' }]);
setIsSending(true);
try {
const response = await fetch('/api/ask', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: prompt }),
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullResponse = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const events = chunk.split('\n\n');
for (const event of events) {
if (event.startsWith('data:')) {
const dataStr = event.substring(5).trim();
if (dataStr) {
try {
const dataObj = JSON.parse(dataStr);
if (dataObj.token) {
fullResponse += dataObj.token;
// Update the last message in history with the streaming content
setChatHistory(prev => {
const newHistory = [...prev];
newHistory[newHistory.length - 1] = { role: 'assistant', message: fullResponse + '▌' };
return newHistory;
});
}
} catch (e) {
console.error("Error parsing stream data:", e);
}
}
}
}
}
// Final update to remove the cursor
setChatHistory(prev => {
const newHistory = [...prev];
newHistory[newHistory.length - 1] = { role: 'assistant', message: fullResponse };
return newHistory;
});
} catch (error) {
console.error("Error fetching AI response:", error);
setChatHistory(prev => {
const newHistory = [...prev];
newHistory[newHistory.length - 1] = { role: 'assistant', message: "Sorry, I couldn't get a response. Please try again." };
return newHistory;
});
} finally {
setIsSending(false);
}
}, [isSending]);
return (
<div className="chat-main">
<div className="chat-messages">
{chatHistory.length === 0 ? (
<div className="empty-chat-placeholder">
<h1>Dobby is here to help!</h1>
<p>Start the conversation by typing or using your voice.</p>
</div>
) : (
chatHistory.map((chat, index) => (
<div key={index} className={`message-wrapper ${chat.role}`}>
<div className="chat-avatar">
{chat.role === 'assistant' ? (
<img src={chatbotLogo} alt="Dobby" className="avatar-image" />
) : '👤'}
</div>
<div className="message-bubble">{chat.message}</div>
</div>
))
)}
<div ref={chatEndRef} />
</div>
<ChatInput
onSendMessage={handleSendMessage}
isLoading={isSending}
setInputText={setInputText}
inputText={inputText}
/>
</div>
);
};
// =================================================================================
// Auth Components (Login and SignUp)
// =================================================================================
const AuthForm = ({ isLogin = false }) => {
const navigate = useNavigate();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
setError('');
if (!email || !password) {
setError('Please fill in all fields.');
return;
}
// In a real app, you'd integrate with Firebase or your backend for authentication
// For this mock, we just set isLoggedIn to true
mockUser.isLoggedIn = true;
navigate('/chat');
};
return (
<div className="auth-container">
<form className="auth-form" onSubmit={handleSubmit}>
<h2>{isLogin ? 'Log In' : 'Sign Up'}</h2>
<div className="form-group">
<label htmlFor="email">Email</label>
<input type="email" id="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input type="password" id="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
</div>
{error && <p className="error-message">{error}</p>}
<button type="submit" className="btn-solid-green full-width">
{isLogin ? 'Log In' : 'Sign Up'}
</button>
<div className="auth-switch">
{isLogin ? (
<p>Don't have an account? <Link to="/signup">Sign Up</Link></p>
) : (
<p>Already have an account? <Link to="/login">Log In</Link></p>
)}
</div>
</form>
</div>
);
};
const Login = () => <AuthForm isLogin={true} />;
const SignUp = () => <AuthForm isLogin={false} />;
// =================================================================================
// Header and Main App Component
// =================================================================================
const Header = ({ user }) => {
const navigate = useNavigate();
const handleLogout = () => {
mockUser.isLoggedIn = false;
navigate('/login');
};
return (
<header className="app-header">
<div className="logo">
{/* Changed order: Dobby text first, then logo */}
<div className="brand-names">
<span className="main-brand">Dobby</span>
<span className="sub-brand">GUVI Assistant</span>
</div>
<img src={chatbotLogo} alt="Dobby Logo" className="header-logo" />
</div>
<nav>
{user && user.isLoggedIn ? (
<button onClick={handleLogout} className="btn-solid-green">Log Out</button>
) : (
<>
<Link to="/login" className="link-green">Log In</Link>
<Link to="/signup" className="btn-solid-green">Sign Up</Link>
</>
)}
</nav>
</header>
);
};
function App() {
const [user] = useState(mockUser);
return (
<Router>
<div className="app-wrapper">
<Header user={user} />
<main className="main-content">
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<SignUp />} />
<Route path="/chat" element={user.isLoggedIn ? <ChatPage /> : <Navigate to="/login" />} />
<Route path="/" element={user.isLoggedIn ? <Navigate to="/chat" /> : <Navigate to="/login" />} />
</Routes>
</main>
</div>
</Router>
);
}
export default App;