diff --git a/app/lib/desktop/pages/memories/widgets/desktop_memory_dialog.dart b/app/lib/desktop/pages/memories/widgets/desktop_memory_dialog.dart index 0aa7a19a3c4..e1b19091cc3 100644 --- a/app/lib/desktop/pages/memories/widgets/desktop_memory_dialog.dart +++ b/app/lib/desktop/pages/memories/widgets/desktop_memory_dialog.dart @@ -26,6 +26,8 @@ class _DesktopMemoryDialogState extends State { late TextEditingController _textController; late MemoryVisibility _selectedVisibility; late MemoryCategory _selectedCategory; + bool _isSaving = false; + bool _saveFailed = false; @override void initState() { @@ -187,7 +189,42 @@ class _DesktopMemoryDialogState extends State { ], ), - const SizedBox(height: 32), + const SizedBox(height: 24), + + // Error message + if (_saveFailed) ...[ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.red.withOpacity(0.3), + width: 1, + ), + ), + child: Row( + children: [ + Icon( + Icons.error_outline, + color: Colors.red.shade400, + size: 18, + ), + const SizedBox(width: 8), + const Expanded( + child: Text( + 'Failed to save. Please check your connection.', + style: TextStyle( + color: ResponsiveHelper.textSecondary, + fontSize: 13, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + ], // Actions Row( @@ -217,8 +254,12 @@ class _DesktopMemoryDialogState extends State { ), const SizedBox(width: 12), OmiButton( - label: widget.memory != null ? 'Save Changes' : 'Create Memory', - onPressed: _saveMemory, + label: _isSaving + ? 'Saving...' + : _saveFailed + ? 'Retry' + : (widget.memory != null ? 'Save Changes' : 'Create Memory'), + onPressed: _isSaving ? null : _saveMemory, ), ], ), @@ -228,24 +269,49 @@ class _DesktopMemoryDialogState extends State { ); } - void _saveMemory() { + Future _saveMemory() async { final content = _textController.text.trim(); if (content.isEmpty) return; - if (widget.memory != null) { - // Edit existing memory - widget.provider.editMemory(widget.memory!, content); - if (widget.memory!.visibility != _selectedVisibility) { - widget.provider.updateMemoryVisibility(widget.memory!, _selectedVisibility); + setState(() { + _isSaving = true; + _saveFailed = false; + }); + + bool success; + + try { + if (widget.memory != null) { + // Edit existing memory + success = await widget.provider.editMemory(widget.memory!, content); + if (success && widget.memory!.visibility != _selectedVisibility) { + await widget.provider.updateMemoryVisibility(widget.memory!, _selectedVisibility); + } + if (success) { + MixpanelManager().memoriesPageEditedMemory(); + } + } else { + // Create new memory + success = await widget.provider.createMemory(content, _selectedVisibility, _selectedCategory); + if (success) { + MixpanelManager().memoriesPageCreatedMemory(_selectedCategory); + } } - MixpanelManager().memoriesPageEditedMemory(); - } else { - // Create new memory - widget.provider.createMemory(content, _selectedVisibility, _selectedCategory); - MixpanelManager().memoriesPageCreatedMemory(_selectedCategory); + } catch (e) { + success = false; + debugPrint('Error saving memory: $e'); } - Navigator.pop(context); + if (!mounted) return; + + setState(() { + _isSaving = false; + _saveFailed = !success; + }); + + if (success) { + Navigator.pop(context); + } } void _showDeleteConfirmation() { diff --git a/app/lib/pages/memories/widgets/memory_dialog.dart b/app/lib/pages/memories/widgets/memory_dialog.dart index 7a885ec4432..e9eb7e51377 100644 --- a/app/lib/pages/memories/widgets/memory_dialog.dart +++ b/app/lib/pages/memories/widgets/memory_dialog.dart @@ -23,6 +23,8 @@ class MemoryDialog extends StatefulWidget { class _MemoryDialogState extends State { late TextEditingController contentController; + bool _isSaving = false; + bool _saveFailed = false; @override void initState() { @@ -122,37 +124,47 @@ class _MemoryDialogState extends State { ), ), const SizedBox(height: 24), + if (_saveFailed) ...[ + const Text( + 'Failed to save. Please check your connection.', + style: TextStyle( + color: Colors.redAccent, + fontSize: 13, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + ], SizedBox( width: double.infinity, child: ElevatedButton( - onPressed: () { - if (contentController.text.trim().isNotEmpty) { - if (isEditing) { - widget.provider.editMemory(widget.memory!, contentController.text); - MixpanelManager().memoriesPageEditedMemory(); - } else { - widget.provider - .createMemory(contentController.text, MemoryVisibility.private, MemoryCategory.manual); - MixpanelManager().memoriesPageCreatedMemory(MemoryCategory.manual); - } - Navigator.pop(context); - } - }, + onPressed: _isSaving ? null : _handleSave, style: ElevatedButton.styleFrom( - backgroundColor: Colors.deepPurpleAccent, + backgroundColor: _saveFailed ? Colors.orange : Colors.deepPurpleAccent, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), + disabledBackgroundColor: Colors.deepPurpleAccent.withOpacity(0.5), + disabledForegroundColor: Colors.white.withOpacity(0.7), ), - child: const Text( - 'Save Memory', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), + child: _isSaving + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : Text( + _saveFailed ? 'Retry' : 'Save Memory', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), ), ), ], @@ -161,6 +173,50 @@ class _MemoryDialogState extends State { ); } + Future _handleSave() async { + if (contentController.text.trim().isEmpty) return; + + setState(() { + _isSaving = true; + _saveFailed = false; + }); + + final isEditing = widget.memory != null; + bool success; + + try { + if (isEditing) { + success = await widget.provider.editMemory(widget.memory!, contentController.text); + if (success) { + MixpanelManager().memoriesPageEditedMemory(); + } + } else { + success = await widget.provider.createMemory( + contentController.text, + MemoryVisibility.private, + MemoryCategory.manual, + ); + if (success) { + MixpanelManager().memoriesPageCreatedMemory(MemoryCategory.manual); + } + } + } catch (e) { + success = false; + debugPrint('Error saving memory: $e'); + } + + if (!mounted) return; + + setState(() { + _isSaving = false; + _saveFailed = !success; + }); + + if (success) { + Navigator.pop(context); + } + } + Future _showDeleteConfirmation(BuildContext context) async { if (widget.memory == null) return; diff --git a/app/lib/pages/memories/widgets/memory_edit_sheet.dart b/app/lib/pages/memories/widgets/memory_edit_sheet.dart index 267c64f7949..a3e21289ce3 100644 --- a/app/lib/pages/memories/widgets/memory_edit_sheet.dart +++ b/app/lib/pages/memories/widgets/memory_edit_sheet.dart @@ -23,6 +23,8 @@ class MemoryEditSheet extends StatefulWidget { class _MemoryEditSheetState extends State { late final TextEditingController contentController; + bool _isSaving = false; + bool _saveFailed = false; @override void initState() { @@ -108,30 +110,47 @@ class _MemoryEditSheetState extends State { ), ), const SizedBox(height: 24), + if (_saveFailed) ...[ + const Text( + 'Failed to save. Please check your connection.', + style: TextStyle( + color: Colors.redAccent, + fontSize: 13, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + ], SizedBox( width: double.infinity, child: ElevatedButton( - onPressed: () { - if (contentController.text.trim().isNotEmpty) { - widget.provider.editMemory(widget.memory, contentController.text, widget.memory.category); - Navigator.pop(context); - } - }, + onPressed: _isSaving ? null : _handleSave, style: ElevatedButton.styleFrom( - backgroundColor: Colors.deepPurpleAccent, + backgroundColor: _saveFailed ? Colors.orange : Colors.deepPurpleAccent, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), + disabledBackgroundColor: Colors.deepPurpleAccent.withOpacity(0.5), + disabledForegroundColor: Colors.white.withOpacity(0.7), ), - child: const Text( - 'Save Memory', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), + child: _isSaving + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : Text( + _saveFailed ? 'Retry' : 'Save Memory', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), ), ), ], @@ -140,6 +159,39 @@ class _MemoryEditSheetState extends State { ); } + Future _handleSave() async { + if (contentController.text.trim().isEmpty) return; + + setState(() { + _isSaving = true; + _saveFailed = false; + }); + + bool success; + + try { + success = await widget.provider.editMemory( + widget.memory, + contentController.text, + widget.memory.category, + ); + } catch (e) { + success = false; + debugPrint('Error saving memory: $e'); + } + + if (!mounted) return; + + setState(() { + _isSaving = false; + _saveFailed = !success; + }); + + if (success) { + Navigator.pop(context); + } + } + Future _showDeleteConfirmation(BuildContext context) async { final shouldDelete = await DeleteConfirmation.show(context); if (shouldDelete) { diff --git a/app/lib/providers/memories_provider.dart b/app/lib/providers/memories_provider.dart index c16e3791e98..532cd3e5d80 100644 --- a/app/lib/providers/memories_provider.dart +++ b/app/lib/providers/memories_provider.dart @@ -170,25 +170,29 @@ class MemoriesProvider extends ChangeNotifier { _setCategories(); } - void createMemory(String content, + Future createMemory(String content, [MemoryVisibility visibility = MemoryVisibility.public, MemoryCategory category = MemoryCategory.interesting]) async { - final newMemory = Memory( - id: const Uuid().v4(), - uid: SharedPreferencesUtil().uid, - content: content, - category: category, - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - conversationId: null, - reviewed: false, - manuallyAdded: true, - visibility: visibility, - ); - - await createMemoryServer(content, visibility.name, category.name); - _memories.add(newMemory); - _setCategories(); + final success = await createMemoryServer(content, visibility.name, category.name); + + if (success) { + final newMemory = Memory( + id: const Uuid().v4(), + uid: SharedPreferencesUtil().uid, + content: content, + category: category, + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + conversationId: null, + reviewed: false, + manuallyAdded: true, + visibility: visibility, + ); + _memories.add(newMemory); + _setCategories(); + } + + return success; } Future updateMemoryVisibility(Memory memory, MemoryVisibility visibility) async { @@ -206,27 +210,31 @@ class MemoriesProvider extends ChangeNotifier { } } - void editMemory(Memory memory, String value, [MemoryCategory? category]) async { - await editMemoryServer(memory.id, value); + Future editMemory(Memory memory, String value, [MemoryCategory? category]) async { + final success = await editMemoryServer(memory.id, value); - final idx = _memories.indexWhere((m) => m.id == memory.id); - if (idx != -1) { - memory.content = value; - if (category != null) { - memory.category = category; - } - memory.updatedAt = DateTime.now(); - memory.edited = true; - _memories[idx] = memory; - - // Remove from unreviewed if it was there - final unreviewedIdx = _unreviewed.indexWhere((m) => m.id == memory.id); - if (unreviewedIdx != -1) { - _unreviewed.removeAt(unreviewedIdx); - } + if (success) { + final idx = _memories.indexWhere((m) => m.id == memory.id); + if (idx != -1) { + memory.content = value; + if (category != null) { + memory.category = category; + } + memory.updatedAt = DateTime.now(); + memory.edited = true; + _memories[idx] = memory; - _setCategories(); + // Remove from unreviewed if it was there + final unreviewedIdx = _unreviewed.indexWhere((m) => m.id == memory.id); + if (unreviewedIdx != -1) { + _unreviewed.removeAt(unreviewedIdx); + } + + _setCategories(); + } } + + return success; } void reviewMemory(Memory memory, bool approved, String source) async {