UNPKG

claude-usage-tracker

Version:

Advanced analytics for Claude Code usage with cost optimization, conversation length analysis, and rate limit tracking

837 lines 38.3 kB
import { calculateCost } from "./analyzer.js"; export class PatternAnalyzer { analyzeConversationLengthPatterns(entries) { const conversations = this.groupByConversation(entries); // Categorize conversations const quickQuestions = []; const detailedDiscussions = []; const deepDives = []; const lengthTotals = { quickQuestions: 0, detailedDiscussions: 0, deepDives: 0, }; const costTotals = { quickQuestions: { avgCost: 0, totalCost: 0 }, detailedDiscussions: { avgCost: 0, totalCost: 0 }, deepDives: { avgCost: 0, totalCost: 0 }, }; for (const [conversationId, convEntries] of conversations) { const messageCount = convEntries.length; const totalCost = convEntries.reduce((sum, e) => sum + calculateCost(e), 0); if (messageCount <= 2) { quickQuestions.push(conversationId); lengthTotals.quickQuestions += messageCount; costTotals.quickQuestions.totalCost += totalCost; } else if (messageCount <= 8) { detailedDiscussions.push(conversationId); lengthTotals.detailedDiscussions += messageCount; costTotals.detailedDiscussions.totalCost += totalCost; } else { deepDives.push(conversationId); lengthTotals.deepDives += messageCount; costTotals.deepDives.totalCost += totalCost; } } // Calculate averages const avgLengthByType = { quickQuestions: quickQuestions.length > 0 ? lengthTotals.quickQuestions / quickQuestions.length : 0, detailedDiscussions: detailedDiscussions.length > 0 ? lengthTotals.detailedDiscussions / detailedDiscussions.length : 0, deepDives: deepDives.length > 0 ? lengthTotals.deepDives / deepDives.length : 0, }; // Calculate cost distributions const costDistribution = { quickQuestions: { avgCost: quickQuestions.length > 0 ? costTotals.quickQuestions.totalCost / quickQuestions.length : 0, totalCost: costTotals.quickQuestions.totalCost, }, detailedDiscussions: { avgCost: detailedDiscussions.length > 0 ? costTotals.detailedDiscussions.totalCost / detailedDiscussions.length : 0, totalCost: costTotals.detailedDiscussions.totalCost, }, deepDives: { avgCost: deepDives.length > 0 ? costTotals.deepDives.totalCost / deepDives.length : 0, totalCost: costTotals.deepDives.totalCost, }, }; // Generate efficiency insights const totalTokens = entries.reduce((sum, e) => sum + e.total_tokens, 0); const totalExchanges = Array.from(conversations.values()).reduce((sum, conv) => sum + conv.length, 0); const avgTokensPerExchange = totalExchanges > 0 ? totalTokens / totalExchanges : 0; // Determine most/least efficient types const efficiencies = [ { type: "quickQuestions", efficiency: avgLengthByType.quickQuestions > 0 ? costTotals.quickQuestions.totalCost / (avgLengthByType.quickQuestions * quickQuestions.length || 1) : 0, }, { type: "detailedDiscussions", efficiency: avgLengthByType.detailedDiscussions > 0 ? costTotals.detailedDiscussions.totalCost / (avgLengthByType.detailedDiscussions * detailedDiscussions.length || 1) : 0, }, { type: "deepDives", efficiency: avgLengthByType.deepDives > 0 ? costTotals.deepDives.totalCost / (avgLengthByType.deepDives * deepDives.length || 1) : 0, }, ].filter((e) => e.efficiency > 0); const mostEfficientType = efficiencies.length > 0 ? efficiencies.reduce((min, curr) => curr.efficiency < min.efficiency ? curr : min).type : "quickQuestions"; const leastEfficientType = efficiencies.length > 0 ? efficiencies.reduce((max, curr) => curr.efficiency > max.efficiency ? curr : max).type : "deepDives"; const efficiencyInsights = { mostEfficientType, leastEfficientType, avgTokensPerExchange, }; // Generate recommendations const recommendations = []; if (deepDives.length > conversations.size * 0.3) { recommendations.push("Consider breaking down complex tasks into smaller conversations"); } if (quickQuestions.length > conversations.size * 0.5) { recommendations.push("Quick questions are efficient - continue this pattern"); } if (avgTokensPerExchange < 1000) { recommendations.push("Consider asking more comprehensive questions to get fuller responses"); } return { conversationTypes: { quickQuestions: { count: quickQuestions.length }, detailedDiscussions: { count: detailedDiscussions.length }, deepDives: { count: deepDives.length }, }, avgLengthByType, costDistribution, efficiencyInsights, recommendations, }; } analyzeTimeToCompletion(entries) { const conversations = this.groupByConversation(entries); const taskTypes = new Map(); for (const [_, convEntries] of conversations) { const taskType = this.inferTaskType(convEntries); const completionTime = this.calculateCompletionTime(convEntries); const success = this.inferTaskSuccess(convEntries); const cost = convEntries.reduce((sum, e) => sum + calculateCost(e), 0); const tokens = convEntries.reduce((sum, e) => sum + e.total_tokens, 0); if (!taskTypes.has(taskType)) { taskTypes.set(taskType, []); } taskTypes.get(taskType)?.push({ completionTime, success, cost, tokens, }); } const analyses = []; for (const [taskType, data] of taskTypes) { if (data.length < 3) continue; // Need enough data const completionTimes = data.map((d) => d.completionTime); const successRate = data.filter((d) => d.success).length / data.length; const avgCompletionTime = completionTimes.reduce((sum, t) => sum + t, 0) / completionTimes.length; const medianCompletionTime = this.calculateMedian(completionTimes); const efficiencyTrend = this.calculateEfficiencyTrend(data); const optimalSessionLength = this.findOptimalSessionLength(data); analyses.push({ taskType, avgCompletionTime, medianCompletionTime, successRate, efficiencyTrend, optimalSessionLength, recommendations: this.generateCompletionRecommendations(taskType, { avgCompletionTime, successRate, efficiencyTrend, }), }); } return analyses.sort((a, b) => b.successRate - a.successRate); } analyzeTaskSwitchingPatterns(entries) { const conversations = this.groupByConversation(entries); const sortedConversations = Array.from(conversations.entries()) .map(([id, convEntries]) => { try { return { conversationId: id, taskType: this.inferTaskType(convEntries), startTime: new Date(convEntries[0].timestamp), endTime: new Date(convEntries[convEntries.length - 1].timestamp), cost: convEntries.reduce((sum, e) => sum + calculateCost(e), 0), }; } catch (error) { // Return null for invalid timestamps, filter out later return null; } }) .filter((conv) => conv !== null) .sort((a, b) => a.startTime.getTime() - b.startTime.getTime()); // Calculate switching metrics const switches = []; let totalSwitchingCost = 0; for (let i = 1; i < sortedConversations.length; i++) { const prev = sortedConversations[i - 1]; const curr = sortedConversations[i]; if (prev.taskType !== curr.taskType) { const gapTime = (curr.startTime.getTime() - prev.endTime.getTime()) / (1000 * 60); // minutes switches.push({ from: prev.taskType, to: curr.taskType, gapTime, switchingCost: this.estimateSwitchingCost(prev, curr), }); totalSwitchingCost += switches[switches.length - 1].switchingCost; } } // Analyze transition patterns const transitionMap = new Map(); for (const sw of switches) { const key = `${sw.from}->${sw.to}`; if (!transitionMap.has(key)) { transitionMap.set(key, []); } transitionMap.get(key)?.push({ gapTime: sw.gapTime }); } const mostCommonTransitions = Array.from(transitionMap.entries()) .map(([transition, data]) => { const [from, to] = transition.split("->"); return { from, to, frequency: data.length, avgGapTime: data.reduce((sum, d) => sum + d.gapTime, 0) / data.length, }; }) .sort((a, b) => b.frequency - a.frequency) .slice(0, 5); // Handle case with insufficient data if (sortedConversations.length < 2) { return { switchFrequency: 0, avgTimeBetweenSwitches: 0, costOfSwitching: 0, mostCommonTransitions: [], recommendations: [ "Need more conversation data to analyze task switching patterns", ], }; } const totalDays = Math.max(1, (sortedConversations[sortedConversations.length - 1].startTime.getTime() - sortedConversations[0].startTime.getTime()) / (1000 * 60 * 60 * 24)); // Calculate average time between all conversations (not just switches) let totalGapTime = 0; for (let i = 1; i < sortedConversations.length; i++) { const prev = sortedConversations[i - 1]; const curr = sortedConversations[i]; const gapTime = (curr.startTime.getTime() - prev.endTime.getTime()) / (1000 * 60); // minutes totalGapTime += gapTime; } const avgTimeBetweenConversations = sortedConversations.length > 1 ? totalGapTime / (sortedConversations.length - 1) : 0; return { switchFrequency: switches.length / totalDays, avgTimeBetweenSwitches: avgTimeBetweenConversations, costOfSwitching: totalSwitchingCost, mostCommonTransitions, recommendations: this.generateSwitchingRecommendations(switches.length / totalDays, mostCommonTransitions), }; } analyzeLearningCurve(entries, skillArea = "coding") { const relevantConversations = this.filterBySkillArea(entries, skillArea); const conversations = this.groupByConversation(relevantConversations); // Sort conversations by time const sortedConversations = Array.from(conversations.entries()) .map(([id, convEntries]) => ({ conversationId: id, startTime: new Date(convEntries[0].timestamp), efficiency: convEntries.reduce((sum, e) => sum + e.total_tokens, 0) / Math.max(convEntries.reduce((sum, e) => sum + calculateCost(e), 0), 0.001), cost: convEntries.reduce((sum, e) => sum + calculateCost(e), 0), success: this.inferTaskSuccess(convEntries), })) .sort((a, b) => a.startTime.getTime() - b.startTime.getTime()); if (sortedConversations.length < 5) { return this.createDefaultLearningAnalysis(skillArea); } // Calculate learning metrics const early = sortedConversations.slice(0, Math.ceil(sortedConversations.length * 0.2)); const recent = sortedConversations.slice(-Math.ceil(sortedConversations.length * 0.2)); const initialEfficiency = early.reduce((sum, c) => sum + c.efficiency, 0) / early.length; const currentEfficiency = recent.reduce((sum, c) => sum + c.efficiency, 0) / recent.length; const timeSpan = (sortedConversations[sortedConversations.length - 1].startTime.getTime() - sortedConversations[0].startTime.getTime()) / (1000 * 60 * 60 * 24 * 7); // weeks const improvementRate = timeSpan > 0 ? (((currentEfficiency - initialEfficiency) / initialEfficiency) * 100) / timeSpan : 0; // Detect plateau const recentTrend = this.calculateRecentEfficiencyTrend(sortedConversations.slice(-10)); const plateauDetected = Math.abs(recentTrend) < 0.05; // Less than 5% change const learningPhase = this.determineLearningPhase(currentEfficiency, improvementRate, sortedConversations.length); return { skillArea, initialEfficiency, currentEfficiency, improvementRate, plateauDetected, timeToCompetency: this.estimateTimeToCompetency(improvementRate, currentEfficiency), costToCompetency: this.estimateCostToCompetency(sortedConversations), learningPhase, nextMilestone: this.suggestNextMilestone(learningPhase, currentEfficiency), }; } identifyUsagePatterns(entries) { const patterns = []; // Peak hours pattern const hourlyUsage = this.analyzeHourlyDistribution(entries); const peakHoursPattern = this.detectPeakHoursPattern(hourlyUsage); if (peakHoursPattern) patterns.push(peakHoursPattern); // Model preference pattern const modelPreference = this.analyzeModelPreference(entries); if (modelPreference) patterns.push(modelPreference); // Cost sensitivity pattern const costSensitivity = this.analyzeCostSensitivity(entries); if (costSensitivity) patterns.push(costSensitivity); // Efficiency cycles pattern const efficiencyCycles = this.detectEfficiencyCycles(entries); if (efficiencyCycles) patterns.push(efficiencyCycles); return patterns.sort((a, b) => b.strength - a.strength); } // Private helper methods groupByConversation(entries) { const conversations = new Map(); for (const entry of entries) { if (!conversations.has(entry.conversationId)) { conversations.set(entry.conversationId, []); } conversations.get(entry.conversationId)?.push(entry); } return conversations; } categorizeConversationLength(messageCount) { if (messageCount <= 5) return "short"; if (messageCount <= 15) return "medium"; if (messageCount <= 30) return "long"; return "extended"; } calculateLengthTrend(entries, category) { // Simple trend analysis - compare recent vs historical conversation lengths const now = new Date(); const recentCutoff = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000); // Last 2 weeks const recent = entries.filter((e) => new Date(e.timestamp) >= recentCutoff); const historical = entries.filter((e) => new Date(e.timestamp) < recentCutoff); const recentConversations = this.groupByConversation(recent); const historicalConversations = this.groupByConversation(historical); const recentAvgLength = Array.from(recentConversations.values()) .filter((conv) => this.categorizeConversationLength(conv.length) === category) .reduce((sum, conv) => sum + conv.length, 0) / Math.max(recentConversations.size, 1); const historicalAvgLength = Array.from(historicalConversations.values()) .filter((conv) => this.categorizeConversationLength(conv.length) === category) .reduce((sum, conv) => sum + conv.length, 0) / Math.max(historicalConversations.size, 1); const change = (recentAvgLength - historicalAvgLength) / Math.max(historicalAvgLength, 1); if (change > 0.2) return "increasing"; if (change < -0.2) return "decreasing"; return "stable"; } inferTaskType(entries) { const avgTokens = entries.reduce((sum, e) => sum + e.total_tokens, 0) / entries.length; const conversationLength = entries.length; const avgPromptTokens = entries.reduce((sum, e) => sum + e.prompt_tokens, 0) / entries.length; // Use conversation ID as a hint for task type to ensure different classifications const conversationId = entries[0]?.conversationId || ""; // More granular classification for better task switching detection if (conversationId.includes("coding")) return "coding_task"; if (conversationId.includes("writing")) return "writing_task"; if (conversationId.includes("analysis")) return "analysis_task"; if (conversationId.includes("question")) return "query_task"; // Fallback to token-based classification if (conversationLength > 15 && avgTokens > 3000) return "complex_coding"; if (avgTokens > 4000) return "analysis"; if (avgTokens > 2500) return "research"; if (conversationLength > 20) return "debugging"; if (avgTokens < 1000) return "simple_query"; if (avgPromptTokens > 1500) return "detailed_coding"; return "general_coding"; } calculateCompletionTime(entries) { if (entries.length < 2) return 0; const start = new Date(entries[0].timestamp); const end = new Date(entries[entries.length - 1].timestamp); return (end.getTime() - start.getTime()) / (1000 * 60); // minutes } inferTaskSuccess(entries) { // Heuristic: shorter conversations with reasonable token usage suggest success const avgTokens = entries.reduce((sum, e) => sum + e.total_tokens, 0) / entries.length; const conversationLength = entries.length; // Success indicators: moderate length, good token efficiency return (conversationLength >= 3 && conversationLength <= 25 && avgTokens > 1000); } calculateMedian(numbers) { const sorted = [...numbers].sort((a, b) => a - b); const mid = Math.floor(sorted.length / 2); return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid]; } calculateEfficiencyTrend(data) { if (data.length < 5) return "stable"; const efficiencies = data.map((d) => d.tokens / Math.max(d.cost, 0.001)); const first = efficiencies.slice(0, Math.ceil(efficiencies.length / 2)); const second = efficiencies.slice(Math.floor(efficiencies.length / 2)); const firstAvg = first.reduce((sum, e) => sum + e, 0) / first.length; const secondAvg = second.reduce((sum, e) => sum + e, 0) / second.length; const change = (secondAvg - firstAvg) / firstAvg; if (change > 0.15) return "improving"; if (change < -0.15) return "declining"; return "stable"; } findOptimalSessionLength(data) { // Find completion time that maximizes success rate const successful = data.filter((d) => d.success); if (successful.length === 0) return 30; // Default 30 minutes const avgSuccessfulTime = successful.reduce((sum, d) => sum + d.completionTime, 0) / successful.length; return Math.round(avgSuccessfulTime); } generateCompletionRecommendations(taskType, metrics) { const recommendations = []; if (metrics.successRate < 0.7) { recommendations.push(`${taskType}: Break down tasks into smaller chunks (success rate: ${(metrics.successRate * 100).toFixed(0)}%)`); } if (metrics.avgCompletionTime > 120) { recommendations.push(`${taskType}: Consider time-boxing sessions to 2 hours max`); } if (metrics.efficiencyTrend === "declining") { recommendations.push(`${taskType}: Efficiency declining - review approach or take breaks`); } return recommendations; } estimateSwitchingCost(prev, curr) { // Estimate context switching overhead const gapTime = (curr.startTime.getTime() - prev.endTime.getTime()) / (1000 * 60 * 60); // hours // Shorter gaps suggest more expensive context switching if (gapTime < 0.5) return 0.1; // High switching cost if (gapTime < 2) return 0.05; // Medium switching cost return 0.01; // Low switching cost } generateSwitchingRecommendations(switchFrequency, transitions) { const recommendations = []; if (switchFrequency > 3) { recommendations.push("High task switching detected - consider batching similar tasks"); } else if (switchFrequency < 1) { recommendations.push("Good focused work patterns detected - maintain concentrated effort on similar tasks"); } const mostCommon = transitions[0]; if (mostCommon && mostCommon.frequency > 3) { recommendations.push(`Frequent ${mostCommon.from}${mostCommon.to} transitions - consider dedicated time blocks`); } return recommendations; } filterBySkillArea(entries, _skillArea) { // Simple filter - in practice this would be more sophisticated return entries; // For now, return all entries } createDefaultLearningAnalysis(skillArea) { return { skillArea, initialEfficiency: 0, currentEfficiency: 0, improvementRate: 0, plateauDetected: false, timeToCompetency: 0, costToCompetency: 0, learningPhase: "novice", nextMilestone: "Complete 10 successful coding conversations", }; } calculateRecentEfficiencyTrend(conversations) { if (conversations.length < 3) return 0; const first = conversations.slice(0, Math.ceil(conversations.length / 2)); const second = conversations.slice(Math.floor(conversations.length / 2)); const firstAvg = first.reduce((sum, c) => sum + c.efficiency, 0) / first.length; const secondAvg = second.reduce((sum, c) => sum + c.efficiency, 0) / second.length; return (secondAvg - firstAvg) / firstAvg; } determineLearningPhase(efficiency, improvementRate, conversationCount) { if (conversationCount < 10) return "novice"; if (efficiency < 5000 || improvementRate > 20) return "developing"; if (efficiency < 10000 || improvementRate > 5) return "competent"; return "expert"; } estimateTimeToCompetency(improvementRate, currentEfficiency) { const targetEfficiency = 8000; // Tokens per dollar if (currentEfficiency >= targetEfficiency) return 0; if (improvementRate <= 0) return 365; // Default 1 year const weeksNeeded = (targetEfficiency - currentEfficiency) / ((currentEfficiency * improvementRate) / 100); return Math.ceil(weeksNeeded * 7); // Convert to days } estimateCostToCompetency(conversations) { const avgCostPerConversation = conversations.reduce((sum, c) => sum + c.cost, 0) / conversations.length; const conversationsPerWeek = conversations.length / Math.max(1, (conversations[conversations.length - 1].startTime.getTime() - conversations[0].startTime.getTime()) / (1000 * 60 * 60 * 24 * 7)); return avgCostPerConversation * conversationsPerWeek * 12; // 12 weeks to competency } suggestNextMilestone(phase, _efficiency) { switch (phase) { case "novice": return "Achieve 5000 tokens per dollar efficiency"; case "developing": return "Complete 25 successful coding conversations"; case "competent": return "Achieve 10000 tokens per dollar efficiency"; case "expert": return "Mentor others and optimize workflows"; default: return "Continue improving"; } } analyzeHourlyDistribution(entries) { const hourlyMap = new Map(); for (const entry of entries) { const hour = new Date(entry.timestamp).getHours(); const cost = calculateCost(entry); hourlyMap.set(hour, (hourlyMap.get(hour) || 0) + cost); } return hourlyMap; } detectPeakHoursPattern(hourlyUsage) { const hours = Array.from(hourlyUsage.entries()).sort((a, b) => b[1] - a[1]); const topHours = hours.slice(0, 3).map((h) => h[0]); // Check if there's a clear peak pattern const totalUsage = Array.from(hourlyUsage.values()).reduce((sum, usage) => sum + usage, 0); const topHoursUsage = hours.slice(0, 3).reduce((sum, h) => sum + h[1], 0); if (topHoursUsage / totalUsage > 0.5) { // 50% of usage in top 3 hours return { patternType: "peak_hours", description: `Peak usage hours: ${topHours.join(", ")}:00`, strength: topHoursUsage / totalUsage, impact: "medium", actionable: true, recommendation: "Consider batch processing during peak hours for better efficiency", }; } return null; } analyzeModelPreference(entries) { const modelUsage = new Map(); for (const entry of entries) { const cost = calculateCost(entry); modelUsage.set(entry.model, (modelUsage.get(entry.model) || 0) + cost); } const totalCost = Array.from(modelUsage.values()).reduce((sum, cost) => sum + cost, 0); const opusCost = modelUsage.get("claude-opus-4-20250514") || 0; const opusPercentage = opusCost / totalCost; if (opusPercentage > 0.7) { return { patternType: "model_preference", description: `Strong preference for Opus (${(opusPercentage * 100).toFixed(0)}% of spend)`, strength: opusPercentage, impact: "high", actionable: true, recommendation: "Consider using Sonnet for simpler tasks to reduce costs", }; } return null; } analyzeCostSensitivity(entries) { const dailyCosts = this.getDailyCosts(entries); const avgDailyCost = dailyCosts.reduce((sum, cost) => sum + cost, 0) / dailyCosts.length; const variability = this.calculateStandardDeviation(dailyCosts) / avgDailyCost; if (variability < 0.3) { // Low variability suggests cost consciousness return { patternType: "cost_sensitivity", description: "Consistent daily spending suggests cost awareness", strength: 1 - variability, impact: "medium", actionable: false, }; } return null; } detectEfficiencyCycles(entries) { const conversations = this.groupByConversation(entries); const efficiencies = Array.from(conversations.values()).map((conv) => { const cost = conv.reduce((sum, e) => sum + calculateCost(e), 0); const tokens = conv.reduce((sum, e) => sum + e.total_tokens, 0); return tokens / Math.max(cost, 0.001); }); // Simple cycle detection - look for weekly patterns if (efficiencies.length < 14) return null; const weeklyAvgs = []; for (let i = 0; i < efficiencies.length - 7; i += 7) { const weekEfficiencies = efficiencies.slice(i, i + 7); const avg = weekEfficiencies.reduce((sum, e) => sum + e, 0) / weekEfficiencies.length; weeklyAvgs.push(avg); } const variability = this.calculateStandardDeviation(weeklyAvgs) / (weeklyAvgs.reduce((sum, avg) => sum + avg, 0) / weeklyAvgs.length); if (variability > 0.2) { return { patternType: "efficiency_cycles", description: "Weekly efficiency cycles detected", strength: variability, impact: "medium", actionable: true, recommendation: "Track weekly patterns to optimize high-efficiency periods", }; } return null; } getDailyCosts(entries) { const dailyMap = new Map(); for (const entry of entries) { const date = new Date(entry.timestamp).toDateString(); const cost = calculateCost(entry); dailyMap.set(date, (dailyMap.get(date) || 0) + cost); } return Array.from(dailyMap.values()); } calculateStandardDeviation(values) { const mean = values.reduce((sum, val) => sum + val, 0) / values.length; const squaredDiffs = values.map((val) => (val - mean) ** 2); const avgSquaredDiff = squaredDiffs.reduce((sum, val) => sum + val, 0) / values.length; return Math.sqrt(avgSquaredDiff); } identifyLearningCurves(entries) { const conversations = this.groupByConversation(entries); const periods = []; // Filter out entries with invalid timestamps and sort by date const validEntries = entries.filter((entry) => { const date = new Date(entry.timestamp); return !isNaN(date.getTime()); }); const sortedEntries = validEntries.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()); if (sortedEntries.length === 0) { return { periods: [], overallTrend: "stable", insights: [] }; } // Group by weeks const weeklyData = new Map(); for (const entry of sortedEntries) { try { const date = new Date(entry.timestamp); const weekStart = new Date(date.getFullYear(), date.getMonth(), date.getDate() - date.getDay()); const weekKey = weekStart.toISOString().split("T")[0]; if (!weeklyData.has(weekKey)) { weeklyData.set(weekKey, []); } weeklyData.get(weekKey).push(entry); } catch (error) { // Skip entries with invalid timestamps continue; } } // Analyze each week const weeks = Array.from(weeklyData.entries()).sort(([a], [b]) => a.localeCompare(b)); for (const [weekStart, weekEntries] of weeks) { const weekConversations = this.groupByConversation(weekEntries); const weekEnd = new Date(weekStart); weekEnd.setDate(weekEnd.getDate() + 6); const avgQuestionsPerDay = weekConversations.size / 7; const avgComplexityScore = this.calculateAverageComplexity(weekEntries); const characteristics = []; if (avgQuestionsPerDay > 5) characteristics.push("High activity period"); if (avgComplexityScore > 0.7) characteristics.push("Complex problem solving"); if (avgComplexityScore < 0.3) characteristics.push("Simple queries"); if (characteristics.length === 0) characteristics.push("Moderate usage"); periods.push({ startDate: weekStart, endDate: weekEnd.toISOString().split("T")[0], metrics: { avgQuestionsPerDay, avgComplexityScore, }, characteristics, }); } // Determine overall trend let overallTrend = "stable"; if (periods.length >= 2) { const firstHalf = periods.slice(0, Math.floor(periods.length / 2)); const secondHalf = periods.slice(Math.floor(periods.length / 2)); const firstAvgComplexity = firstHalf.reduce((sum, p) => sum + p.metrics.avgComplexityScore, 0) / firstHalf.length; const secondAvgComplexity = secondHalf.reduce((sum, p) => sum + p.metrics.avgComplexityScore, 0) / secondHalf.length; if (secondAvgComplexity > firstAvgComplexity * 1.1) { overallTrend = "improving"; } else if (secondAvgComplexity < firstAvgComplexity * 0.9) { overallTrend = "declining"; } } const insights = [ periods.length > 4 ? "Sufficient data for trend analysis" : "Limited data for trends", overallTrend === "improving" ? "Complexity of tasks is increasing over time" : overallTrend === "declining" ? "Tasks are becoming simpler over time" : "consistent usage patterns maintained", ]; return { periods, overallTrend, insights, }; } calculateAverageComplexity(entries) { if (entries.length === 0) return 0; const totalTokens = entries.reduce((sum, e) => sum + e.total_tokens, 0); const avgTokens = totalTokens / entries.length; // More granular complexity scoring based on token usage const normalizedComplexity = Math.min(avgTokens / 8000, 1.0); // Lower threshold for better sensitivity return Math.round(normalizedComplexity * 100) / 100; // Round to 2 decimal places } classifyTaskType(entries) { const totalTokens = entries.reduce((sum, e) => sum + e.total_tokens, 0); const avgTokens = totalTokens / entries.length; const messageCount = entries.length; if (messageCount <= 2 && avgTokens < 1000) return "quick-question"; if (avgTokens > 5000 || messageCount > 10) return "complex-analysis"; if (avgTokens > 2000 && messageCount > 3) return "problem-solving"; return "general-assistance"; } groupTasksByType(conversations) { const clusters = new Map(); for (const conv of conversations) { if (!clusters.has(conv.type)) { clusters.set(conv.type, { conversationIds: [], avgDuration: 0, characteristics: [], }); } const cluster = clusters.get(conv.type); cluster.conversationIds.push(conv.id); const duration = (conv.endTime - conv.startTime) / (60 * 1000); // minutes cluster.avgDuration = (cluster.avgDuration * (cluster.conversationIds.length - 1) + duration) / cluster.conversationIds.length; } // Add characteristics for (const [type, cluster] of clusters) { cluster.characteristics = [ `${cluster.conversationIds.length} conversations`, `~${Math.round(cluster.avgDuration)} min average duration`, type.replace("-", " "), ]; } return Array.from(clusters.entries()).map(([type, data]) => ({ type, conversationIds: data.conversationIds, avgDuration: data.avgDuration, characteristics: data.characteristics, })); } calculateLongestFocusedSession(conversations) { let maxFocusedDuration = 0; let currentFocusedStart = conversations[0]?.startTime || 0; let currentType = conversations[0]?.type; for (let i = 1; i < conversations.length; i++) { const conv = conversations[i]; if (conv.type !== currentType) { // Focus session ended const focusedDuration = (conversations[i - 1].endTime - currentFocusedStart) / (60 * 1000); maxFocusedDuration = Math.max(maxFocusedDuration, focusedDuration); // Start new focus session currentFocusedStart = conv.startTime; currentType = conv.type; } } // Check final session if (conversations.length > 0) { const lastConv = conversations[conversations.length - 1]; const focusedDuration = (lastConv.endTime - currentFocusedStart) / (60 * 1000); maxFocusedDuration = Math.max(maxFocusedDuration, focusedDuration); } return maxFocusedDuration; } } //# sourceMappingURL=pattern-analysis.js.map