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'; import 'widgets/assistant_message_card.dart'; import 'widgets/system_message_card.dart'; import 'widgets/user_message_card.dart'; class TimelineScreen extends StatelessWidget { const TimelineScreen({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 entries = provider.filteredEntries; return Scaffold( backgroundColor: AppColors.background, body: Column( children: [ _FilterBar(provider: provider), Expanded( child: entries.isEmpty ? const Center( child: Text('No matching entries', style: TextStyle(color: AppColors.textMuted)), ) : ListView.builder( padding: const EdgeInsets.fromLTRB(24, 8, 24, 24), itemCount: entries.length, itemBuilder: (context, index) { return _buildEntryCard(entries[index], index); }, ), ), ], ), ); } Widget _buildEntryCard(LogEntry entry, int index) { if (entry is UserEntry && !entry.isToolResult) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: UserMessageCard(entry: entry, index: index), ); } if (entry is AssistantEntry) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: AssistantMessageCard(entry: entry, index: index), ); } if (entry is SystemEntry) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: SystemMessageCard(entry: entry), ); } return const SizedBox.shrink(); } } class _FilterBar extends StatelessWidget { final SessionProvider provider; const _FilterBar({required this.provider}); @override Widget build(BuildContext context) { final session = provider.session!; return Container( padding: const EdgeInsets.fromLTRB(24, 16, 24, 12), decoration: const BoxDecoration( color: AppColors.surface, border: Border( bottom: BorderSide(color: AppColors.surfaceBorder, width: 1), ), ), child: Row( children: [ // Session info Expanded( child: Row( children: [ Text( session.sessionId?.substring(0, 8) ?? 'Session', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), const SizedBox(width: 12), if (session.version != null) _InfoChip(label: 'v${session.version}'), const SizedBox(width: 8), _InfoChip( label: '${provider.filteredEntries.length} entries'), const SizedBox(width: 8), _InfoChip( label: '${_formatTokens(session.totalUsage.totalTokens)} tokens'), ], ), ), // Agent filter if (session.agents.length > 1) ...[ const SizedBox(width: 12), Container( padding: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( color: AppColors.surfaceLight, borderRadius: BorderRadius.circular(6), border: Border.all(color: AppColors.surfaceBorder), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: provider.selectedAgentId, isDense: true, dropdownColor: AppColors.surfaceLight, style: const TextStyle( fontSize: 12, color: AppColors.textPrimary), hint: const Text('All Agents', style: TextStyle( fontSize: 12, color: AppColors.textSecondary)), items: [ const DropdownMenuItem( value: null, child: Text('All Agents'), ), ...session.agents.map((a) => DropdownMenuItem( value: a.id, child: Text(a.name, overflow: TextOverflow.ellipsis), )), ], onChanged: (v) => provider.selectAgent(v), ), ), ), ], const SizedBox(width: 12), // Type toggles _TypeToggle( label: 'User', color: AppColors.user, active: provider.visibleTypes.contains('user'), onTap: () => provider.toggleTypeFilter('user'), ), const SizedBox(width: 4), _TypeToggle( label: 'Assistant', color: AppColors.assistant, active: provider.visibleTypes.contains('assistant'), onTap: () => provider.toggleTypeFilter('assistant'), ), const SizedBox(width: 4), _TypeToggle( label: 'System', color: AppColors.system, active: provider.visibleTypes.contains('system'), onTap: () => provider.toggleTypeFilter('system'), ), const SizedBox(width: 12), // Search SizedBox( width: 180, child: TextField( onChanged: provider.setSearchQuery, style: const TextStyle(fontSize: 12, color: AppColors.textPrimary), decoration: InputDecoration( hintText: 'Search...', hintStyle: const TextStyle(fontSize: 12, color: AppColors.textMuted), prefixIcon: const Icon(Icons.search, size: 16, color: AppColors.textMuted), isDense: true, contentPadding: const EdgeInsets.symmetric(vertical: 8), filled: true, fillColor: AppColors.surfaceLight, border: OutlineInputBorder( borderRadius: BorderRadius.circular(6), borderSide: const BorderSide(color: AppColors.surfaceBorder), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(6), borderSide: const BorderSide(color: AppColors.surfaceBorder), ), ), ), ), ], ), ); } String _formatTokens(int tokens) { if (tokens >= 1000000) return '${(tokens / 1000000).toStringAsFixed(1)}M'; if (tokens >= 1000) return '${(tokens / 1000).toStringAsFixed(1)}K'; return '$tokens'; } } class _InfoChip extends StatelessWidget { final String label; const _InfoChip({required this.label}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: AppColors.surfaceLight, borderRadius: BorderRadius.circular(4), ), child: Text( label, style: const TextStyle(fontSize: 11, color: AppColors.textMuted), ), ); } } class _TypeToggle extends StatelessWidget { final String label; final Color color; final bool active; final VoidCallback onTap; const _TypeToggle({ required this.label, required this.color, required this.active, required this.onTap, }); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(4), child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: active ? color.withAlpha(25) : Colors.transparent, borderRadius: BorderRadius.circular(4), border: Border.all( color: active ? color.withAlpha(80) : AppColors.surfaceBorder, ), ), child: Text( label, style: TextStyle( fontSize: 11, color: active ? color : AppColors.textMuted, fontWeight: active ? FontWeight.w600 : FontWeight.w400, ), ), ), ); } }