Girish Jeswani commited on
Commit
dfe42d4
·
1 Parent(s): 34f34a3

add sidebar component

Browse files
phd-advisor-frontend/src/pages/ChatPage.js CHANGED
@@ -1,5 +1,5 @@
1
  import React, { useState, useEffect, useRef } from 'react';
2
- import { Home, MessageCircle, Reply, X, Sparkles, Users, Settings2, FileText } from 'lucide-react';
3
  import EnhancedChatInput from '../components/EnhancedChatInput';
4
  import MessageBubble from '../components/MessageBubble';
5
  import ThinkingIndicator from '../components/ThinkingIndicator';
@@ -7,12 +7,13 @@ import SuggestionsPanel from '../components/SuggestionsPanel';
7
  import ThemeToggle from '../components/ThemeToggle';
8
  import ProviderDropdown from '../components/ProviderDropdown';
9
  import ExportButton from '../components/ExportButton';
 
10
  import { advisors, getAdvisorColors } from '../data/advisors';
11
  import { useTheme } from '../contexts/ThemeContext';
12
  import '../styles/ChatPage.css';
13
  import '../styles/EnhancedChatInput.css';
14
 
15
- const ChatPage = ({ onNavigateToHome }) => {
16
  const [messages, setMessages] = useState([]);
17
  const [isLoading, setIsLoading] = useState(false);
18
  const [thinkingAdvisors, setThinkingAdvisors] = useState([]);
@@ -24,6 +25,13 @@ const ChatPage = ({ onNavigateToHome }) => {
24
  const messagesEndRef = useRef(null);
25
  const { isDark } = useTheme();
26
 
 
 
 
 
 
 
 
27
  const scrollToBottom = () => {
28
  messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
29
  };
@@ -49,6 +57,8 @@ const ChatPage = ({ onNavigateToHome }) => {
49
  }
50
  };
51
 
 
 
52
  const handleProviderSwitch = async (newProvider) => {
53
  if (newProvider === currentProvider || isProviderSwitching) return;
54
 
@@ -104,66 +114,204 @@ const ChatPage = ({ onNavigateToHome }) => {
104
  return Date.now().toString() + Math.random().toString(36).substr(2, 9);
105
  };
106
 
107
- const handleFileUploaded = (file, response) => {
108
- // Add the uploaded document to our list
109
- const docInfo = {
110
- id: generateMessageId(),
111
- name: file.name,
112
- size: file.size,
113
- type: file.type,
114
- uploadTime: new Date()
115
- };
116
- setUploadedDocuments(prev => [...prev, docInfo]);
 
 
 
 
117
 
118
- // Add a system message about the upload
119
- const uploadMessage = {
120
- id: generateMessageId(),
121
- type: 'document_upload',
122
- content: `Successfully uploaded "${file.name}". Your advisors can now reference this document in their responses.`,
123
- timestamp: new Date(),
124
- documentInfo: docInfo
125
- };
126
-
127
- // Force update messages state
128
- setMessages(prev => {
129
- const newMessages = [...prev, uploadMessage];
130
- console.log('Added upload message:', uploadMessage); // Debug log
131
- return newMessages;
 
 
 
 
 
 
 
 
 
 
 
 
132
  });
133
 
134
- // Scroll to bottom to show the new message
135
- setTimeout(() => {
136
- scrollToBottom();
137
- }, 100);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  };
 
 
 
 
 
 
 
 
 
139
 
140
  const handleSendMessage = async (inputMessage) => {
141
- if (replyingTo) {
142
- await handleReplyToAdvisor(inputMessage, replyingTo);
143
- return;
144
- }
145
 
146
- // Add user message
147
  const userMessage = {
148
- id: generateMessageId(),
149
  type: 'user',
150
  content: inputMessage,
151
  timestamp: new Date()
152
  };
 
 
153
  setMessages(prev => [...prev, userMessage]);
154
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  setIsLoading(true);
156
- // Show thinking indicators for all advisors (backend will decide which ones respond)
157
- setThinkingAdvisors(['methodologist', 'theorist', 'pragmatist']);
158
 
159
  try {
 
160
  const response = await fetch('http://localhost:8000/chat-sequential', {
161
  method: 'POST',
162
  headers: {
163
  'Content-Type': 'application/json',
164
  },
165
  body: JSON.stringify({
166
- user_input: inputMessage
 
167
  }),
168
  });
169
 
@@ -172,10 +320,9 @@ const ChatPage = ({ onNavigateToHome }) => {
172
  }
173
 
174
  const data = await response.json();
175
- console.log('Backend response:', data);
176
 
177
  if (data.type === 'sequential_responses' && data.responses) {
178
- // Simply map each advisor response in the order backend provides
179
  const advisorMessages = data.responses.map((advisor) => ({
180
  id: generateMessageId(),
181
  type: 'advisor',
@@ -185,8 +332,14 @@ const ChatPage = ({ onNavigateToHome }) => {
185
  timestamp: new Date()
186
  }));
187
 
 
188
  setMessages(prev => [...prev, ...advisorMessages]);
189
 
 
 
 
 
 
190
  } else if (data.type === 'error') {
191
  const errorMessage = {
192
  id: generateMessageId(),
@@ -195,6 +348,7 @@ const ChatPage = ({ onNavigateToHome }) => {
195
  timestamp: new Date()
196
  };
197
  setMessages(prev => [...prev, errorMessage]);
 
198
  }
199
 
200
  } catch (error) {
@@ -206,6 +360,7 @@ const ChatPage = ({ onNavigateToHome }) => {
206
  timestamp: new Date()
207
  };
208
  setMessages(prev => [...prev, errorMessage]);
 
209
  }
210
 
211
  setIsLoading(false);
@@ -225,8 +380,12 @@ const ChatPage = ({ onNavigateToHome }) => {
225
  },
226
  timestamp: new Date()
227
  };
 
228
  setMessages(prev => [...prev, replyMessage]);
229
 
 
 
 
230
  setIsLoading(true);
231
  setThinkingAdvisors([replyContext.advisorId]);
232
 
@@ -260,6 +419,9 @@ const ChatPage = ({ onNavigateToHome }) => {
260
  timestamp: new Date()
261
  };
262
  setMessages(prev => [...prev, replyResponseMessage]);
 
 
 
263
  }
264
 
265
  } catch (error) {
@@ -271,6 +433,9 @@ const ChatPage = ({ onNavigateToHome }) => {
271
  timestamp: new Date()
272
  };
273
  setMessages(prev => [...prev, errorMessage]);
 
 
 
274
  }
275
 
276
  setIsLoading(false);
@@ -301,6 +466,9 @@ const ChatPage = ({ onNavigateToHome }) => {
301
  };
302
  setMessages(prev => [...prev, expandMessage]);
303
 
 
 
 
304
  setIsLoading(true);
305
  setThinkingAdvisors([advisorId]);
306
 
@@ -322,11 +490,8 @@ const ChatPage = ({ onNavigateToHome }) => {
322
 
323
  const data = await response.json();
324
 
325
- // Clean response handling without RAG metadata
326
- let expandedMessage = null;
327
-
328
  if (data.persona && data.response) {
329
- expandedMessage = {
330
  id: generateMessageId(),
331
  type: 'advisor',
332
  advisorId: advisorId,
@@ -336,10 +501,10 @@ const ChatPage = ({ onNavigateToHome }) => {
336
  expandsMessageId: messageId,
337
  timestamp: new Date()
338
  };
339
- }
340
-
341
- if (expandedMessage) {
342
  setMessages(prev => [...prev, expandedMessage]);
 
 
 
343
  } else {
344
  const errorMessage = {
345
  id: generateMessageId(),
@@ -348,6 +513,9 @@ const ChatPage = ({ onNavigateToHome }) => {
348
  timestamp: new Date()
349
  };
350
  setMessages(prev => [...prev, errorMessage]);
 
 
 
351
  }
352
 
353
  } catch (error) {
@@ -355,10 +523,13 @@ const ChatPage = ({ onNavigateToHome }) => {
355
  const errorMessage = {
356
  id: generateMessageId(),
357
  type: 'error',
358
- content: 'Sorry, I encountered an error expanding the response. Please try again.',
359
  timestamp: new Date()
360
  };
361
  setMessages(prev => [...prev, errorMessage]);
 
 
 
362
  }
363
 
364
  setIsLoading(false);
@@ -393,184 +564,222 @@ const ChatPage = ({ onNavigateToHome }) => {
393
  const hasConversationMessages = messages.filter(m => m.type !== 'system' && m.type !== 'document_upload').length > 0;
394
 
395
  return (
396
- <div className="modern-chat-page">
397
- {/* Floating Header */}
398
- <div className="floating-header">
399
- <div className="header-left">
400
- <button onClick={onNavigateToHome} className="modern-home-btn">
401
- <Home size={20} />
402
- </button>
403
- <div className="header-brand">
404
- <div className="brand-icon">
405
- <Users size={24} />
406
- </div>
407
- <div className="brand-text">
408
- <h1>PhD Advisory</h1>
409
- <p>AI-Powered Academic Guidance</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
  </div>
411
- </div>
412
- </div>
413
-
414
- <div className="header-right">
415
- <div className="advisor-pills">
416
- {Object.entries(advisors).map(([id, advisor]) => {
417
- const Icon = advisor.icon;
418
- const colors = getAdvisorColors(id, isDark);
419
- const isThinking = thinkingAdvisors.includes(id);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
 
421
- return (
422
- <div
423
- key={id}
424
- className={`advisor-pill ${isThinking ? 'thinking' : ''}`}
425
- style={{
426
- '--advisor-color': colors.color,
427
- '--advisor-bg': colors.bgColor
428
- }}
429
- title={`${advisor.name} - ${advisor.expertise}`}
 
 
 
 
430
  >
431
- <Icon size={16} />
432
- <span>{advisor.name}</span>
433
- {isThinking && (
434
- <div className="thinking-dots">
435
- <div className="dot"></div>
436
- <div className="dot"></div>
437
- <div className="dot"></div>
438
- </div>
439
- )}
440
- </div>
441
- );
442
- })}
443
- </div>
444
-
445
- <div className="header-controls">
446
- {/* Export Button */}
447
- <ExportButton hasMessages={hasConversationMessages} />
448
-
449
- {/* Provider Dropdown */}
450
- <ProviderDropdown
451
- currentProvider={currentProvider}
452
- onProviderChange={handleProviderSwitch}
453
- isLoading={isProviderSwitching}
454
- />
455
-
456
- {/* Theme Toggle */}
457
- <ThemeToggle />
458
  </div>
459
- </div>
460
- </div>
461
 
462
- {/* Main Content */}
463
- <div className="chat-content">
464
- {!hasMessages ? (
465
- <SuggestionsPanel onSuggestionClick={handleSendMessage} />
466
- ) : (
467
- <div className="messages-container">
468
- <div className="messages-list">
469
- <div className="messages-scroll">
470
- {messages.map((message) => (
471
- <div key={message.id}>
472
- {message.type === 'user' && (
473
- <div className="user-message-container">
474
- <div className="user-message">
475
- {message.replyTo && (
476
- <div className="reply-indicator">
477
- <Reply size={12} />
478
- <span>Reply to {message.replyTo.advisorName}</span>
 
 
 
 
 
 
 
 
 
 
 
479
  </div>
480
- )}
481
- <p>{message.content}</p>
482
- </div>
483
- </div>
484
- )}
485
-
486
- {message.type === 'advisor' && (
487
- <MessageBubble
488
- message={message}
489
- onReply={handleReplyToMessage}
490
- onExpand={handleExpandMessage}
491
- onClick={handleMessageClick}
492
- showReplyButton={true}
493
- />
494
- )}
 
 
 
 
 
495
 
496
- {message.type === 'error' && (
497
- <div className="error-message-container">
498
- <div className="error-message">
499
- <p>{message.content}</p>
500
- </div>
 
 
 
 
 
 
 
 
 
 
 
501
  </div>
502
- )}
503
 
504
- {message.type === 'system' && (
505
- <div className="system-message-container">
506
- <div className="system-message">
507
- <p>{message.content}</p>
508
  </div>
509
- </div>
510
- )}
511
-
512
- {message.type === 'document_upload' && (
513
- <div className="system-message-container">
514
- <div className="system-message document-upload">
515
- <FileText size={16} />
516
- <p>{message.content}</p>
517
  </div>
518
  </div>
519
  )}
520
- </div>
521
- ))}
 
 
522
 
523
- {thinkingAdvisors.includes('system') && (
524
- <div className="orchestrator-thinking">
525
- <div className="thinking-bubble">
526
- <MessageCircle size={20} />
527
- </div>
528
- <div className="thinking-content">
529
- <span className="thinking-label">Orchestrator is thinking...</span>
530
- <div className="thinking-animation">
531
- <div className="dot"></div>
532
- <div className="dot"></div>
533
- <div className="dot"></div>
534
- </div>
535
- </div>
536
  </div>
537
- )}
538
-
539
- {thinkingAdvisors.filter(id => id !== 'system').map(advisorId => (
540
- <ThinkingIndicator key={advisorId} advisorId={advisorId} />
541
- ))}
542
-
543
- <div ref={messagesEndRef} />
544
  </div>
545
- </div>
546
  </div>
547
- )}
548
- </div>
549
 
550
- <div className="floating-input-area">
551
- {replyingTo && (
552
- <div className="reply-banner">
553
- <div className="reply-info">
554
- <Reply size={16} />
555
- <span>Replying to <strong>{replyingTo.advisorName}</strong></span>
556
- </div>
557
- <button onClick={cancelReply} className="cancel-reply">
558
- <X size={16} />
559
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
560
  </div>
561
- )}
562
-
563
- <EnhancedChatInput
564
- onSendMessage={handleSendMessage}
565
- onFileUploaded={handleFileUploaded}
566
- uploadedDocuments={uploadedDocuments}
567
- isLoading={isLoading}
568
- placeholder={
569
- replyingTo
570
- ? `Reply to ${replyingTo.advisorName}...`
571
- : "Ask your advisors anything about your PhD journey..."
572
- }
573
- />
574
  </div>
575
  </div>
576
  );
 
1
  import React, { useState, useEffect, useRef } from 'react';
2
+ import { Home, MessageCircle, Reply, X, Sparkles, Users, Settings2, FileText , LogOut} from 'lucide-react';
3
  import EnhancedChatInput from '../components/EnhancedChatInput';
4
  import MessageBubble from '../components/MessageBubble';
5
  import ThinkingIndicator from '../components/ThinkingIndicator';
 
7
  import ThemeToggle from '../components/ThemeToggle';
8
  import ProviderDropdown from '../components/ProviderDropdown';
9
  import ExportButton from '../components/ExportButton';
10
+ import Sidebar from '../components/Sidebar';
11
  import { advisors, getAdvisorColors } from '../data/advisors';
12
  import { useTheme } from '../contexts/ThemeContext';
13
  import '../styles/ChatPage.css';
14
  import '../styles/EnhancedChatInput.css';
15
 
16
+ const ChatPage = ({ user, authToken, onNavigateToHome, onSignOut }) => {
17
  const [messages, setMessages] = useState([]);
18
  const [isLoading, setIsLoading] = useState(false);
19
  const [thinkingAdvisors, setThinkingAdvisors] = useState([]);
 
25
  const messagesEndRef = useRef(null);
26
  const { isDark } = useTheme();
27
 
28
+ const [currentSessionId, setCurrentSessionId] = useState(null);
29
+ const [currentSessionTitle, setCurrentSessionTitle] = useState('');
30
+ const [isSavingSession, setIsSavingSession] = useState(false);
31
+ const [isLoadingSession, setIsLoadingSession] = useState(false);
32
+
33
+
34
+
35
  const scrollToBottom = () => {
36
  messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
37
  };
 
57
  }
58
  };
59
 
60
+
61
+
62
  const handleProviderSwitch = async (newProvider) => {
63
  if (newProvider === currentProvider || isProviderSwitching) return;
64
 
 
114
  return Date.now().toString() + Math.random().toString(36).substr(2, 9);
115
  };
116
 
117
+ const createNewSession = async (firstMessage = null) => {
118
+ try {
119
+ const title = firstMessage
120
+ ? `${firstMessage.substring(0, 30)}...`
121
+ : `Chat ${new Date().toLocaleDateString()}`;
122
+
123
+ const response = await fetch('http://localhost:8000/api/chat-sessions', {
124
+ method: 'POST',
125
+ headers: {
126
+ 'Authorization': `Bearer ${authToken}`,
127
+ 'Content-Type': 'application/json'
128
+ },
129
+ body: JSON.stringify({ title })
130
+ });
131
 
132
+ if (response.ok) {
133
+ const newSession = await response.json();
134
+ setCurrentSessionId(newSession.id);
135
+ setCurrentSessionTitle(newSession.title);
136
+ return newSession.id;
137
+ } else {
138
+ console.error('Failed to create new session');
139
+ return null;
140
+ }
141
+ } catch (error) {
142
+ console.error('Error creating new session:', error);
143
+ return null;
144
+ }
145
+ };
146
+
147
+ // Load an existing chat session
148
+ const loadChatSession = async (sessionId) => {
149
+ if (!sessionId || isLoadingSession) return;
150
+
151
+ setIsLoadingSession(true);
152
+ try {
153
+ const response = await fetch(`http://localhost:8000/api/chat-sessions/${sessionId}`, {
154
+ headers: {
155
+ 'Authorization': `Bearer ${authToken}`,
156
+ 'Content-Type': 'application/json'
157
+ }
158
  });
159
 
160
+ if (response.ok) {
161
+ const session = await response.json();
162
+ setCurrentSessionId(session.id);
163
+ setCurrentSessionTitle(session.title);
164
+
165
+ // Convert stored messages back to proper format
166
+ const formattedMessages = session.messages.map(msg => ({
167
+ ...msg,
168
+ timestamp: new Date(msg.timestamp)
169
+ }));
170
+
171
+ setMessages(formattedMessages);
172
+ setReplyingTo(null);
173
+ setThinkingAdvisors([]);
174
+ } else {
175
+ console.error('Failed to load session');
176
+ }
177
+ } catch (error) {
178
+ console.error('Error loading session:', error);
179
+ } finally {
180
+ setIsLoadingSession(false);
181
+ }
182
+ };
183
+
184
+ // Save a message to the current session
185
+ const saveMessageToSession = async (message) => {
186
+ if (!currentSessionId || !authToken) return;
187
+
188
+ try {
189
+ await fetch(`http://localhost:8000/api/chat-sessions/${currentSessionId}/messages`, {
190
+ method: 'POST',
191
+ headers: {
192
+ 'Authorization': `Bearer ${authToken}`,
193
+ 'Content-Type': 'application/json'
194
+ },
195
+ body: JSON.stringify({
196
+ session_id: currentSessionId,
197
+ message: {
198
+ ...message,
199
+ timestamp: message.timestamp.toISOString()
200
+ }
201
+ })
202
+ });
203
+ } catch (error) {
204
+ console.error('Error saving message to session:', error);
205
+ }
206
+ };
207
+
208
+ // Update session title based on first message
209
+ const updateSessionTitle = async (sessionId, newTitle) => {
210
+ if (!sessionId || !authToken) return;
211
+
212
+ try {
213
+ await fetch(`http://localhost:8000/api/chat-sessions/${sessionId}`, {
214
+ method: 'PUT',
215
+ headers: {
216
+ 'Authorization': `Bearer ${authToken}`,
217
+ 'Content-Type': 'application/json'
218
+ },
219
+ body: JSON.stringify({ title: newTitle })
220
+ });
221
+ setCurrentSessionTitle(newTitle);
222
+ } catch (error) {
223
+ console.error('Error updating session title:', error);
224
+ }
225
+ };
226
+
227
+ // Handle selecting a session from sidebar
228
+ const handleSelectSession = async (sessionId) => {
229
+ if (sessionId === currentSessionId) return;
230
+ await loadChatSession(sessionId);
231
+ };
232
+
233
+ // Handle creating new chat from sidebar
234
+ const handleNewChat = async (sessionId = null) => {
235
+ if (sessionId) {
236
+ // If specific session ID provided, load it
237
+ await loadChatSession(sessionId);
238
+ } else {
239
+ // Create a completely new session
240
+ setMessages([]);
241
+ setCurrentSessionId(null);
242
+ setCurrentSessionTitle('');
243
+ setReplyingTo(null);
244
+ setThinkingAdvisors([]);
245
+ setUploadedDocuments([]);
246
+ }
247
+ };
248
+
249
+ const handleFileUploaded = async (fileInfo) => {
250
+ const documentMessage = {
251
+ id: generateMessageId(),
252
+ type: 'document_upload',
253
+ content: `Document uploaded: ${fileInfo.filename}`,
254
+ timestamp: new Date()
255
  };
256
+
257
+ setMessages(prev => [...prev, documentMessage]);
258
+ setUploadedDocuments(prev => [...prev, fileInfo]);
259
+
260
+ // Save document upload message to database if we have a current session
261
+ if (currentSessionId) {
262
+ await saveMessageToSession(documentMessage);
263
+ }
264
+ };
265
 
266
  const handleSendMessage = async (inputMessage) => {
267
+ if (!inputMessage.trim()) return;
 
 
 
268
 
269
+ // Create user message
270
  const userMessage = {
271
+ id: generateMessageId(), // This uses your existing function
272
  type: 'user',
273
  content: inputMessage,
274
  timestamp: new Date()
275
  };
276
+
277
+ // Add to local state immediately
278
  setMessages(prev => [...prev, userMessage]);
279
+
280
+ // Create new session if we don't have one
281
+ let sessionId = currentSessionId;
282
+ if (!sessionId) {
283
+ sessionId = await createNewSession(inputMessage);
284
+ if (!sessionId) {
285
+ console.error('Failed to create session');
286
+ return;
287
+ }
288
+ }
289
+
290
+ // Save user message to database
291
+ await saveMessageToSession(userMessage);
292
+
293
+ // Update session title if this is the first message and title is generic
294
+ if (messages.length === 0 && currentSessionTitle.includes('Chat ')) {
295
+ const newTitle = inputMessage.length > 30
296
+ ? `${inputMessage.substring(0, 30)}...`
297
+ : inputMessage;
298
+ await updateSessionTitle(sessionId, newTitle);
299
+ }
300
+
301
+ // Set loading state
302
  setIsLoading(true);
303
+ setThinkingAdvisors(['system']);
 
304
 
305
  try {
306
+ // Your existing API call logic here...
307
  const response = await fetch('http://localhost:8000/chat-sequential', {
308
  method: 'POST',
309
  headers: {
310
  'Content-Type': 'application/json',
311
  },
312
  body: JSON.stringify({
313
+ user_input: inputMessage,
314
+ response_length: 'medium'
315
  }),
316
  });
317
 
 
320
  }
321
 
322
  const data = await response.json();
 
323
 
324
  if (data.type === 'sequential_responses' && data.responses) {
325
+ // Create advisor messages
326
  const advisorMessages = data.responses.map((advisor) => ({
327
  id: generateMessageId(),
328
  type: 'advisor',
 
332
  timestamp: new Date()
333
  }));
334
 
335
+ // Add to local state
336
  setMessages(prev => [...prev, ...advisorMessages]);
337
 
338
+ // Save each advisor message to database
339
+ for (const advisorMessage of advisorMessages) {
340
+ await saveMessageToSession(advisorMessage);
341
+ }
342
+
343
  } else if (data.type === 'error') {
344
  const errorMessage = {
345
  id: generateMessageId(),
 
348
  timestamp: new Date()
349
  };
350
  setMessages(prev => [...prev, errorMessage]);
351
+ await saveMessageToSession(errorMessage);
352
  }
353
 
354
  } catch (error) {
 
360
  timestamp: new Date()
361
  };
362
  setMessages(prev => [...prev, errorMessage]);
363
+ await saveMessageToSession(errorMessage);
364
  }
365
 
366
  setIsLoading(false);
 
380
  },
381
  timestamp: new Date()
382
  };
383
+
384
  setMessages(prev => [...prev, replyMessage]);
385
 
386
+ // Save reply message to database
387
+ await saveMessageToSession(replyMessage);
388
+
389
  setIsLoading(true);
390
  setThinkingAdvisors([replyContext.advisorId]);
391
 
 
419
  timestamp: new Date()
420
  };
421
  setMessages(prev => [...prev, replyResponseMessage]);
422
+
423
+ // Save advisor reply to database
424
+ await saveMessageToSession(replyResponseMessage);
425
  }
426
 
427
  } catch (error) {
 
433
  timestamp: new Date()
434
  };
435
  setMessages(prev => [...prev, errorMessage]);
436
+
437
+ // Save error message to database
438
+ await saveMessageToSession(errorMessage);
439
  }
440
 
441
  setIsLoading(false);
 
466
  };
467
  setMessages(prev => [...prev, expandMessage]);
468
 
469
+ // Save expand request to database
470
+ await saveMessageToSession(expandMessage);
471
+
472
  setIsLoading(true);
473
  setThinkingAdvisors([advisorId]);
474
 
 
490
 
491
  const data = await response.json();
492
 
 
 
 
493
  if (data.persona && data.response) {
494
+ const expandedMessage = {
495
  id: generateMessageId(),
496
  type: 'advisor',
497
  advisorId: advisorId,
 
501
  expandsMessageId: messageId,
502
  timestamp: new Date()
503
  };
 
 
 
504
  setMessages(prev => [...prev, expandedMessage]);
505
+
506
+ // Save expanded response to database
507
+ await saveMessageToSession(expandedMessage);
508
  } else {
509
  const errorMessage = {
510
  id: generateMessageId(),
 
513
  timestamp: new Date()
514
  };
515
  setMessages(prev => [...prev, errorMessage]);
516
+
517
+ // Save error message to database
518
+ await saveMessageToSession(errorMessage);
519
  }
520
 
521
  } catch (error) {
 
523
  const errorMessage = {
524
  id: generateMessageId(),
525
  type: 'error',
526
+ content: 'Sorry, I encountered an error while expanding the message. Please try again.',
527
  timestamp: new Date()
528
  };
529
  setMessages(prev => [...prev, errorMessage]);
530
+
531
+ // Save error message to database
532
+ await saveMessageToSession(errorMessage);
533
  }
534
 
535
  setIsLoading(false);
 
564
  const hasConversationMessages = messages.filter(m => m.type !== 'system' && m.type !== 'document_upload').length > 0;
565
 
566
  return (
567
+ <div className="chat-page-with-sidebar">
568
+ {/* Sidebar Component */}
569
+ <Sidebar
570
+ user={user}
571
+ currentSessionId={currentSessionId}
572
+ onSelectSession={handleSelectSession}
573
+ onNewChat={handleNewChat}
574
+ onSignOut={onSignOut}
575
+ authToken={authToken}
576
+ />
577
+
578
+ <div className="main-chat-area">
579
+ <div className="modern-chat-page">
580
+ {/* Floating Header */}
581
+ <div className="floating-header">
582
+ <div className="header-left">
583
+ <button onClick={onNavigateToHome} className="modern-home-btn">
584
+ <Home size={20} />
585
+ </button>
586
+ <div className="header-brand">
587
+ <div className="brand-icon">
588
+ <Users size={24} />
589
+ </div>
590
+ <div className="brand-text">
591
+ <h1>PhD Advisory</h1>
592
+ <p>AI-Powered Academic Guidance</p>
593
+ </div>
594
+ </div>
595
  </div>
596
+
597
+ <div className="header-right">
598
+ <div className="advisor-pills">
599
+ {Object.entries(advisors).map(([id, advisor]) => {
600
+ const Icon = advisor.icon;
601
+ const colors = getAdvisorColors(id, isDark);
602
+ const isThinking = thinkingAdvisors.includes(id);
603
+
604
+ return (
605
+ <div
606
+ key={id}
607
+ className={`advisor-pill ${isThinking ? 'thinking' : ''}`}
608
+ style={{
609
+ '--advisor-color': colors.color,
610
+ '--advisor-bg': colors.bgColor
611
+ }}
612
+ title={`${advisor.name} - ${advisor.expertise}`}
613
+ >
614
+ <Icon size={16} />
615
+ <span>{advisor.name}</span>
616
+ {isThinking && (
617
+ <div className="thinking-dots">
618
+ <div className="dot"></div>
619
+ <div className="dot"></div>
620
+ <div className="dot"></div>
621
+ </div>
622
+ )}
623
+ </div>
624
+ );
625
+ })}
626
+ </div>
627
 
628
+ <div className="header-controls">
629
+ {/* Add session title display */}
630
+ {currentSessionTitle && (
631
+ <div className="session-title-display">
632
+ <span>{currentSessionTitle}</span>
633
+ </div>
634
+ )}
635
+
636
+ {/* Optional: Add header sign out button */}
637
+ <button
638
+ className="header-signout-btn"
639
+ onClick={onSignOut}
640
+ title="Sign Out"
641
  >
642
+ <LogOut size={16} />
643
+ </button>
644
+
645
+ {/* Export Button */}
646
+ <ExportButton hasMessages={hasConversationMessages} />
647
+
648
+ {/* Provider Dropdown */}
649
+ <ProviderDropdown
650
+ currentProvider={currentProvider}
651
+ onProviderChange={handleProviderSwitch}
652
+ isLoading={isProviderSwitching}
653
+ />
654
+
655
+ {/* Theme Toggle */}
656
+ <ThemeToggle />
657
+ </div>
658
+ </div>
 
 
 
 
 
 
 
 
 
 
659
  </div>
 
 
660
 
661
+ {/* Main Content */}
662
+ <div className="chat-content">
663
+ {!hasMessages ? (
664
+ <SuggestionsPanel onSuggestionClick={handleSendMessage} />
665
+ ) : (
666
+ <div className="messages-container">
667
+ {/* Add loading session indicator */}
668
+ {isLoadingSession && (
669
+ <div className="loading-session">
670
+ <div className="loading-spinner"></div>
671
+ <span>Loading chat session...</span>
672
+ </div>
673
+ )}
674
+
675
+ <div className="messages-list">
676
+ <div className="messages-scroll">
677
+ {messages.map((message) => (
678
+ <div key={message.id}>
679
+ {message.type === 'user' && (
680
+ <div className="user-message-container">
681
+ <div className="user-message">
682
+ {message.replyTo && (
683
+ <div className="reply-indicator">
684
+ <Reply size={12} />
685
+ <span>Reply to {message.replyTo.advisorName}</span>
686
+ </div>
687
+ )}
688
+ <p>{message.content}</p>
689
  </div>
690
+ </div>
691
+ )}
692
+
693
+ {message.type === 'advisor' && (
694
+ <MessageBubble
695
+ message={message}
696
+ onReply={handleReplyToMessage}
697
+ onExpand={handleExpandMessage}
698
+ onClick={handleMessageClick}
699
+ showReplyButton={true}
700
+ />
701
+ )}
702
+
703
+ {message.type === 'error' && (
704
+ <div className="error-message-container">
705
+ <div className="error-message">
706
+ <p>{message.content}</p>
707
+ </div>
708
+ </div>
709
+ )}
710
 
711
+ {message.type === 'system' && (
712
+ <div className="system-message-container">
713
+ <div className="system-message">
714
+ <p>{message.content}</p>
715
+ </div>
716
+ </div>
717
+ )}
718
+
719
+ {message.type === 'document_upload' && (
720
+ <div className="system-message-container">
721
+ <div className="system-message document-upload">
722
+ <FileText size={16} />
723
+ <p>{message.content}</p>
724
+ </div>
725
+ </div>
726
+ )}
727
  </div>
728
+ ))}
729
 
730
+ {thinkingAdvisors.includes('system') && (
731
+ <div className="orchestrator-thinking">
732
+ <div className="thinking-bubble">
733
+ <MessageCircle size={20} />
734
  </div>
735
+ <div className="thinking-content">
736
+ <span className="thinking-label">Orchestrator is thinking...</span>
737
+ <div className="thinking-animation">
738
+ <div className="dot"></div>
739
+ <div className="dot"></div>
740
+ <div className="dot"></div>
741
+ </div>
 
742
  </div>
743
  </div>
744
  )}
745
+
746
+ {thinkingAdvisors.filter(id => id !== 'system').map(advisorId => (
747
+ <ThinkingIndicator key={advisorId} advisorId={advisorId} />
748
+ ))}
749
 
750
+ <div ref={messagesEndRef} />
 
 
 
 
 
 
 
 
 
 
 
 
751
  </div>
752
+ </div>
 
 
 
 
 
 
753
  </div>
754
+ )}
755
  </div>
 
 
756
 
757
+ <div className="floating-input-area">
758
+ {replyingTo && (
759
+ <div className="reply-banner">
760
+ <div className="reply-info">
761
+ <Reply size={16} />
762
+ <span>Replying to <strong>{replyingTo.advisorName}</strong></span>
763
+ </div>
764
+ <button onClick={cancelReply} className="cancel-reply">
765
+ <X size={16} />
766
+ </button>
767
+ </div>
768
+ )}
769
+
770
+ <EnhancedChatInput
771
+ onSendMessage={handleSendMessage}
772
+ onFileUploaded={handleFileUploaded}
773
+ uploadedDocuments={uploadedDocuments}
774
+ isLoading={isLoading}
775
+ placeholder={
776
+ replyingTo
777
+ ? `Reply to ${replyingTo.advisorName}...`
778
+ : "Ask your advisors anything about your PhD journey..."
779
+ }
780
+ />
781
  </div>
782
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
783
  </div>
784
  </div>
785
  );
phd-advisor-frontend/src/styles/ChatPage.css CHANGED
@@ -440,6 +440,102 @@
440
  color: var(--text-secondary);
441
  }
442
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
443
  /* Document Indicator Styles */
444
  .document-indicator {
445
  display: flex;
@@ -715,4 +811,8 @@
715
  width: auto;
716
  min-width: unset;
717
  }
 
 
 
 
718
  }
 
440
  color: var(--text-secondary);
441
  }
442
 
443
+ .chat-page-with-sidebar {
444
+ display: flex;
445
+ height: 100vh;
446
+ overflow: hidden;
447
+ }
448
+
449
+ .main-chat-area {
450
+ flex: 1;
451
+ margin-left: 300px; /* Width of sidebar */
452
+ display: flex;
453
+ flex-direction: column;
454
+ height: 100vh;
455
+ transition: margin-left 0.3s ease;
456
+ }
457
+
458
+ /* Session title display in header */
459
+ .session-title-display {
460
+ display: flex;
461
+ align-items: center;
462
+ padding: 6px 12px;
463
+ background: var(--bg-secondary, #f3f4f6);
464
+ border-radius: 6px;
465
+ margin-right: 8px;
466
+ }
467
+
468
+ .dark .session-title-display {
469
+ background: var(--bg-secondary-dark, #374151);
470
+ }
471
+
472
+ .session-title-display span {
473
+ font-size: 13px;
474
+ font-weight: 500;
475
+ color: var(--text-primary, #111827);
476
+ max-width: 150px;
477
+ white-space: nowrap;
478
+ overflow: hidden;
479
+ text-overflow: ellipsis;
480
+ }
481
+
482
+ .dark .session-title-display span {
483
+ color: var(--text-primary-dark, #f9fafb);
484
+ }
485
+
486
+ /* Optional header sign out button */
487
+ .header-signout-btn {
488
+ display: flex;
489
+ align-items: center;
490
+ justify-content: center;
491
+ padding: 8px;
492
+ border: none;
493
+ background: none;
494
+ border-radius: 6px;
495
+ cursor: pointer;
496
+ color: var(--text-secondary, #6b7280);
497
+ transition: all 0.2s ease;
498
+ margin-right: 8px;
499
+ }
500
+
501
+ .header-signout-btn:hover {
502
+ background: var(--bg-secondary, #f3f4f6);
503
+ color: #ef4444;
504
+ }
505
+
506
+ .dark .header-signout-btn:hover {
507
+ background: var(--bg-secondary-dark, #374151);
508
+ }
509
+
510
+ /* Loading session indicator */
511
+ .loading-session {
512
+ display: flex;
513
+ align-items: center;
514
+ justify-content: center;
515
+ gap: 12px;
516
+ padding: 20px;
517
+ color: var(--text-secondary, #6b7280);
518
+ font-size: 14px;
519
+ }
520
+
521
+ .dark .loading-session {
522
+ color: var(--text-secondary-dark, #9ca3af);
523
+ }
524
+
525
+ .loading-session .loading-spinner {
526
+ width: 16px;
527
+ height: 16px;
528
+ border: 2px solid var(--border-light, #e5e7eb);
529
+ border-top: 2px solid var(--primary-color, #3b82f6);
530
+ border-radius: 50%;
531
+ animation: spin 1s linear infinite;
532
+ }
533
+
534
+ .dark .loading-session .loading-spinner {
535
+ border-color: var(--border-dark, #374151);
536
+ border-top-color: var(--primary-color, #3b82f6);
537
+ }
538
+
539
  /* Document Indicator Styles */
540
  .document-indicator {
541
  display: flex;
 
811
  width: auto;
812
  min-width: unset;
813
  }
814
+
815
+ .main-chat-area {
816
+ margin-left: 0;
817
+ }
818
  }