Krish-05 commited on
Commit
80488e6
·
verified ·
1 Parent(s): a0d1eea

Update frontend/src/App.jsx

Browse files
Files changed (1) hide show
  1. frontend/src/App.jsx +148 -110
frontend/src/App.jsx CHANGED
@@ -1,73 +1,82 @@
1
  import React, { useState, useEffect, useRef, useCallback } from 'react';
2
  import { BrowserRouter as Router, Routes, Route, Navigate, Link, useNavigate } from 'react-router-dom';
3
- import SpeechRecognition, { useSpeechRecognition } from 'react-speech-recognition';
4
- import './App.css';
 
5
 
6
- // To keep the code clean, we define a simple "user" object.
7
- // In a real app, this would come from your Firebase auth state.
8
  const mockUser = {
9
  isLoggedIn: true,
10
  };
11
 
 
12
  // =================================================================================
13
  // Chat Components (ChatPage and ChatInput)
14
  // =================================================================================
15
 
16
- const ChatInput = ({ onSendMessage, isLoading }) => {
17
- const [message, setMessage] = useState('');
18
- const {
19
- transcript,
20
- listening,
21
- resetTranscript,
22
- browserSupportsSpeechRecognition,
23
- } = useSpeechRecognition();
24
 
25
- // This effect automatically sends the message when the user stops speaking.
26
- useEffect(() => {
27
- // Check if listening has just stopped and there is a transcript to send.
28
- if (!listening && transcript) {
29
- onSendMessage(transcript);
30
- resetTranscript(); // Clear the transcript for the next message
31
- }
32
- }, [listening, transcript, onSendMessage, resetTranscript]);
33
 
34
- // Update the input field with the live transcript while listening.
35
  useEffect(() => {
36
- if (listening) {
37
- setMessage(transcript);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  }
39
- }, [transcript, listening]);
40
 
41
- if (!browserSupportsSpeechRecognition) {
42
- return <div className="error-message">Speech recognition is not supported by your browser.</div>;
43
- }
44
 
45
  const handleTextChange = (e) => {
46
- setMessage(e.target.value);
47
  };
48
 
49
  const handleSubmit = (e) => {
50
  e.preventDefault();
51
- if (message.trim()) {
52
- onSendMessage(message);
53
- setMessage('');
54
- resetTranscript();
55
  }
56
  };
57
 
58
- // Handlers for the voice input button
59
- const handleVoiceMouseDown = () => {
60
- if (!listening) {
61
- resetTranscript();
62
- setMessage(''); // Clear text field on new voice input
63
- SpeechRecognition.startListening({ continuous: true });
64
- }
65
- };
66
-
67
- const handleVoiceMouseUp = () => {
68
- if (listening) {
69
- SpeechRecognition.stopListening();
70
- }
71
  };
72
 
73
  return (
@@ -75,23 +84,22 @@ const ChatInput = ({ onSendMessage, isLoading }) => {
75
  <form onSubmit={handleSubmit} className="chat-form">
76
  <input
77
  type="text"
78
- value={message}
79
  onChange={handleTextChange}
80
- placeholder={listening ? "Listening..." : "Type or hold the mic to talk..."}
81
- disabled={isLoading || listening}
82
  />
83
  <div className="button-container">
84
  {/* Voice Input Button */}
85
  <button
86
  type="button"
87
- className={`voice-button ${listening ? 'recording' : ''}`}
88
- onMouseDown={handleVoiceMouseDown}
89
- onMouseUp={handleVoiceMouseUp}
90
- onTouchStart={handleVoiceMouseDown} // For mobile
91
- onTouchEnd={handleVoiceMouseUp} // For mobile
92
- disabled={isLoading}
93
  >
94
- {/* Microphone SVG */}
95
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
96
  <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" />
97
  <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" />
@@ -102,9 +110,8 @@ const ChatInput = ({ onSendMessage, isLoading }) => {
102
  <button
103
  type="submit"
104
  className="send-button"
105
- disabled={isLoading || !message.trim() || listening}
106
  >
107
- {/* Arrow SVG */}
108
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
109
  <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" />
110
  </svg>
@@ -119,9 +126,9 @@ const ChatInput = ({ onSendMessage, isLoading }) => {
119
  const ChatPage = () => {
120
  const [chatHistory, setChatHistory] = useState([]);
121
  const [isSending, setIsSending] = useState(false);
 
122
  const chatEndRef = useRef(null);
123
 
124
- // Automatically scroll to the latest message
125
  useEffect(() => {
126
  chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
127
  }, [chatHistory]);
@@ -130,25 +137,72 @@ const ChatPage = () => {
130
  if (!prompt.trim() || isSending) return;
131
 
132
  const userMessage = { role: 'user', message: prompt };
133
- setChatHistory(prev => [...prev, userMessage]);
 
134
  setIsSending(true);
135
 
136
- // Placeholder for the bot's response
137
- setChatHistory(prev => [...prev, { role: 'assistant', message: '...' }]);
138
-
139
- // --- Simulate API call for a response ---
140
- // In a real app, you would make a fetch/axios call to your backend here.
141
- setTimeout(() => {
142
- const botResponse = `This is a simulated response to: "${prompt}"`;
143
- // Replace the placeholder with the actual response
144
- setChatHistory(prev => {
145
- const newHistory = [...prev];
146
- newHistory[newHistory.length - 1] = { role: 'assistant', message: botResponse };
147
- return newHistory;
148
- });
149
- setIsSending(false);
150
- }, 1500); // Simulate network delay
151
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  }, [isSending]);
153
 
154
  return (
@@ -162,21 +216,30 @@ const ChatPage = () => {
162
  ) : (
163
  chatHistory.map((chat, index) => (
164
  <div key={index} className={`message-wrapper ${chat.role}`}>
165
- <div className="chat-avatar">{chat.role === 'assistant' ? '🤖' : '👤'}</div>
 
 
 
 
166
  <div className="message-bubble">{chat.message}</div>
167
  </div>
168
  ))
169
  )}
170
  <div ref={chatEndRef} />
171
  </div>
172
- <ChatInput onSendMessage={handleSendMessage} isLoading={isSending} />
 
 
 
 
 
173
  </div>
174
  );
175
  };
176
 
177
 
178
  // =================================================================================
179
- // Auth Components (Login and SignUp)
180
  // =================================================================================
181
 
182
  const AuthForm = ({ isLogin = false }) => {
@@ -187,15 +250,11 @@ const AuthForm = ({ isLogin = false }) => {
187
 
188
  const handleSubmit = (e) => {
189
  e.preventDefault();
190
- setError(''); // Clear previous errors
191
  if (!email || !password) {
192
  setError('Please fill in all fields.');
193
  return;
194
  }
195
- // --- Mock Authentication ---
196
- // In a real app, this is where you'd call Firebase's signInWithEmailAndPassword or createUserWithEmailAndPassword
197
- console.log(`Attempting to ${isLogin ? 'log in' : 'sign up'} with:`, { email, password });
198
- // Simulate a successful login/signup and navigate to the chat page
199
  mockUser.isLoggedIn = true;
200
  navigate('/chat');
201
  };
@@ -206,23 +265,11 @@ const AuthForm = ({ isLogin = false }) => {
206
  <h2>{isLogin ? 'Log In' : 'Sign Up'}</h2>
207
  <div className="form-group">
208
  <label htmlFor="email">Email</label>
209
- <input
210
- type="email"
211
- id="email"
212
- value={email}
213
- onChange={(e) => setEmail(e.target.value)}
214
- required
215
- />
216
  </div>
217
  <div className="form-group">
218
  <label htmlFor="password">Password</label>
219
- <input
220
- type="password"
221
- id="password"
222
- value={password}
223
- onChange={(e) => setPassword(e.target.value)}
224
- required
225
- />
226
  </div>
227
  {error && <p className="error-message">{error}</p>}
228
  <button type="submit" className="btn-solid-green full-width">
@@ -249,9 +296,7 @@ const SignUp = () => <AuthForm isLogin={false} />;
249
 
250
  const Header = ({ user }) => {
251
  const navigate = useNavigate();
252
-
253
  const handleLogout = () => {
254
- // In a real app, call Firebase's signOut()
255
  mockUser.isLoggedIn = false;
256
  navigate('/login');
257
  };
@@ -259,9 +304,10 @@ const Header = ({ user }) => {
259
  return (
260
  <header className="app-header">
261
  <div className="logo">
 
262
  <div className="brand-names">
263
  <span className="main-brand">Dobby</span>
264
- <span className="sub-brand">Your Assistant</span>
265
  </div>
266
  </div>
267
  <nav>
@@ -280,7 +326,6 @@ const Header = ({ user }) => {
280
 
281
 
282
  function App() {
283
- // This state would be managed by onAuthStateChanged in a real Firebase app.
284
  const [user] = useState(mockUser);
285
 
286
  return (
@@ -291,15 +336,8 @@ function App() {
291
  <Routes>
292
  <Route path="/login" element={<Login />} />
293
  <Route path="/signup" element={<SignUp />} />
294
- <Route
295
- path="/chat"
296
- element={user.isLoggedIn ? <ChatPage /> : <Navigate to="/login" />}
297
- />
298
- {/* Redirect root to chat if logged in, otherwise to login */}
299
- <Route
300
- path="/"
301
- element={user.isLoggedIn ? <Navigate to="/chat" /> : <Navigate to="/login" />}
302
- />
303
  </Routes>
304
  </main>
305
  </div>
 
1
  import React, { useState, useEffect, useRef, useCallback } from 'react';
2
  import { BrowserRouter as Router, Routes, Route, Navigate, Link, useNavigate } from 'react-router-dom';
3
+ import { useReactMediaRecorder } from "react-media-recorder";
4
+ import './App.css'; // Your existing App.css
5
+ import chatbotLogo from './assets/chatbot.png'; // Import the logo
6
 
7
+ // Mock user object for demonstration. In your real app, this comes from Firebase.
 
8
  const mockUser = {
9
  isLoggedIn: true,
10
  };
11
 
12
+
13
  // =================================================================================
14
  // Chat Components (ChatPage and ChatInput)
15
  // =================================================================================
16
 
17
+ const ChatInput = ({ onSendMessage, isLoading, setInputText, inputText }) => {
18
+ const { status, startRecording, stopRecording, mediaBlobUrl } = useReactMediaRecorder({
19
+ audio: true,
20
+ mimeType: "audio/webm", // Explicitly set mimetype
21
+ });
 
 
 
22
 
23
+ const [isTranscribing, setIsTranscribing] = useState(false);
24
+ const isRecording = status === "recording";
 
 
 
 
 
 
25
 
26
+ // This effect handles the recorded audio blob
27
  useEffect(() => {
28
+ // When a blob URL is available and recording has stopped
29
+ if (mediaBlobUrl && status === 'stopped') {
30
+ const transcribeAudio = async () => {
31
+ setIsTranscribing(true);
32
+ try {
33
+ // Fetch the audio blob from its URL
34
+ const audioBlob = await fetch(mediaBlobUrl).then((res) => res.blob());
35
+
36
+ // Create a FormData object to send the file
37
+ const formData = new FormData();
38
+ formData.append("audio_file", audioBlob, "recording.webm");
39
+
40
+ // Send the audio to the backend transcription endpoint
41
+ const response = await fetch("/api/transcribe-audio", {
42
+ method: "POST",
43
+ body: formData,
44
+ });
45
+
46
+ if (!response.ok) {
47
+ throw new Error("Transcription failed");
48
+ }
49
+
50
+ const result = await response.json();
51
+ setInputText(result.transcription); // Set the input field with the transcribed text
52
+ } catch (error) {
53
+ console.error("Error transcribing audio:", error);
54
+ alert("Could not transcribe audio. Please try again.");
55
+ } finally {
56
+ setIsTranscribing(false);
57
+ }
58
+ };
59
+ transcribeAudio();
60
  }
61
+ }, [mediaBlobUrl, status, setInputText]);
62
 
 
 
 
63
 
64
  const handleTextChange = (e) => {
65
+ setInputText(e.target.value);
66
  };
67
 
68
  const handleSubmit = (e) => {
69
  e.preventDefault();
70
+ if (inputText.trim()) {
71
+ onSendMessage(inputText);
72
+ setInputText('');
 
73
  }
74
  };
75
 
76
+ const getPlaceholderText = () => {
77
+ if (isRecording) return "Recording...";
78
+ if (isTranscribing) return "Transcribing...";
79
+ return "Type or hold the mic to talk...";
 
 
 
 
 
 
 
 
 
80
  };
81
 
82
  return (
 
84
  <form onSubmit={handleSubmit} className="chat-form">
85
  <input
86
  type="text"
87
+ value={inputText}
88
  onChange={handleTextChange}
89
+ placeholder={getPlaceholderText()}
90
+ disabled={isLoading || isRecording || isTranscribing}
91
  />
92
  <div className="button-container">
93
  {/* Voice Input Button */}
94
  <button
95
  type="button"
96
+ className={`voice-button ${isRecording ? 'recording' : ''}`}
97
+ onMouseDown={startRecording}
98
+ onMouseUp={stopRecording}
99
+ onTouchStart={startRecording}
100
+ onTouchEnd={stopRecording}
101
+ disabled={isLoading || isTranscribing}
102
  >
 
103
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
104
  <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" />
105
  <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" />
 
110
  <button
111
  type="submit"
112
  className="send-button"
113
+ disabled={isLoading || !inputText.trim()}
114
  >
 
115
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
116
  <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" />
117
  </svg>
 
126
  const ChatPage = () => {
127
  const [chatHistory, setChatHistory] = useState([]);
128
  const [isSending, setIsSending] = useState(false);
129
+ const [inputText, setInputText] = useState(""); // State for the input field
130
  const chatEndRef = useRef(null);
131
 
 
132
  useEffect(() => {
133
  chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
134
  }, [chatHistory]);
 
137
  if (!prompt.trim() || isSending) return;
138
 
139
  const userMessage = { role: 'user', message: prompt };
140
+ // Add user message and a temporary placeholder for the bot's response
141
+ setChatHistory(prev => [...prev, userMessage, { role: 'assistant', message: '...' }]);
142
  setIsSending(true);
143
 
144
+ try {
145
+ const response = await fetch('/api/ask', {
146
+ method: 'POST',
147
+ headers: { 'Content-Type': 'application/json' },
148
+ body: JSON.stringify({ text: prompt }),
149
+ });
150
+
151
+ if (!response.ok) {
152
+ throw new Error(`HTTP error! Status: ${response.status}`);
153
+ }
154
+
155
+ const reader = response.body.getReader();
156
+ const decoder = new TextDecoder();
157
+ let fullResponse = '';
158
+
159
+ while (true) {
160
+ const { value, done } = await reader.read();
161
+ if (done) break;
162
+
163
+ const chunk = decoder.decode(value, { stream: true });
164
+ const events = chunk.split('\n\n');
165
+
166
+ for (const event of events) {
167
+ if (event.startsWith('data:')) {
168
+ const dataStr = event.substring(5).trim();
169
+ if (dataStr) {
170
+ try {
171
+ const dataObj = JSON.parse(dataStr);
172
+ if (dataObj.token) {
173
+ fullResponse += dataObj.token;
174
+ // Update the last message in history with the streaming content
175
+ setChatHistory(prev => {
176
+ const newHistory = [...prev];
177
+ newHistory[newHistory.length - 1] = { role: 'assistant', message: fullResponse + '▌' };
178
+ return newHistory;
179
+ });
180
+ }
181
+ } catch (e) {
182
+ console.error("Error parsing stream data:", e);
183
+ }
184
+ }
185
+ }
186
+ }
187
+ }
188
+
189
+ // Final update to remove the cursor
190
+ setChatHistory(prev => {
191
+ const newHistory = [...prev];
192
+ newHistory[newHistory.length - 1] = { role: 'assistant', message: fullResponse };
193
+ return newHistory;
194
+ });
195
+
196
+ } catch (error) {
197
+ console.error("Error fetching AI response:", error);
198
+ setChatHistory(prev => {
199
+ const newHistory = [...prev];
200
+ newHistory[newHistory.length - 1] = { role: 'assistant', message: "Sorry, I couldn't get a response. Please try again." };
201
+ return newHistory;
202
+ });
203
+ } finally {
204
+ setIsSending(false);
205
+ }
206
  }, [isSending]);
207
 
208
  return (
 
216
  ) : (
217
  chatHistory.map((chat, index) => (
218
  <div key={index} className={`message-wrapper ${chat.role}`}>
219
+ <div className="chat-avatar">
220
+ {chat.role === 'assistant' ? (
221
+ <img src={chatbotLogo} alt="Dobby" className="avatar-image" />
222
+ ) : '👤'}
223
+ </div>
224
  <div className="message-bubble">{chat.message}</div>
225
  </div>
226
  ))
227
  )}
228
  <div ref={chatEndRef} />
229
  </div>
230
+ <ChatInput
231
+ onSendMessage={handleSendMessage}
232
+ isLoading={isSending}
233
+ setInputText={setInputText}
234
+ inputText={inputText}
235
+ />
236
  </div>
237
  );
238
  };
239
 
240
 
241
  // =================================================================================
242
+ // Auth Components (Login and SignUp) - No changes needed here
243
  // =================================================================================
244
 
245
  const AuthForm = ({ isLogin = false }) => {
 
250
 
251
  const handleSubmit = (e) => {
252
  e.preventDefault();
253
+ setError('');
254
  if (!email || !password) {
255
  setError('Please fill in all fields.');
256
  return;
257
  }
 
 
 
 
258
  mockUser.isLoggedIn = true;
259
  navigate('/chat');
260
  };
 
265
  <h2>{isLogin ? 'Log In' : 'Sign Up'}</h2>
266
  <div className="form-group">
267
  <label htmlFor="email">Email</label>
268
+ <input type="email" id="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
 
 
 
 
 
 
269
  </div>
270
  <div className="form-group">
271
  <label htmlFor="password">Password</label>
272
+ <input type="password" id="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
 
 
 
 
 
 
273
  </div>
274
  {error && <p className="error-message">{error}</p>}
275
  <button type="submit" className="btn-solid-green full-width">
 
296
 
297
  const Header = ({ user }) => {
298
  const navigate = useNavigate();
 
299
  const handleLogout = () => {
 
300
  mockUser.isLoggedIn = false;
301
  navigate('/login');
302
  };
 
304
  return (
305
  <header className="app-header">
306
  <div className="logo">
307
+ <img src={chatbotLogo} alt="Dobby Logo" className="header-logo" />
308
  <div className="brand-names">
309
  <span className="main-brand">Dobby</span>
310
+ <span className="sub-brand">GUVI Assistant</span>
311
  </div>
312
  </div>
313
  <nav>
 
326
 
327
 
328
  function App() {
 
329
  const [user] = useState(mockUser);
330
 
331
  return (
 
336
  <Routes>
337
  <Route path="/login" element={<Login />} />
338
  <Route path="/signup" element={<SignUp />} />
339
+ <Route path="/chat" element={user.isLoggedIn ? <ChatPage /> : <Navigate to="/login" />} />
340
+ <Route path="/" element={user.isLoggedIn ? <Navigate to="/chat" /> : <Navigate to="/login" />} />
 
 
 
 
 
 
 
341
  </Routes>
342
  </main>
343
  </div>