f1e0f6b5ef
- Add complete server infrastructure with Gitea, API, PostgreSQL - Implement Obsidian-compatible markdown mindmap converter - Add Flutter service for server synchronization - Configure GitHub Actions for automated build and deploy - Add production docker-compose with health checks - Include VPS setup and deployment scripts - Add comprehensive deployment documentation
291 lines
9.4 KiB
Dart
291 lines
9.4 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
class AIStatsScreen extends ConsumerStatefulWidget {
|
|
const AIStatsScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<AIStatsScreen> createState() => _AIStatsScreenState();
|
|
}
|
|
|
|
class _AIStatsScreenState extends ConsumerState<AIStatsScreen> {
|
|
int _totalRequests = 0;
|
|
int _totalTokens = 0;
|
|
int _mockRequests = 0;
|
|
int _realRequests = 0;
|
|
Map<String, int> _requestsByType = {};
|
|
bool _isLoading = true;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadStats();
|
|
}
|
|
|
|
Future<void> _loadStats() async {
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
setState(() {
|
|
_totalRequests = prefs.getInt('ai_total_requests') ?? 0;
|
|
_totalTokens = prefs.getInt('ai_total_tokens') ?? 0;
|
|
_mockRequests = prefs.getInt('ai_mock_requests') ?? 0;
|
|
_realRequests = prefs.getInt('ai_real_requests') ?? 0;
|
|
_requestsByType = {
|
|
'Генерация подзадач': prefs.getInt('ai_subtasks') ?? 0,
|
|
'Приоритизация': prefs.getInt('ai_priority') ?? 0,
|
|
'Описания': prefs.getInt('ai_descriptions') ?? 0,
|
|
'Mind-карты': prefs.getInt('ai_mindmaps') ?? 0,
|
|
'Планирование': prefs.getInt('ai_scheduling') ?? 0,
|
|
};
|
|
_isLoading = false;
|
|
});
|
|
} catch (e) {
|
|
setState(() => _isLoading = false);
|
|
}
|
|
}
|
|
|
|
Future<void> _resetStats() async {
|
|
final confirmed = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Сбросить статистику?'),
|
|
content: const Text('Все данные об использовании AI будут удалены'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, false),
|
|
child: const Text('Отмена'),
|
|
),
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, true),
|
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
|
child: const Text('Сбросить'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (confirmed == true) {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.remove('ai_total_requests');
|
|
await prefs.remove('ai_total_tokens');
|
|
await prefs.remove('ai_mock_requests');
|
|
await prefs.remove('ai_real_requests');
|
|
await prefs.remove('ai_subtasks');
|
|
await prefs.remove('ai_priority');
|
|
await prefs.remove('ai_descriptions');
|
|
await prefs.remove('ai_mindmaps');
|
|
await prefs.remove('ai_scheduling');
|
|
|
|
await _loadStats();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_isLoading) {
|
|
return const Scaffold(
|
|
body: Center(child: CircularProgressIndicator()),
|
|
);
|
|
}
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('AI Статистика'),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.refresh),
|
|
onPressed: _loadStats,
|
|
tooltip: 'Обновить',
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.delete_sweep),
|
|
onPressed: _resetStats,
|
|
tooltip: 'Сбросить',
|
|
),
|
|
],
|
|
),
|
|
body: ListView(
|
|
padding: const EdgeInsets.all(16),
|
|
children: [
|
|
// Общая статистика
|
|
_buildStatCard(
|
|
'Общая статистика',
|
|
[
|
|
_buildBigStat('Всего запросов', _totalRequests.toString()),
|
|
const Divider(),
|
|
_buildStatRow('Mock запросов', _mockRequests.toString()),
|
|
_buildStatRow('Реальных запросов', _realRequests.toString()),
|
|
_buildStatRow('Использовано токенов', _totalTokens.toString()),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Запросы по типам
|
|
_buildStatCard(
|
|
'Запросы по типам',
|
|
_requestsByType.entries
|
|
.map((e) => _buildStatRow(e.key, e.value.toString()))
|
|
.toList(),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// График использования (простой)
|
|
_buildStatCard(
|
|
'Использование',
|
|
[
|
|
SizedBox(
|
|
height: 150,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: _requestsByType.entries.map((entry) {
|
|
final max = _requestsByType.values.fold(
|
|
1, (prev, element) => prev > element ? prev : element);
|
|
final height = (entry.value / max) * 100;
|
|
return Column(
|
|
children: [
|
|
Text(entry.value.toString()),
|
|
const SizedBox(height: 4),
|
|
Container(
|
|
width: 30,
|
|
height: height,
|
|
decoration: BoxDecoration(
|
|
gradient: const LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [Colors.blue, Colors.blueAccent],
|
|
),
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
SizedBox(
|
|
width: 60,
|
|
child: Text(
|
|
entry.key.split(' ')[0],
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(fontSize: 10),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Информация
|
|
_buildInfoCard(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatCard(String title, List<Widget> children) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).cardColor,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
...children,
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBigStat(String label, String value) {
|
|
return Column(
|
|
children: [
|
|
Text(
|
|
value,
|
|
style: const TextStyle(
|
|
fontSize: 36,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.blue,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(label, style: TextStyle(color: Colors.grey[600])),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildStatRow(String label, String value) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(label, style: TextStyle(color: Colors.grey[600])),
|
|
Text(
|
|
value,
|
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildInfoCard() {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: Colors.blue.withOpacity(0.3)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.info_outline, color: Colors.blue),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Информация',
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.blue,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
const Text(
|
|
'• Сейчас работает MOCK режим\n'
|
|
'• Реальные запросы к DeepSeek отключены\n'
|
|
'• Токены не расходуются\n\n'
|
|
'Для включения реального AI:\n'
|
|
'1. Пополните баланс DeepSeek\n'
|
|
'2. Установите useMockMode = false\n'
|
|
'3. Перезапустите приложение',
|
|
style: TextStyle(fontSize: 13),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|