CODEX_ADK/FRONTEND/lib/pages/conversations_page.dart
Svrnty 229a0698a3 Initial commit: CODEX_ADK monorepo
Multi-agent AI laboratory with ASP.NET Core 8.0 backend and Flutter frontend.
Implements CQRS architecture, OpenAPI contract-first API design.

BACKEND: Agent management, conversations, executions with PostgreSQL + Ollama
FRONTEND: Cross-platform UI with strict typing and Result-based error handling

Co-Authored-By: Jean-Philippe Brule <jp@svrnty.io>
2025-10-26 23:12:32 -04:00

468 lines
14 KiB
Dart

import 'package:flutter/material.dart';
import 'package:iconsax/iconsax.dart';
import 'package:animate_do/animate_do.dart';
import 'package:timeago/timeago.dart' as timeago;
import '../api/api.dart';
/// Conversations management page
///
/// Displays all conversations with details about messages, activity, and status.
/// Integrates with backend CQRS API for conversation listing and management.
class ConversationsPage extends StatefulWidget {
const ConversationsPage({super.key});
@override
State<ConversationsPage> createState() => _ConversationsPageState();
}
class _ConversationsPageState extends State<ConversationsPage> {
final CqrsApiClient _apiClient = CqrsApiClient(
config: ApiClientConfig.development,
);
List<ConversationListItemDto>? _conversations;
bool _isLoading = true;
String? _errorMessage;
String _filterStatus = 'all'; // all, active, inactive
@override
void initState() {
super.initState();
_loadConversations();
}
@override
void dispose() {
_apiClient.dispose();
super.dispose();
}
Future<void> _loadConversations() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
final Result<List<ConversationListItemDto>> result =
await _apiClient.listConversations();
result.when(
success: (List<ConversationListItemDto> conversations) {
if (mounted) {
setState(() {
_conversations = conversations;
_isLoading = false;
});
}
},
error: (ApiErrorInfo error) {
if (mounted) {
setState(() {
_errorMessage = error.message;
_isLoading = false;
});
}
},
);
}
List<ConversationListItemDto> get _filteredConversations {
if (_conversations == null) return [];
switch (_filterStatus) {
case 'active':
return _conversations!.where((c) => c.isActive).toList();
case 'inactive':
return _conversations!.where((c) => !c.isActive).toList();
default:
return _conversations!;
}
}
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header with actions
_buildHeader(colorScheme),
const SizedBox(height: 24),
// Filter chips
_buildFilterChips(colorScheme),
const SizedBox(height: 24),
// Conversations list
Expanded(
child: _buildConversationsList(colorScheme),
),
],
),
);
}
Widget _buildHeader(ColorScheme colorScheme) {
return Row(
children: [
Icon(
Iconsax.messages_3,
color: colorScheme.primary,
size: 28,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Conversations',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
Text(
_conversations != null
? '${_conversations!.length} total conversations'
: 'Loading...',
style: TextStyle(
fontSize: 14,
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
IconButton(
icon: Icon(Iconsax.refresh, color: colorScheme.primary),
onPressed: _loadConversations,
tooltip: 'Refresh conversations',
),
],
);
}
Widget _buildFilterChips(ColorScheme colorScheme) {
return Row(
children: [
_buildFilterChip('All', 'all', colorScheme),
const SizedBox(width: 8),
_buildFilterChip('Active', 'active', colorScheme),
const SizedBox(width: 8),
_buildFilterChip('Inactive', 'inactive', colorScheme),
],
);
}
Widget _buildFilterChip(
String label,
String value,
ColorScheme colorScheme,
) {
final bool isSelected = _filterStatus == value;
return FilterChip(
label: Text(label),
selected: isSelected,
onSelected: (bool selected) {
setState(() {
_filterStatus = value;
});
},
backgroundColor: colorScheme.surfaceContainerHigh,
selectedColor: colorScheme.primaryContainer,
labelStyle: TextStyle(
color: isSelected
? colorScheme.onPrimaryContainer
: colorScheme.onSurface,
),
);
}
Widget _buildConversationsList(ColorScheme colorScheme) {
if (_isLoading) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(color: colorScheme.primary),
const SizedBox(height: 16),
Text(
'Loading conversations...',
style: TextStyle(color: colorScheme.onSurfaceVariant),
),
],
),
);
}
if (_errorMessage != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Iconsax.warning_2,
size: 48,
color: colorScheme.error,
),
const SizedBox(height: 16),
Text(
'Failed to load conversations',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 8),
Text(
_errorMessage!,
style: TextStyle(color: colorScheme.error),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _loadConversations,
icon: const Icon(Iconsax.refresh),
label: const Text('Retry'),
),
],
),
);
}
final List<ConversationListItemDto> filtered = _filteredConversations;
if (filtered.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Iconsax.message_text,
size: 64,
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.3),
),
const SizedBox(height: 16),
Text(
'No conversations found',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 8),
Text(
_filterStatus == 'all'
? 'Start a conversation to see it here'
: 'No $_filterStatus conversations',
style: TextStyle(color: colorScheme.onSurfaceVariant),
),
],
),
);
}
return ListView.builder(
itemCount: filtered.length,
itemBuilder: (BuildContext context, int index) {
return FadeInUp(
duration: Duration(milliseconds: 300 + (index * 50)),
child: _buildConversationCard(filtered[index], colorScheme),
);
},
);
}
Widget _buildConversationCard(
ConversationListItemDto conversation,
ColorScheme colorScheme,
) {
return Card(
margin: const EdgeInsets.only(bottom: 16),
elevation: 0,
color: colorScheme.surfaceContainerLow,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: colorScheme.outline.withValues(alpha: 0.2),
width: 1,
),
),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
// TODO: Navigate to conversation detail
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content:
Text('View conversation: ${conversation.title} (not implemented)'),
),
);
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Icon
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: conversation.isActive
? colorScheme.primaryContainer
: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Iconsax.messages_3,
color: conversation.isActive
? colorScheme.onPrimaryContainer
: colorScheme.onSurfaceVariant,
size: 24,
),
),
const SizedBox(width: 16),
// Content
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title and status
Row(
children: [
Expanded(
child: Text(
conversation.title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: conversation.isActive
? colorScheme.primaryContainer
: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4),
),
child: Text(
conversation.isActive ? 'Active' : 'Inactive',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: conversation.isActive
? colorScheme.onPrimaryContainer
: colorScheme.onSurfaceVariant,
),
),
),
],
),
const SizedBox(height: 8),
// Summary
if (conversation.summary != null)
Text(
conversation.summary!,
style: TextStyle(
fontSize: 14,
color: colorScheme.onSurfaceVariant,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 12),
// Stats
Row(
children: [
_buildStat(
Iconsax.message_text,
'${conversation.messageCount}',
'messages',
colorScheme,
),
const SizedBox(width: 16),
_buildStat(
Iconsax.flash_1,
'${conversation.executionCount}',
'executions',
colorScheme,
),
const Spacer(),
Icon(
Iconsax.clock,
size: 14,
color: colorScheme.onSurfaceVariant,
),
const SizedBox(width: 4),
Text(
timeago.format(conversation.lastMessageAt),
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
],
),
],
),
),
],
),
),
),
);
}
Widget _buildStat(
IconData icon,
String value,
String label,
ColorScheme colorScheme,
) {
return Row(
children: [
Icon(
icon,
size: 16,
color: colorScheme.primary,
),
const SizedBox(width: 4),
Text(
value,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
const SizedBox(width: 2),
Text(
label,
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
),
],
);
}
}