diff --git a/models/CategoryAnalytics.js b/models/CategoryAnalytics.js new file mode 100644 index 00000000..59592b6d --- /dev/null +++ b/models/CategoryAnalytics.js @@ -0,0 +1,228 @@ +const mongoose = require('mongoose'); + +const categoryAnalyticsSchema = new mongoose.Schema({ + user: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + date: { + type: Date, + default: Date.now + }, + totalPredictions: { + type: Number, + default: 0 + }, + correctPredictions: { + type: Number, + default: 0 + }, + accuracy: { + type: Number, + default: 0, + min: 0, + max: 1 + }, + methodBreakdown: { + tensorflow: { + predictions: { type: Number, default: 0 }, + correct: { type: Number, default: 0 }, + accuracy: { type: Number, default: 0 } + }, + pattern: { + predictions: { type: Number, default: 0 }, + correct: { type: Number, default: 0 }, + accuracy: { type: Number, default: 0 } + }, + 'rule-based': { + predictions: { type: Number, default: 0 }, + correct: { type: Number, default: 0 }, + accuracy: { type: Number, default: 0 } + } + }, + categoryBreakdown: { + food: { + predictions: { type: Number, default: 0 }, + correct: { type: Number, default: 0 }, + accuracy: { type: Number, default: 0 } + }, + transport: { + predictions: { type: Number, default: 0 }, + correct: { type: Number, default: 0 }, + accuracy: { type: Number, default: 0 } + }, + entertainment: { + predictions: { type: Number, default: 0 }, + correct: { type: Number, default: 0 }, + accuracy: { type: Number, default: 0 } + }, + utilities: { + predictions: { type: Number, default: 0 }, + correct: { type: Number, default: 0 }, + accuracy: { type: Number, default: 0 } + }, + healthcare: { + predictions: { type: Number, default: 0 }, + correct: { type: Number, default: 0 }, + accuracy: { type: Number, default: 0 } + }, + shopping: { + predictions: { type: Number, default: 0 }, + correct: { type: Number, default: 0 }, + accuracy: { type: Number, default: 0 } + }, + other: { + predictions: { type: Number, default: 0 }, + correct: { type: Number, default: 0 }, + accuracy: { type: Number, default: 0 } + } + }, + averageConfidence: { + type: Number, + default: 0 + }, + trainingDataUsed: { + type: Number, + default: 0 + } +}, { + timestamps: true +}); + +// Indexes +categoryAnalyticsSchema.index({ user: 1, date: -1 }); +categoryAnalyticsSchema.index({ user: 1, date: 1 }); + +// Static method to record prediction +categoryAnalyticsSchema.statics.recordPrediction = async function(userId, prediction, actualCategory, confidence) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + let analytics = await this.findOne({ + user: userId, + date: today + }); + + if (!analytics) { + analytics = new this({ + user: userId, + date: today + }); + } + + analytics.totalPredictions += 1; + const isCorrect = prediction.category === actualCategory; + if (isCorrect) { + analytics.correctPredictions += 1; + } + + // Update method breakdown + if (analytics.methodBreakdown[prediction.method]) { + analytics.methodBreakdown[prediction.method].predictions += 1; + if (isCorrect) { + analytics.methodBreakdown[prediction.method].correct += 1; + } + analytics.methodBreakdown[prediction.method].accuracy = + analytics.methodBreakdown[prediction.method].correct / + analytics.methodBreakdown[prediction.method].predictions; + } + + // Update category breakdown + if (analytics.categoryBreakdown[actualCategory]) { + analytics.categoryBreakdown[actualCategory].predictions += 1; + if (isCorrect) { + analytics.categoryBreakdown[actualCategory].correct += 1; + } + analytics.categoryBreakdown[actualCategory].accuracy = + analytics.categoryBreakdown[actualCategory].correct / + analytics.categoryBreakdown[actualCategory].predictions; + } + + // Update overall accuracy + analytics.accuracy = analytics.correctPredictions / analytics.totalPredictions; + + // Update average confidence + analytics.averageConfidence = ( + (analytics.averageConfidence * (analytics.totalPredictions - 1)) + confidence + ) / analytics.totalPredictions; + + return await analytics.save(); +}; + +// Static method to get user analytics +categoryAnalyticsSchema.statics.getUserAnalytics = async function(userId, days = 30) { + const startDate = new Date(); + startDate.setDate(startDate.getDate() - days); + + const analytics = await this.find({ + user: userId, + date: { $gte: startDate } + }).sort({ date: -1 }); + + if (analytics.length === 0) { + return { + totalPredictions: 0, + overallAccuracy: 0, + averageConfidence: 0, + methodBreakdown: {}, + categoryBreakdown: {}, + dailyStats: [] + }; + } + + // Aggregate stats + const totalPredictions = analytics.reduce((sum, day) => sum + day.totalPredictions, 0); + const totalCorrect = analytics.reduce((sum, day) => sum + day.correctPredictions, 0); + const overallAccuracy = totalCorrect / totalPredictions; + + const averageConfidence = analytics.reduce((sum, day) => sum + day.averageConfidence, 0) / analytics.length; + + // Aggregate method breakdown + const methodBreakdown = {}; + analytics.forEach(day => { + Object.keys(day.methodBreakdown).forEach(method => { + if (!methodBreakdown[method]) { + methodBreakdown[method] = { predictions: 0, correct: 0 }; + } + methodBreakdown[method].predictions += day.methodBreakdown[method].predictions; + methodBreakdown[method].correct += day.methodBreakdown[method].correct; + }); + }); + + Object.keys(methodBreakdown).forEach(method => { + methodBreakdown[method].accuracy = methodBreakdown[method].correct / methodBreakdown[method].predictions; + }); + + // Aggregate category breakdown + const categoryBreakdown = {}; + analytics.forEach(day => { + Object.keys(day.categoryBreakdown).forEach(category => { + if (!categoryBreakdown[category]) { + categoryBreakdown[category] = { predictions: 0, correct: 0 }; + } + categoryBreakdown[category].predictions += day.categoryBreakdown[category].predictions; + categoryBreakdown[category].correct += day.categoryBreakdown[category].correct; + }); + }); + + Object.keys(categoryBreakdown).forEach(category => { + categoryBreakdown[category].accuracy = categoryBreakdown[category].correct / categoryBreakdown[category].predictions; + }); + + return { + totalPredictions, + overallAccuracy, + averageConfidence, + methodBreakdown, + categoryBreakdown, + dailyStats: analytics.map(day => ({ + date: day.date, + predictions: day.totalPredictions, + accuracy: day.accuracy, + confidence: day.averageConfidence + })) + }; +}; + +module.exports = mongoose.model('CategoryAnalytics', categoryAnalyticsSchema); diff --git a/models/CategoryModel.js b/models/CategoryModel.js new file mode 100644 index 00000000..d03020de --- /dev/null +++ b/models/CategoryModel.js @@ -0,0 +1,93 @@ +const mongoose = require('mongoose'); + +const categoryModelSchema = new mongoose.Schema({ + user: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + modelData: { + type: Buffer, + required: true + }, + modelType: { + type: String, + enum: ['tensorflow', 'brainjs'], + default: 'tensorflow' + }, + version: { + type: Number, + default: 1 + }, + accuracy: { + type: Number, + default: 0, + min: 0, + max: 1 + }, + trainingSamples: { + type: Number, + default: 0 + }, + lastTrained: { + type: Date, + default: Date.now + }, + isActive: { + type: Boolean, + default: true + }, + metadata: { + layers: Number, + inputSize: Number, + outputSize: Number, + trainingTime: Number, + epochs: Number + } +}, { + timestamps: true +}); + +// Indexes +categoryModelSchema.index({ user: 1, isActive: 1 }); +categoryModelSchema.index({ user: 1, version: -1 }); + +// Static method to get active model for user +categoryModelSchema.statics.getActiveModel = async function(userId) { + return await this.findOne({ + user: userId, + isActive: true + }).sort({ version: -1 }); +}; + +// Static method to save model +categoryModelSchema.statics.saveModel = async function(userId, modelData, metadata = {}) { + // Deactivate previous models + await this.updateMany( + { user: userId, isActive: true }, + { $set: { isActive: false } } + ); + + // Get next version + const lastModel = await this.findOne({ user: userId }).sort({ version: -1 }); + const nextVersion = lastModel ? lastModel.version + 1 : 1; + + const newModel = new this({ + user: userId, + modelData, + version: nextVersion, + metadata, + lastTrained: new Date() + }); + + return await newModel.save(); +}; + +// Static method to get model history +categoryModelSchema.statics.getModelHistory = async function(userId, limit = 10) { + return await this.find({ user: userId }) + .sort({ version: -1 }) + .limit(limit); +}; + +module.exports = mongoose.model('CategoryModel', categoryModelSchema); diff --git a/public/ai-insights-dashboard.js b/public/ai-insights-dashboard.js new file mode 100644 index 00000000..e69de29b diff --git a/routes/categorization.js b/routes/categorization.js index 37b5571a..93dff00f 100644 --- a/routes/categorization.js +++ b/routes/categorization.js @@ -217,7 +217,7 @@ router.delete('/patterns', auth, async (req, res) => { router.get('/stats', auth, async (req, res) => { try { const stats = await categorizationService.getUserStats(req.user._id); - + res.json({ success: true, data: stats @@ -232,4 +232,62 @@ router.get('/stats', auth, async (req, res) => { } }); +/** + * @route GET /api/categorization/analytics + * @desc Get user's categorization analytics + * @access Private + */ +router.get('/analytics', auth, async (req, res) => { + try { + const { days = 30 } = req.query; + const CategoryAnalytics = require('../models/CategoryAnalytics'); + + const analytics = await CategoryAnalytics.getUserAnalytics(req.user._id, parseInt(days)); + + res.json({ + success: true, + data: analytics + }); + } catch (error) { + console.error('Get analytics error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching analytics', + error: error.message + }); + } +}); + +/** + * @route POST /api/categorization/record-prediction + * @desc Record a prediction result for analytics + * @access Private + */ +router.post('/record-prediction', auth, async (req, res) => { + try { + const { prediction, actualCategory, confidence } = req.body; + const CategoryAnalytics = require('../models/CategoryAnalytics'); + + const analytics = await CategoryAnalytics.recordPrediction( + req.user._id, + prediction, + actualCategory, + confidence + ); + + res.json({ + success: true, + message: 'Prediction recorded', + data: analytics + }); + } catch (error) { + console.error('Record prediction error:', error); + res.status(500).json({ + success: false, + message: 'Error recording prediction', + error: error.message + }); + } +}); + module.exports = router; diff --git a/services/aiInsightsService.js b/services/aiInsightsService.js index 249e6e6c..88be46d6 100644 --- a/services/aiInsightsService.js +++ b/services/aiInsightsService.js @@ -6,6 +6,7 @@ const AnalyticsCache = require('../models/AnalyticsCache'); const notificationService = require('./notificationService'); const mongoose = require('mongoose'); const crypto = require('crypto'); +const tf = require('@tensorflow/tfjs-node'); /** * Smart Budget Forecasting & AI Financial Insights Service