CODEX_ADK/FRONTEND/lib/components/message_bubble.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

188 lines
6.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:animate_do/animate_do.dart';
import 'package:iconsax/iconsax.dart';
/// Represents a single chat message bubble
class MessageBubble extends StatelessWidget {
final String message;
final bool isUser;
final String senderName;
final DateTime timestamp;
final int? inputTokens;
final int? outputTokens;
final double? estimatedCost;
const MessageBubble({
super.key,
required this.message,
required this.isUser,
required this.senderName,
required this.timestamp,
this.inputTokens,
this.outputTokens,
this.estimatedCost,
});
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
return FadeInUp(
duration: const Duration(milliseconds: 300),
child: Align(
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.7,
),
child: Column(
crossAxisAlignment:
isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
// Sender name and timestamp
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
senderName,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(width: 8),
Text(
_formatTimestamp(timestamp),
style: TextStyle(
fontSize: 10,
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
),
),
],
),
),
// Message bubble
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
decoration: BoxDecoration(
color: isUser
? colorScheme.primary.withValues(alpha: 0.15)
: colorScheme.secondary.withValues(alpha: 0.15),
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(16),
topRight: const Radius.circular(16),
bottomLeft: Radius.circular(isUser ? 16 : 4),
bottomRight: Radius.circular(isUser ? 4 : 16),
),
border: Border.all(
color: isUser
? colorScheme.primary.withValues(alpha: 0.3)
: colorScheme.secondary.withValues(alpha: 0.3),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
message,
style: TextStyle(
fontSize: 14,
color: colorScheme.onSurface,
height: 1.4,
),
),
// Token metrics (only for agent responses)
if (!isUser && (inputTokens != null || outputTokens != null))
Padding(
padding: const EdgeInsets.only(top: 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Iconsax.cpu,
size: 10,
color: colorScheme.onSurfaceVariant,
),
const SizedBox(width: 4),
Text(
'${inputTokens ?? 0} in',
style: TextStyle(
fontSize: 10,
color: colorScheme.onSurfaceVariant,
fontFamily: 'IBM Plex Mono',
),
),
const SizedBox(width: 4),
Text(
'',
style: TextStyle(
fontSize: 10,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(width: 4),
Text(
'${outputTokens ?? 0} out',
style: TextStyle(
fontSize: 10,
color: colorScheme.onSurfaceVariant,
fontFamily: 'IBM Plex Mono',
),
),
if (estimatedCost != null) ...[
const SizedBox(width: 4),
Text(
'',
style: TextStyle(
fontSize: 10,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(width: 4),
Text(
'\$${estimatedCost!.toStringAsFixed(4)}',
style: TextStyle(
fontSize: 10,
color: colorScheme.secondary,
fontFamily: 'IBM Plex Mono',
fontWeight: FontWeight.w600,
),
),
],
],
),
),
],
),
),
],
),
),
),
);
}
String _formatTimestamp(DateTime dt) {
final DateTime now = DateTime.now();
final Duration diff = now.difference(dt);
if (diff.inMinutes < 1) {
return 'Just now';
} else if (diff.inHours < 1) {
return '${diff.inMinutes}m ago';
} else if (diff.inDays < 1) {
return '${diff.inHours}h ago';
} else {
return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
}
}
}