NeonClary
Defer thinking indicators until backend reports selected advisors
8924405
Raw
History Blame Contribute Delete
37.4 kB
import React, { useState, useEffect, useRef, useMemo } from 'react';
import { MessageCircle, Reply, X, Sparkles, Users, Settings2, FileText, HelpCircle } from 'lucide-react';
import EnhancedChatInput from '../components/EnhancedChatInput';
import MessageBubble from '../components/MessageBubble';
import ThinkingIndicator from '../components/ThinkingIndicator';
import SuggestionsPanel from '../components/SuggestionsPanel';
import AppHeader from '../components/AppHeader';
import AdvisorStatusDropdown from '../components/AdvisorStatusDropdown';
import ExportButton from '../components/ExportButton';
import Sidebar from '../components/Sidebar';
import { useAppConfig } from '../contexts/AppConfigContext';
import { useTheme } from '../contexts/ThemeContext';
import '../styles/ChatPage.css';
import '../styles/EnhancedChatInput.css';
import AdvisorCarousel from '../components/AdvisorCarousel';
import OnboardingChat from '../components/OnboardingChat';
import ProfileWalkthrough from '../components/ProfileWalkthrough';
import ClearDataModal from '../components/ClearDataModal';
import AccountModal from '../components/AccountModal';
const ACTIVE_ADVISORS_STORAGE_KEY = 'cybersecurityActiveAdvisorIds';
const ChatPage = ({ user, authToken, onNavigateToHome, onNavigateToCanvas, onSignOut }) => {
const { config, advisors, getAdvisorColors } = useAppConfig();
const [messages, setMessages] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [thinkingAdvisors, setThinkingAdvisors] = useState([]);
const [activeAdvisorIds, setActiveAdvisorIds] = useState([]);
const [collectedInfo, setCollectedInfo] = useState({});
const [replyingTo, setReplyingTo] = useState(null);
const [currentProvider, setCurrentProvider] = useState('gemini');
const [isProviderSwitching, setIsProviderSwitching] = useState(false);
const [uploadedDocuments, setUploadedDocuments] = useState([]);
const messagesEndRef = useRef(null);
const { isDark } = useTheme();
const [currentSessionId, setCurrentSessionId] = useState(null);
const [currentSessionTitle, setCurrentSessionTitle] = useState('');
const [isSavingSession, setIsSavingSession] = useState(false);
const [isLoadingSession, setIsLoadingSession] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [sidebarRefreshTrigger, setSidebarRefreshTrigger] = useState(0);
const [userAvatarId, setUserAvatarId] = useState(
() => localStorage.getItem('userAvatarId') || (user?.avatarId ?? null)
);
const avatarOptions = config?.app?.user_avatars || [];
const handleAvatarChange = async (id) => {
setUserAvatarId(id);
localStorage.setItem('userAvatarId', id);
try {
await fetch(`${process.env.REACT_APP_API_URL}/auth/me`, {
method: 'PATCH',
headers: { Authorization: `Bearer ${authToken}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ avatarId: id }),
});
} catch (e) {
console.error('Failed to save avatar:', e);
}
};
const [showOnboarding, setShowOnboarding] = useState(false);
const [showProfileForm, setShowProfileForm] = useState(false);
const [showClearData, setShowClearData] = useState(false);
const [showAccount, setShowAccount] = useState(false);
const [userProfile, setUserProfile] = useState(null);
const loadProfile = async () => {
try {
const resp = await fetch(`${process.env.REACT_APP_API_URL}/api/users/me/profile`, {
headers: { Authorization: `Bearer ${authToken}` },
});
if (resp.ok) setUserProfile(await resp.json());
} catch (e) {
/* ignore */
}
};
useEffect(() => {
if (authToken) loadProfile();
}, [authToken]);
useEffect(() => {
const allIds = Object.keys(advisors || {});
if (allIds.length === 0) return;
setActiveAdvisorIds((prev) => {
let next = prev.filter((id) => allIds.includes(id));
if (next.length === 0) {
try {
const stored = JSON.parse(localStorage.getItem(ACTIVE_ADVISORS_STORAGE_KEY) || 'null');
if (Array.isArray(stored)) {
const valid = stored.filter((id) => allIds.includes(id));
if (valid.length > 0) next = valid;
}
} catch {
/* ignore */
}
}
if (next.length === 0) next = [...allIds];
if (
prev.length === next.length &&
prev.every((id, index) => id === next[index])
) {
return prev;
}
return next;
});
}, [advisors]);
const persistActiveAdvisorIds = (ids) => {
localStorage.setItem(ACTIVE_ADVISORS_STORAGE_KEY, JSON.stringify(ids));
};
const handleSetActiveAdvisors = (ids) => {
setActiveAdvisorIds(ids);
persistActiveAdvisorIds(ids);
};
const handleToggleAdvisor = (id) => {
setActiveAdvisorIds((prev) => {
const set = new Set(prev);
if (set.has(id)) {
if (set.size <= 1) return prev;
set.delete(id);
} else {
set.add(id);
}
const next = Array.from(set);
persistActiveAdvisorIds(next);
return next;
});
};
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
const handleMobileMenuToggle = () => {
setIsMobileMenuOpen(!isMobileMenuOpen);
};
useEffect(() => {
scrollToBottom();
}, [messages, thinkingAdvisors]);
useEffect(() => {
fetchCurrentProvider();
}, []);
const fetchCurrentProvider = async () => {
try {
const response = await fetch(`${process.env.REACT_APP_API_URL}/current-provider`);
if (response.ok) {
const data = await response.json();
setCurrentProvider(data.current_provider);
console.log('Loaded provider:', data.current_provider, 'Available:', data.available_providers);
}
} catch (error) {
console.error('Error fetching current provider:', error);
}
};
const handleProviderSwitch = async (newProvider) => {
if (newProvider === currentProvider || isProviderSwitching) return;
setIsProviderSwitching(true);
try {
const response = await fetch(`${process.env.REACT_APP_API_URL}/switch-provider`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
provider: newProvider
}),
});
if (response.ok) {
const data = await response.json();
setCurrentProvider(newProvider);
const switchMessage = {
id: generateMessageId(),
type: 'system',
content: `✨ Switched to ${newProvider.charAt(0).toUpperCase() + newProvider.slice(1)} provider. Your advisors are now ready with the new AI model.`,
timestamp: new Date()
};
setMessages(prev => [...prev, switchMessage]);
} else {
const error = await response.json();
console.error('Failed to switch provider:', error);
const errorMessage = {
id: generateMessageId(),
type: 'error',
content: `Failed to switch to ${newProvider}: ${error.detail || 'Unknown error'}`,
timestamp: new Date()
};
setMessages(prev => [...prev, errorMessage]);
}
} catch (error) {
console.error('Error switching provider:', error);
const errorMessage = {
id: generateMessageId(),
type: 'error',
content: `Error switching to ${newProvider}. Please try again.`,
timestamp: new Date()
};
setMessages(prev => [...prev, errorMessage]);
} finally {
setIsProviderSwitching(false);
}
};
const generateMessageId = () => {
return Date.now().toString() + Math.random().toString(36).substr(2, 9);
};
const createNewSession = async (firstMessage = null) => {
try {
const title = firstMessage
? `${firstMessage.substring(0, 30)}...`
: `Chat ${new Date().toLocaleDateString()}`;
const response = await fetch(`${process.env.REACT_APP_API_URL}/api/chat-sessions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ title })
});
if (response.ok) {
const newSession = await response.json();
// Update state immediately
setCurrentSessionId(newSession.id);
setCurrentSessionTitle(newSession.title);
console.log('MongoDB session created:', newSession.id);
return newSession.id;
} else {
console.error('Failed to create new session');
return null;
}
} catch (error) {
console.error('Error creating new session:', error);
return null;
}
};
// Load an existing chat session
const loadChatSession = async (sessionId) => {
if (!sessionId || isLoadingSession) return;
setIsLoadingSession(true);
try {
// Use the new switch-chat endpoint that syncs context
const response = await fetch(`${process.env.REACT_APP_API_URL}/switch-chat`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
chat_session_id: sessionId
})
});
if (response.ok) {
const result = await response.json();
if (result.status === 'success') {
setCurrentSessionId(sessionId);
setCurrentSessionTitle(''); // Will be set from MongoDB data
// Load the messages from the synced context
const formattedMessages = result.context.messages.map(msg => ({
...msg,
timestamp: new Date(msg.timestamp),
persona_id: msg.persona_id || msg.advisor || msg.advisorId
}));
setMessages(formattedMessages);
setReplyingTo(null);
setThinkingAdvisors([]);
// Also get the session title from MongoDB
const sessionResponse = await fetch(`${process.env.REACT_APP_API_URL}/api/chat-sessions/${sessionId}`, {
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
});
if (sessionResponse.ok) {
const sessionData = await sessionResponse.json();
setCurrentSessionTitle(sessionData.title);
}
}
}
} catch (error) {
console.error('Error loading session:', error);
} finally {
setIsLoadingSession(false);
}
};
// Save a message to the current session (optional sessionId when state is not updated yet)
const saveMessageToSession = async (message, sessionIdOverride) => {
const sid = sessionIdOverride || currentSessionId;
if (!sid || !authToken) return;
try {
await fetch(`${process.env.REACT_APP_API_URL}/api/chat-sessions/${sid}/messages`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
session_id: sid,
message: {
...message,
timestamp: message.timestamp.toISOString()
}
})
});
} catch (error) {
console.error('Error saving message to session:', error);
}
};
// Update session title based on first message
const updateSessionTitle = async (sessionId, newTitle) => {
if (!sessionId || !authToken) return;
try {
await fetch(`${process.env.REACT_APP_API_URL}/api/chat-sessions/${sessionId}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ title: newTitle })
});
setCurrentSessionTitle(newTitle);
} catch (error) {
console.error('Error updating session title:', error);
}
};
// Handle selecting a session from sidebar
const handleSelectSession = async (sessionId) => {
if (sessionId === currentSessionId) return;
await loadChatSession(sessionId);
};
// Sidebar deleted the currently-active chat. Clear local state without
// creating a replacement session.
const handleCurrentSessionDeleted = () => {
setCurrentSessionId(null);
setCurrentSessionTitle('');
setMessages([]);
setReplyingTo(null);
setThinkingAdvisors([]);
setUploadedDocuments([]);
};
// Handle creating new chat from sidebar
const handleNewChat = async (sessionId = null) => {
if (sessionId) {
// Loading existing session
await loadChatSession(sessionId);
return; // Return early for existing session loading
} else {
// Creating completely new chat with fresh context
try {
// Step 1: Reset memory session
const response = await fetch(`${process.env.REACT_APP_API_URL}/new-chat`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: `Chat ${new Date().toLocaleDateString()}`
})
});
if (response.ok) {
const result = await response.json();
if (result.status === 'success') {
// Step 2: Immediately create MongoDB session
const newSessionId = await createNewSession(`Chat ${new Date().toLocaleDateString()}`);
if (newSessionId) {
// Reset all state to fresh with the new session
setMessages([]);
setCurrentSessionId(newSessionId); // Set the new session ID immediately
setCurrentSessionTitle(`Chat ${new Date().toLocaleDateString()}`);
setReplyingTo(null);
setThinkingAdvisors([]);
setUploadedDocuments([]);
console.log('New chat created with MongoDB session:', newSessionId);
// Wait a bit to ensure state has updated
await new Promise(resolve => setTimeout(resolve, 100));
return newSessionId; // Return the session ID for the sidebar
} else {
throw new Error('Failed to create MongoDB session');
}
} else {
throw new Error('Failed to create memory session');
}
} else {
throw new Error(`HTTP error: ${response.status}`);
}
} catch (error) {
console.error('Error creating new chat:', error);
// Fallback to local reset
setMessages([]);
setCurrentSessionId(null);
setCurrentSessionTitle('');
setReplyingTo(null);
setThinkingAdvisors([]);
setUploadedDocuments([]);
// Re-throw the error so the sidebar knows something went wrong
throw error;
}
}
};
const handleFileUploaded = async (file, uploadResult) => {
// FIXED: Use the upload result data for better messaging
const documentMessage = {
id: generateMessageId(),
type: 'document_upload',
content: `Document uploaded: ${uploadResult.filename || file.name} (${uploadResult.chunks_created || 0} sections processed)`,
timestamp: new Date()
};
setMessages(prev => [...prev, documentMessage]);
setUploadedDocuments(prev => [...prev, file]);
// FIXED: Log document access info
console.log('File uploaded to session:', {
filename: uploadResult.filename,
session_id: uploadResult.session_id,
chat_session_id: uploadResult.chat_session_id,
current_session_id: currentSessionId
});
// Save document upload message to database if we have a current session
if (currentSessionId) {
await saveMessageToSession(documentMessage);
}
};
const handleSendMessage = async (inputMessage) => {
if (!inputMessage.trim()) return;
// Create user message
const userMessage = {
id: generateMessageId(),
type: 'user',
content: inputMessage,
timestamp: new Date()
};
// Add to local state immediately
setMessages(prev => [...prev, userMessage]);
// Create new session if we don't have one
let sessionId = currentSessionId;
if (!sessionId) {
sessionId = await createNewSession(inputMessage);
if (!sessionId) {
console.error('Failed to create session');
return;
}
}
saveMessageToSession(userMessage, sessionId).catch(err =>
console.error('Failed to persist user message:', err)
);
// Update session title if this is the first message and title is generic
if (messages.length === 0 && currentSessionTitle.includes('Chat ')) {
const newTitle = inputMessage.length > 30
? `${inputMessage.substring(0, 30)}...`
: inputMessage;
await updateSessionTitle(sessionId, newTitle);
}
// Set loading state
setIsLoading(true);
const advisorsForRequest = activeAdvisorIds.length > 0
? activeAdvisorIds
: Object.keys(advisors || {});
// Start with just the orchestrator's thinking bubble. The backend will
// emit a `progress { phase: 'selected', selected_advisors: [...] }` event
// naming the 3 advisors it picked, and that handler will add their
// ThinkingIndicators. This prevents the brief flash of thinking indicators
// for every advisor in the active pool before ranking has run.
setThinkingAdvisors(['system']);
try {
const response = await fetch(`${process.env.REACT_APP_API_URL}/chat-stream`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`,
},
body: JSON.stringify({
user_input: inputMessage,
response_length: 'medium',
chat_session_id: sessionId,
active_advisors: advisorsForRequest,
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() ?? '';
for (const line of lines) {
if (!line.trim()) continue;
const payload = JSON.parse(line);
const d = payload.data || {};
switch (payload.type) {
case 'advisor': {
const msg = {
id: generateMessageId(),
type: 'advisor',
persona_id: d.persona_id,
content: d.content,
timestamp: new Date(),
advisorName: d.persona_name || d.persona_id,
used_documents: d.used_documents || false,
document_chunks_used: d.document_chunks_used || 0,
};
setMessages(prev => [...prev, msg]);
setThinkingAdvisors(prev => prev.filter(a => a !== d.persona_id));
await saveMessageToSession(msg, sessionId);
break;
}
case 'clarification':
setMessages(prev => [...prev, {
id: generateMessageId(),
type: 'clarification',
content: d.message,
suggestions: d.suggestions || [],
timestamp: new Date(),
}]);
break;
case 'progress':
if (d.phase === 'selected' && Array.isArray(d.selected_advisors)) {
setThinkingAdvisors(prev => {
const next = new Set(prev);
next.add('system');
d.selected_advisors.forEach(id => next.add(id));
return Array.from(next);
});
break;
}
if (d.phase === 'complete') {
break;
}
if (d.persona_id != null) {
setThinkingAdvisors(prev => prev.filter(a => a !== d.persona_id));
}
break;
case 'error':
setMessages(prev => [...prev, {
id: generateMessageId(),
type: 'error',
content: d.detail || 'An error occurred',
timestamp: new Date(),
}]);
break;
default:
break;
}
}
}
} catch (error) {
console.error('Error sending message:', error);
setMessages(prev => [...prev, {
id: generateMessageId(),
type: 'error',
content: `Failed to send message: ${error.message}`,
timestamp: new Date()
}]);
} finally {
setIsLoading(false);
setThinkingAdvisors([]);
setSidebarRefreshTrigger(prev => prev + 1);
}
};
const handleReplyToAdvisor = async (inputMessage, replyContext) => {
// Ensure we have a session before proceeding
let sessionId = currentSessionId;
if (!sessionId) {
sessionId = await createNewSession(inputMessage);
if (!sessionId) {
console.error('Failed to create session for reply');
return;
}
}
const replyMessage = {
id: generateMessageId(),
type: 'user',
content: inputMessage,
replyTo: {
advisorId: replyContext.persona_id,
advisorName: replyContext.advisorName,
messageId: replyContext.messageId
},
timestamp: new Date()
};
setMessages(prev => [...prev, replyMessage]);
// Save reply message to database with explicit session ID
await saveMessageToSession(replyMessage, sessionId);
setIsLoading(true);
setThinkingAdvisors([replyContext.persona_id]);
try {
const response = await fetch(`${process.env.REACT_APP_API_URL}/reply-to-advisor`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
user_input: inputMessage,
advisor_id: replyContext.advisorId,
original_message_id: replyContext.messageId,
chat_session_id: sessionId // Use confirmed session ID
}),
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
if (data.type === 'advisor_reply') {
const replyResponseMessage = {
id: generateMessageId(),
type: 'advisor',
persona_id: data.persona_id,
advisorName: data.persona,
content: data.response,
isReply: true,
timestamp: new Date()
};
setMessages(prev => [...prev, replyResponseMessage]);
// Save advisor reply to database
await saveMessageToSession(replyResponseMessage, sessionId);
}
} catch (error) {
console.error('Error replying to advisor:', error);
const errorMessage = {
id: generateMessageId(),
type: 'error',
content: 'Sorry, I encountered an error with your reply. Please try again.',
timestamp: new Date()
};
setMessages(prev => [...prev, errorMessage]);
// Save error message to database
await saveMessageToSession(errorMessage, sessionId);
}
setIsLoading(false);
setThinkingAdvisors([]);
};
const handleCopyMessage = (messageId, content) => {
// Optional: Show a toast notification or add to message history
console.log(`Copied message ${messageId}: ${content.substring(0, 50)}...`);
};
const handleExpandMessage = async (messageId, advisorId) => {
const advisor = advisors[advisorId];
if (!advisor) return;
const originalMessage = messages.find(msg => msg.id === messageId);
if (!originalMessage) return;
const expandPrompt = `Please expand on your previous response: "${originalMessage.content.substring(0, 100)}..." Provide more detail and depth.`;
const expandMessage = {
id: generateMessageId(),
type: 'user',
content: expandPrompt,
timestamp: new Date(),
isExpandRequest: true,
expandsMessageId: messageId
};
setMessages(prev => [...prev, expandMessage]);
// Save expand request to database
await saveMessageToSession(expandMessage);
setIsLoading(true);
setThinkingAdvisors([advisorId]);
try {
const response = await fetch(`${process.env.REACT_APP_API_URL}/chat/${advisorId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
user_input: expandPrompt,
response_length: 'long'
}),
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
if (data.persona && data.response) {
const expandedMessage = {
id: generateMessageId(),
type: 'advisor',
persona_id: advisorId,
advisorName: advisor.name,
content: data.response,
isExpansion: true,
expandsMessageId: messageId,
timestamp: new Date()
};
setMessages(prev => [...prev, expandedMessage]);
// Save expanded response to database
await saveMessageToSession(expandedMessage);
} else {
const errorMessage = {
id: generateMessageId(),
type: 'error',
content: 'Sorry, I received an unexpected response format. Please try again.',
timestamp: new Date()
};
setMessages(prev => [...prev, errorMessage]);
// Save error message to database
await saveMessageToSession(errorMessage);
}
} catch (error) {
console.error('Error expanding message:', error);
const errorMessage = {
id: generateMessageId(),
type: 'error',
content: 'Sorry, I encountered an error while expanding the message. Please try again.',
timestamp: new Date()
};
setMessages(prev => [...prev, errorMessage]);
// Save error message to database
await saveMessageToSession(errorMessage);
}
setIsLoading(false);
setThinkingAdvisors([]);
};
const handleReplyToMessage = (message) => {
const advisor = advisors[message.persona_id];
setReplyingTo({
advisorId: message.persona_id,
messageId: message.id,
advisorName: advisor?.name || message.advisorName || 'Advisor',
persona_id: message.persona_id
});
};
const handleMessageClick = (message) => {
if (message.type === 'advisor') {
const advisor = advisors[message.persona_id];
setReplyingTo({
advisorId: message.persona_id,
messageId: message.id,
advisorName: advisor?.name || message.advisorName || 'Advisor',
persona_id: message.persona_id
});
}
};
/** Group consecutive advisor messages so we can render them in a horizontal carousel */
const messageGroups = useMemo(() => {
const groups = [];
let i = 0;
while (i < messages.length) {
if (messages[i].type === 'advisor') {
const advisorGroup = [];
while (i < messages.length && messages[i].type === 'advisor') {
advisorGroup.push(messages[i]);
i++;
}
groups.push({ type: 'advisor_group', messages: advisorGroup });
} else {
groups.push({ type: 'single', message: messages[i] });
i++;
}
}
return groups;
}, [messages]);
const handleInputSubmit = async (inputMessage) => {
if (replyingTo) {
// This is a reply to a specific message
await handleReplyToAdvisor(inputMessage, replyingTo);
} else {
// This is a regular message
await handleSendMessage(inputMessage);
}
};
const cancelReply = () => {
setReplyingTo(null);
};
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const handleSidebarToggle = (isCollapsed) => {
setIsSidebarCollapsed(isCollapsed);
};
const hasMessages = messages.length > 0;
const hasConversationMessages = messages.filter(m => m.type !== 'system' && m.type !== 'document_upload').length > 0;
const chatPlaceholder = config?.chat_page?.placeholder || "Ask your advisors anything...";
return (
<div className="chat-page-with-sidebar">
{/* Sidebar Component */}
<Sidebar
user={user}
currentSessionId={currentSessionId}
onSelectSession={handleSelectSession}
onNewChat={handleNewChat}
onCurrentSessionDeleted={handleCurrentSessionDeleted}
onSignOut={onSignOut}
authToken={authToken}
onSidebarToggle={handleSidebarToggle}
isMobileOpen={isMobileMenuOpen}
onMobileToggle={setIsMobileMenuOpen}
onNavigateToCanvas={onNavigateToCanvas}
refreshTrigger={sidebarRefreshTrigger}
userAvatarId={userAvatarId}
onAvatarChange={handleAvatarChange}
onOpenProfile={() => setShowProfileForm(true)}
onOpenAccount={() => setShowAccount(true)}
onOpenClearData={() => setShowClearData(true)}
/>
<div className={`main-chat-area ${isSidebarCollapsed ? 'sidebar-collapsed' : ''}`}>
<div className="modern-chat-page">
<AppHeader
currentPage="chat"
onNavigateToHome={onNavigateToHome}
onNavigateToChat={() => {}}
onNavigateToCanvas={onNavigateToCanvas}
onMobileMenu={handleMobileMenuToggle}
>
<AdvisorStatusDropdown
advisors={advisors}
activeAdvisorIds={activeAdvisorIds}
onToggleAdvisor={handleToggleAdvisor}
onSetActiveAdvisors={handleSetActiveAdvisors}
thinkingAdvisors={thinkingAdvisors}
getAdvisorColors={getAdvisorColors}
isDark={isDark}
/>
<ExportButton
hasMessages={hasConversationMessages}
currentSessionId={currentSessionId}
authToken={authToken}
/>
{/* User guide button (from main) — slotted into AppHeader's children */}
<button
className="icon-btn header-help-btn"
onClick={() => window.dispatchEvent(new CustomEvent('open-user-guide'))}
title="Open user guide"
>
<HelpCircle size={18} />
</button>
</AppHeader>
{/* Main Content */}
<div className="chat-content">
{!hasMessages ? (
<div className="welcome-state">
<AdvisorCarousel />
<SuggestionsPanel onSuggestionClick={handleSendMessage} />
</div>
) : (
<div className="messages-container">
{/* Add loading session indicator */}
{isLoadingSession && (
<div className="loading-session">
<div className="loading-spinner"></div>
<span>Loading chat session...</span>
</div>
)}
<div className="messages-list">
<div className="messages-scroll">
{messageGroups.map((group) => (
group.type === 'advisor_group' ? (
<AdvisorCarousel
key={group.messages.map(m => m.id).join('-')}
messages={group.messages}
onReply={handleReplyToMessage}
onExpand={handleExpandMessage}
onClick={handleMessageClick}
/>
) : (
<div key={group.message.id}>
{group.message.type === 'user' && (
<div className="user-message-container">
<div className="user-message">
{group.message.replyTo && (
<div className="reply-indicator">
<Reply size={12} />
<span>Reply to {group.message.replyTo.advisorName}</span>
</div>
)}
<p>{group.message.content}</p>
</div>
</div>
)}
{group.message.type === 'error' && (
<div className="error-message-container">
<div className="error-message">
<p>{group.message.content}</p>
</div>
</div>
)}
{group.message.type === 'system' && (
<div className="system-message-container">
<div className="system-message">
<p>{group.message.content}</p>
</div>
</div>
)}
{group.message.type === 'document_upload' && (
<div className="system-message-container">
<div className="system-message document-upload">
<FileText size={16} />
<p>{group.message.content}</p>
</div>
</div>
)}
{group.message.type === 'clarification' && (
<div className="clarification-message-container">
<div className="clarification-message">
<div className="clarification-header">
<MessageCircle size={16} />
<span>I need a bit more information</span>
</div>
<p>{group.message.content}</p>
{group.message.suggestions && group.message.suggestions.length > 0 && (
<div className="clarification-suggestions">
<p className="suggestions-label">Here are some ways you could be more specific:</p>
<div className="suggestions-list">
{group.message.suggestions.map((suggestion, index) => (
<button
key={index}
className="suggestion-button"
onClick={() => handleSendMessage(suggestion)}
>
{suggestion}
</button>
))}
</div>
</div>
)}
</div>
</div>
)}
</div>
)
))}
{thinkingAdvisors.includes('system') && (
<div className="orchestrator-thinking">
<div className="thinking-bubble">
<MessageCircle size={20} />
</div>
<div className="thinking-content">
<span className="thinking-label">Orchestrator is thinking...</span>
<div className="thinking-animation">
<div className="dot"></div>
<div className="dot"></div>
<div className="dot"></div>
</div>
</div>
</div>
)}
{thinkingAdvisors.filter(id => id !== 'system').map(advisorId => (
<ThinkingIndicator key={advisorId} advisorId={advisorId} />
))}
<div ref={messagesEndRef} />
</div>
</div>
</div>
)}
</div>
<div className="floating-input-area">
{replyingTo && (
<div className="reply-banner">
<div className="reply-info">
<Reply size={16} />
<span>Replying to <strong>{replyingTo.advisorName}</strong></span>
</div>
<button onClick={cancelReply} className="cancel-reply">
<X size={16} />
</button>
</div>
)}
<EnhancedChatInput
onSendMessage={handleInputSubmit}
onFileUploaded={handleFileUploaded}
uploadedDocuments={uploadedDocuments}
isLoading={isLoading}
currentChatSessionId={currentSessionId}
authToken={authToken}
placeholder={
replyingTo
? `Reply to ${replyingTo.advisorName}...`
: chatPlaceholder
}
/>
</div>
</div>
</div>
</div>
);
};
export default ChatPage;