- Add detailed report viewing with full-screen ReportDetail component for both company and employee reports - Fix company wiki to display onboarding Q&A in card format matching Figma designs - Exclude company owners from employee submission counts (owners contribute to wiki, not employee data) - Fix employee report generation to include company context (wiki + company report + employee answers) - Fix company report generation to use filtered employee submissions only - Add proper error handling for submission data format variations - Update Firebase functions to use gpt-4o model instead of deprecated gpt-4.1 - Fix UI syntax errors and improve report display functionality - Add comprehensive logging for debugging report generation flow 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
167 lines
8.1 KiB
TypeScript
167 lines
8.1 KiB
TypeScript
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';
|
|
|
|
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>
|
|
);
|
|
};
|
|
|
|
export default Chat;
|