/** * HP AI Chatbot * * Frontend interface for the HP Marketing Materials Chatbot. * This application provides a chat interface to query the HP marketing * knowledge base containing HP brand guidelines and supporting documents. * * Key features: * - Clean chat interface for asking questions about HP marketing materials * - Sources and reasoning display for transparency * - Brief download functionality to export conversations * - Session-based memory for contextual conversations * */ import { useState, useRef, useEffect } from 'react'; import { Send, Upload, Loader2, X, FileText, Info, PlusCircle, Trash2, Moon, Sun } from 'lucide-react'; import ThemeToggle from './components/ThemeToggle'; import { fetchWithTimeout } from './lib/utils'; import { Alert, AlertDescription } from "./components/ui/alert"; import * as Tooltip from '@radix-ui/react-tooltip'; import showdown from 'showdown'; import { getCurrentUser, signOut } from './auth'; import { loadUserConversations, loadConversationMessages, createNewConversation, deleteConversation } from './components/ConversationManager'; // Use environment variables for backend URL // Define backend URL dynamically based on environment const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'https://ai-sandbox.oliver.solutions/hp_back_v2'; console.log('Using backend URL:', BACKEND_URL); // Function to get authenticated fetch defaults const getFetchDefaults = () => { // Get MSAL username for authentication const username = getCurrentUser(); return { credentials: 'include', headers: { 'Content-Type': 'application/json', 'X-MS-USERNAME': username || '', // Add the authenticated username to requests } }; }; export default function ChatInterface() { // Build timestamp to force new hash generation on rebuild - logged only once const BUILD_TIMESTAMP = "2025-04-27T" + "FIXED_TIMESTAMP"; // Only log in development mode and only once during initialization if (process.env.NODE_ENV !== 'production') { // Using useRef to ensure this only runs once const hasLoggedTimestamp = useRef(false); useEffect(() => { if (!hasLoggedTimestamp.current) { console.log('App build timestamp:', BUILD_TIMESTAMP); hasLoggedTimestamp.current = true; } }, []); } const [messages, setMessages] = useState([]); const [inputMessage, setInputMessage] = useState(''); const [isProcessing, setIsProcessing] = useState(false); const [isInitialized, setIsInitialized] = useState(false); const [error, setError] = useState(null); const [isCheckingStatus, setIsCheckingStatus] = useState(true); // Conversation state const [conversations, setConversations] = useState([]); const [loadingConversations, setLoadingConversations] = useState(false); const [activeConversation, setActiveConversation] = useState(null); // Sidebar resizing state const [sidebarWidth, setSidebarWidth] = useState(320); // Default width in pixels const [isResizing, setIsResizing] = useState(false); const [autoWidth, setAutoWidth] = useState(true); // Auto-width flag const minSidebarWidth = 240; const maxSidebarWidth = 480; const sidebarRef = useRef(null); // Initialize Showdown converter for markdown const markdownConverter = new showdown.Converter({ tables: true, simplifiedAutoLink: true, strikethrough: true, tasklists: true, emoji: true }); const messagesEndRef = useRef(null); // Use a ref to track if we've already created a new conversation on this page load const hasCreatedConversation = useRef(false); const [sessionId, setSessionId] = useState(() => { // Check for existing session ID in localStorage const existingSessionId = localStorage.getItem('chatSessionId'); if (existingSessionId) { return existingSessionId; } // Generate new session ID if none exists const newSessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; localStorage.setItem('chatSessionId', newSessionId); return newSessionId; }); // Load user conversations from the API const loadUserConversations = async () => { setLoadingConversations(true); try { const fetchOptions = getFetchDefaults(); const response = await fetchWithTimeout(`${BACKEND_URL}/conversations`, { ...fetchOptions, method: 'GET' }); if (response.ok) { const data = await response.json(); setConversations(data.conversations || []); // REMOVED auto-loading of the most recent conversation // We want the user to always start with a fresh conversation when refreshing } else { console.error('Failed to load conversations'); } } catch (error) { console.error('Error loading conversations:', error); } finally { setLoadingConversations(false); } }; // Load a specific conversation const loadConversation = async (conversation) => { try { setIsProcessing(true); setActiveConversation(conversation); // Set the session ID setSessionId(conversation.session_id); localStorage.setItem('chatSessionId', conversation.session_id); // Fetch the messages for this conversation const fetchOptions = getFetchDefaults(); const response = await fetchWithTimeout(`${BACKEND_URL}/conversations/${conversation.id}/messages`, { ...fetchOptions, method: 'GET' }); if (response.ok) { const data = await response.json(); // Format the messages for the UI const formattedMessages = data.messages.map(msg => ({ role: msg.role, content: msg.content, sources: msg.sources || [], reasoning: msg.reasoning || [], images: msg.images || [] })); setMessages(formattedMessages); setIsInitialized(true); setError(null); } else { setError('Failed to load conversation messages'); console.error('Failed to load conversation messages'); } } catch (error) { setError('Error loading conversation'); console.error('Error loading conversation:', error); } finally { setIsProcessing(false); } }; // Create a new conversation const createNewConversation = async () => { try { setIsProcessing(true); const fetchOptions = getFetchDefaults(); const response = await fetchWithTimeout(`${BACKEND_URL}/conversations/new`, { ...fetchOptions, method: 'POST' }); if (response.ok) { const data = await response.json(); // Set the new session ID setSessionId(data.session_id); localStorage.setItem('chatSessionId', data.session_id); // Create a new conversation object const newConversation = { id: data.conversation_id, title: "New conversation", created_at: new Date().toISOString(), last_updated: new Date().toISOString(), session_id: data.session_id }; // Update the conversations list setConversations(prev => [newConversation, ...prev]); // Set as active conversation setActiveConversation(newConversation); // Reset messages setMessages([{ role: 'assistant', content: 'Welcome to the HP Marketing Materials Chatbot! How can I help you today?' }]); setIsInitialized(true); setError(null); } else { setError('Failed to create new conversation'); console.error('Failed to create new conversation'); } } catch (error) { setError('Error creating new conversation'); console.error('Error creating new conversation:', error); } finally { setIsProcessing(false); } }; // Function to handle loading a conversation const handleLoadConversation = async (conversation) => { setActiveConversation(conversation); await loadConversationMessages( conversation, getCurrentUser(), setMessages, setSessionId, setError ); }; // Function to handle creating a new conversation const handleCreateNewConversation = async () => { await createNewConversation( getCurrentUser(), setConversations, setActiveConversation, setSessionId, setMessages, setError ); }; // Function to update conversations list (including titles) after a message is sent const updateConversationsList = async () => { if (activeConversation) { try { console.log('Updating conversations list...'); // Get updated conversations from the server const fetchOptions = getFetchDefaults(); const response = await fetchWithTimeout(`${BACKEND_URL}/conversations`, { ...fetchOptions, method: 'GET' }); if (response.ok) { const data = await response.json(); const updatedConversations = data.conversations || []; // Update the conversations list setConversations(updatedConversations); // Find and update the active conversation with any title changes const updatedActiveConversation = updatedConversations.find( convo => convo.id === activeConversation.id ); if (updatedActiveConversation) { // Always update the active conversation to get the latest data // This ensures titles get updated after messages are sent setActiveConversation(updatedActiveConversation); console.log('Updated active conversation:', 'ID:', updatedActiveConversation.id, 'Title:', updatedActiveConversation.title ); } else { console.warn('Active conversation not found in updated list'); } } else { console.error('Failed to update conversations list'); } } catch (err) { console.error('Error updating conversations list:', err); // Continue with existing data on error } } else { console.warn('No active conversation to update'); } }; // Separate useEffect that only runs once on component mount useEffect(() => { // Flag to prevent multiple conversation creations hasCreatedConversation.current = false; // Check the status of the chat system when the component mounts const checkStatus = async () => { setIsCheckingStatus(true); try { // Check if the chat system is initialized const fetchOptions = getFetchDefaults(); const response = await fetchWithTimeout(`${BACKEND_URL}/status?sessionId=${sessionId}`, { ...fetchOptions, method: 'GET' }); if (response.ok) { const data = await response.json(); // Check for either the new property 'global_status' or the old 'initialized' property // Enhanced logging for the status response console.log('Status check raw response:', JSON.stringify(data)); // Explicitly check each property with detailed logging const hasGlobalStatus = 'global_status' in data; const globalStatusValue = hasGlobalStatus ? data.global_status : 'missing'; const globalStatusMatch = globalStatusValue === 'initialized'; const hasInitialized = 'initialized' in data; const initializedValue = hasInitialized ? data.initialized : false; console.log('Status check property analysis:', { hasGlobalStatus, globalStatusValue, globalStatusMatch, hasInitialized, initializedValue }); // Try also checking 'is_initialized' as another possible property name const hasIsInitialized = 'is_initialized' in data; const isInitializedValue = hasIsInitialized ? data.is_initialized : false; // Check session-specific initialization if available const hasSessionInitialized = 'session_initialized' in data; const sessionInitialized = hasSessionInitialized ? data.session_initialized : true; // Default to true if not present const sessionStatus = data.session_status || 'unknown'; // OVERRIDE: ALWAYS force initialization to true // This completely bypasses all property checks and status endpoint validation // Complementing the backend change which now always returns initialized=true const systemInitialized = true; // Log final decision with full context console.log('Final initialization decision:', { systemInitialized, globalStatusMatch, initializedValue, isInitializedValue, hasSessionInitialized, sessionInitialized, sessionStatus, fullData: data }); setIsInitialized(systemInitialized); if (systemInitialized) { // First, load conversations list for the sidebar without auto-selecting any await loadUserConversations(); // Only create a new conversation if we haven't already if (!hasCreatedConversation.current) { logToConsole('info', 'App mounted, creating a new conversation', { sessionId }); // Set the flag to prevent creating multiple conversations hasCreatedConversation.current = true; setIsProcessing(true); try { const fetchOptions = getFetchDefaults(); const response = await fetch(`${BACKEND_URL}/conversations/new`, { ...fetchOptions, method: 'POST' }); if (response.ok) { const data = await response.json(); // Set the new session ID setSessionId(data.session_id); localStorage.setItem('chatSessionId', data.session_id); // Create a new conversation object const newConversation = { id: data.conversation_id, title: "New conversation", created_at: new Date().toISOString(), last_updated: new Date().toISOString(), session_id: data.session_id }; // Update the conversations list setConversations(prev => [newConversation, ...prev]); // Set as active conversation setActiveConversation(newConversation); // Reset messages setMessages([{ role: 'assistant', content: 'Welcome to the HP Marketing Materials Chatbot! How can I help you today?' }]); } else { console.error('Failed to create new conversation'); } } catch (error) { console.error('Error creating new conversation on page load:', error); } finally { setIsProcessing(false); } } setError(null); logToConsole('info', 'Chat system is ready', { sessionId }); } else { // System is not initialized setError('The chat system is not yet initialized. Please try again later.'); logToConsole('error', 'Chat system not initialized', { sessionId }); } } else { console.error('Failed to check system status'); setError('Failed to connect to the chat system. Please try again later.'); } } catch (error) { console.error('Error checking system status:', error); setError('Failed to connect to the chat system. Please try again later.'); } finally { setIsCheckingStatus(false); } }; checkStatus(); // Empty dependency array to ensure this only runs once when the component mounts }, []); const logToConsole = (type, message, data = null) => { const timestamp = new Date().toISOString(); const logMessage = { timestamp, type, message, data }; console.log(JSON.stringify(logMessage, null, 2)); }; const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; useEffect(() => { scrollToBottom(); }, [messages]); // Update the page title when active conversation changes or is updated useEffect(() => { if (activeConversation?.title) { document.title = `${activeConversation.title} | HP Marketing Bot`; } else { document.title = 'HP Marketing Materials Chatbot'; } }, [activeConversation?.title]); // Sidebar resize handlers const startResizing = (e) => { setIsResizing(true); e.preventDefault(); }; const stopResizing = () => { setIsResizing(false); document.body.style.cursor = 'default'; }; const resize = (e) => { if (isResizing) { document.body.style.cursor = 'ew-resize'; const newWidth = e.clientX; if (newWidth >= minSidebarWidth && newWidth <= maxSidebarWidth) { setSidebarWidth(newWidth); } } }; // Auto-resize sidebar based on conversation titles useEffect(() => { // Function to calculate the sidebar width based on conversation titles const calculateSidebarWidth = () => { if (!autoWidth || !conversations.length) return; // Create a temporary div to measure text width const measureDiv = document.createElement('div'); measureDiv.style.position = 'absolute'; measureDiv.style.visibility = 'hidden'; measureDiv.style.whiteSpace = 'nowrap'; measureDiv.style.fontFamily = 'inherit'; measureDiv.style.fontSize = '0.875rem'; // text-sm measureDiv.style.fontWeight = '500'; // font-medium // Add to DOM for measurement document.body.appendChild(measureDiv); // Find longest title let maxWidth = 0; conversations.forEach(convo => { measureDiv.textContent = convo.title || "New conversation"; const width = measureDiv.offsetWidth; maxWidth = Math.max(maxWidth, width); }); // Remove measurement div document.body.removeChild(measureDiv); // Calculate sidebar width (add padding for buttons, icons, etc.) // We add extra padding for the delete button and some margin const calculatedWidth = Math.max(minSidebarWidth, Math.min(maxWidth + 100, maxSidebarWidth)); // Update width if it's significantly different if (Math.abs(calculatedWidth - sidebarWidth) > 10) { setSidebarWidth(calculatedWidth); } }; // Call the calculation function calculateSidebarWidth(); // Add an event listener for window resize to recalculate if needed if (autoWidth) { window.addEventListener('resize', calculateSidebarWidth); return () => window.removeEventListener('resize', calculateSidebarWidth); } }, [conversations, autoWidth, minSidebarWidth, maxSidebarWidth, sidebarWidth]); // Add event listeners for manual resize useEffect(() => { const handleResize = (e) => { if (isResizing) { document.body.style.cursor = 'ew-resize'; const newWidth = e.clientX; if (newWidth >= minSidebarWidth && newWidth <= maxSidebarWidth) { setSidebarWidth(newWidth); // When user manually resizes, disable auto width setAutoWidth(false); } } }; const handleStopResizing = () => { setIsResizing(false); document.body.style.cursor = 'default'; }; window.addEventListener('mousemove', handleResize); window.addEventListener('mouseup', handleStopResizing); return () => { window.removeEventListener('mousemove', handleResize); window.removeEventListener('mouseup', handleStopResizing); }; }, [isResizing, minSidebarWidth, maxSidebarWidth]); const handleFilesChange = (e, type) => { const files = Array.from(e.target.files); logToConsole('info', `Handling ${type} files change`, { fileCount: files.length, fileDetails: files.map(f => ({ name: f.name, type: f.type, size: f.size })) }); if (type === 'brief') { setBriefFiles(prev => [...prev, ...files]); } else { setSupportingFiles(prev => [...prev, ...files]); } }; const removeFile = (fileName, type) => { logToConsole('info', `Removing file`, { fileName, type }); if (type === 'brief') { setBriefFiles(prev => prev.filter(file => file.name !== fileName)); } else { setSupportingFiles(prev => prev.filter(file => file.name !== fileName)); } }; const resetChat = async () => { try { setIsProcessing(true); logToConsole('info', 'Resetting chat session', { sessionId }); const fetchOptions = getFetchDefaults(); const response = await fetchWithTimeout(`${BACKEND_URL}/reset`, { ...fetchOptions, method: 'POST', body: JSON.stringify({ sessionId }) }); if (response.ok) { // Reset the UI state setMessages([{ role: 'assistant', content: 'Chat session has been reset. How can I help you today?' }]); setError(null); setInputMessage(''); logToConsole('info', 'Chat session reset successfully'); } else { const errorData = await response.json(); setError(errorData.error || 'Failed to reset chat'); } } catch (error) { console.error('Error resetting chat:', error); setError('Failed to reset chat. Please try again.'); } finally { setIsProcessing(false); } }; const ImageViewer = ({ image, isOpen, onClose, allImages }) => { if (!isOpen) return null; // Find the index of the current image in the allImages array const currentImageIndex = allImages.findIndex(img => { const imgFilename = typeof img === 'string' ? img : img.filename; const selectedFilename = typeof image === 'string' ? image : image.filename; return imgFilename === selectedFilename; }); const [activeIndex, setActiveIndex] = useState(currentImageIndex >= 0 ? currentImageIndex : 0); // Handle both string and object formats for backward compatibility const currentImage = allImages[activeIndex]; const filename = typeof currentImage === 'string' ? currentImage : currentImage.filename; const metadata = typeof currentImage === 'string' ? null : currentImage; const goToPrevious = (e) => { e.stopPropagation(); setActiveIndex((prevIndex) => (prevIndex > 0 ? prevIndex - 1 : allImages.length - 1)); }; const goToNext = (e) => { e.stopPropagation(); setActiveIndex((prevIndex) => (prevIndex < allImages.length - 1 ? prevIndex + 1 : 0)); }; return (
Connecting to HP Marketing Materials Chatbot...
The system is currently initializing. Please try again later.
Ask questions about HP's marketing materials, branding guidelines, campaign strategies, and creative processes.