From 396b06b05ca2e858ce9f961b7258f7e720027b13 Mon Sep 17 00:00:00 2001 From: Shivering Ocelot Date: Tue, 8 Jul 2025 18:01:46 -0500 Subject: [PATCH] Introduce message kinds, and handle them appropriately in user and agent chat --- backend/app_mixins/chat.py | 5 ++- backend/chat.py | 19 +++++++-- backend/server.py | 5 ++- frontend/lib/chat_screen.dart | 78 +++++++++++++++++++++++++++++------ 4 files changed, 89 insertions(+), 18 deletions(-) diff --git a/backend/app_mixins/chat.py b/backend/app_mixins/chat.py index a399c1b..23fe801 100644 --- a/backend/app_mixins/chat.py +++ b/backend/app_mixins/chat.py @@ -86,14 +86,15 @@ class ChatMixin: print(f"Added agent #{agent_id} to chat #{chat_id}") # If an integer is passed in sender, it is interpreted as an agent id - def send_chat_message(self, chat_id, content, sender='user', thread_id=None): + def send_chat_message(self, chat_id, content, sender='user', thread_id=None, kind=None, subkind=None, format=None): with self.sql_session() as session: message = db.Message() message.chat_id = chat_id message.time = int(datetime.now().timestamp()) message.last_edit = message.time message.sender = sender - message.kind = 'text' # default kind + message.kind = kind or 'message' + message.subkind = subkind message.content = content message.thread_id = thread_id diff --git a/backend/chat.py b/backend/chat.py index 493e747..9bb86a2 100644 --- a/backend/chat.py +++ b/backend/chat.py @@ -7,11 +7,13 @@ import database as db MESSAGE_PAGE_SIZE = 256 class Message: - def __init__(self, id, content, sender, time=None): + def __init__(self, id, content, sender, time=None, kind='message', subkind=None): self.id = id self.content = content self.sender = sender self.time = time + self.kind = kind + self.subkind = subkind def from_db_record(record): return Message(record.id, record.content, record.sender, record.time) @@ -27,6 +29,8 @@ class Thread: self.description = description self.messages = [] + self.notes = [] + self.memories = [] class ChatView: def __init__(self, app, chat_id): @@ -44,6 +48,9 @@ class ChatView: self.thread_map = {} # Long term summary of this chat self.summary = None + # Separate list for note messages from any thread + # Is it better to exclude them or include them from threads? atm they're included + self.notes = [] # Id of the thread currently in focus, by default the last thread appearing in the conversation self.current_thread_id = None @@ -92,8 +99,14 @@ class ChatView: self.thread_map[thread.id] = thread self.threads.insert(0, thread) # This is the oldest message so far, add it to the start of the chat - message = Message(record.id, record.content, record.sender, record.time) - thread.messages.insert(0, message) + message = Message(record.id, record.content, record.sender, record.time, record.kind, record.subkind) + + if message.kind == 'note': + thread.notes.insert(0, message) + elif message.kind == 'memory': + thread.memories.insert(0, message) + else: + thread.messages.insert(0, message) def get_thread(self, thread_id): if thread_id == None: diff --git a/backend/server.py b/backend/server.py index 11e6bee..6e427da 100644 --- a/backend/server.py +++ b/backend/server.py @@ -185,8 +185,11 @@ def start_server(app): elif action == "send-chat-message": message = app.send_chat_message( chat_id=body['chatId'], - content=body['content'], thread_id=body.get('threadId'), + content=body['content'], + kind=body.get('kind'), + subkind=body.get('subkind'), + format=body.get('format'), ) await send({'message': "message sent", 'messageId': message['id'], 'nonce': nonce}) diff --git a/frontend/lib/chat_screen.dart b/frontend/lib/chat_screen.dart index aaf4351..35d53bb 100644 --- a/frontend/lib/chat_screen.dart +++ b/frontend/lib/chat_screen.dart @@ -25,12 +25,34 @@ class Message extends ChatEntry { class ThreadHeader extends ChatEntry { final int threadId; - final int messageCount; + int messageCount; ThreadHeader({required this.threadId, required this.messageCount}) : super(content: "Thread ($messageCount messages)", isFromUser: false); } +class NoteMessage extends Message { + NoteMessage({sender, content, id, threadId}) : + super(sender: sender, content: content, id: id, threadId: threadId); +} +class HiddenMessage extends Message { + HiddenMessage({sender, content, id, threadId}) : + super(sender: sender, content: content, id: id, threadId: threadId); +} + +Message createMessageFromData (Map data) { + switch (data['kind']) { + case 'message': + return Message(sender: data['sender'], content: data['content'], threadId: data['threadId']); + break; + case 'note': + return NoteMessage(sender: data['sender'], content: data['content'], threadId: data['threadId']); + break; + default: + return HiddenMessage(sender: data['sender'], content: data['content'], threadId: data['threadId']); + } +} + class ChatScreen extends StatefulWidget { final int chatId; @@ -39,9 +61,7 @@ class ChatScreen extends StatefulWidget { static Future> getChatMessages(int chatId) async { final response = await Backend.sendAction("get-chat-messages", {'chatId': chatId}); List messageObjects = response['messages']; - List messages = messageObjects.map((msg) => - Message(sender: msg['sender'], content: msg['content'], id: msg['id'], threadId: msg['threadId']) - ).toList(); + List messages = messageObjects.map((msg) => createMessageFromData(msg)).toList(); return messages; } @@ -58,6 +78,7 @@ class _ChatScreenState extends State { // UI State Set expandedThreads = {}; int? editingMessageId = null; + bool noteMode = false; // Internal Funcionality Offset? _longPressOffset; @@ -70,7 +91,7 @@ class _ChatScreenState extends State { super.initState(); _loadInitialData(); _chatSubscription = Backend.chatMessages(widget.chatId).listen((data) { - final message = Message(sender: data['sender'], content: data['content'], threadId: data['threadId']); + final message = createMessageFromData(data); if (!message.isFromUser) { addMessage(message); } @@ -117,8 +138,8 @@ class _ChatScreenState extends State { // Second pass: Build the display list with correct ordering for (var message in _messages) { - if (message.threadId == null) { - // Non-threaded messages go directly to the result + if (message is NoteMessage || message.threadId == null) { + // Non-threaded messages and notes go directly to the result result.add(message); } else { final threadId = message.threadId!; @@ -164,29 +185,40 @@ class _ChatScreenState extends State { 'chatId': widget.chatId, 'content': content, 'threadId': currentThreadId, + 'kind': noteMode ? 'note' : null, }).then((response) { if (response['error'] != null) { print("Error sending chat message: ${response['error']}"); } }); - final newMessage = Message(sender: "user", content: content, threadId: currentThreadId); + final newMessage = noteMode ? + NoteMessage(sender: "user", content: content, threadId: currentThreadId) : + Message(sender: "user", content: content, threadId: currentThreadId); addMessage(newMessage); } else { + Message message = _messages.firstWhere((element) => element.id == editingMessageId!); + Backend.sendAction("update-chat-message", { 'messageId': editingMessageId, 'content': content, + 'kind': noteMode ? 'note' : null, }).then((response) { if (response['error'] != null) { print("Error updating message: ${response['error']}"); } }); - Message message = _messages.firstWhere((element) => element.id == editingMessageId!); setState(() { message.content = content; }); } + + if (noteMode) { + setState(() { + noteMode = false; + }); + } } void createThread () async { @@ -276,6 +308,18 @@ class _ChatScreenState extends State { @override Widget build(BuildContext context) { final isWideScreen = MediaQuery.of(context).size.width > 600; + final String? editInfo; + if (editingMessageId != null) { + if (noteMode) { + editInfo = 'Editing Note ${editingMessageId}'; + } else { + editInfo = 'Editing ${editingMessageId}'; + } + } else if (noteMode) { + editInfo = 'Editing Note'; + } else { + editInfo = null; + } return Scaffold( appBar: AppBar( @@ -297,6 +341,7 @@ class _ChatScreenState extends State { if (entry is ThreadHeader) { return _buildThreadHeader(context, entry); } else if (entry is Message) { + final isNote = entry is NoteMessage; final message = entry as Message; return GestureDetector( onLongPressStart: (details) { @@ -318,7 +363,8 @@ class _ChatScreenState extends State { padding: EdgeInsets.all(10), decoration: BoxDecoration( color: getMessageColor(context), - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(isNote ? 0 : 12), + border: isNote ? Border.all(color: Colors.white) : null, ), child: SelectableText( message.content, @@ -343,8 +389,8 @@ class _ChatScreenState extends State { padding: const EdgeInsets.all(8.0), child: Column( children: [ - if (editingMessageId!= null) ...[ - Text('Editing ${editingMessageId}'), + if (editInfo != null) ...[ + Text(editInfo!), ], Expanded( child: KeyboardListener( @@ -380,6 +426,14 @@ class _ChatScreenState extends State { }, child: Text('Start New Topic'), ), + ElevatedButton( + onPressed: () { + setState(() { + noteMode = !noteMode; + }); + }, + child: Text(noteMode ? 'Switch to Message' : 'Make Note'), + ), if (editingMessageId!= null) ...[ ElevatedButton( onPressed: () {