import 'package:ac_web/Api/AcApi.dart'; import 'package:ac_web/Api/Dtos/AgentDto.dart'; import 'package:ac_web/Api/Dtos/AgentTaskDto.dart'; import 'package:ac_web/Widgets/AgentStatusIndicator.dart'; import 'package:ac_web/Widgets/AgentTaskCard.dart'; import 'package:ac_web/Widgets/AgentTypeIcons.dart'; import 'package:flutter/material.dart'; /// Page that shows every detail of a single agent, including all of its tasks /// together with the status updates the agent reported for them. class AgentDetails extends StatefulWidget { const AgentDetails({super.key, required this.agent}); /// The agent as it was known when the page got opened. final AgentDto agent; @override State createState() => _AgentDetailsState(); } class _AgentDetailsState extends State { late AgentDto mAgent; bool mIsReloading = false; @override void initState() { super.initState(); mAgent = widget.agent; } /// The api only offers the whole list of agents, so the agent this page /// shows is looked up by its name again. Future ReloadAgent() async { if (mIsReloading) { return; } setState(() { mIsReloading = true; }); try { final List agents = await AcApi.instance.agents.getAgents(); final AgentDto? updated = agents .where((agent) => agent.name == mAgent.name) .firstOrNull; if (!mounted) { return; } setState(() { mIsReloading = false; if (updated != null) { mAgent = updated; } }); if (updated == null) { _showMessage("This agent is no longer reported by the api."); } } catch (error) { if (!mounted) { return; } setState(() { mIsReloading = false; }); _showMessage("Could not reload the agent: $error"); } } void _showMessage(String message) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(mAgent.name.isEmpty ? "Agent" : mAgent.name), actions: [ IconButton( onPressed: mIsReloading ? null : ReloadAgent, icon: const Icon(Icons.refresh), tooltip: "Reload", ), ], ), body: RefreshIndicator( onRefresh: ReloadAgent, child: ListView( padding: EdgeInsetsGeometry.all(16), children: [ _buildHeader(context), const SizedBox(height: 12), _buildInfoRow(context, "Type", mAgent.type.displayName), const SizedBox(height: 8), _buildStatusRow(context), const SizedBox(height: 8), _buildInfoRow(context, "Last seen", _formatTimestamp(mAgent.lastSeen.toLocal())), const SizedBox(height: 8), _buildInfoRow(context, "Tasks", _taskCountLabel()), const Divider(height: 32), Text("Tasks", style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 8), ..._buildTasks(context), ], ), ), ); } Widget _buildHeader(BuildContext context) { return Row( crossAxisAlignment: CrossAxisAlignment.center, spacing: 16, children: [ CircleAvatar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, child: Icon(mAgent.type.icon, color: Theme.of(context).colorScheme.onSurface), ), Expanded( child: Text( mAgent.name.isEmpty ? "Unnamed Agent" : mAgent.name, style: Theme.of(context).textTheme.headlineSmall, ), ), ], ); } /// Same layout as the other info rows, but with the colored status circle. Widget _buildStatusRow(BuildContext context) { return Row( crossAxisAlignment: CrossAxisAlignment.start, spacing: 8, children: [ SizedBox( width: 96, child: Text("Status", style: Theme.of(context).textTheme.labelLarge), ), AgentStatusIndicator(status: mAgent.status, size: 14), const SizedBox(width: 4), Expanded(child: Text(mAgent.status.displayName)), ], ); } Widget _buildInfoRow(BuildContext context, String label, String value) { return Row( crossAxisAlignment: CrossAxisAlignment.start, spacing: 8, children: [ SizedBox( width: 96, child: Text(label, style: Theme.of(context).textTheme.labelLarge), ), Expanded(child: SelectableText(value)), ], ); } /// One card per task, newest first, with the newest one already expanded. List _buildTasks(BuildContext context) { if (!mAgent.hasTasks) { return [ Text( "This agent is currently not working on any task.", style: Theme.of(context).textTheme.bodyMedium, ), ]; } final List tasks = mAgent.tasks.toList() ..sort(_byStartTimeDescending); return [ for (int i = 0; i < tasks.length; i++) AgentTaskCard(task: tasks[i], initiallyExpanded: i == 0), ]; } /// Newest task first. Tasks without a start time are kept at the end. int _byStartTimeDescending(AgentTaskDto left, AgentTaskDto right) { if (left.startTime == null && right.startTime == null) { return 0; } if (left.startTime == null) { return 1; } if (right.startTime == null) { return -1; } return right.startTime!.compareTo(left.startTime!); } String _taskCountLabel() { final int count = mAgent.tasks.length; if (count == 0) { return "No running tasks"; } return count == 1 ? "1 running task" : "$count running tasks"; } /// "15. August 2025, 14:32" String _formatTimestamp(DateTime timestamp) { final String hour = timestamp.hour.toString().padLeft(2, '0'); final String minute = timestamp.minute.toString().padLeft(2, '0'); return "${timestamp.day}. ${_monthNames[timestamp.month - 1]} ${timestamp.year}, $hour:$minute"; } static const List _monthNames = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ]; }