362 lines
12 KiB
Dart
362 lines
12 KiB
Dart
// SPDX-FileCopyrightText: © 2025 Nøkken.io <nokken.io@proton.me>
|
|
// SPDX-License-Identifier: AGPL-3.0
|
|
//
|
|
// insights_screen.dart
|
|
//
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:nokken/src/core/ui/theme/app_colors.dart';
|
|
import 'package:nokken/src/core/ui/theme/app_text_styles.dart';
|
|
import 'package:nokken/src/core/ui/theme/app_theme.dart';
|
|
import 'package:nokken/src/features/stats/models/stat_analysis.dart';
|
|
import 'package:nokken/src/features/stats/utils/stat_utils.dart';
|
|
import 'package:nokken/src/features/stats/widgets/loading_error_views.dart';
|
|
import 'package:nokken/src/features/stats/charts/impact_chart.dart';
|
|
import 'package:nokken/src/features/stats/screens/emotion_analysis_screen.dart';
|
|
|
|
class InsightsScreen extends ConsumerWidget {
|
|
final ComprehensiveAnalysis analysis;
|
|
final String timeframe;
|
|
|
|
const InsightsScreen({
|
|
super.key,
|
|
required this.analysis,
|
|
required this.timeframe,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Health Insights'),
|
|
actions: [
|
|
// Add navigation to Health Analysis
|
|
IconButton(
|
|
icon: const Icon(Icons.analytics_outlined),
|
|
tooltip: 'Detailed Analysis',
|
|
onPressed: () {
|
|
Navigator.of(context).push(MaterialPageRoute(
|
|
builder: (context) => EmotionAnalysisScreen(
|
|
initialTimeFrame: timeframe,
|
|
),
|
|
));
|
|
},
|
|
),
|
|
],
|
|
),
|
|
body: ListView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
|
|
children: [
|
|
// Introduction card with analysis button
|
|
_buildIntroCard(context),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Top Patterns Section
|
|
if (analysis.patterns.isNotEmpty) ...[
|
|
_buildHeader('Key Patterns', Icons.trending_up, Colors.indigo),
|
|
const SizedBox(height: 4),
|
|
...analysis.patterns
|
|
.take(3)
|
|
.map((pattern) => _buildCompactInsightCard(
|
|
title: pattern.name,
|
|
description: pattern.description,
|
|
icon: StatUtils.getPatternIcon(pattern.type),
|
|
color: pattern.patternColor,
|
|
strength: pattern.significance,
|
|
)),
|
|
const SizedBox(height: 16),
|
|
],
|
|
|
|
// Top Correlations Section
|
|
if (analysis.relationships.isNotEmpty) ...[
|
|
_buildHeader(
|
|
'Strongest Correlations', Icons.compare_arrows, Colors.purple),
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 26, bottom: 4),
|
|
child: Text(
|
|
'Shows how two health factors relate to each other',
|
|
style: AppTextStyles.bodySmall
|
|
.copyWith(fontSize: 10, color: Colors.grey),
|
|
),
|
|
),
|
|
...analysis.relationships
|
|
.take(3)
|
|
.map((relationship) => _buildCompactInsightCard(
|
|
title: relationship.factorNames.join(' & '),
|
|
description: relationship.description,
|
|
icon: Icons.compare_arrows,
|
|
color: relationship.relationshipColor,
|
|
strength: relationship.strength.abs(),
|
|
)),
|
|
const SizedBox(height: 16),
|
|
],
|
|
|
|
// Key Factors Section
|
|
if (analysis.factorRankings.isNotEmpty) ...[
|
|
_buildHeader('Impact Factors', Icons.bar_chart, Colors.teal),
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 26, bottom: 4),
|
|
child: Text(
|
|
'Shows which factors have the most impact on specific outcomes',
|
|
style: AppTextStyles.bodySmall
|
|
.copyWith(fontSize: 10, color: Colors.grey),
|
|
),
|
|
),
|
|
...analysis.factorRankings
|
|
.take(3)
|
|
.map((ranking) => _buildCompactFactorCard(ranking: ranking)),
|
|
const SizedBox(height: 16),
|
|
],
|
|
|
|
// Anomalies & Unusual Data
|
|
if (analysis.anomalies.isNotEmpty) ...[
|
|
_buildHeader(
|
|
'Unusual Data Points', Icons.warning_amber, Colors.orange),
|
|
const SizedBox(height: 4),
|
|
...analysis.anomalies
|
|
.take(3)
|
|
.map((anomaly) => _buildCompactInsightCard(
|
|
title:
|
|
'${anomaly.factorName} on ${anomaly.formattedDate}',
|
|
description: anomaly.description,
|
|
icon: Icons.warning,
|
|
color: anomaly.anomalyColor,
|
|
strength: anomaly.anomalyScore,
|
|
)),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildIntroCard(BuildContext context) {
|
|
return Card(
|
|
margin: EdgeInsets.zero,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12.0),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.withAlpha(20),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child:
|
|
const Icon(Icons.info_outline, color: Colors.blue, size: 18),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Health insights based on your tracked data. Insights update as you add more entries.',
|
|
style: AppTextStyles.bodySmall,
|
|
),
|
|
const SizedBox(height: 6),
|
|
TextButton(
|
|
style: TextButton.styleFrom(
|
|
padding: EdgeInsets.zero,
|
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
minimumSize: const Size(0, 0),
|
|
),
|
|
onPressed: () {
|
|
Navigator.of(context).push(MaterialPageRoute(
|
|
builder: (context) => EmotionAnalysisScreen(
|
|
initialTimeFrame: timeframe,
|
|
),
|
|
));
|
|
},
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Text('View Detailed Analysis'),
|
|
const SizedBox(width: 4),
|
|
Icon(Icons.arrow_forward, size: 14),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHeader(String title, IconData icon, Color color) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(left: 4.0, bottom: 2.0),
|
|
child: Row(
|
|
children: [
|
|
Icon(icon, color: color, size: 16),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
title,
|
|
style: AppTextStyles.titleSmall.copyWith(color: color),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCompactInsightCard({
|
|
required String title,
|
|
required String description,
|
|
required IconData icon,
|
|
required Color color,
|
|
required double strength,
|
|
}) {
|
|
// Format strength as percentage
|
|
final strengthPercent = (strength * 100).toInt();
|
|
|
|
return Card(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
elevation: 0,
|
|
color: AppColors.surfaceContainer,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(10),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Header with strength indicator
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(6),
|
|
decoration: BoxDecoration(
|
|
color: color.withAlpha(30),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
icon,
|
|
color: color,
|
|
size: 14,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
title,
|
|
style: AppTextStyles.bodySmall.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: color.withAlpha(20),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Text(
|
|
'$strengthPercent%',
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
color: color,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
description,
|
|
style: AppTextStyles.bodySmall,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCompactFactorCard({
|
|
required FactorImpactRanking ranking,
|
|
}) {
|
|
return Card(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
elevation: 0,
|
|
color: AppColors.surfaceContainer,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(10),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Header
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(6),
|
|
decoration: BoxDecoration(
|
|
color: Colors.teal.withAlpha(30),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
Icons.insights,
|
|
color: Colors.teal,
|
|
size: 14,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
'Factors affecting ${ranking.targetFactor}',
|
|
style: AppTextStyles.bodySmall.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 6),
|
|
|
|
// Top factors list (just names)
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: ranking.impactingFactors.take(3).map((factor) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 2),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
Icons.arrow_right,
|
|
color: factor.impactColor,
|
|
size: 12,
|
|
),
|
|
const SizedBox(width: 2),
|
|
Expanded(
|
|
child: Text(
|
|
factor.name,
|
|
style: AppTextStyles.bodySmall,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
Text(
|
|
'${(factor.impactScore * 100).toInt()}%',
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
color: factor.impactColor,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|