Spaces:
Running
Running
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=
|
| 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=
|
| 33 |
style={{
|
| 34 |
backgroundColor: colors.bgColor,
|
| 35 |
-
borderColor: colors.color + '40'
|
| 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 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 === '
|
| 72 |
-
// Show
|
| 73 |
-
|
| 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
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
:
|
|
|
|
|
|
|
| 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={
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
})}
|
| 209 |
|
| 210 |
{/* Thinking Indicators */}
|
|
@@ -236,8 +331,29 @@ const ChatPage = ({ onNavigateToHome }) => {
|
|
| 236 |
<div ref={messagesEndRef} />
|
| 237 |
</div>
|
| 238 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
{/* Input Area */}
|
| 240 |
-
<ChatInput
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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;
|