import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../models/log_entry.dart'; import '../../providers/session_provider.dart'; import '../../theme/app_theme.dart'; class TokensScreen extends StatelessWidget { const TokensScreen({super.key}); @override Widget build(BuildContext context) { final provider = context.watch(); final session = provider.session; if (session == null) { return const Center( child: Text('No session loaded', style: TextStyle(color: AppColors.textMuted)), ); } final usage = session.totalUsage; final assistantEntries = session.allEntries .whereType() .where((e) => e.usage != null) .toList(); return Scaffold( backgroundColor: AppColors.background, body: ListView( padding: const EdgeInsets.all(24), children: [ const Text( 'Token Usage', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: AppColors.textPrimary, ), ), const SizedBox(height: 24), // Summary cards Row( children: [ Expanded( child: _TokenCard( label: 'Input Tokens', value: usage.inputTokens, color: AppColors.user, icon: Icons.arrow_downward, ), ), const SizedBox(width: 12), Expanded( child: _TokenCard( label: 'Output Tokens', value: usage.outputTokens, color: AppColors.assistant, icon: Icons.arrow_upward, ), ), const SizedBox(width: 12), Expanded( child: _TokenCard( label: 'Cache Created', value: usage.cacheCreationInputTokens, color: AppColors.tool, icon: Icons.cached, ), ), const SizedBox(width: 12), Expanded( child: _TokenCard( label: 'Cache Read', value: usage.cacheReadInputTokens, color: AppColors.agent, icon: Icons.speed, ), ), ], ), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(8), border: Border.all(color: AppColors.surfaceBorder), ), child: Row( children: [ const Text( 'Total Tokens: ', style: TextStyle( fontSize: 14, color: AppColors.textSecondary), ), Text( _formatTokens(usage.totalTokens), style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.textPrimary, ), ), const Spacer(), if (session.duration != null) ...[ const Text( 'Session Duration: ', style: TextStyle( fontSize: 14, color: AppColors.textSecondary), ), Text( _formatDuration(session.duration!), style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.textPrimary, ), ), ], ], ), ), const SizedBox(height: 24), // Per-agent breakdown if (session.agents.length > 1) ...[ const Text( 'TOKENS BY AGENT', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: AppColors.textMuted, letterSpacing: 1.2, ), ), const SizedBox(height: 12), Container( height: 200, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(8), border: Border.all(color: AppColors.surfaceBorder), ), child: Row( children: [ Expanded( child: PieChart( PieChartData( sectionsSpace: 2, centerSpaceRadius: 40, sections: session.agents .where( (a) => a.aggregatedUsage.totalTokens > 0) .toList() .asMap() .entries .map((e) { final agent = e.value; final color = AppColors.chartPalette[ e.key % AppColors.chartPalette.length]; return PieChartSectionData( color: color, value: agent.aggregatedUsage.totalTokens.toDouble(), title: '', radius: 30, ); }).toList(), ), ), ), const SizedBox(width: 24), Expanded( child: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: session.agents .where( (a) => a.aggregatedUsage.totalTokens > 0) .toList() .asMap() .entries .map((e) { final agent = e.value; final color = AppColors.chartPalette[ e.key % AppColors.chartPalette.length]; return Padding( padding: const EdgeInsets.only(bottom: 6), child: Row( children: [ Container( width: 10, height: 10, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(width: 8), Expanded( child: Text( agent.name, style: const TextStyle( fontSize: 12, color: AppColors.textSecondary, ), overflow: TextOverflow.ellipsis, ), ), Text( _formatTokens( agent.aggregatedUsage.totalTokens), style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: AppColors.textPrimary, fontFamily: 'JetBrains Mono', ), ), ], ), ); }).toList(), ), ), ), ], ), ), const SizedBox(height: 24), ], // Per-message table const Text( 'PER-MESSAGE BREAKDOWN', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: AppColors.textMuted, letterSpacing: 1.2, ), ), const SizedBox(height: 12), Container( decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(8), border: Border.all(color: AppColors.surfaceBorder), ), child: DataTable( headingRowColor: WidgetStateProperty.all(AppColors.surfaceLight), dataRowColor: WidgetStateProperty.all(Colors.transparent), columnSpacing: 24, headingTextStyle: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: AppColors.textMuted, ), dataTextStyle: const TextStyle( fontSize: 12, color: AppColors.textPrimary, fontFamily: 'JetBrains Mono', ), columns: const [ DataColumn(label: Text('#')), DataColumn(label: Text('Time')), DataColumn(label: Text('Model')), DataColumn(label: Text('Input'), numeric: true), DataColumn(label: Text('Output'), numeric: true), DataColumn(label: Text('Cache Create'), numeric: true), DataColumn(label: Text('Cache Read'), numeric: true), DataColumn(label: Text('Total'), numeric: true), ], rows: assistantEntries.asMap().entries.map((e) { final entry = e.value; final u = entry.usage!; return DataRow(cells: [ DataCell(Text('${e.key + 1}')), DataCell(Text(entry.timestamp != null ? '${entry.timestamp!.hour.toString().padLeft(2, '0')}:${entry.timestamp!.minute.toString().padLeft(2, '0')}:${entry.timestamp!.second.toString().padLeft(2, '0')}' : '-')), DataCell(Text(entry.model ?? '-', style: const TextStyle(fontSize: 10))), DataCell(Text(_formatTokens(u.inputTokens))), DataCell(Text(_formatTokens(u.outputTokens))), DataCell( Text(_formatTokens(u.cacheCreationInputTokens))), DataCell(Text(_formatTokens(u.cacheReadInputTokens))), DataCell(Text( _formatTokens(u.totalTokens), style: const TextStyle(fontWeight: FontWeight.w600), )), ]); }).toList(), ), ), ], ), ); } String _formatTokens(int tokens) { if (tokens >= 1000000) return '${(tokens / 1000000).toStringAsFixed(1)}M'; if (tokens >= 1000) return '${(tokens / 1000).toStringAsFixed(1)}K'; return '$tokens'; } String _formatDuration(Duration d) { final minutes = d.inMinutes; final seconds = d.inSeconds % 60; if (minutes > 0) return '${minutes}m ${seconds}s'; return '${seconds}s'; } } class _TokenCard extends StatelessWidget { final String label; final int value; final Color color; final IconData icon; const _TokenCard({ required this.label, required this.value, required this.color, required this.icon, }); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(8), border: Border.all(color: AppColors.surfaceBorder), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(icon, size: 16, color: color), const SizedBox(width: 6), Text( label, style: const TextStyle(fontSize: 12, color: AppColors.textMuted), ), ], ), const SizedBox(height: 8), Text( _format(value), style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: color, fontFamily: 'JetBrains Mono', ), ), ], ), ); } String _format(int v) { if (v >= 1000000) return '${(v / 1000000).toStringAsFixed(1)}M'; if (v >= 1000) return '${(v / 1000).toStringAsFixed(1)}K'; return '$v'; } }