Spaces:
Sleeping
Sleeping
| 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; |