import React, { useState, useRef, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '../contexts/AuthContext'; import { useOrg } from '../contexts/OrgContext'; import { apiPost } from '../services/api'; import Sidebar from '../components/figma/Sidebar'; interface Message { id: string; role: 'user' | 'assistant'; content: string; timestamp: Date; mentions?: Array<{ id: string; name: string }>; attachments?: Array<{ name: string; type: string; size: number; data?: string; // Base64 encoded file data }>; } interface ChatState { messages: Message[]; isLoading: boolean; showEmployeeMenu: boolean; mentionQuery: string; mentionStartIndex: number; selectedEmployeeIndex: number; hasUploadedFiles: boolean; uploadedFiles: Array<{ name: string; type: string; size: number; data?: string; // Base64 encoded file data }>; } const ChatNew: React.FC = () => { const { user } = useAuth(); const { employees, orgId, org } = useOrg(); const navigate = useNavigate(); const inputRef = useRef(null); const fileInputRef = useRef(null); const messagesEndRef = useRef(null); const [state, setState] = useState({ messages: [], isLoading: false, showEmployeeMenu: false, mentionQuery: '', mentionStartIndex: -1, selectedEmployeeIndex: 0, hasUploadedFiles: false, uploadedFiles: [] }); const [currentInput, setCurrentInput] = useState(''); const [selectedCategory, setSelectedCategory] = useState('Accountability'); const [isInputFocused, setIsInputFocused] = useState(false); // Auto-resize textarea function const adjustTextareaHeight = () => { if (inputRef.current) { inputRef.current.style.height = 'auto'; const scrollHeight = inputRef.current.scrollHeight; const maxHeight = 150; // Maximum height in pixels inputRef.current.style.height = `${Math.min(scrollHeight, maxHeight)}px`; } }; useEffect(() => { if (!user) { navigate('/login'); } }, [user, navigate]); // Auto-scroll to bottom when new messages arrive useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [state.messages]); const questionStarters = [ "How can the company serve them better?", "What are our team's main strengths?", "Which areas need improvement?", "How is employee satisfaction?" ]; const categories = ['Accountability', 'Employee Growth', 'Customer Focus', 'Teamwork']; // Enhanced filtering for Google-style autocomplete const filteredEmployees = state.mentionQuery ? employees.filter(emp => { const query = state.mentionQuery.toLowerCase(); const nameWords = emp.name.toLowerCase().split(' '); const email = emp.email.toLowerCase(); // Match if query starts any word in name, or is contained in email return nameWords.some(word => word.startsWith(query)) || email.includes(query) || emp.name.toLowerCase().includes(query); }).sort((a, b) => { // Prioritize exact matches at start of name const aStartsWithQuery = a.name.toLowerCase().startsWith(state.mentionQuery.toLowerCase()); const bStartsWithQuery = b.name.toLowerCase().startsWith(state.mentionQuery.toLowerCase()); if (aStartsWithQuery && !bStartsWithQuery) return -1; if (!aStartsWithQuery && bStartsWithQuery) return 1; // Then alphabetical return a.name.localeCompare(b.name); }) : employees.slice(0, 10); // Show max 10 when no query const handleSendMessage = async () => { if (!currentInput.trim() && state.uploadedFiles.length === 0) return; const messageText = currentInput.trim(); const mentions: Array<{ id: string; name: string }> = []; // Extract mentions from the message const mentionRegex = /@(\w+(?:\s+\w+)*)/g; let match; while ((match = mentionRegex.exec(messageText)) !== null) { const mentionedName = match[1]; const employee = employees.find(emp => emp.name === mentionedName); if (employee) { mentions.push({ id: employee.id, name: employee.name }); } } const newMessage: Message = { id: Date.now().toString(), role: 'user', content: messageText, timestamp: new Date(), mentions, attachments: state.uploadedFiles.length > 0 ? [...state.uploadedFiles] : undefined }; setState(prev => ({ ...prev, messages: [...prev.messages, newMessage], isLoading: true, // Clear uploaded files after sending uploadedFiles: [], hasUploadedFiles: false })); setCurrentInput(''); try { // Get mentioned employees' data for context const mentionedEmployees = mentions.map(mention => employees.find(emp => emp.id === mention.id) ).filter(Boolean); // Call actual AI API with full context const res = await apiPost('/chat', { message: messageText, mentions: mentionedEmployees, attachments: state.uploadedFiles.length > 0 ? state.uploadedFiles : undefined, context: { org: org, employees: employees, messageHistory: state.messages.slice(-5) // Last 5 messages for context } }, orgId); if (!res.ok) { const errorData = await res.json(); throw new Error(errorData.error || 'Failed to get AI response'); } const data = await res.json(); const aiResponse: Message = { id: (Date.now() + 1).toString(), role: 'assistant', content: data.response || 'I apologize, but I encountered an issue processing your request.', timestamp: new Date() }; setState(prev => ({ ...prev, messages: [...prev.messages, aiResponse], isLoading: false })); } catch (error) { console.error('Chat API error:', error); // Fallback response with context awareness const fallbackMessage: Message = { id: (Date.now() + 1).toString(), role: 'assistant', content: `I understand you're asking about ${mentions.length > 0 ? mentions.map(m => m.name).join(', ') : 'your team'}. I'm currently experiencing some connection issues, but I'd be happy to help you analyze employee data, company metrics, or provide insights about your organization once the connection is restored.`, timestamp: new Date() }; setState(prev => ({ ...prev, messages: [...prev.messages, fallbackMessage], isLoading: false })); } }; const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value; const cursorPosition = e.target.selectionStart; setCurrentInput(value); // Auto-resize textarea setTimeout(adjustTextareaHeight, 0); // Enhanced @ mention detection for real-time search const beforeCursor = value.substring(0, cursorPosition); const lastAtIndex = beforeCursor.lastIndexOf('@'); if (lastAtIndex !== -1) { // Check if we're still within a mention context const afterAt = beforeCursor.substring(lastAtIndex + 1); const hasSpaceOrNewline = /[\s\n]/.test(afterAt); if (!hasSpaceOrNewline) { // We're in a mention - show menu and filter setState(prev => ({ ...prev, showEmployeeMenu: true, mentionQuery: afterAt, mentionStartIndex: lastAtIndex, selectedEmployeeIndex: 0 })); } else { setState(prev => ({ ...prev, showEmployeeMenu: false })); } } else { setState(prev => ({ ...prev, showEmployeeMenu: false })); } }; const handleEmployeeSelect = (employee: { id: string; name: string }) => { if (state.mentionStartIndex === -1) return; const beforeMention = currentInput.substring(0, state.mentionStartIndex); const afterCursor = currentInput.substring(inputRef.current?.selectionStart || currentInput.length); const newValue = `${beforeMention}@${employee.name} ${afterCursor}`; setCurrentInput(newValue); setState(prev => ({ ...prev, showEmployeeMenu: false, mentionQuery: '', mentionStartIndex: -1 })); // Focus back to input and position cursor after the mention setTimeout(() => { if (inputRef.current) { const newCursorPosition = beforeMention.length + employee.name.length + 2; inputRef.current.focus(); inputRef.current.setSelectionRange(newCursorPosition, newCursorPosition); } }, 0); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (state.showEmployeeMenu && filteredEmployees.length > 0) { switch (e.key) { case 'ArrowDown': e.preventDefault(); setState(prev => ({ ...prev, selectedEmployeeIndex: prev.selectedEmployeeIndex < filteredEmployees.length - 1 ? prev.selectedEmployeeIndex + 1 : 0 })); break; case 'ArrowUp': e.preventDefault(); setState(prev => ({ ...prev, selectedEmployeeIndex: prev.selectedEmployeeIndex > 0 ? prev.selectedEmployeeIndex - 1 : filteredEmployees.length - 1 })); break; case 'Enter': case 'Tab': e.preventDefault(); if (filteredEmployees[state.selectedEmployeeIndex]) { handleEmployeeSelect(filteredEmployees[state.selectedEmployeeIndex]); } break; case 'Escape': e.preventDefault(); setState(prev => ({ ...prev, showEmployeeMenu: false })); break; } } else if (e.key === 'Enter' && !e.shiftKey && !e.altKey) { e.preventDefault(); handleSendMessage(); } // Allow Shift+Enter and Alt+Enter for line breaks (default behavior) }; const handleFileUpload = async (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); if (files.length > 0) { const uploadedFiles = await Promise.all(files.map(async (file) => { // Convert file to base64 for API transmission const base64 = await new Promise((resolve) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result as string); reader.readAsDataURL(file); }); return { name: file.name, type: file.type, size: file.size, data: base64 // Add the actual file data }; })); setState(prev => ({ ...prev, hasUploadedFiles: true, uploadedFiles: [...prev.uploadedFiles, ...uploadedFiles] })); } }; const removeFile = (index: number) => { setState(prev => ({ ...prev, uploadedFiles: prev.uploadedFiles.filter((_, i) => i !== index), hasUploadedFiles: prev.uploadedFiles.length > 1 })); }; const handleQuestionClick = (question: string) => { setCurrentInput(question); }; const renderEmployeeMenu = () => { if (!state.showEmployeeMenu || filteredEmployees.length === 0) return null; return (
{state.mentionQuery && (
{filteredEmployees.length} employee{filteredEmployees.length !== 1 ? 's' : ''} found
)} {filteredEmployees.map((employee, index) => (
handleEmployeeSelect({ id: employee.id, name: employee.name })} onMouseEnter={() => setState(prev => ({ ...prev, selectedEmployeeIndex: index }))} className={`px-3 py-2 rounded-xl flex items-center space-x-3 cursor-pointer transition-colors ${index === state.selectedEmployeeIndex ? 'bg-[--Neutrals-NeutralSlate100] text-[--Neutrals-NeutralSlate950]' : 'hover:bg-[--Neutrals-NeutralSlate50]' }`} >
{employee.initials || employee.name.split(' ').map(n => n[0]).join('').toUpperCase()}
{employee.name}
{employee.role || employee.email}
))}
); }; const renderUploadedFiles = () => { if (state.uploadedFiles.length === 0) return null; return (
{state.uploadedFiles.map((file, index) => (
{file.name}
removeFile(index)} className="cursor-pointer">
))}
); }; const renderChatInterface = () => { if (state.messages.length === 0) { return (
What would you like to understand?
{categories.map((category) => (
setSelectedCategory(category)} className={`px-3 py-1.5 rounded-lg shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)] shadow-[inset_0px_-2px_0px_0px_rgba(10,13,18,0.05)] shadow-[inset_0px_0px_0px_1px_rgba(10,13,18,0.18)] flex justify-center items-center gap-1 overflow-hidden cursor-pointer ${selectedCategory === category ? 'bg-white' : '' }`} >
{category}
))}
{questionStarters.map((question, index) => (
handleQuestionClick(question)} className="flex-1 h-48 px-3 py-4 bg-[--Neutrals-NeutralSlate50] rounded-2xl inline-flex flex-col justify-between items-start overflow-hidden cursor-pointer hover:bg-[--Neutrals-NeutralSlate100]" >
{question}
))}
{/* Enhanced instructions for @ mentions */}
Ask about your team, company data, or get insights.
Use @ to mention team members.
{/* Sample questions */}
Try asking:
"How is the team performing overall?"
"What are the main strengths of our organization?"
"Tell me about @[employee name]'s recent feedback"
{renderChatInput()}
); } return (
{state.messages.map((message) => (
{message.content}
{message.attachments && message.attachments.length > 0 && (
{message.attachments.map((file, index) => (
{file.type.startsWith('image/') ? ( ) : ( )}
{file.name}
))}
)}
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
{message.mentions && message.mentions.length > 0 && (
Mentioned: {message.mentions.map(m => m.name).join(', ')}
)}
))} {state.isLoading && (
AI is analyzing...
)}
{renderChatInput()}
); }; const renderChatInput = () => { return (
{renderUploadedFiles()}
{currentInput || "Ask anything, use @ to tag staff and ask questions."} {/* Custom blinking cursor when focused and has text */} {currentInput && isInputFocused && ( )} {/* Custom blinking cursor when focused and no text */} {!currentInput && isInputFocused && ( )}
fileInputRef.current?.click()} className="cursor-pointer">
fileInputRef.current?.click()} className="cursor-pointer">
0 ? 'bg-[--Neutrals-NeutralSlate700]' : 'bg-[--Neutrals-NeutralSlate400]' }`} >
{/* Enhanced help text for keyboard navigation */}
{state.showEmployeeMenu ? '↑↓ Navigate • Enter/Tab Select • Esc Cancel' : 'Enter to send • Shift+Enter new line'}
{renderEmployeeMenu()}