From 483fb2a45bbe1bedab652e9c25b110d248d07763 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Tue, 14 Jan 2025 16:14:51 +0530 Subject: [PATCH 1/5] message action sheet --- app/lib/pages/chat/page.dart | 133 +++++++++++++++--- .../chat/widgets/message_action_menu.dart | 97 +++++++++++++ 2 files changed, 211 insertions(+), 19 deletions(-) create mode 100644 app/lib/pages/chat/widgets/message_action_menu.dart diff --git a/app/lib/pages/chat/page.dart b/app/lib/pages/chat/page.dart index 7422d82c4..08c3e68ef 100644 --- a/app/lib/pages/chat/page.dart +++ b/app/lib/pages/chat/page.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; import 'package:friend_private/backend/http/api/messages.dart'; import 'package:friend_private/backend/preferences.dart'; import 'package:friend_private/backend/schema/app.dart'; @@ -17,10 +18,14 @@ import 'package:friend_private/providers/conversation_provider.dart'; import 'package:friend_private/providers/message_provider.dart'; import 'package:friend_private/utils/analytics/mixpanel.dart'; import 'package:friend_private/widgets/dialog.dart'; +import 'package:friend_private/widgets/extensions/string.dart'; import 'package:gradient_borders/gradient_borders.dart'; import 'package:provider/provider.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:uuid/uuid.dart'; +import 'widgets/message_action_menu.dart'; + class ChatPage extends StatefulWidget { const ChatPage({ super.key, @@ -224,25 +229,115 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin { ? 200 : 170 : 0; - return Padding( - key: ValueKey(message.id), - padding: - EdgeInsets.only(bottom: bottomPadding, left: 18, right: 18, top: topPadding), - child: message.sender == MessageSender.ai - ? AIMessage( - showTypingIndicator: provider.showTypingIndicator && chatIndex == 0, - message: message, - sendMessage: _sendMessageUtil, - displayOptions: provider.messages.length <= 1, - appSender: provider.messageSenderApp(message.appId), - updateConversation: (ServerConversation conversation) { - context.read().updateConversation(conversation); - }, - setMessageNps: (int value) { - provider.setMessageNps(message, value); - }, - ) - : HumanMessage(message: message), + return GestureDetector( + onLongPress: () { + showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (context) => MessageActionMenu( + message: message.text.decodeString, + onCopy: () async { + await Clipboard.setData(ClipboardData(text: message.text.decodeString)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Message copied to clipboard.', + style: TextStyle( + color: Color.fromARGB(255, 255, 255, 255), + fontSize: 12.0, + ), + ), + duration: Duration(milliseconds: 2000), + ), + ); + Navigator.pop(context); + }, + onShare: () { + MixpanelManager() + .track('Chat Message Shared', properties: {'message': message.text}); + Share.share( + '${message.text.decodeString}\n\nResponse from Omi. Get yours at https://omi.me', + subject: 'Chat with Omi', + ); + Navigator.pop(context); + }, + onReport: () { + if (message.sender == MessageSender.human) { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'You cannot report your own messages.', + style: TextStyle( + color: Color.fromARGB(255, 255, 255, 255), + fontSize: 12.0, + ), + ), + duration: Duration(milliseconds: 2000), + ), + ); + return; + } + showDialog( + context: context, + builder: (context) { + return getDialog( + context, + () { + Navigator.of(context).pop(); + }, + () { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + context.read().removeLocalMessage(message.id); + reportMessageServer(message.id); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Message reported successfully.', + style: TextStyle( + color: Color.fromARGB(255, 255, 255, 255), + fontSize: 12.0, + ), + ), + duration: Duration(milliseconds: 2000), + ), + ); + }, + 'Report Message', + 'Are you sure you want to report this message?', + ); + }, + ); + // Navigator.pop(context); + }, + ), + ); + }, + child: Padding( + key: ValueKey(message.id), + padding: + EdgeInsets.only(bottom: bottomPadding, left: 18, right: 18, top: topPadding), + child: message.sender == MessageSender.ai + ? AIMessage( + showTypingIndicator: provider.showTypingIndicator && chatIndex == 0, + message: message, + sendMessage: _sendMessageUtil, + displayOptions: provider.messages.length <= 1, + appSender: provider.messageSenderApp(message.appId), + updateConversation: (ServerConversation conversation) { + context.read().updateConversation(conversation); + }, + setMessageNps: (int value) { + provider.setMessageNps(message, value); + }, + ) + : HumanMessage(message: message), + ), ); }, ), diff --git a/app/lib/pages/chat/widgets/message_action_menu.dart b/app/lib/pages/chat/widgets/message_action_menu.dart new file mode 100644 index 000000000..4875168c0 --- /dev/null +++ b/app/lib/pages/chat/widgets/message_action_menu.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; + +import 'markdown_message_widget.dart'; + +class MessageActionMenu extends StatelessWidget { + final Function()? onCopy; + final Function()? onShare; + final Function()? onReport; + final String message; + + const MessageActionMenu({ + super.key, + this.onCopy, + this.onShare, + this.onReport, + required this.message, + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 22.0, horizontal: 22.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: BorderRadius.circular(12), + ), + child: + getMarkdownWidget(context, '${message.substring(0, message.length > 200 ? 200 : message.length)}...'), + ), + const SizedBox(height: 16), + _buildActionButton( + title: 'Copy', + icon: Icons.copy, + onTap: onCopy, + ), + _buildActionButton( + title: 'Share', + icon: Icons.share, + onTap: onShare, + ), + _buildActionButton( + title: 'Report', + icon: Icons.report_gmailerrorred, + onTap: onReport, + isDestructive: true, + ), + const SizedBox(height: 20), + ], + ), + ), + ); + } + + Widget _buildActionButton({ + required String title, + required IconData icon, + required Function()? onTap, + bool isDestructive = false, + }) { + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: Row( + children: [ + Text( + title, + style: TextStyle( + fontSize: 16, + color: isDestructive ? Colors.red : Colors.white, + ), + ), + const Spacer(), + Icon( + icon, + size: 20, + color: isDestructive ? Colors.red : Colors.white, + ), + ], + ), + ), + ); + } +} From c7694500ab75929760fb288671425c992da18f19 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Tue, 14 Jan 2025 16:15:12 +0530 Subject: [PATCH 2/5] messages cleanup --- app/lib/pages/chat/widgets/ai_message.dart | 121 ++++++------------ .../chat/widgets/markdown_message_widget.dart | 28 ++++ app/lib/pages/chat/widgets/user_message.dart | 2 - app/lib/providers/message_provider.dart | 5 + 4 files changed, 72 insertions(+), 84 deletions(-) create mode 100644 app/lib/pages/chat/widgets/markdown_message_widget.dart diff --git a/app/lib/pages/chat/widgets/ai_message.dart b/app/lib/pages/chat/widgets/ai_message.dart index bdd1b5a38..8629a98db 100644 --- a/app/lib/pages/chat/widgets/ai_message.dart +++ b/app/lib/pages/chat/widgets/ai_message.dart @@ -4,7 +4,6 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:friend_private/backend/http/api/conversations.dart'; import 'package:friend_private/backend/preferences.dart'; import 'package:friend_private/backend/schema/app.dart'; @@ -23,6 +22,8 @@ import 'package:friend_private/widgets/extensions/string.dart'; import 'package:provider/provider.dart'; import 'package:shimmer/shimmer.dart'; +import 'markdown_message_widget.dart'; + class AIMessage extends StatefulWidget { final bool showTypingIndicator; final ServerMessage message; @@ -150,32 +151,6 @@ Widget buildMessageWidget( } } -Widget _getMarkdownWidget(BuildContext context, String content) { - var style = TextStyle(color: Colors.grey.shade300, fontSize: 15, height: 1.3); - return MarkdownBody( - shrinkWrap: true, - styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith( - a: style, - p: style, - blockquote: style.copyWith( - backgroundColor: Colors.transparent, - color: Colors.black, - ), - blockquoteDecoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(4), - ), - code: style.copyWith( - backgroundColor: Colors.transparent, - decoration: TextDecoration.none, - color: Colors.white, - fontWeight: FontWeight.w600, - ), - ), - data: content, - ); -} - Widget _getNpsWidget(BuildContext context, ServerMessage message, Function(int) setMessageNps) { if (!message.askForNps) return const SizedBox(); @@ -217,24 +192,18 @@ class InitialMessageWidget extends StatelessWidget { Widget build(BuildContext context) { return Column( children: [ - SelectionArea( - child: showTypingIndicator - ? const Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - SizedBox(width: 4), - TypingIndicator(), - Spacer(), - ], - ) - : _getMarkdownWidget(context, messageText) - // AutoSizeText( - // messageText, - // style: TextStyle(fontSize: 15.0, fontWeight: FontWeight.w500, color: Colors.grey.shade300), - // ), - ), + showTypingIndicator + ? const Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox(width: 4), + TypingIndicator(), + Spacer(), + ], + ) + : getMarkdownWidget(context, messageText), const SizedBox(height: 8), const SizedBox(height: 8), InitialOptionWidget(optionText: 'What did I do yesterday?', sendMessage: sendMessage), @@ -270,20 +239,18 @@ class DaySummaryWidget extends StatelessWidget { ), ), const SizedBox(height: 16), - SelectionArea( - child: showTypingIndicator - ? const Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - SizedBox(width: 4), - TypingIndicator(), - Spacer(), - ], - ) - : daySummaryMessagesList(messageText), - ), + showTypingIndicator + ? const Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox(width: 4), + TypingIndicator(), + Spacer(), + ], + ) + : daySummaryMessagesList(messageText), ], ); } @@ -430,11 +397,8 @@ class NormalMessageWidget extends StatelessWidget { ), ) : const SizedBox.shrink(), - SelectionArea( - child: messageText.isEmpty ? const SizedBox.shrink() : _getMarkdownWidget(context, messageText), - ), + messageText.isEmpty ? const SizedBox.shrink() : getMarkdownWidget(context, messageText), _getNpsWidget(context, message, setMessageNps), - if (!showTypingIndicator) CopyButton(messageText: messageText), ], ); } @@ -488,25 +452,18 @@ class _MemoriesMessageWidgetState extends State { ), ), ), - SelectionArea( - child: widget.showTypingIndicator - ? const Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - SizedBox(width: 4), - TypingIndicator(), - Spacer(), - ], - ) - : _getMarkdownWidget(context, widget.messageText) - // AutoSizeText( - // widget.messageText, - // style: TextStyle(fontSize: 15.0, fontWeight: FontWeight.w500, color: Colors.grey.shade300), - // ), - ), - CopyButton(messageText: widget.messageText), + widget.showTypingIndicator + ? const Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox(width: 4), + TypingIndicator(), + Spacer(), + ], + ) + : getMarkdownWidget(context, widget.messageText), const SizedBox(height: 16), for (var data in widget.messageMemories.indexed) ...[ Padding( diff --git a/app/lib/pages/chat/widgets/markdown_message_widget.dart b/app/lib/pages/chat/widgets/markdown_message_widget.dart new file mode 100644 index 000000000..df2aa536d --- /dev/null +++ b/app/lib/pages/chat/widgets/markdown_message_widget.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; + +Widget getMarkdownWidget(BuildContext context, String content) { + var style = TextStyle(color: Colors.grey.shade300, fontSize: 15, height: 1.3); + return MarkdownBody( + shrinkWrap: true, + styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith( + a: style, + p: style, + blockquote: style.copyWith( + backgroundColor: Colors.transparent, + color: Colors.black, + ), + blockquoteDecoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(4), + ), + code: style.copyWith( + backgroundColor: Colors.transparent, + decoration: TextDecoration.none, + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + data: content, + ); +} diff --git a/app/lib/pages/chat/widgets/user_message.dart b/app/lib/pages/chat/widgets/user_message.dart index 1fe6b3e5b..9589d0be2 100644 --- a/app/lib/pages/chat/widgets/user_message.dart +++ b/app/lib/pages/chat/widgets/user_message.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:friend_private/backend/schema/message.dart'; -import 'package:friend_private/pages/chat/widgets/ai_message.dart'; import 'package:friend_private/widgets/extensions/string.dart'; import 'package:friend_private/utils/other/temp.dart'; @@ -42,7 +41,6 @@ class HumanMessage extends StatelessWidget { ), ], ), - CopyButton(messageText: message.text, isUserMessage: true,), ], ), ); diff --git a/app/lib/providers/message_provider.dart b/app/lib/providers/message_provider.dart index 1f3325e98..78c074da2 100644 --- a/app/lib/providers/message_provider.dart +++ b/app/lib/providers/message_provider.dart @@ -49,6 +49,11 @@ class MessageProvider extends ChangeNotifier { notifyListeners(); } + void removeLocalMessage(String id) { + messages.removeWhere((m) => m.id == id); + notifyListeners(); + } + Future refreshMessages({bool dropdownSelected = false}) async { setLoadingMessages(true); if (SharedPreferencesUtil().cachedMessages.isNotEmpty) { From 5c7c3e40921d23c080c4cb66985409b23ed2a773 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Tue, 14 Jan 2025 16:15:58 +0530 Subject: [PATCH 3/5] get and report message logic --- app/lib/backend/http/api/messages.dart | 13 ++++++++++++ backend/database/chat.py | 29 ++++++++++++++++++++++++++ backend/models/chat.py | 2 ++ backend/routers/chat.py | 20 +++++++++++++++++- 4 files changed, 63 insertions(+), 1 deletion(-) diff --git a/app/lib/backend/http/api/messages.dart b/app/lib/backend/http/api/messages.dart index b53b18d37..8f5394ee8 100644 --- a/app/lib/backend/http/api/messages.dart +++ b/app/lib/backend/http/api/messages.dart @@ -261,3 +261,16 @@ Future> sendVoiceMessageServer(List files) async { throw Exception('An error occurred uploadSample: $e'); } } + +Future reportMessageServer(String messageId) async { + var response = await makeApiCall( + url: '${Env.apiBaseUrl}v1/messages/$messageId/report', + headers: {}, + method: 'POST', + body: '', + ); + if (response == null) throw Exception('Failed to report message'); + if (response.statusCode != 200) { + throw Exception('Failed to report message'); + } +} diff --git a/backend/database/chat.py b/backend/database/chat.py index 5198e0bca..d0bfd2c0a 100644 --- a/backend/database/chat.py +++ b/backend/database/chat.py @@ -142,6 +142,35 @@ def get_messages( return messages +def get_message(uid: str, message_id: str) -> Message | None: + user_ref = db.collection('users').document(uid) + message_ref = user_ref.collection('messages').where('id', '==', message_id).limit(1).stream() + message = message_ref.stream().get().to_dict() + + if not message: + return None + + if message.get('deleted') is True: + return None + + return message + + +def report_message(uid: str, msg_doc_id: str): + user_ref = db.collection('users').document(uid) + message_ref = user_ref.collection('messages').document(msg_doc_id) + message = message_ref.stream().get().to_dict() + + if not message: + return {"message": "Message not found"} + + if message.get('deleted') is True: + return {"message": "Message already deleted"} + + message_ref.update({'deleted': True, 'reported': True}) + return None + + def batch_delete_messages(parent_doc_ref, batch_size=450, plugin_id: Optional[str] = None): messages_ref = ( parent_doc_ref.collection('messages') diff --git a/backend/models/chat.py b/backend/models/chat.py index a886f6eec..7e8b61447 100644 --- a/backend/models/chat.py +++ b/backend/models/chat.py @@ -37,6 +37,8 @@ class Message(BaseModel): memories_id: List[str] = [] # used in db memories: List[MessageMemory] = [] # used front facing deleted: bool = False + reported: bool = False + report_reason: Optional[str] = None @staticmethod def get_messages_as_string( diff --git a/backend/routers/chat.py b/backend/routers/chat.py index 2bbea712b..ecdc81838 100644 --- a/backend/routers/chat.py +++ b/backend/routers/chat.py @@ -113,6 +113,23 @@ async def generate_stream(): media_type="text/event-stream" ) + +@router.post('/v1/messages/{message_id}/report', tags=['chat'], response_model=dict) +def report_message( + message_id: str, uid: str = Depends(auth.get_current_user_uid) +): + print('report_message', message_id, uid) + message = chat_db.get_message(uid, message_id) + if message is None: + raise HTTPException(status_code=404, detail='Message not found') + if message.sender != 'ai': + raise HTTPException(status_code=400, detail='Only AI messages can be reported') + if message.reported: + raise HTTPException(status_code=400, detail='Message already reported') + chat_db.report_message(uid, message.id) + return {'message': 'Message reported'} + + @router.post('/v1/messages', tags=['chat'], response_model=ResponseMessage) def send_message_v1( data: SendMessageRequest, plugin_id: Optional[str] = None, uid: str = Depends(auth.get_current_user_uid) @@ -275,7 +292,8 @@ async def create_voice_message(files: List[UploadFile] = File(...), uid: str = D @router.post("/v2/voice-messages") -async def create_voice_message_stream(files: List[UploadFile] = File(...), uid: str = Depends(auth.get_current_user_uid)): +async def create_voice_message_stream(files: List[UploadFile] = File(...), + uid: str = Depends(auth.get_current_user_uid)): # wav paths = retrieve_file_paths(files, uid) if len(paths) == 0: From fc50742217a24d103e8c5eda6e74bf41404fe4d2 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Tue, 14 Jan 2025 17:10:06 +0530 Subject: [PATCH 4/5] misc --- backend/database/chat.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/database/chat.py b/backend/database/chat.py index d0bfd2c0a..aaff21bb8 100644 --- a/backend/database/chat.py +++ b/backend/database/chat.py @@ -145,12 +145,13 @@ def get_messages( def get_message(uid: str, message_id: str) -> Message | None: user_ref = db.collection('users').document(uid) message_ref = user_ref.collection('messages').where('id', '==', message_id).limit(1).stream() - message = message_ref.stream().get().to_dict() + message_doc = next(message_ref, None) + message = Message(**message_doc.to_dict()) if message_doc else None if not message: return None - if message.get('deleted') is True: + if message.deleted is True: return None return message @@ -159,7 +160,7 @@ def get_message(uid: str, message_id: str) -> Message | None: def report_message(uid: str, msg_doc_id: str): user_ref = db.collection('users').document(uid) message_ref = user_ref.collection('messages').document(msg_doc_id) - message = message_ref.stream().get().to_dict() + message = message_ref.get().to_dict() if not message: return {"message": "Message not found"} From 2cbda09b7c778cdbf9ac61537f92b252c031fe18 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Tue, 14 Jan 2025 17:28:24 +0530 Subject: [PATCH 5/5] cleanup and improve logic --- backend/database/chat.py | 20 ++++++++------------ backend/routers/chat.py | 6 +++--- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/backend/database/chat.py b/backend/database/chat.py index aaff21bb8..0b961c82e 100644 --- a/backend/database/chat.py +++ b/backend/database/chat.py @@ -142,7 +142,7 @@ def get_messages( return messages -def get_message(uid: str, message_id: str) -> Message | None: +def get_message(uid: str, message_id: str) -> tuple[Message, str] | None: user_ref = db.collection('users').document(uid) message_ref = user_ref.collection('messages').where('id', '==', message_id).limit(1).stream() message_doc = next(message_ref, None) @@ -154,22 +154,18 @@ def get_message(uid: str, message_id: str) -> Message | None: if message.deleted is True: return None - return message + return message, message_doc.id def report_message(uid: str, msg_doc_id: str): user_ref = db.collection('users').document(uid) message_ref = user_ref.collection('messages').document(msg_doc_id) - message = message_ref.get().to_dict() - - if not message: - return {"message": "Message not found"} - - if message.get('deleted') is True: - return {"message": "Message already deleted"} - - message_ref.update({'deleted': True, 'reported': True}) - return None + try: + message_ref.update({'deleted': True, 'reported': True}) + return {"message": "Message reported"} + except Exception as e: + print("Update failed:", e) + return {"message": f"Update failed: {e}"} def batch_delete_messages(parent_doc_ref, batch_size=450, plugin_id: Optional[str] = None): diff --git a/backend/routers/chat.py b/backend/routers/chat.py index ecdc81838..780b80d12 100644 --- a/backend/routers/chat.py +++ b/backend/routers/chat.py @@ -118,15 +118,15 @@ async def generate_stream(): def report_message( message_id: str, uid: str = Depends(auth.get_current_user_uid) ): - print('report_message', message_id, uid) - message = chat_db.get_message(uid, message_id) + + message, msg_doc_id = chat_db.get_message(uid, message_id) if message is None: raise HTTPException(status_code=404, detail='Message not found') if message.sender != 'ai': raise HTTPException(status_code=400, detail='Only AI messages can be reported') if message.reported: raise HTTPException(status_code=400, detail='Message already reported') - chat_db.report_message(uid, message.id) + chat_db.report_message(uid, msg_doc_id) return {'message': 'Message reported'}