import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../models/content_block.dart'; import '../../providers/session_provider.dart'; import '../../theme/app_theme.dart'; import '../../widgets/common/expandable_card.dart'; import '../../widgets/common/json_tree_view.dart'; class ToolbeltScreen extends StatelessWidget { const ToolbeltScreen({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 toolsByName = session.toolsByName; final sortedTools = toolsByName.entries.toList() ..sort((a, b) => b.value.length.compareTo(a.value.length)); return Scaffold( backgroundColor: AppColors.background, body: ListView( padding: const EdgeInsets.all(24), children: [ const Text( 'Toolbelt', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: AppColors.textPrimary, ), ), const SizedBox(height: 8), Text( '${toolsByName.length} unique tools, ${toolsByName.values.fold(0, (sum, list) => sum + list.length)} total invocations', style: const TextStyle(fontSize: 13, color: AppColors.textSecondary), ), const SizedBox(height: 24), // Horizontal bar chart if (sortedTools.isNotEmpty) ...[ Container( height: (sortedTools.length.clamp(1, 15) * 40.0) + 32, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(8), border: Border.all(color: AppColors.surfaceBorder), ), child: BarChart( BarChartData( alignment: BarChartAlignment.spaceAround, maxY: sortedTools.first.value.length.toDouble() * 1.15, barGroups: sortedTools .take(15) .toList() .asMap() .entries .map((e) { return BarChartGroupData( x: e.key, barRods: [ BarChartRodData( toY: e.value.value.length.toDouble(), color: AppColors.chartPalette[ e.key % AppColors.chartPalette.length], width: 22, borderRadius: const BorderRadius.only( topLeft: Radius.circular(4), topRight: Radius.circular(4), ), ), ], showingTooltipIndicators: [0], ); }).toList(), titlesData: FlTitlesData( leftTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { final idx = value.toInt(); if (idx >= sortedTools.length || idx >= 15) { return const SizedBox.shrink(); } return Padding( padding: const EdgeInsets.only(top: 6), child: Text( sortedTools[idx].key, style: const TextStyle( fontSize: 11, color: AppColors.textSecondary, ), ), ); }, reservedSize: 24, ), ), ), gridData: FlGridData( show: true, drawVerticalLine: false, getDrawingHorizontalLine: (value) => FlLine( color: AppColors.surfaceBorder, strokeWidth: 1, ), ), borderData: FlBorderData(show: false), barTouchData: BarTouchData( enabled: true, touchTooltipData: BarTouchTooltipData( tooltipPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), tooltipMargin: 4, getTooltipColor: (_) => AppColors.surfaceLight, getTooltipItem: (group, groupIndex, rod, rodIndex) { return BarTooltipItem( '${rod.toY.toInt()} calls', const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ); }, ), ), ), ), ), const SizedBox(height: 24), ], // Tool details for (final entry in sortedTools) ...[ _ToolDetailCard( toolName: entry.key, invocations: entry.value, ), const SizedBox(height: 8), ], ], ), ); } } class _ToolDetailCard extends StatelessWidget { final String toolName; final List invocations; const _ToolDetailCard({ required this.toolName, required this.invocations, }); @override Widget build(BuildContext context) { return ExpandableCard( backgroundColor: AppColors.toolBg, borderColor: AppColors.tool.withAlpha(40), header: Row( children: [ const Icon(Icons.build_outlined, size: 14, color: AppColors.tool), const SizedBox(width: 8), Text( toolName, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.tool, ), ), const Spacer(), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: AppColors.tool.withAlpha(20), borderRadius: BorderRadius.circular(3), ), child: Text( '${invocations.length} calls', style: const TextStyle(fontSize: 10, color: AppColors.tool), ), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: invocations.asMap().entries.map((e) { final block = e.value; final hasResult = block.linkedResult != null; final isError = block.linkedResult?.isError ?? false; return Padding( padding: const EdgeInsets.only(bottom: 8), child: ExpandableCard( backgroundColor: AppColors.surface, borderColor: AppColors.surfaceBorder, header: Row( children: [ Text( 'Call #${e.key + 1}', style: const TextStyle( fontSize: 11, color: AppColors.textSecondary), ), const Spacer(), if (hasResult) Icon( isError ? Icons.error_outline : Icons.check_circle_outline, size: 14, color: isError ? AppColors.error : AppColors.assistant, ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text( 'ARGUMENTS', style: TextStyle( fontSize: 10, fontWeight: FontWeight.w600, color: AppColors.textMuted, letterSpacing: 1, ), ), const SizedBox(height: 4), JsonTreeView(data: block.input, initiallyExpanded: true), if (hasResult) ...[ const SizedBox(height: 8), Text( isError ? 'ERROR' : 'RESULT', style: TextStyle( fontSize: 10, fontWeight: FontWeight.w600, color: isError ? AppColors.error : AppColors.textMuted, letterSpacing: 1, ), ), const SizedBox(height: 4), Container( constraints: const BoxConstraints(maxHeight: 200), child: SingleChildScrollView( child: SelectableText( block.linkedResult!.textContent, style: TextStyle( fontFamily: 'JetBrains Mono', fontSize: 11, color: isError ? AppColors.error : AppColors.textSecondary, ), ), ), ), ], ], ), ), ); }).toList(), ), ); } }