Girish Jeswani commited on
Commit
9f5fa59
·
1 Parent(s): 1ec7693

add reply functionality

Browse files
phd-advisor-frontend/src/components/ChatInput.js CHANGED
@@ -1,7 +1,7 @@
1
  import React, { useState } from 'react';
2
  import { Send } from 'lucide-react';
3
 
4
- const ChatInput = ({ onSendMessage, isLoading }) => {
5
  const [inputMessage, setInputMessage] = useState('');
6
 
7
  const handleSend = () => {
@@ -25,7 +25,7 @@ const ChatInput = ({ onSendMessage, isLoading }) => {
25
  value={inputMessage}
26
  onChange={(e) => setInputMessage(e.target.value)}
27
  onKeyPress={handleKeyPress}
28
- placeholder="Ask your advisors anything about your PhD journey..."
29
  className="message-input"
30
  rows="2"
31
  disabled={isLoading}
 
1
  import React, { useState } from 'react';
2
  import { Send } from 'lucide-react';
3
 
4
+ const ChatInput = ({ onSendMessage, isLoading, placeholder = "Ask your advisors anything about your PhD journey..." }) => {
5
  const [inputMessage, setInputMessage] = useState('');
6
 
7
  const handleSend = () => {
 
25
  value={inputMessage}
26
  onChange={(e) => setInputMessage(e.target.value)}
27
  onKeyPress={handleKeyPress}
28
+ placeholder={placeholder}
29
  className="message-input"
30
  rows="2"
31
  disabled={isLoading}
phd-advisor-frontend/src/components/MessageBubble.js CHANGED
@@ -1,14 +1,21 @@
1
  import React from 'react';
 
2
  import { advisors, getAdvisorColors } from '../data/advisors';
3
  import { useTheme } from '../contexts/ThemeContext';
4
 
5
- const MessageBubble = ({ message }) => {
6
  const { isDark } = useTheme();
7
 
8
  if (message.type === 'user') {
9
  return (
10
  <div className="user-message-container">
11
  <div className="user-message">
 
 
 
 
 
 
12
  <p>{message.content}</p>
13
  </div>
14
  </div>
@@ -29,11 +36,12 @@ const MessageBubble = ({ message }) => {
29
  <Icon style={{ color: colors.color }} />
30
  </div>
31
  <div
32
- className="advisor-message-bubble"
33
  style={{
34
  backgroundColor: colors.bgColor,
35
- borderColor: colors.color + '40' // Adding transparency to the border
36
  }}
 
37
  >
38
  <div className="advisor-message-header">
39
  <h4
@@ -41,6 +49,7 @@ const MessageBubble = ({ message }) => {
41
  style={{ color: colors.color }}
42
  >
43
  {advisor.name}
 
44
  </h4>
45
  <span
46
  className="message-time"
@@ -63,6 +72,24 @@ const MessageBubble = ({ message }) => {
63
  >
64
  {message.content}
65
  </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  </div>
67
  </div>
68
  );
 
1
  import React from 'react';
2
+ import { Reply } from 'lucide-react';
3
  import { advisors, getAdvisorColors } from '../data/advisors';
4
  import { useTheme } from '../contexts/ThemeContext';
5
 
6
+ const MessageBubble = ({ message, onClick, showReplyButton = false }) => {
7
  const { isDark } = useTheme();
8
 
9
  if (message.type === 'user') {
10
  return (
11
  <div className="user-message-container">
12
  <div className="user-message">
13
+ {message.replyTo && (
14
+ <div className="reply-indicator">
15
+ <Reply size={14} />
16
+ <span>to {message.replyTo.advisorName}</span>
17
+ </div>
18
+ )}
19
  <p>{message.content}</p>
20
  </div>
21
  </div>
 
36
  <Icon style={{ color: colors.color }} />
37
  </div>
38
  <div
39
+ className={`advisor-message-bubble ${showReplyButton ? 'clickable' : ''}`}
40
  style={{
41
  backgroundColor: colors.bgColor,
42
+ borderColor: colors.color + '40'
43
  }}
44
+ onClick={onClick}
45
  >
46
  <div className="advisor-message-header">
47
  <h4
 
49
  style={{ color: colors.color }}
50
  >
51
  {advisor.name}
52
+ {message.isReply && <span className="reply-badge">↳ Reply</span>}
53
  </h4>
54
  <span
55
  className="message-time"
 
72
  >
73
  {message.content}
74
  </p>
75
+ {showReplyButton && (
76
+ <div className="message-actions">
77
+ <button
78
+ className="reply-button"
79
+ onClick={(e) => {
80
+ e.stopPropagation();
81
+ onClick();
82
+ }}
83
+ style={{
84
+ color: colors.color,
85
+ borderColor: colors.color + '40'
86
+ }}
87
+ >
88
+ <Reply size={14} />
89
+ <span>Reply</span>
90
+ </button>
91
+ </div>
92
+ )}
93
  </div>
94
  </div>
95
  );
phd-advisor-frontend/src/pages/ChatPage.js CHANGED
@@ -1,5 +1,5 @@
1
  import React, { useState, useEffect, useRef } from 'react';
2
- import { Home, MessageCircle } from 'lucide-react';
3
  import ChatInput from '../components/ChatInput';
4
  import MessageBubble from '../components/MessageBubble';
5
  import ThinkingIndicator from '../components/ThinkingIndicator';
@@ -13,6 +13,7 @@ const ChatPage = ({ onNavigateToHome }) => {
13
  const [isLoading, setIsLoading] = useState(false);
14
  const [thinkingAdvisors, setThinkingAdvisors] = useState([]);
15
  const [collectedInfo, setCollectedInfo] = useState({});
 
16
  const messagesEndRef = useRef(null);
17
  const { isDark } = useTheme();
18
 
@@ -24,9 +25,20 @@ const ChatPage = ({ onNavigateToHome }) => {
24
  scrollToBottom();
25
  }, [messages, thinkingAdvisors]);
26
 
 
 
 
 
27
  const handleSendMessage = async (inputMessage) => {
28
- // Add user message immediately
 
 
 
 
 
 
29
  const userMessage = {
 
30
  type: 'user',
31
  content: inputMessage,
32
  timestamp: new Date()
@@ -37,7 +49,7 @@ const ChatPage = ({ onNavigateToHome }) => {
37
  setThinkingAdvisors(['system']); // Show thinking indicator for orchestrator/system
38
 
39
  try {
40
- const response = await fetch('http://localhost:8000/chat', {
41
  method: 'POST',
42
  headers: {
43
  'Content-Type': 'application/json',
@@ -62,59 +74,23 @@ const ChatPage = ({ onNavigateToHome }) => {
62
  if (data.type === 'orchestrator_question') {
63
  // Orchestrator is asking for clarification
64
  const orchestratorMessage = {
 
65
  type: 'orchestrator',
66
  content: data.responses[0].response,
67
  timestamp: new Date()
68
  };
69
  setMessages(prev => [...prev, orchestratorMessage]);
70
 
71
- } else if (data.type === 'advisor_responses') {
72
- // Show thinking indicators for advisors before their responses
73
- setThinkingAdvisors(['methodist', 'theorist', 'pragmatist']);
74
-
75
- // Add a small delay then show advisor responses
76
- setTimeout(() => {
77
- setThinkingAdvisors([]);
78
-
79
- // Add advisor responses with staggered timing
80
- data.responses.forEach((advisorResponse, index) => {
81
- setTimeout(() => {
82
- let advisorId = 'methodist';
83
-
84
- if (advisorResponse.persona.toLowerCase().includes('methodist')) {
85
- advisorId = 'methodist';
86
- } else if (advisorResponse.persona.toLowerCase().includes('theorist')) {
87
- advisorId = 'theorist';
88
- } else if (advisorResponse.persona.toLowerCase().includes('pragmatist')) {
89
- advisorId = 'pragmatist';
90
- }
91
-
92
- const message = {
93
- type: 'advisor',
94
- advisorId,
95
- content: advisorResponse.response,
96
- timestamp: new Date()
97
- };
98
-
99
- setMessages(prev => [...prev, message]);
100
- }, index * 800);
101
- });
102
- }, 1000); // Small delay to show advisor thinking
103
-
104
- } else if (data.type === 'error') {
105
- // Handle error response
106
- const errorMessage = {
107
- type: 'error',
108
- content: data.responses[0].response,
109
- timestamp: new Date()
110
- };
111
- setMessages(prev => [...prev, errorMessage]);
112
  }
113
 
114
  } catch (error) {
115
  console.error('Error sending message:', error);
116
  setThinkingAdvisors([]);
117
  setMessages(prev => [...prev, {
 
118
  type: 'error',
119
  content: 'Sorry, there was an error processing your message. Please try again.',
120
  timestamp: new Date()
@@ -124,8 +100,118 @@ const ChatPage = ({ onNavigateToHome }) => {
124
  }
125
  };
126
 
127
- const handleSuggestionClick = (suggestion) => {
128
- handleSendMessage(suggestion);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  };
130
 
131
  return (
@@ -145,7 +231,9 @@ const ChatPage = ({ onNavigateToHome }) => {
145
  <p className="chat-subtitle">
146
  {Object.keys(collectedInfo).length > 0
147
  ? `Context: ${Object.entries(collectedInfo).map(([k,v]) => `${k}: ${v}`).join(', ')}`
148
- : 'Consulting with personas'
 
 
149
  }
150
  </p>
151
  </div>
@@ -176,14 +264,14 @@ const ChatPage = ({ onNavigateToHome }) => {
176
  <div className="chat-box">
177
  {/* Messages */}
178
  <div className="messages-container">
179
- {messages.length === 0 && (
180
- <SuggestionsPanel onSuggestionClick={handleSuggestionClick} />
181
  )}
182
 
183
  {messages.map((message, index) => {
184
  if (message.type === 'orchestrator') {
185
  return (
186
- <div key={index} className="advisor-message-container">
187
  <div className="advisor-avatar orchestrator-avatar">
188
  <MessageCircle className="orchestrator-icon" />
189
  </div>
@@ -204,7 +292,14 @@ const ChatPage = ({ onNavigateToHome }) => {
204
  </div>
205
  );
206
  }
207
- return <MessageBubble key={index} message={message} />;
 
 
 
 
 
 
 
208
  })}
209
 
210
  {/* Thinking Indicators */}
@@ -236,8 +331,29 @@ const ChatPage = ({ onNavigateToHome }) => {
236
  <div ref={messagesEndRef} />
237
  </div>
238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  {/* Input Area */}
240
- <ChatInput onSendMessage={handleSendMessage} isLoading={isLoading} />
 
 
 
 
 
 
 
 
241
  </div>
242
  </div>
243
  </div>
 
1
  import React, { useState, useEffect, useRef } from 'react';
2
+ import { Home, MessageCircle, Reply, X } from 'lucide-react';
3
  import ChatInput from '../components/ChatInput';
4
  import MessageBubble from '../components/MessageBubble';
5
  import ThinkingIndicator from '../components/ThinkingIndicator';
 
13
  const [isLoading, setIsLoading] = useState(false);
14
  const [thinkingAdvisors, setThinkingAdvisors] = useState([]);
15
  const [collectedInfo, setCollectedInfo] = useState({});
16
+ const [replyingTo, setReplyingTo] = useState(null); // { advisorId, messageId, advisorName }
17
  const messagesEndRef = useRef(null);
18
  const { isDark } = useTheme();
19
 
 
25
  scrollToBottom();
26
  }, [messages, thinkingAdvisors]);
27
 
28
+ const generateMessageId = () => {
29
+ return Date.now().toString() + Math.random().toString(36).substr(2, 9);
30
+ };
31
+
32
  const handleSendMessage = async (inputMessage) => {
33
+ // Check if this is a reply to a specific advisor
34
+ if (replyingTo) {
35
+ await handleReplyToAdvisor(inputMessage, replyingTo);
36
+ return;
37
+ }
38
+
39
+ // Regular message flow - add user message immediately
40
  const userMessage = {
41
+ id: generateMessageId(),
42
  type: 'user',
43
  content: inputMessage,
44
  timestamp: new Date()
 
49
  setThinkingAdvisors(['system']); // Show thinking indicator for orchestrator/system
50
 
51
  try {
52
+ const response = await fetch('http://localhost:8000/chat-sequential', {
53
  method: 'POST',
54
  headers: {
55
  'Content-Type': 'application/json',
 
74
  if (data.type === 'orchestrator_question') {
75
  // Orchestrator is asking for clarification
76
  const orchestratorMessage = {
77
+ id: generateMessageId(),
78
  type: 'orchestrator',
79
  content: data.responses[0].response,
80
  timestamp: new Date()
81
  };
82
  setMessages(prev => [...prev, orchestratorMessage]);
83
 
84
+ } else if (data.type === 'sequential_responses') {
85
+ // Show advisor responses sequentially with realistic timing
86
+ await showSequentialResponses(data.responses);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  }
88
 
89
  } catch (error) {
90
  console.error('Error sending message:', error);
91
  setThinkingAdvisors([]);
92
  setMessages(prev => [...prev, {
93
+ id: generateMessageId(),
94
  type: 'error',
95
  content: 'Sorry, there was an error processing your message. Please try again.',
96
  timestamp: new Date()
 
100
  }
101
  };
102
 
103
+ const showSequentialResponses = async (responses) => {
104
+ const advisorOrder = ['methodist', 'theorist', 'pragmatist'];
105
+
106
+ for (let i = 0; i < advisorOrder.length; i++) {
107
+ const advisorId = advisorOrder[i];
108
+
109
+ // Show thinking indicator for this specific advisor
110
+ setThinkingAdvisors([advisorId]);
111
+
112
+ // Wait a bit to simulate thinking time (since responses are pre-generated)
113
+ await new Promise(resolve => setTimeout(resolve, 800));
114
+
115
+ // Find the response for this advisor
116
+ const advisorResponse = responses.find(r => r.persona_id === advisorId);
117
+
118
+ if (advisorResponse) {
119
+ // Remove thinking indicator and add message
120
+ setThinkingAdvisors([]);
121
+
122
+ const message = {
123
+ id: generateMessageId(),
124
+ type: 'advisor',
125
+ advisorId: advisorResponse.persona_id,
126
+ content: advisorResponse.response,
127
+ timestamp: new Date()
128
+ };
129
+
130
+ setMessages(prev => [...prev, message]);
131
+
132
+ // Small delay before next advisor starts thinking
133
+ if (i < advisorOrder.length - 1) {
134
+ await new Promise(resolve => setTimeout(resolve, 300));
135
+ }
136
+ }
137
+ }
138
+
139
+ // Clear any remaining thinking indicators
140
+ setThinkingAdvisors([]);
141
+ };
142
+
143
+ const handleReplyToAdvisor = async (inputMessage, replyInfo) => {
144
+ // Add user reply message
145
+ const userReplyMessage = {
146
+ id: generateMessageId(),
147
+ type: 'user',
148
+ content: inputMessage,
149
+ timestamp: new Date(),
150
+ replyTo: replyInfo
151
+ };
152
+ setMessages(prev => [...prev, userReplyMessage]);
153
+
154
+ // Show thinking for the specific advisor
155
+ setThinkingAdvisors([replyInfo.advisorId]);
156
+ setReplyingTo(null); // Clear reply state
157
+
158
+ try {
159
+ const response = await fetch('http://localhost:8000/reply-to-advisor', {
160
+ method: 'POST',
161
+ headers: {
162
+ 'Content-Type': 'application/json',
163
+ },
164
+ body: JSON.stringify({
165
+ user_input: inputMessage,
166
+ advisor_id: replyInfo.advisorId,
167
+ original_message_id: replyInfo.messageId
168
+ }),
169
+ });
170
+
171
+ if (!response.ok) {
172
+ throw new Error('Failed to send reply');
173
+ }
174
+
175
+ const data = await response.json();
176
+ setThinkingAdvisors([]);
177
+
178
+ // Add advisor reply
179
+ const advisorReplyMessage = {
180
+ id: generateMessageId(),
181
+ type: 'advisor',
182
+ advisorId: data.persona_id,
183
+ content: data.response,
184
+ timestamp: new Date(),
185
+ isReply: true
186
+ };
187
+
188
+ setMessages(prev => [...prev, advisorReplyMessage]);
189
+
190
+ } catch (error) {
191
+ console.error('Error sending reply:', error);
192
+ setThinkingAdvisors([]);
193
+ setMessages(prev => [...prev, {
194
+ id: generateMessageId(),
195
+ type: 'error',
196
+ content: 'Sorry, there was an error sending your reply. Please try again.',
197
+ timestamp: new Date()
198
+ }]);
199
+ }
200
+ };
201
+
202
+ const handleMessageClick = (message) => {
203
+ if (message.type === 'advisor') {
204
+ const advisor = advisors[message.advisorId];
205
+ setReplyingTo({
206
+ advisorId: message.advisorId,
207
+ messageId: message.id,
208
+ advisorName: advisor.name
209
+ });
210
+ }
211
+ };
212
+
213
+ const cancelReply = () => {
214
+ setReplyingTo(null);
215
  };
216
 
217
  return (
 
231
  <p className="chat-subtitle">
232
  {Object.keys(collectedInfo).length > 0
233
  ? `Context: ${Object.entries(collectedInfo).map(([k,v]) => `${k}: ${v}`).join(', ')}`
234
+ : replyingTo
235
+ ? `Replying to ${replyingTo.advisorName}`
236
+ : 'Consulting with your three advisors'
237
  }
238
  </p>
239
  </div>
 
264
  <div className="chat-box">
265
  {/* Messages */}
266
  <div className="messages-container">
267
+ {messages.length === 0 && !replyingTo && (
268
+ <SuggestionsPanel onSuggestionClick={handleSendMessage} />
269
  )}
270
 
271
  {messages.map((message, index) => {
272
  if (message.type === 'orchestrator') {
273
  return (
274
+ <div key={message.id || index} className="advisor-message-container">
275
  <div className="advisor-avatar orchestrator-avatar">
276
  <MessageCircle className="orchestrator-icon" />
277
  </div>
 
292
  </div>
293
  );
294
  }
295
+ return (
296
+ <MessageBubble
297
+ key={message.id || index}
298
+ message={message}
299
+ onClick={() => handleMessageClick(message)}
300
+ showReplyButton={message.type === 'advisor'}
301
+ />
302
+ );
303
  })}
304
 
305
  {/* Thinking Indicators */}
 
331
  <div ref={messagesEndRef} />
332
  </div>
333
 
334
+ {/* Reply Banner */}
335
+ {replyingTo && (
336
+ <div className="reply-banner">
337
+ <div className="reply-info">
338
+ <Reply className="reply-icon" />
339
+ <span>Replying to {replyingTo.advisorName}</span>
340
+ </div>
341
+ <button onClick={cancelReply} className="cancel-reply-btn">
342
+ <X size={16} />
343
+ </button>
344
+ </div>
345
+ )}
346
+
347
  {/* Input Area */}
348
+ <ChatInput
349
+ onSendMessage={handleSendMessage}
350
+ isLoading={isLoading}
351
+ placeholder={
352
+ replyingTo
353
+ ? `Reply to ${replyingTo.advisorName}...`
354
+ : "Ask your advisors anything about your PhD journey..."
355
+ }
356
+ />
357
  </div>
358
  </div>
359
  </div>
phd-advisor-frontend/src/styles/components.css CHANGED
@@ -568,6 +568,99 @@
568
  transform: translateY(0);
569
  }
570
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
  /* Orchestrator/PhD Assistant Styles */
572
  .orchestrator-avatar {
573
  background-color: var(--bg-tertiary) !important;
 
568
  transform: translateY(0);
569
  }
570
 
571
+ /* Reply Functionality Styles */
572
+ .reply-banner {
573
+ background-color: var(--bg-tertiary);
574
+ border-top: 1px solid var(--border-primary);
575
+ padding: 12px 16px;
576
+ display: flex;
577
+ align-items: center;
578
+ justify-content: space-between;
579
+ }
580
+
581
+ .reply-info {
582
+ display: flex;
583
+ align-items: center;
584
+ gap: 8px;
585
+ color: var(--text-secondary);
586
+ font-size: 14px;
587
+ }
588
+
589
+ .reply-icon {
590
+ width: 16px;
591
+ height: 16px;
592
+ color: var(--accent-primary);
593
+ }
594
+
595
+ .cancel-reply-btn {
596
+ background: transparent;
597
+ border: none;
598
+ color: var(--text-secondary);
599
+ cursor: pointer;
600
+ padding: 4px;
601
+ border-radius: 4px;
602
+ transition: background-color 0.2s ease;
603
+ }
604
+
605
+ .cancel-reply-btn:hover {
606
+ background-color: var(--bg-primary);
607
+ color: var(--text-primary);
608
+ }
609
+
610
+ .reply-indicator {
611
+ display: flex;
612
+ align-items: center;
613
+ gap: 4px;
614
+ font-size: 12px;
615
+ opacity: 0.8;
616
+ margin-bottom: 4px;
617
+ }
618
+
619
+ .reply-badge {
620
+ font-size: 11px;
621
+ opacity: 0.7;
622
+ margin-left: 8px;
623
+ }
624
+
625
+ .advisor-message-bubble.clickable {
626
+ cursor: pointer;
627
+ transition: all 0.2s ease;
628
+ position: relative;
629
+ }
630
+
631
+ .advisor-message-bubble.clickable:hover {
632
+ transform: translateY(-1px);
633
+ box-shadow: var(--shadow-md);
634
+ }
635
+
636
+ .message-actions {
637
+ margin-top: 8px;
638
+ opacity: 0;
639
+ transition: opacity 0.2s ease;
640
+ }
641
+
642
+ .advisor-message-bubble.clickable:hover .message-actions {
643
+ opacity: 1;
644
+ }
645
+
646
+ .reply-button {
647
+ background: transparent;
648
+ border: 1px solid;
649
+ border-radius: 6px;
650
+ padding: 4px 8px;
651
+ font-size: 12px;
652
+ cursor: pointer;
653
+ display: flex;
654
+ align-items: center;
655
+ gap: 4px;
656
+ transition: all 0.2s ease;
657
+ }
658
+
659
+ .reply-button:hover {
660
+ background-color: var(--bg-tertiary);
661
+ transform: translateY(-1px);
662
+ }
663
+
664
  /* Orchestrator/PhD Assistant Styles */
665
  .orchestrator-avatar {
666
  background-color: var(--bg-tertiary) !important;