/** * 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 (
{allImages.length > 1 && ( )}
{metadata {metadata && (
{metadata.document}
Page {metadata.page} {allImages.length > 1 ? `(${activeIndex + 1}/${allImages.length})` : ''}
)}
{allImages.length > 1 && ( )}
); }; const MessageBubble = ({ message }) => { const [selectedImage, setSelectedImage] = useState(null); const formatSources = (sources) => { if (!sources || !Array.isArray(sources)) return []; return sources.map((source) => { if (source && typeof source === 'object') { if (source.content) { return { text: typeof source.content === 'string' ? source.content : JSON.stringify(source.content), tool: source.tool_name || '' }; } return { text: JSON.stringify(source), tool: '' }; } return { text: String(source), tool: '' }; }).filter(source => source.text); }; const formatReasoning = (reasoning) => { if (!reasoning || !Array.isArray(reasoning)) return []; return reasoning.map((step, index) => { const stepType = step.type || 'thought'; let content = ''; if (step.action) { content = `${step.action}${step.action_input ? `: ${step.action_input}` : ''}`; } else if (step.observation) { content = step.observation; } else if (step.response) { content = step.response; } else if (step.thought) { content = step.thought; } return { id: index, type: stepType, content: content, thought: step.thought }; }).filter(step => step.content); }; const sources = formatSources(message.sources); const reasoningSteps = formatReasoning(message.reasoning); const images = message.images || []; // Handle image click - open full-size viewer const openImage = (image) => { setSelectedImage(image); }; // Close image viewer const closeImage = () => { setSelectedImage(null); }; // Handle escape key to close image useEffect(() => { const handleEscKey = (e) => { if (e.key === 'Escape' && selectedImage) { closeImage(); } }; window.addEventListener('keydown', handleEscKey); return () => window.removeEventListener('keydown', handleEscKey); }, [selectedImage]); return (
{/* Display image thumbnails if available */} {images.length > 0 && (
Relevant Document Screenshots:
{images.map((image, idx) => { // Handle both string and object formats for backward compatibility const filename = typeof image === 'string' ? image : image.filename; const metadata = typeof image === 'string' ? null : image; return (
openImage(image)} > {metadata {metadata && (
pg. {metadata.page}
)}
); })}
)}
{sources.length > 0 && (

Sources Used:

    {sources.map((source, idx) => (
  • {source.tool && (
    Tool: {source.tool}
    )}
    {source.text}
  • ))}
)} {reasoningSteps.length > 0 && (

Reasoning Steps:

    {reasoningSteps.map((step) => (
  • {step.type}:
    {step.content}
    {step.thought && step.thought !== step.content && (
    Thought: {step.thought}
    )}
  • ))}
)}
{/* Image viewer modal */}
); }; const handleSubmit = async () => { if (!inputMessage.trim() || isProcessing) return; const currentMessage = inputMessage; setMessages(prev => [...prev, { role: 'user', content: currentMessage }]); setInputMessage(''); setIsProcessing(true); // ENHANCED DEBUGGING: Log the current state of sessionId and related variables console.log('SUBMIT DEBUG DATA:', { sessionId, sessionIdType: typeof sessionId, sessionIdLength: sessionId ? sessionId.length : 0, hasActiveConversation: !!activeConversation, activeConversationId: activeConversation ? activeConversation.id : null, activeConversationSessionId: activeConversation ? activeConversation.session_id : null, localStorageSessionId: localStorage.getItem('chatSessionId') }); // Check if this is a blank chat with no active conversation - create one if needed if (!activeConversation) { logToConsole('info', 'No active conversation found when submitting message, creating a new one', { sessionId }); try { await handleCreateNewConversation(); } catch (err) { console.error('Error creating new conversation before sending message:', err); // Continue with the chat request anyway, as the backend might handle this } } // ENHANCEMENT: Try to ensure we have a valid sessionId const effectiveSessionId = sessionId || (activeConversation ? activeConversation.session_id : null) || localStorage.getItem('chatSessionId'); if (!effectiveSessionId) { console.error('No valid sessionId found from any source'); setError('Session ID is missing. Please refresh the page.'); setIsProcessing(false); return; } try { // Log the sessionId to help debug console.log('Sending chat request with sessionId:', effectiveSessionId); const fetchOptions = getFetchDefaults(); const response = await fetchWithTimeout(`${BACKEND_URL}/chat`, { ...fetchOptions, method: 'POST', body: JSON.stringify({ message: currentMessage, sessionId: effectiveSessionId }) }); if (!response.ok) { const errorData = await response.json(); // If the server says chat is not initialized, reset UI to show upload screen if (errorData.error === 'Chat system not initialized') { setIsInitialized(false); throw new Error('Chat system not initialized. Please upload files first.'); } throw new Error(errorData.error || 'Failed to get response'); } const responseData = await response.json(); let assistantMessage; if (responseData.status === 'success' && responseData.data) { assistantMessage = { role: 'assistant', content: responseData.data.response || '', sources: responseData.data.sources || [], reasoning: responseData.data.reasoning || [], images: responseData.data.images || [] }; } else if (responseData.result) { assistantMessage = { role: 'assistant', content: responseData.result.response || '', sources: responseData.result.sources || [], reasoning: responseData.result.reasoning || [], images: responseData.result.images || [] }; } else { assistantMessage = { role: 'assistant', content: responseData.response || '', sources: responseData.sources || [], reasoning: responseData.reasoning || [], images: responseData.images || [] }; } console.log('Processed assistant message:', assistantMessage); setMessages(prev => [...prev, assistantMessage]); setError(null); // Update the conversations list to get the AI-generated title // Add a longer delay to allow backend to generate title using OpenAI setTimeout(() => { // Need a longer delay for OpenAI to generate the title updateConversationsList(); // Check again after a bit longer in case the first attempt was too soon setTimeout(() => { updateConversationsList(); }, 3000); // Check again after 3 more seconds }, 2000); // Initial 2 second delay } catch (e) { console.error('Error in chat:', e); setError('Failed to process response. Please try again.'); } finally { setIsProcessing(false); } }; // Helper function to slice file into chunks and upload them const uploadFileInChunks = async (file, fileType, sessionId, controller) => { const chunkSize = 1024 * 1024; // 1MB chunks const totalChunks = Math.ceil(file.size / chunkSize); // Step 1: Initialize the chunked upload const fetchOptions = getFetchDefaults(); const initResponse = await fetch(`${BACKEND_URL}/init-chunked-upload`, { method: 'POST', headers: { ...fetchOptions.headers, 'Content-Type': 'application/json' }, signal: controller.signal, body: JSON.stringify({ fileName: file.name, fileType: fileType, // 'brief' or 'supporting' fileSize: file.size, mimeType: file.type, totalChunks, sessionId }) }); if (!initResponse.ok) { const error = await initResponse.json(); throw new Error(error.message || 'Failed to initialize chunked upload'); } const { uploadId } = await initResponse.json(); // Step 2: Upload each chunk for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) { const start = chunkIndex * chunkSize; const end = Math.min(start + chunkSize, file.size); const chunk = file.slice(start, end); const chunkFormData = new FormData(); chunkFormData.append('chunk', chunk); chunkFormData.append('uploadId', uploadId); chunkFormData.append('chunkIndex', chunkIndex); const fetchOptions = getFetchDefaults(); const chunkResponse = await fetch(`${BACKEND_URL}/upload-chunk`, { method: 'POST', body: chunkFormData, signal: controller.signal, headers: { ...fetchOptions.headers, 'X-Requested-With': 'XMLHttpRequest' } }); if (!chunkResponse.ok) { throw new Error(`Failed to upload chunk ${chunkIndex}`); } } // Step 3: Finalize the upload const finalizeOptions = getFetchDefaults(); const finalizeResponse = await fetch(`${BACKEND_URL}/finalize-upload`, { method: 'POST', headers: { ...finalizeOptions.headers, 'Content-Type': 'application/json' }, signal: controller.signal, body: JSON.stringify({ uploadId }) }); if (!finalizeResponse.ok) { throw new Error('Failed to finalize upload'); } return { fileName: file.name, uploadId }; }; // Main function to initialize chat const initializeChat = async () => { if (briefFiles.length === 0) { setError('Please upload at least one brief file.'); return; } logToConsole('info', 'Starting chat initialization', { briefFilesCount: briefFiles.length, supportingFilesCount: supportingFiles.length }); setIsProcessing(true); // Create an AbortController for proper timeout handling const controller = new AbortController(); const timeoutId = setTimeout(() => { controller.abort(); logToConsole('error', 'Request timed out after 10 minutes'); }, 600000); // 10 minute timeout try { // Check for large files that might need chunked upload const largeFiles = [...briefFiles, ...supportingFiles].filter(file => file.size > 1024 * 1024); if (largeFiles.length > 0) { logToConsole('info', 'Large files detected, using chunked upload approach', { largeFiles: largeFiles.map(f => ({ name: f.name, size: f.size })) }); } // Upload all brief files const briefUploads = []; for (const file of briefFiles) { if (file.size > 1024 * 1024) { // If file is larger than 1MB, use chunked upload const result = await uploadFileInChunks(file, 'brief', sessionId, controller); briefUploads.push(result); } else { // For small files, use regular upload const smallFormData = new FormData(); smallFormData.append('file', file); smallFormData.append('fileType', 'brief'); smallFormData.append('sessionId', sessionId); const fetchOptions = getFetchDefaults(); const response = await fetch(`${BACKEND_URL}/upload-small-file`, { method: 'POST', body: smallFormData, signal: controller.signal, headers: { ...fetchOptions.headers, 'X-Requested-With': 'XMLHttpRequest' } }); if (!response.ok) { throw new Error(`Failed to upload small file: ${file.name}`); } const result = await response.json(); briefUploads.push(result); } } // Upload all supporting files (similar approach) const supportingUploads = []; for (const file of supportingFiles) { if (file.size > 1024 * 1024) { const result = await uploadFileInChunks(file, 'supporting', sessionId, controller); supportingUploads.push(result); } else { // Small file upload code similar to brief files const smallFormData = new FormData(); smallFormData.append('file', file); smallFormData.append('fileType', 'supporting'); smallFormData.append('sessionId', sessionId); const fetchOptions = getFetchDefaults(); const response = await fetch(`${BACKEND_URL}/upload-small-file`, { method: 'POST', body: smallFormData, signal: controller.signal, headers: { ...fetchOptions.headers, 'X-Requested-With': 'XMLHttpRequest' } }); if (!response.ok) { throw new Error(`Failed to upload small file: ${file.name}`); } const result = await response.json(); supportingUploads.push(result); } } // Now that all files are uploaded, initialize the chat const finalizeData = { sessionId, briefFiles: briefUploads, supportingFiles: supportingUploads }; const finalizeOptions = getFetchDefaults(); const finalizeResponse = await fetch(`${BACKEND_URL}/initialize-from-uploads`, { method: 'POST', headers: { ...finalizeOptions.headers, 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify(finalizeData), signal: controller.signal }); // Clear the timeout clearTimeout(timeoutId); if (!finalizeResponse.ok) { const errorData = await finalizeResponse.json(); throw new Error(errorData.error || `Server responded with ${finalizeResponse.status}`); } const data = await finalizeResponse.json(); logToConsole('info', 'Chat initialization successful'); setIsInitialized(true); setMessages([{ role: 'assistant', content: 'Chat initialized! How can I help you?' }]); setError(null); } catch (err) { // Clear the timeout if there's an error clearTimeout(timeoutId); // Enhanced error logging const errorDetails = { message: err.message, name: err.name, stack: err.stack, isAbortError: err.name === 'AbortError' }; logToConsole('error', 'Initialization error', errorDetails); console.error('Initialization error:', err); // Set appropriate error message for user if (err.name === 'AbortError') { setError('Request timed out. The file(s) may be too large.'); } else { setError(err.message || 'Failed to initialize chat'); } setIsInitialized(false); } finally { setIsProcessing(false); } }; return (
{/* Conversation Sidebar - Fixed to left side */}

Conversations

{loadingConversations ? (
) : conversations.length > 0 ? ( conversations.map(convo => (
handleLoadConversation(convo)} >
{convo.title || "New conversation"}
{new Date(convo.last_updated).toLocaleDateString()}
)) ) : (
No conversations yet
)}
{getCurrentUser()}
{!autoWidth && ( )}
{/* Resize handle */}
{/* Header with HP branding - completely rebuilt without any borders */}
{/* Theme toggle in top right corner */}
{/* HP Logo */}
HP

Marketing Bot

{/* Main Chat Area - Centered with left margin to account for sidebar */}
{isCheckingStatus ? (

Connecting to HP Marketing Materials Chatbot...

) : !isInitialized ? (

HP Marketing Materials Chatbot

The system is currently initializing. Please try again later.

) : ( <>

{activeConversation?.title || "HP Marketing Materials Chatbot"}

Ask questions about HP's marketing materials, branding guidelines, campaign strategies, and creative processes.

{messages.map((message, index) => ( ))}
setInputMessage(e.target.value)} onKeyPress={(e) => { if (e.key === 'Enter') { e.preventDefault(); // Prevent any default behavior if (!isProcessing && inputMessage.trim()) { handleSubmit(); } } }} placeholder="Type your message..." className="flex-1 p-2 border dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded focus:outline-none focus:ring-2 focus:ring-blue-500" disabled={isProcessing} />
)} {error && !isCheckingStatus && ( {error} )}
); }