feat: major UI overhaul with new components and enhanced UX
- Add comprehensive Company Wiki feature with complete state management - CompanyWikiManager, empty states, invite modals - Implement new Chat system with enhanced layout and components - ChatLayout, ChatSidebar, MessageThread, FileUploadInput - Create modern Login and OTP verification flows - LoginNew page, OTPVerification component - Add new Employee Forms system with enhanced controller - Introduce Figma-based design components and multiple choice inputs - Add new font assets (NeueMontreal) and robot images for onboarding - Enhance existing components with improved styling and functionality - Update build configuration and dependencies - Remove deprecated ModernLogin component
This commit is contained in:
165
pages/Chat.tsx
165
pages/Chat.tsx
@@ -1,165 +1,12 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Card, Button } from '../components/UiKit';
|
||||
import { useOrg } from '../contexts/OrgContext';
|
||||
import { CHAT_STARTERS } from '../constants';
|
||||
import { apiPost } from '../services/api';
|
||||
import React from 'react';
|
||||
import ChatLayout from '../components/chat/ChatLayout';
|
||||
import ChatEmptyState from '../components/chat/ChatEmptyState';
|
||||
|
||||
const Chat: React.FC = () => {
|
||||
const { employees, reports, generateEmployeeReport, orgId } = useOrg();
|
||||
const [messages, setMessages] = useState<Array<{ id: string, role: 'user' | 'assistant', text: string }>>([]);
|
||||
const [input, setInput] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [selectedEmployeeId, setSelectedEmployeeId] = useState<string>('');
|
||||
const selectedReport = selectedEmployeeId ? reports[selectedEmployeeId] : undefined;
|
||||
|
||||
const dynamicStarters = useMemo(() => {
|
||||
if (!selectedReport) return CHAT_STARTERS.slice(0, 4);
|
||||
const strengths = selectedReport.insights.strengths?.slice(0, 2) || [];
|
||||
const weaknesses = selectedReport.insights.weaknesses?.slice(0, 1) || [];
|
||||
const risk = selectedReport.retentionRisk;
|
||||
const starters: string[] = [];
|
||||
if (strengths[0]) starters.push(`How can we further leverage ${strengths[0]} for cross-team impact?`);
|
||||
if (weaknesses[0]) starters.push(`What is an actionable plan to address ${weaknesses[0]} this quarter?`);
|
||||
if (risk) starters.push(`What factors contribute to ${selectedReport.employeeId}'s ${risk} retention risk?`);
|
||||
starters.push(`Is ${selectedReport.employeeId} a candidate for expanded scope or leadership?`);
|
||||
while (starters.length < 4) starters.push(CHAT_STARTERS[starters.length] || 'Provide an organizational insight.');
|
||||
return starters.slice(0, 4);
|
||||
}, [selectedReport]);
|
||||
|
||||
const handleSend = async (message?: string) => {
|
||||
const textToSend = message || input;
|
||||
if (!textToSend.trim()) return;
|
||||
|
||||
const userMessage = { id: Date.now().toString(), role: 'user' as const, text: textToSend };
|
||||
setMessages(prev => [...prev, userMessage]);
|
||||
setInput('');
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// Build context for the AI
|
||||
const context = {
|
||||
selectedEmployee: selectedEmployeeId ? employees.find(e => e.id === selectedEmployeeId) : null,
|
||||
selectedReport: selectedReport,
|
||||
totalEmployees: employees.length,
|
||||
organizationScope: !selectedEmployeeId
|
||||
};
|
||||
|
||||
const res = await apiPost('/chat', {
|
||||
message: textToSend,
|
||||
employeeId: selectedEmployeeId,
|
||||
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 = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant' as const,
|
||||
text: data.response || 'I apologize, but I couldn\'t generate a response at this time.'
|
||||
};
|
||||
setMessages(prev => [...prev, aiResponse]);
|
||||
} catch (error) {
|
||||
console.error('Chat error:', error);
|
||||
const errorResponse = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant' as const,
|
||||
text: `I apologize, but I encountered an error: ${error.message}. Please try again.`
|
||||
};
|
||||
setMessages(prev => [...prev, errorResponse]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-4xl mx-auto h-full flex flex-col">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-[--text-primary]">Chat with AI</h1>
|
||||
<p className="text-[--text-secondary] mt-1">Ask questions about your employees and organization</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 flex flex-col md:flex-row md:items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm text-[--text-secondary]">Focus Employee:</label>
|
||||
<select
|
||||
className="px-2 py-1 text-sm bg-[--background-secondary] border border-[--border-color] rounded"
|
||||
value={selectedEmployeeId}
|
||||
onChange={e => setSelectedEmployeeId(e.target.value)}
|
||||
>
|
||||
<option value="">(Organization)</option>
|
||||
{employees.map(emp => <option key={emp.id} value={emp.id}>{emp.name}</option>)}
|
||||
</select>
|
||||
{selectedEmployeeId && !selectedReport && (
|
||||
<Button size="sm" variant="secondary" onClick={() => generateEmployeeReport(employees.find(e => e.id === selectedEmployeeId)!)}>
|
||||
Generate Report
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{messages.length === 0 && (
|
||||
<Card className="mb-6">
|
||||
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Get started with these questions:</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{dynamicStarters.map((starter, idx) => (
|
||||
<Button
|
||||
key={idx}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="text-left justify-start"
|
||||
onClick={() => handleSend(starter)}
|
||||
>
|
||||
{starter}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="flex-1 overflow-y-auto mb-4 space-y-4">
|
||||
{messages.map(message => (
|
||||
<div key={message.id} className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
||||
<div className={`max-w-[70%] p-4 rounded-lg ${message.role === 'user'
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-[--background-secondary] text-[--text-primary]'
|
||||
}`}>
|
||||
{message.text}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{isLoading && (
|
||||
<div className="flex justify-start">
|
||||
<div className="bg-[--background-secondary] text-[--text-primary] p-4 rounded-lg">
|
||||
<div className="flex space-x-1">
|
||||
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce"></div>
|
||||
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
|
||||
<div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card padding="sm">
|
||||
<div className="flex space-x-2">
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleSend()}
|
||||
placeholder="Ask about employees, reports, or company insights..."
|
||||
className="flex-1 px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-secondary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<Button onClick={() => handleSend()} disabled={isLoading || !input.trim()}>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<ChatLayout>
|
||||
<ChatEmptyState />
|
||||
</ChatLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user