Files
iiEsaywebUIapp/lib/features/chat/services/message_batch_service.dart
2025-09-21 22:31:44 +05:30

564 lines
15 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/models/chat_message.dart';
import '../../../core/models/conversation.dart';
/// Service for managing batch operations on messages
class MessageBatchService {
/// Export messages to various formats
Future<BatchOperationResult> exportMessages({
required List<ChatMessage> messages,
required ExportFormat format,
ExportOptions? options,
}) async {
try {
final exportOptions = options ?? const ExportOptions();
String content;
switch (format) {
case ExportFormat.text:
content = _exportToText(messages, exportOptions);
break;
case ExportFormat.markdown:
content = _exportToMarkdown(messages, exportOptions);
break;
case ExportFormat.json:
content = _exportToJson(messages, exportOptions);
break;
case ExportFormat.csv:
content = _exportToCsv(messages, exportOptions);
break;
}
return BatchOperationResult.success(
operation: BatchOperation.export,
data: {'content': content, 'format': format.name},
affectedCount: messages.length,
);
} catch (e) {
return BatchOperationResult.error(
operation: BatchOperation.export,
error: e.toString(),
);
}
}
/// Delete multiple messages
Future<BatchOperationResult> deleteMessages({
required List<String> messageIds,
required Conversation conversation,
}) async {
try {
final updatedMessages = conversation.messages
.where((message) => !messageIds.contains(message.id))
.toList();
final updatedConversation = conversation.copyWith(
messages: updatedMessages,
updatedAt: DateTime.now(),
);
return BatchOperationResult.success(
operation: BatchOperation.delete,
data: {'conversation': updatedConversation},
affectedCount: messageIds.length,
);
} catch (e) {
return BatchOperationResult.error(
operation: BatchOperation.delete,
error: e.toString(),
);
}
}
/// Copy messages to clipboard or another conversation
Future<BatchOperationResult> copyMessages({
required List<ChatMessage> messages,
String? targetConversationId,
CopyFormat? format,
}) async {
try {
final copyFormat = format ?? CopyFormat.markdown;
String content;
switch (copyFormat) {
case CopyFormat.plain:
content = messages.map((m) => m.content).join('\n\n');
break;
case CopyFormat.markdown:
content = _exportToMarkdown(messages, const ExportOptions());
break;
case CopyFormat.json:
content = _exportToJson(messages, const ExportOptions());
break;
}
return BatchOperationResult.success(
operation: BatchOperation.copy,
data: {
'content': content,
'format': copyFormat.name,
'targetConversation': targetConversationId,
},
affectedCount: messages.length,
);
} catch (e) {
return BatchOperationResult.error(
operation: BatchOperation.copy,
error: e.toString(),
);
}
}
/// Move messages to another conversation
Future<BatchOperationResult> moveMessages({
required List<String> messageIds,
required Conversation sourceConversation,
required Conversation targetConversation,
}) async {
try {
final messagesToMove = sourceConversation.messages
.where((message) => messageIds.contains(message.id))
.toList();
final updatedSourceMessages = sourceConversation.messages
.where((message) => !messageIds.contains(message.id))
.toList();
final updatedTargetMessages = [
...targetConversation.messages,
...messagesToMove,
];
final updatedSourceConversation = sourceConversation.copyWith(
messages: updatedSourceMessages,
updatedAt: DateTime.now(),
);
final updatedTargetConversation = targetConversation.copyWith(
messages: updatedTargetMessages,
updatedAt: DateTime.now(),
);
return BatchOperationResult.success(
operation: BatchOperation.move,
data: {
'sourceConversation': updatedSourceConversation,
'targetConversation': updatedTargetConversation,
},
affectedCount: messageIds.length,
);
} catch (e) {
return BatchOperationResult.error(
operation: BatchOperation.move,
error: e.toString(),
);
}
}
/// Archive multiple messages
Future<BatchOperationResult> archiveMessages({
required List<String> messageIds,
required Conversation conversation,
}) async {
try {
final updatedMessages = conversation.messages.map((message) {
if (messageIds.contains(message.id)) {
return message.copyWith(
metadata: {
...?message.metadata,
'archived': true,
'archivedAt': DateTime.now().toIso8601String(),
},
);
}
return message;
}).toList();
final updatedConversation = conversation.copyWith(
messages: updatedMessages,
updatedAt: DateTime.now(),
);
return BatchOperationResult.success(
operation: BatchOperation.archive,
data: {'conversation': updatedConversation},
affectedCount: messageIds.length,
);
} catch (e) {
return BatchOperationResult.error(
operation: BatchOperation.archive,
error: e.toString(),
);
}
}
/// Add tags to multiple messages
Future<BatchOperationResult> tagMessages({
required List<String> messageIds,
required List<String> tags,
required Conversation conversation,
}) async {
try {
final updatedMessages = conversation.messages.map((message) {
if (messageIds.contains(message.id)) {
final existingTags =
(message.metadata?['tags'] as List<String>?) ?? <String>[];
final newTags = <String>{...existingTags, ...tags}.toList();
return message.copyWith(
metadata: {...?message.metadata, 'tags': newTags},
);
}
return message;
}).toList();
final updatedConversation = conversation.copyWith(
messages: updatedMessages,
updatedAt: DateTime.now(),
);
return BatchOperationResult.success(
operation: BatchOperation.tag,
data: {'conversation': updatedConversation},
affectedCount: messageIds.length,
);
} catch (e) {
return BatchOperationResult.error(
operation: BatchOperation.tag,
error: e.toString(),
);
}
}
/// Filter messages by criteria
List<ChatMessage> filterMessages({
required List<ChatMessage> messages,
MessageFilter? filter,
}) {
if (filter == null) return messages;
return messages.where((message) {
// Role filter
if (filter.roles.isNotEmpty && !filter.roles.contains(message.role)) {
return false;
}
// Date range filter
if (filter.dateFrom != null &&
message.timestamp.isBefore(filter.dateFrom!)) {
return false;
}
if (filter.dateTo != null && message.timestamp.isAfter(filter.dateTo!)) {
return false;
}
// Content filter
if (filter.contentFilter != null &&
!message.content.toLowerCase().contains(
filter.contentFilter!.toLowerCase(),
)) {
return false;
}
// Tag filter
if (filter.tags.isNotEmpty) {
final messageTags = (message.metadata?['tags'] as List<String>?) ?? [];
if (!filter.tags.any((tag) => messageTags.contains(tag))) {
return false;
}
}
// Has attachments filter
if (filter.hasAttachments != null) {
final hasAttachments = message.attachmentIds?.isNotEmpty ?? false;
if (filter.hasAttachments! != hasAttachments) {
return false;
}
}
return true;
}).toList();
}
// Export format implementations
String _exportToText(List<ChatMessage> messages, ExportOptions options) {
final buffer = StringBuffer();
if (options.includeMetadata) {
buffer.writeln('Exported on: ${DateTime.now().toIso8601String()}');
buffer.writeln('Messages: ${messages.length}');
buffer.writeln('${'=' * 50}\n');
}
for (final message in messages) {
if (options.includeTimestamps) {
buffer.writeln('[${message.timestamp.toIso8601String()}]');
}
buffer.writeln('${_formatRole(message.role)}: ${message.content}');
if (options.includeMetadata && message.metadata?.isNotEmpty == true) {
buffer.writeln('Metadata: ${message.metadata}');
}
buffer.writeln();
}
return buffer.toString();
}
String _exportToMarkdown(List<ChatMessage> messages, ExportOptions options) {
final buffer = StringBuffer();
if (options.includeMetadata) {
buffer.writeln('# Conversation Export\n');
buffer.writeln('- **Exported on:** ${DateTime.now().toIso8601String()}');
buffer.writeln('- **Messages:** ${messages.length}\n');
buffer.writeln('---\n');
}
for (final message in messages) {
buffer.writeln('## ${_formatRole(message.role)}');
if (options.includeTimestamps) {
buffer.writeln('*${message.timestamp.toIso8601String()}*\n');
}
buffer.writeln(message.content);
buffer.writeln();
}
return buffer.toString();
}
String _exportToJson(List<ChatMessage> messages, ExportOptions options) {
final data = {
if (options.includeMetadata) ...{
'exportedAt': DateTime.now().toIso8601String(),
'messageCount': messages.length,
},
'messages': messages
.map(
(message) => {
'id': message.id,
'role': message.role,
'content': message.content,
if (options.includeTimestamps)
'timestamp': message.timestamp.toIso8601String(),
if (message.model != null) 'model': message.model,
if (message.attachmentIds?.isNotEmpty == true)
'attachmentIds': message.attachmentIds,
if (options.includeMetadata &&
message.metadata?.isNotEmpty == true)
'metadata': message.metadata,
},
)
.toList(),
};
return JsonEncoder.withIndent(' ').convert(data);
}
String _exportToCsv(List<ChatMessage> messages, ExportOptions options) {
final buffer = StringBuffer();
// Header
final headers = ['Role', 'Content'];
if (options.includeTimestamps) headers.insert(1, 'Timestamp');
if (options.includeMetadata) headers.add('Metadata');
buffer.writeln(headers.map(_escapeCsv).join(','));
// Data rows
for (final message in messages) {
final row = <String>[
message.role,
message.content.replaceAll('\n', '\\n'),
];
if (options.includeTimestamps) {
row.insert(1, message.timestamp.toIso8601String());
}
if (options.includeMetadata) {
row.add(message.metadata?.toString() ?? '');
}
buffer.writeln(row.map(_escapeCsv).join(','));
}
return buffer.toString();
}
String _formatRole(String role) {
switch (role.toLowerCase()) {
case 'user':
return 'User';
case 'assistant':
return 'Assistant';
case 'system':
return 'System';
default:
return role;
}
}
String _escapeCsv(String value) {
if (value.contains(',') || value.contains('"') || value.contains('\n')) {
return '"${value.replaceAll('"', '""')}"';
}
return value;
}
}
/// Export formats supported by the batch service
enum ExportFormat { text, markdown, json, csv }
/// Copy formats for clipboard operations
enum CopyFormat { plain, markdown, json }
/// Batch operations that can be performed
enum BatchOperation { export, delete, copy, move, archive, tag }
/// Options for export operations
@immutable
class ExportOptions {
final bool includeTimestamps;
final bool includeMetadata;
final bool includeAttachments;
const ExportOptions({
this.includeTimestamps = true,
this.includeMetadata = false,
this.includeAttachments = true,
});
}
/// Filter criteria for messages
@immutable
class MessageFilter {
final List<String> roles;
final DateTime? dateFrom;
final DateTime? dateTo;
final String? contentFilter;
final List<String> tags;
final bool? hasAttachments;
const MessageFilter({
this.roles = const [],
this.dateFrom,
this.dateTo,
this.contentFilter,
this.tags = const [],
this.hasAttachments,
});
MessageFilter copyWith({
List<String>? roles,
DateTime? dateFrom,
DateTime? dateTo,
String? contentFilter,
List<String>? tags,
bool? hasAttachments,
}) {
return MessageFilter(
roles: roles ?? this.roles,
dateFrom: dateFrom ?? this.dateFrom,
dateTo: dateTo ?? this.dateTo,
contentFilter: contentFilter ?? this.contentFilter,
tags: tags ?? this.tags,
hasAttachments: hasAttachments ?? this.hasAttachments,
);
}
}
/// Result of a batch operation
@immutable
class BatchOperationResult {
final BatchOperation operation;
final bool success;
final String? error;
final Map<String, dynamic>? data;
final int affectedCount;
const BatchOperationResult({
required this.operation,
required this.success,
this.error,
this.data,
this.affectedCount = 0,
});
factory BatchOperationResult.success({
required BatchOperation operation,
Map<String, dynamic>? data,
int affectedCount = 0,
}) {
return BatchOperationResult(
operation: operation,
success: true,
data: data,
affectedCount: affectedCount,
);
}
factory BatchOperationResult.error({
required BatchOperation operation,
required String error,
}) {
return BatchOperationResult(
operation: operation,
success: false,
error: error,
);
}
}
/// Provider for message batch service
final messageBatchServiceProvider = Provider<MessageBatchService>((ref) {
return MessageBatchService();
});
/// Provider for selected messages (for batch operations)
final selectedMessagesProvider =
NotifierProvider<SelectedMessagesNotifier, Set<String>>(
SelectedMessagesNotifier.new,
);
/// Provider for batch operation mode
final batchModeProvider = NotifierProvider<BatchModeNotifier, bool>(
BatchModeNotifier.new,
);
/// Provider for message filter
final messageFilterProvider =
NotifierProvider<MessageFilterNotifier, MessageFilter?>(
MessageFilterNotifier.new,
);
class SelectedMessagesNotifier extends Notifier<Set<String>> {
@override
Set<String> build() => <String>{};
void set(Set<String> messages) => state = Set<String>.from(messages);
void clear() => state = <String>{};
}
class BatchModeNotifier extends Notifier<bool> {
@override
bool build() => false;
void set(bool value) => state = value;
}
class MessageFilterNotifier extends Notifier<MessageFilter?> {
@override
MessageFilter? build() => null;
void set(MessageFilter? filter) => state = filter;
}