import * as React from 'react';
import { Conversation, AnalyticsData, ChatMessage } from '../types';
import { subDays, format, eachDayOfInterval, startOfDay } from 'date-fns';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { Icon } from './ui/Icon';
import { useTranslation } from '../hooks/useI18n';
const STOP_WORDS_LIST = ['i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you', 'your', 'yours', 'yourself', 'yourselves', 'he', 'him', 'his', 'himself', 'she', 'her', 'hers', 'herself', 'it', 'its', 'itself', 'they', 'them', 'their', 'theirs', 'themselves', 'what', 'which', 'who', 'whom', 'this', 'that', 'these', 'those', 'am', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'having', 'do', 'does', 'did', 'doing', 'a', 'an', 'the', 'and', 'but', 'if', 'or', 'because', 'as', 'until', 'while', 'of', 'at', 'by', 'for', 'with', 'about', 'against', 'between', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'to', 'from', 'up', 'down', 'in', 'out', 'on', 'off', 'over', 'under', 'again', 'further', 'then', 'once', 'here', 'there', 'when', 'where', 'why', 'how', 'all', 'any', 'both', 'each', 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 's', 't', 'can', 'will', 'just', 'don', 'should', 'now'];
const STOP_WORDS = new Set(STOP_WORDS_LIST);
const EditResponseModal: React.FC<{
message: ChatMessage;
onClose: () => void;
onSave: (newText: string) => void;
}> = ({ message, onClose, onSave }) => {
const { t } = useTranslation();
const [text, setText] = React.useState(message.text);
return (
e.stopPropagation()}>
{t('analytics.interactions.edit_response_title')}
);
};
interface InteractionAnalyticsProps {
analyticsData: AnalyticsData;
onUpdateConversations: (conversations: Conversation[]) => void;
}
const InteractionAnalytics: React.FC = ({ analyticsData, onUpdateConversations }) => {
// FIX: Destructure 'language' from useTranslation hook to make it available for 'toLocaleString'.
const { t, dateLocale, language } = useTranslation();
const { conversations } = analyticsData;
const [dateFilter, setDateFilter] = React.useState('28');
const [statusFilter, setStatusFilter] = React.useState<'all' | 'open' | 'resolved'>('all');
const [searchTerm, setSearchTerm] = React.useState('');
const [isDarkMode, setIsDarkMode] = React.useState(document.documentElement.classList.contains('dark'));
const [editingMessage, setEditingMessage] = React.useState<{convId: string, msg: ChatMessage} | null>(null);
React.useEffect(() => {
const observer = new MutationObserver(() => setIsDarkMode(document.documentElement.classList.contains('dark')));
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
return () => observer.disconnect();
}, []);
const chartColors = {
grid: isDarkMode ? '#374151' : '#e5e7eb',
text: isDarkMode ? '#9CA3AF' : '#6b7280',
tooltipBg: isDarkMode ? '#1F2937' : '#FFFFFF',
tooltipBorder: isDarkMode ? '#4B5563' : '#d1d5db',
bar1: isDarkMode ? '#10b981' : '#10b981',
bar2: isDarkMode ? '#3b82f6' : '#3b82f6',
};
const filteredConversations = React.useMemo(() => {
const dateLimit = subDays(new Date(), parseInt(dateFilter, 10));
return conversations.filter(c => {
const conversationDate = new Date(c.timestamp);
if (conversationDate < dateLimit) return false;
if (statusFilter !== 'all' && c.status !== statusFilter) return false;
if (searchTerm && !c.interactions.some(i => i.text.toLowerCase().includes(searchTerm.toLowerCase()))) return false;
return true;
});
}, [conversations, dateFilter, statusFilter, searchTerm]);
const analyticsSummary = React.useMemo(() => {
const total = filteredConversations.length;
if (total === 0) return { total: 0, resolved: 0, resolutionRate: '0%', avgInteractions: 0, avgResponseTime: 'N/A', conversationsByDay: [], commonTopics: [], visitorBreakdown: [] };
const resolved = filteredConversations.filter(c => c.status === 'resolved').length;
const resolutionRate = `${((resolved / total) * 100).toFixed(0)}%`;
const totalInteractions = filteredConversations.reduce((sum, c) => sum + c.interactions.length, 0);
const avgInteractions = parseFloat((totalInteractions / total).toFixed(1));
const responseTimes = filteredConversations.map(c => c.firstResponseTime || 0).filter(t => t > 0);
const avgResponseTimeSec = responseTimes.length > 0 ? responseTimes.reduce((sum, t) => sum + t, 0) / responseTimes.length : 0;
const avgResponseTime = avgResponseTimeSec > 0 ? `${Math.round(avgResponseTimeSec)}s` : 'N/A';
const daysInRange = eachDayOfInterval({ start: subDays(new Date(), parseInt(dateFilter) - 1), end: new Date() });
const conversationsByDay = daysInRange.map(day => ({
date: format(day, 'MMM d', { locale: dateLocale }),
conversations: filteredConversations.filter(c => format(startOfDay(new Date(c.timestamp)), 'MMM d', { locale: dateLocale }) === format(day, 'MMM d', { locale: dateLocale })).length
}));
const wordCounts: { [key: string]: number } = {};
filteredConversations.forEach(c => c.interactions.forEach(i => { if(i.sender === 'user') { const words = i.text.toLowerCase().replace(/[^\w\s]/g, '').split(/\s+/); words.forEach(word => { if (word && !STOP_WORDS.has(word)) { wordCounts[word] = (wordCounts[word] || 0) + 1; } }); }}));
const commonTopics = Object.entries(wordCounts).sort(([,a],[,b]) => b - a).slice(0, 7).map(([name, count]) => ({ name, count }));
const visitorStats: {[key: string]: { visits: number, conversations: number}} = {};
filteredConversations.forEach(c => {
if(!visitorStats[c.visitorId]) visitorStats[c.visitorId] = { visits: c.visitCount, conversations: 0 };
visitorStats[c.visitorId].conversations++;
});
const visitorBreakdown = Object.entries(visitorStats).map(([id, stats]) => ({ id, ...stats })).sort((a,b) => b.conversations - a.conversations);
return { total, resolved, resolutionRate, avgInteractions, avgResponseTime, conversationsByDay, commonTopics, visitorBreakdown };
}, [filteredConversations, dateFilter, dateLocale]);
const handleMarkAsResolved = (id: string) => onUpdateConversations(conversations.map(c => (c.id === id ? { ...c, status: 'resolved' } : c)));
const handleSaveResponse = (newText: string) => {
if(!editingMessage) return;
const { convId, msg } = editingMessage;
const updatedConversations = conversations.map(c => {
if (c.id === convId) {
return { ...c, interactions: c.interactions.map(i => i.id === msg.id ? { ...i, text: newText } : i) };
}
return c;
});
onUpdateConversations(updatedConversations);
setEditingMessage(null);
alert(t('analytics.interactions.update_success'));
};
return (
{editingMessage &&
setEditingMessage(null)} onSave={handleSaveResponse} />}
{t('analytics.interactions.title')}
{(['all', 'open', 'resolved'] as const).map(status => (
))}
{t('analytics.interactions.total_conversations')}
{analyticsSummary.total}
{t('analytics.interactions.resolution_rate')}
{analyticsSummary.resolutionRate}
{t('analytics.interactions.avg_interactions')}
{analyticsSummary.avgInteractions}
{t('analytics.interactions.avg_first_response')}
{analyticsSummary.avgResponseTime}
{t('analytics.interactions.visitor_breakdown')}
| {t('analytics.interactions.visitor')} | {t('analytics.interactions.conversations')} |
{analyticsSummary.visitorBreakdown.map(v => | {v.id} | {v.conversations} |
)}
{t('analytics.interactions.common_topics')}
{filteredConversations.length > 0 ? filteredConversations.map(conv => (
{t('analytics.interactions.visitor')} {conv.visitorId}
{new Date(conv.timestamp).toLocaleString(language)}
{conv.visitCount} visits
{conv.status}
{conv.status === 'open' && ( )}
{conv.interactions.map(msg => (
{msg.sender === 'ai' &&
🤖
}
{msg.text}
{msg.sender === 'ai' &&
}
{msg.sender === 'user' &&
👤
}
))}
)) : (
{t('analytics.interactions.no_conversations')}
)}
);
};
export default InteractionAnalytics;