UNPKG

create-automaticgpt-template

Version:

AutomaticGPT - A production-ready Expo template with AI chat, authentication, conversation management, analytics, and sharing features

411 lines (359 loc) 14 kB
/** * useConversationAnalytics Hook * Provides advanced analytics for conversations and messages */ import { useState, useEffect, useCallback } from 'react'; import { db } from '@/lib/supabase'; import { useAuth } from '@/features/auth/hooks/useAuth'; export interface ConversationStats { totalConversations: number; totalMessages: number; totalTokensUsed: number; averageMessagesPerConversation: number; averageResponseTime: number; mostUsedModels: { model: string; count: number; tokens: number }[]; conversationsByDay: { date: string; count: number }[]; messagesByDay: { date: string; count: number; tokens: number }[]; toolUsageStats: { tool: string; count: number; successRate: number }[]; conversationLengthDistribution: { range: string; count: number }[]; responseTimeDistribution: { range: string; count: number }[]; } export interface ConversationAnalytics { id: string; title: string; messageCount: number; totalTokens: number; averageResponseTime: number; createdAt: string; lastMessageAt: string; duration: number; // in minutes toolsUsed: string[]; modelsUsed: string[]; userMessageCount: number; assistantMessageCount: number; } interface UseConversationAnalyticsReturn { stats: ConversationStats | null; analytics: ConversationAnalytics[]; loading: boolean; error: Error | null; refreshStats: () => Promise<void>; getConversationAnalytics: (conversationId: string) => Promise<ConversationAnalytics | null>; getDateRangeStats: (startDate: string, endDate: string) => Promise<ConversationStats | null>; exportData: (format: 'json' | 'csv') => Promise<string | null>; } export const useConversationAnalytics = (): UseConversationAnalyticsReturn => { const [stats, setStats] = useState<ConversationStats | null>(null); const [analytics, setAnalytics] = useState<ConversationAnalytics[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState<Error | null>(null); const { user } = useAuth(); const fetchStats = useCallback(async () => { if (!user?.id) return; try { setLoading(true); setError(null); // Get basic conversation stats const { data: conversations } = await db.getConversations(user.id); if (!conversations) return; // Calculate comprehensive stats const statsPromises = conversations.map(async (conv) => { const { data: messages } = await db.getMessages(conv.id); return { conversation: conv, messages: messages || [] }; }); const conversationData = await Promise.all(statsPromises); // Process analytics const totalConversations = conversations.length; const allMessages = conversationData.flatMap(({ messages }) => messages); const totalMessages = allMessages.length; const totalTokensUsed = allMessages.reduce((sum, msg) => sum + (msg.tokens_used || 0), 0); // Calculate averages const averageMessagesPerConversation = totalConversations > 0 ? totalMessages / totalConversations : 0; const responseTimes = allMessages .filter((msg) => msg.role === 'assistant' && msg.response_time_ms) .map((msg) => msg.response_time_ms!); const averageResponseTime = responseTimes.length > 0 ? responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length : 0; // Model usage stats const modelStats = new Map<string, { count: number; tokens: number }>(); allMessages.forEach((msg) => { if (msg.model_used) { const existing = modelStats.get(msg.model_used) || { count: 0, tokens: 0 }; modelStats.set(msg.model_used, { count: existing.count + 1, tokens: existing.tokens + (msg.tokens_used || 0), }); } }); const mostUsedModels = Array.from(modelStats.entries()) .map(([model, stats]) => ({ model, ...stats })) .sort((a, b) => b.count - a.count) .slice(0, 10); // Time-based analytics const conversationsByDay = getDataByDay(conversations.map((c) => c.created_at)); const messagesByDay = getMessagesByDay(allMessages); // Tool usage analytics const toolUsageStats = getToolUsageStats(allMessages); // Distribution analytics const conversationLengthDistribution = getConversationLengthDistribution(conversationData); const responseTimeDistribution = getResponseTimeDistribution(responseTimes); const analyticsStats: ConversationStats = { totalConversations, totalMessages, totalTokensUsed, averageMessagesPerConversation, averageResponseTime, mostUsedModels, conversationsByDay, messagesByDay, toolUsageStats, conversationLengthDistribution, responseTimeDistribution, }; setStats(analyticsStats); // Generate detailed analytics for each conversation const detailedAnalytics: ConversationAnalytics[] = conversationData.map( ({ conversation, messages }) => { const userMessages = messages.filter((m) => m.role === 'user'); const assistantMessages = messages.filter((m) => m.role === 'assistant'); const totalTokens = messages.reduce((sum, m) => sum + (m.tokens_used || 0), 0); const avgResponseTime = assistantMessages.length > 0 ? assistantMessages.reduce((sum, m) => sum + (m.response_time_ms || 0), 0) / assistantMessages.length : 0; const firstMessage = messages[0]; const lastMessage = messages[messages.length - 1]; const duration = firstMessage && lastMessage ? Math.round( (new Date(lastMessage.created_at).getTime() - new Date(firstMessage.created_at).getTime()) / (1000 * 60) ) : 0; const toolsUsed = messages .flatMap((m) => (m.tool_calls as any[]) || []) .map((tool) => tool.function?.name || tool.type) .filter((name): name is string => typeof name === 'string') as string[]; const modelsUsed = messages .map((m) => m.model_used) .filter((model): model is string => typeof model === 'string') as string[]; return { id: conversation.id, title: conversation.title, messageCount: messages.length, totalTokens, averageResponseTime: avgResponseTime, createdAt: conversation.created_at, lastMessageAt: conversation.updated_at, duration, toolsUsed, modelsUsed, userMessageCount: userMessages.length, assistantMessageCount: assistantMessages.length, }; } ); setAnalytics(detailedAnalytics); } catch (err) { console.error('Error fetching analytics:', err); setError(err instanceof Error ? err : new Error('Failed to fetch analytics')); } finally { setLoading(false); } }, [user?.id]); const getConversationAnalytics = useCallback( async (conversationId: string): Promise<ConversationAnalytics | null> => { try { const { data: conversation } = await db.getConversation(conversationId); const { data: messages } = await db.getMessages(conversationId); if (!conversation || !messages) return null; const userMessages = messages.filter((m) => m.role === 'user'); const assistantMessages = messages.filter((m) => m.role === 'assistant'); const totalTokens = messages.reduce((sum, m) => sum + (m.tokens_used || 0), 0); const avgResponseTime = assistantMessages.length > 0 ? assistantMessages.reduce((sum, m) => sum + (m.response_time_ms || 0), 0) / assistantMessages.length : 0; const firstMessage = messages[0]; const lastMessage = messages[messages.length - 1]; const duration = firstMessage && lastMessage ? Math.round( (new Date(lastMessage.created_at).getTime() - new Date(firstMessage.created_at).getTime()) / (1000 * 60) ) : 0; const toolsUsed = messages .flatMap((m) => (m.tool_calls as any[]) || []) .map((tool) => tool.function?.name || tool.type) .filter((name): name is string => typeof name === 'string') as string[]; const modelsUsed = messages .map((m) => m.model_used) .filter((model): model is string => typeof model === 'string') as string[]; return { id: conversation.id, title: conversation.title, messageCount: messages.length, totalTokens, averageResponseTime: avgResponseTime, createdAt: conversation.created_at, lastMessageAt: conversation.updated_at, duration, toolsUsed, modelsUsed, userMessageCount: userMessages.length, assistantMessageCount: assistantMessages.length, }; } catch (err) { console.error('Error fetching conversation analytics:', err); return null; } }, [] ); const getDateRangeStats = useCallback( async (startDate: string, endDate: string): Promise<ConversationStats | null> => { // This would filter conversations and messages by date range // For now, return current stats as placeholder return stats; }, [stats] ); const exportData = useCallback( async (format: 'json' | 'csv'): Promise<string | null> => { if (!stats || analytics.length === 0) return null; try { if (format === 'json') { return JSON.stringify({ stats, analytics }, null, 2); } else { // Convert to CSV format const headers = [ 'Conversation ID', 'Title', 'Message Count', 'Total Tokens', 'Average Response Time', 'Created At', 'Duration (minutes)', 'Tools Used', 'Models Used', ]; const rows = analytics.map((a) => [ a.id, a.title, a.messageCount, a.totalTokens, a.averageResponseTime.toFixed(2), a.createdAt, a.duration, a.toolsUsed.join(';'), a.modelsUsed.join(';'), ]); const csvContent = [headers, ...rows] .map((row) => row.map((cell) => `"${cell}"`).join(',')) .join('\n'); return csvContent; } } catch (err) { console.error('Error exporting data:', err); return null; } }, [stats, analytics] ); useEffect(() => { fetchStats(); }, [fetchStats]); return { stats, analytics, loading, error, refreshStats: fetchStats, getConversationAnalytics, getDateRangeStats, exportData, }; }; // Helper functions for analytics calculations function getDataByDay(dates: string[]) { const dayMap = new Map<string, number>(); dates.forEach((date) => { const day = new Date(date).toISOString().split('T')[0]; dayMap.set(day, (dayMap.get(day) || 0) + 1); }); return Array.from(dayMap.entries()) .map(([date, count]) => ({ date, count })) .sort((a, b) => a.date.localeCompare(b.date)); } function getMessagesByDay(messages: any[]) { const dayMap = new Map<string, { count: number; tokens: number }>(); messages.forEach((message) => { const day = new Date(message.created_at).toISOString().split('T')[0]; const existing = dayMap.get(day) || { count: 0, tokens: 0 }; dayMap.set(day, { count: existing.count + 1, tokens: existing.tokens + (message.tokens_used || 0), }); }); return Array.from(dayMap.entries()) .map(([date, stats]) => ({ date, ...stats })) .sort((a, b) => a.date.localeCompare(b.date)); } function getToolUsageStats(messages: any[]) { const toolMap = new Map<string, { total: number; successful: number }>(); messages.forEach((message) => { if (message.tool_calls && Array.isArray(message.tool_calls)) { message.tool_calls.forEach((tool: any) => { const toolName = tool.function?.name || tool.type || 'unknown'; const existing = toolMap.get(toolName) || { total: 0, successful: 0 }; toolMap.set(toolName, { total: existing.total + 1, successful: existing.successful + (tool.error ? 0 : 1), }); }); } }); return Array.from(toolMap.entries()) .map(([tool, stats]) => ({ tool, count: stats.total, successRate: stats.total > 0 ? (stats.successful / stats.total) * 100 : 0, })) .sort((a, b) => b.count - a.count); } function getConversationLengthDistribution(conversationData: any[]) { const ranges = [ { min: 0, max: 5, label: '1-5 messages' }, { min: 6, max: 10, label: '6-10 messages' }, { min: 11, max: 25, label: '11-25 messages' }, { min: 26, max: 50, label: '26-50 messages' }, { min: 51, max: Infinity, label: '50+ messages' }, ]; return ranges.map((range) => ({ range: range.label, count: conversationData.filter( ({ messages }) => messages.length >= range.min && messages.length <= range.max ).length, })); } function getResponseTimeDistribution(responseTimes: number[]) { const ranges = [ { min: 0, max: 1000, label: '< 1s' }, { min: 1000, max: 3000, label: '1-3s' }, { min: 3000, max: 5000, label: '3-5s' }, { min: 5000, max: 10000, label: '5-10s' }, { min: 10000, max: Infinity, label: '10s+' }, ]; return ranges.map((range) => ({ range: range.label, count: responseTimes.filter((time) => time >= range.min && time < range.max).length, })); }