Files
auditly/src/pages/ChatNew.tsx

677 lines
39 KiB
TypeScript

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<HTMLTextAreaElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const [state, setState] = useState<ChatState>({
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<HTMLTextAreaElement>) => {
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<HTMLInputElement>) => {
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<string>((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 (
<div className="absolute bottom-full left-[285px] mb-2 w-64 p-2 bg-[--Neutrals-NeutralSlate0] rounded-2xl shadow-[0px_1px_4px_4px_rgba(14,18,27,0.08)] border border-[--Neutrals-NeutralSlate200] max-h-64 overflow-y-auto z-50">
{state.mentionQuery && (
<div className="px-3 py-2 text-xs text-[--Neutrals-NeutralSlate500] border-b border-[--Neutrals-NeutralSlate100]">
{filteredEmployees.length} employee{filteredEmployees.length !== 1 ? 's' : ''} found
</div>
)}
{filteredEmployees.map((employee, index) => (
<div
key={employee.id}
onClick={() => 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]'
}`}
>
<div className="w-8 h-8 bg-[--Brand-Orange] rounded-full flex items-center justify-center text-white text-sm font-medium">
{employee.initials || employee.name.split(' ').map(n => n[0]).join('').toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<div className={`font-medium text-sm text-[--Neutrals-NeutralSlate950] truncate ${index === state.selectedEmployeeIndex ? 'font-medium' : 'font-normal'
}`}>
{employee.name}
</div>
<div className="text-xs text-[--Neutrals-NeutralSlate500] truncate">
{employee.role || employee.email}
</div>
</div>
</div>
))}
</div>
);
};
const renderUploadedFiles = () => {
if (state.uploadedFiles.length === 0) return null;
return (
<div className="inline-flex justify-start items-center gap-3 mb-4">
{state.uploadedFiles.map((file, index) => (
<div key={index} className="w-40 max-w-40 p-2 bg-[--Neutrals-NeutralSlate0] rounded-full outline outline-1 outline-offset-[-1px] outline-[--Neutrals-NeutralSlate200] inline-flex flex-col justify-start items-start gap-2.5 overflow-hidden">
<div className="self-stretch pr-2 inline-flex justify-between items-center">
<div className="flex-1 flex justify-start items-center gap-1.5">
<div className="w-6 h-6 relative bg-[--Neutrals-NeutralSlate600] rounded-full overflow-hidden">
<div className="left-[6px] top-[6px] absolute">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 1.13477V3.20004C7 3.48006 7 3.62007 7.0545 3.72703C7.10243 3.82111 7.17892 3.8976 7.273 3.94554C7.37996 4.00004 7.51997 4.00004 7.8 4.00004H9.86527M8 6.5H4M8 8.5H4M5 4.5H4M7 1H4.4C3.55992 1 3.13988 1 2.81901 1.16349C2.53677 1.3073 2.3073 1.53677 2.16349 1.81901C2 2.13988 2 2.55992 2 3.4V8.6C2 9.44008 2 9.86012 2.16349 10.181C2.3073 10.4632 2.53677 10.6927 2.81901 10.8365C3.13988 11 3.55992 11 4.4 11H7.6C8.44008 11 8.86012 11 9.18099 10.8365C9.46323 10.6927 9.6927 10.4632 9.83651 10.181C10 9.86012 10 9.44008 10 8.6V4L7 1Z" stroke="[--Text-White-00, #FDFDFD]" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
</div>
<div className="flex-1 justify-start text-[--Neutrals-NeutralSlate950] text-sm font-medium font-['Inter'] leading-tight">{file.name}</div>
</div>
<div onClick={() => removeFile(index)} className="cursor-pointer">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 4L4 12M4 4L12 12" stroke="[--Icon-Gray-400, #A4A7AE]" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
</div>
</div>
))}
</div>
);
};
const renderChatInterface = () => {
if (state.messages.length === 0) {
return (
<div className="w-[736px] flex-1 max-w-[736px] pt-48 flex flex-col justify-between items-center">
<div className="self-stretch flex flex-col justify-start items-center gap-6">
<div className="justify-start text-[--Neutrals-NeutralSlate800] text-2xl font-medium font-['Neue_Montreal'] leading-normal">What would you like to understand?</div>
<div className="p-1 bg-[--Neutrals-NeutralSlate100] rounded-xl inline-flex justify-start items-center gap-1">
{categories.map((category) => (
<div
key={category}
onClick={() => 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' : ''
}`}
>
<div className="px-0.5 flex justify-center items-center">
<div className={`justify-start text-xs font-medium font-['Inter'] leading-none ${selectedCategory === category ? 'text-[--Neutrals-NeutralSlate900]' : 'text-[--Neutrals-NeutralSlate600]'
}`}>{category}</div>
</div>
</div>
))}
</div>
<div className="self-stretch flex flex-col justify-start items-start gap-3">
<div className="self-stretch inline-flex justify-start items-center gap-3">
{questionStarters.map((question, index) => (
<div
key={index}
onClick={() => 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]"
>
<div>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#clip0_818_19557)">
<path d="M7.57496 7.5013C7.77088 6.94436 8.15759 6.47472 8.66659 6.17558C9.17559 5.87643 9.77404 5.76708 10.3559 5.8669C10.9378 5.96671 11.4656 6.26924 11.8459 6.72091C12.2261 7.17258 12.4342 7.74424 12.4333 8.33464C12.4333 10.0013 9.93329 10.8346 9.93329 10.8346M9.99996 14.168H10.0083M18.3333 10.0013C18.3333 14.6037 14.6023 18.3346 9.99996 18.3346C5.39759 18.3346 1.66663 14.6037 1.66663 10.0013C1.66663 5.39893 5.39759 1.66797 9.99996 1.66797C14.6023 1.66797 18.3333 5.39893 18.3333 10.0013Z" stroke="[--Text-Gray-500, #717680]" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</g>
<defs>
<clipPath id="clip0_818_19557">
<rect width="20" height="20" fill="white" />
</clipPath>
</defs>
</svg>
</div>
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate800] text-base font-normal font-['Inter'] leading-normal">{question}</div>
</div>
))}
</div>
</div>
{/* Enhanced instructions for @ mentions */}
<div className="text-center text-[--Neutrals-NeutralSlate500] mt-8">
<div className="text-sm mb-2">Ask about your team, company data, or get insights.</div>
<div className="text-sm">Use <span className="bg-[--Neutrals-NeutralSlate100] px-2 py-1 rounded text-[--Neutrals-NeutralSlate800] font-mono">@</span> to mention team members.</div>
{/* Sample questions */}
<div className="mt-6 space-y-2">
<div className="text-sm font-medium text-[--Neutrals-NeutralSlate700] mb-3">Try asking:</div>
<div className="space-y-2 text-sm">
<div className="bg-[--Neutrals-NeutralSlate50] p-3 rounded-lg text-left max-w-md mx-auto">
"How is the team performing overall?"
</div>
<div className="bg-[--Neutrals-NeutralSlate50] p-3 rounded-lg text-left max-w-md mx-auto">
"What are the main strengths of our organization?"
</div>
<div className="bg-[--Neutrals-NeutralSlate50] p-3 rounded-lg text-left max-w-md mx-auto">
"Tell me about @[employee name]'s recent feedback"
</div>
</div>
</div>
</div>
</div>
{renderChatInput()}
</div>
);
}
return (
<div className="w-[736px] flex-1 max-w-[736px] flex flex-col">
<div className="flex-1 overflow-y-auto py-6">
{state.messages.map((message) => (
<div key={message.id} className={`mb-4 flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}>
<div className={`max-w-[80%] p-4 rounded-2xl ${message.role === 'user'
? 'bg-[--Brand-Orange] text-white'
: 'bg-[--Neutrals-NeutralSlate100] text-[--Neutrals-NeutralSlate950] border border-[--Neutrals-NeutralSlate200]'
}`}>
<div className="text-sm leading-relaxed whitespace-pre-wrap">{message.content}</div>
{message.attachments && message.attachments.length > 0 && (
<div className="flex flex-wrap gap-2 mt-3">
{message.attachments.map((file, index) => (
<div key={index} className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs ${message.role === 'user'
? 'bg-white/20 text-white/90'
: 'bg-[--Neutrals-NeutralSlate200] text-[--Neutrals-NeutralSlate700]'
}`}>
<div className="w-4 h-4">
{file.type.startsWith('image/') ? (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.8 14H4.62091C4.21704 14 4.0151 14 3.92159 13.9202C3.84045 13.8508 3.79739 13.7469 3.80577 13.6405C3.81542 13.5179 3.95821 13.3751 4.24379 13.0895L9.9124 7.42091C10.1764 7.15691 10.3084 7.02491 10.4606 6.97544C10.5946 6.93194 10.7388 6.93194 10.8728 6.97544C11.0249 7.02491 11.1569 7.15691 11.4209 7.42091L14 10V10.8M10.8 14C11.9201 14 12.4802 14 12.908 13.782C13.2843 13.5902 13.5902 13.2843 13.782 12.908C14 12.4802 14 11.9201 14 10.8M10.8 14H5.2C4.07989 14 3.51984 14 3.09202 13.782C2.71569 13.5902 2.40973 13.2843 2.21799 12.908C2 12.4802 2 11.9201 2 10.8V5.2C2 4.07989 2 3.51984 2.21799 3.09202C2.40973 2.71569 2.71569 2.40973 3.09202 2.21799C3.51984 2 4.07989 2 5.2 2H10.8C11.9201 2 12.4802 2 12.908 2.21799C2.71569 12.908 2.40973 2.21799 3.09202 2 3.51984 2 4.07989 2 5.2V10.8M7 5.66667C7 6.40305 6.40305 7 5.66667 7C4.93029 7 4.33333 6.40305 4.33333 5.66667C4.33333 4.93029 4.93029 4.33333 5.66667 4.33333C6.40305 4.33333 7 4.93029 7 5.66667Z" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
) : (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.33333 1.33333V2.8C9.33333 3.12001 9.33333 3.28002 9.40533 3.39467C9.46867 3.49422 9.57245 3.56756 9.672 3.63089C9.78665 3.70289 9.94666 3.70289 10.2667 3.70289H11.7333M10.6667 5.5H5.33333M10.6667 7.5H5.33333M6.66667 3.5H5.33333M9.33333 1.33333H5.86667C5.13029 1.33333 4.76209 1.33333 4.47852 1.47866C4.23137 1.60584 4.03918 1.79804 3.912 2.04518C3.76667 2.32876 3.76667 2.69695 3.76667 3.43333V12.5667C3.76667 13.303 3.76667 13.6712 3.912 13.9548C4.03918 14.2019 4.23137 14.3941 4.47852 14.5213C4.76209 14.6667 5.13029 14.6667 5.86667 14.6667H10.1333C10.8697 14.6667 11.2379 14.6667 11.5215 14.5213C11.7686 14.3941 11.9608 14.2019 12.088 13.9548C12.2333 13.6712 12.2333 13.303 12.2333 12.5667V5.33333L9.33333 1.33333Z" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)}
</div>
<span className="truncate max-w-[100px]">{file.name}</span>
</div>
))}
</div>
)}
<div className={`text-xs mt-2 ${message.role === 'user' ? 'text-white/70' : 'text-[--Neutrals-NeutralSlate500]'}`}>
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</div>
{message.mentions && message.mentions.length > 0 && (
<div className={`text-xs mt-1 ${message.role === 'user' ? 'text-white/60' : 'text-[--Neutrals-NeutralSlate400]'}`}>
Mentioned: {message.mentions.map(m => m.name).join(', ')}
</div>
)}
</div>
</div>
))}
{state.isLoading && (
<div className="flex justify-start mb-4">
<div className="bg-[--Neutrals-NeutralSlate100] text-[--Neutrals-NeutralSlate950] border border-[--Neutrals-NeutralSlate200] p-4 rounded-2xl">
<div className="flex items-center space-x-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-[--Brand-Orange]"></div>
<span className="text-sm">AI is analyzing...</span>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{renderChatInput()}
</div>
);
};
const renderChatInput = () => {
return (
<div className="self-stretch pl-5 pr-3 pt-5 pb-3 relative bg-[--Neutrals-NeutralSlate50] rounded-3xl flex flex-col justify-start items-start gap-4 min-h-[80px]">
{renderUploadedFiles()}
<div className="self-stretch justify-start text-[--Neutrals-NeutralSlate500] text-base font-normal font-['Inter'] leading-normal relative pointer-events-none min-h-[24px] whitespace-pre-wrap">
<span className={`${currentInput ? 'text-[--Neutrals-NeutralSlate950]' : 'text-[--Neutrals-NeutralSlate500]'}`}>
{currentInput || "Ask anything, use @ to tag staff and ask questions."}
</span>
{/* Custom blinking cursor when focused and has text */}
{currentInput && isInputFocused && (
<span
className="inline-block w-0.5 h-6 bg-[--Neutrals-NeutralSlate800] ml-0.5"
style={{
animation: 'blink 1s infinite',
verticalAlign: 'text-top'
}}
></span>
)}
{/* Custom blinking cursor when focused and no text */}
{!currentInput && isInputFocused && (
<span
className="absolute left-0 top-0 w-0.5 h-6 bg-[--Neutrals-NeutralSlate800]"
style={{
animation: 'blink 1s infinite'
}}
></span>
)}
</div>
<div className="self-stretch inline-flex justify-between items-center relative z-20">
<div className="flex justify-start items-center gap-4">
<div onClick={() => fileInputRef.current?.click()} className="cursor-pointer">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.6271 9.08442L10.1141 16.5974C8.40556 18.306 5.63546 18.306 3.92692 16.5974C2.21837 14.8889 2.21837 12.1188 3.92692 10.4102L11.4399 2.89724C12.579 1.75821 14.4257 1.75821 15.5647 2.89724C16.7037 4.03627 16.7037 5.883 15.5647 7.02203L8.34633 14.2404C7.77682 14.8099 6.85345 14.8099 6.28394 14.2404C5.71442 13.6709 5.71442 12.7475 6.28394 12.178L12.6184 5.84352" stroke="var(--color-gray-500)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
<div onClick={() => fileInputRef.current?.click()} className="cursor-pointer">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.5 17.5H5.77614C5.2713 17.5 5.01887 17.5 4.90199 17.4002C4.80056 17.3135 4.74674 17.1836 4.75721 17.0506C4.76927 16.8974 4.94776 16.7189 5.30474 16.3619L12.3905 9.27614C12.7205 8.94613 12.8855 8.78112 13.0758 8.7193C13.2432 8.66492 13.4235 8.66492 13.5908 8.7193C13.7811 8.78112 13.9461 8.94613 14.2761 9.27614L17.5 12.5V13.5M13.5 17.5C14.9001 17.5 15.6002 17.5 16.135 17.2275C16.6054 16.9878 16.9878 16.6054 17.2275 16.135C17.5 15.6002 17.5 14.9001 17.5 13.5M13.5 17.5H6.5C5.09987 17.5 4.3998 17.5 3.86502 17.2275C3.39462 16.9878 3.01217 16.6054 2.77248 16.135C2.5 15.6002 2.5 14.9001 2.5 13.5V6.5C2.5 5.09987 2.5 4.3998 2.77248 3.86502C3.01217 3.39462 3.39462 3.01217 3.86502 2.77248C4.3998 2.5 5.09987 2.5 6.5 2.5H13.5C14.9001 2.5 15.6002 2.5 16.135 2.77248C16.6054 3.01217 16.9878 3.39462 17.2275 3.86502C17.5 4.3998 17.5 5.09987 17.5 6.5V13.5M8.75 7.08333C8.75 8.00381 8.00381 8.75 7.08333 8.75C6.16286 8.75 5.41667 8.00381 5.41667 7.08333C5.41667 6.16286 6.16286 5.41667 7.08333 5.41667C8.00381 5.41667 8.75 6.16286 8.75 7.08333Z" stroke="var(--color-gray-500)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
<div className="cursor-pointer">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#clip0_818_19694)">
<path d="M13.3334 6.66745V10.8341C13.3334 11.4972 13.5968 12.133 14.0657 12.6019C14.5345 13.0707 15.1704 13.3341 15.8334 13.3341C16.4965 13.3341 17.1324 13.0707 17.6012 12.6019C18.07 12.133 18.3334 11.4972 18.3334 10.8341V10.0008C18.3333 8.11998 17.6969 6.29452 16.5278 4.82123C15.3587 3.34794 13.7256 2.31347 11.894 1.88603C10.0624 1.45859 8.14003 1.66332 6.43955 2.46692C4.73906 3.27053 3.36042 4.62575 2.5278 6.31222C1.69519 7.99869 1.45756 9.91723 1.85356 11.7559C2.24956 13.5945 3.2559 15.2451 4.70895 16.4393C6.16199 17.6335 7.97628 18.3011 9.85681 18.3334C11.7373 18.3657 13.5735 17.761 15.0668 16.6175M13.3334 10.0008C13.3334 11.8417 11.841 13.3341 10.0001 13.3341C8.15914 13.3341 6.66676 11.8417 6.66676 10.0008C6.66676 8.15984 8.15914 6.66745 10.0001 6.66745C11.841 6.66745 13.3334 8.15984 13.3334 10.0008Z" stroke="var(--Neutrals-NeutralSlate500)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</g>
<defs>
<clipPath id="clip0_818_19694">
<rect width="20" height="20" fill="white" />
</clipPath>
</defs>
</svg>
</div>
</div>
<div
onClick={handleSendMessage}
className={`p-2.5 rounded-[999px] flex justify-start items-center gap-2.5 overflow-hidden cursor-pointer ${currentInput.trim() || state.uploadedFiles.length > 0
? 'bg-[--Neutrals-NeutralSlate700]'
: 'bg-[--Neutrals-NeutralSlate400]'
}`}
>
<div>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 13.3346V2.66797M8 2.66797L4 6.66797M8 2.66797L12 6.66797" stroke="var(--Neutrals-NeutralSlate100)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
</div>
</div>
{/* Enhanced help text for keyboard navigation */}
<div className="absolute bottom-2 right-16 text-xs text-[--Neutrals-NeutralSlate400]">
{state.showEmployeeMenu ? '↑↓ Navigate • Enter/Tab Select • Esc Cancel' : 'Enter to send • Shift+Enter new line'}
</div>
{renderEmployeeMenu()}
<textarea
ref={inputRef}
value={currentInput}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
className="absolute inset-0 w-full resize-none outline-none bg-transparent text-transparent caret-transparent text-base font-normal font-['Inter'] leading-normal p-5 z-10 overflow-hidden"
placeholder=""
style={{
paddingTop: '20px', // Align with the display text
paddingLeft: '20px',
paddingRight: '12px',
paddingBottom: '60px', // Leave space for buttons
lineHeight: 'normal',
minHeight: '50px',
maxHeight: '150px'
}}
/>
<input
ref={fileInputRef}
type="file"
multiple
onChange={handleFileUpload}
className="hidden"
accept=".pdf,.doc,.docx,.txt,.png,.jpg,.jpeg"
/>
</div>
);
};
return (
<div className="w-full h-full inline-flex justify-start items-start overflow-hidden">
<div className="flex-1 self-stretch rounded-3xl shadow-[0px_0px_15px_0px_rgba(0,0,0,0.08)] flex justify-between items-start overflow-hidden">
<Sidebar companyName="Zitlac Media" />
<div className="flex-1 self-stretch py-6 bg-[--Neutrals-NeutralSlate0] inline-flex flex-col justify-center items-center gap-2.5">
{renderChatInterface()}
</div>
</div>
</div>
);
};
export default ChatNew;