Introduce message kinds, and handle them appropriately in user and agent chat

This commit is contained in:
Shivering Ocelot committed 2025-07-08 18:01:46 -05:00
1 parent cba41f1e7e
commit 396b06b05c
4 files changed
+89 -18

No files matched your search

+3 -2
View File
@@ -86,14 +86,15 @@ class ChatMixin:
print(f"Added agent #{agent_id} to chat #{chat_id}") print(f"Added agent #{agent_id} to chat #{chat_id}")
# If an integer is passed in sender, it is interpreted as an agent 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: with self.sql_session() as session:
message = db.Message() message = db.Message()
message.chat_id = chat_id message.chat_id = chat_id
message.time = int(datetime.now().timestamp()) message.time = int(datetime.now().timestamp())
message.last_edit = message.time message.last_edit = message.time
message.sender = sender message.sender = sender
message.kind = 'text' # default kind message.kind = kind or 'message'
message.subkind = subkind
message.content = content message.content = content
message.thread_id = thread_id message.thread_id = thread_id
+16 -3
View File
@@ -7,11 +7,13 @@ import database as db
MESSAGE_PAGE_SIZE = 256 MESSAGE_PAGE_SIZE = 256
class Message: 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.id = id
self.content = content self.content = content
self.sender = sender self.sender = sender
self.time = time self.time = time
self.kind = kind
self.subkind = subkind
def from_db_record(record): def from_db_record(record):
return Message(record.id, record.content, record.sender, record.time) return Message(record.id, record.content, record.sender, record.time)
@@ -27,6 +29,8 @@ class Thread:
self.description = description self.description = description
self.messages = [] self.messages = []
self.notes = []
self.memories = []
class ChatView: class ChatView:
def __init__(self, app, chat_id): def __init__(self, app, chat_id):
@@ -44,6 +48,9 @@ class ChatView:
self.thread_map = {} self.thread_map = {}
# Long term summary of this chat # Long term summary of this chat
self.summary = None 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 # Id of the thread currently in focus, by default the last thread appearing in the conversation
self.current_thread_id = None self.current_thread_id = None
@@ -92,8 +99,14 @@ class ChatView:
self.thread_map[thread.id] = thread self.thread_map[thread.id] = thread
self.threads.insert(0, thread) self.threads.insert(0, thread)
# This is the oldest message so far, add it to the start of the chat # 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) message = Message(record.id, record.content, record.sender, record.time, record.kind, record.subkind)
thread.messages.insert(0, message)
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): def get_thread(self, thread_id):
if thread_id == None: if thread_id == None:
+4 -1
View File
@@ -185,8 +185,11 @@ def start_server(app):
elif action == "send-chat-message": elif action == "send-chat-message":
message = app.send_chat_message( message = app.send_chat_message(
chat_id=body['chatId'], chat_id=body['chatId'],
content=body['content'],
thread_id=body.get('threadId'), 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}) await send({'message': "message sent", 'messageId': message['id'], 'nonce': nonce})
+66 -12
View File
@@ -25,12 +25,34 @@ class Message extends ChatEntry {
class ThreadHeader extends ChatEntry { class ThreadHeader extends ChatEntry {
final int threadId; final int threadId;
final int messageCount; int messageCount;
ThreadHeader({required this.threadId, required this.messageCount}) ThreadHeader({required this.threadId, required this.messageCount})
: super(content: "Thread ($messageCount messages)", isFromUser: false); : 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<String, dynamic> 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 { class ChatScreen extends StatefulWidget {
final int chatId; final int chatId;
@@ -39,9 +61,7 @@ class ChatScreen extends StatefulWidget {
static Future<List<Message>> getChatMessages(int chatId) async { static Future<List<Message>> getChatMessages(int chatId) async {
final response = await Backend.sendAction("get-chat-messages", {'chatId': chatId}); final response = await Backend.sendAction("get-chat-messages", {'chatId': chatId});
List<dynamic> messageObjects = response['messages']; List<dynamic> messageObjects = response['messages'];
List<Message> messages = messageObjects.map((msg) => List<Message> messages = messageObjects.map((msg) => createMessageFromData(msg)).toList();
Message(sender: msg['sender'], content: msg['content'], id: msg['id'], threadId: msg['threadId'])
).toList();
return messages; return messages;
} }
@@ -58,6 +78,7 @@ class _ChatScreenState extends State<ChatScreen> {
// UI State // UI State
Set<int> expandedThreads = {}; Set<int> expandedThreads = {};
int? editingMessageId = null; int? editingMessageId = null;
bool noteMode = false;
// Internal Funcionality // Internal Funcionality
Offset? _longPressOffset; Offset? _longPressOffset;
@@ -70,7 +91,7 @@ class _ChatScreenState extends State<ChatScreen> {
super.initState(); super.initState();
_loadInitialData(); _loadInitialData();
_chatSubscription = Backend.chatMessages(widget.chatId).listen((data) { _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) { if (!message.isFromUser) {
addMessage(message); addMessage(message);
} }
@@ -117,8 +138,8 @@ class _ChatScreenState extends State<ChatScreen> {
// Second pass: Build the display list with correct ordering // Second pass: Build the display list with correct ordering
for (var message in _messages) { for (var message in _messages) {
if (message.threadId == null) { if (message is NoteMessage || message.threadId == null) {
// Non-threaded messages go directly to the result // Non-threaded messages and notes go directly to the result
result.add(message); result.add(message);
} else { } else {
final threadId = message.threadId!; final threadId = message.threadId!;
@@ -164,29 +185,40 @@ class _ChatScreenState extends State<ChatScreen> {
'chatId': widget.chatId, 'chatId': widget.chatId,
'content': content, 'content': content,
'threadId': currentThreadId, 'threadId': currentThreadId,
'kind': noteMode ? 'note' : null,
}).then((response) { }).then((response) {
if (response['error'] != null) { if (response['error'] != null) {
print("Error sending chat message: ${response['error']}"); 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); addMessage(newMessage);
} else { } else {
Message message = _messages.firstWhere((element) => element.id == editingMessageId!);
Backend.sendAction("update-chat-message", { Backend.sendAction("update-chat-message", {
'messageId': editingMessageId, 'messageId': editingMessageId,
'content': content, 'content': content,
'kind': noteMode ? 'note' : null,
}).then((response) { }).then((response) {
if (response['error'] != null) { if (response['error'] != null) {
print("Error updating message: ${response['error']}"); print("Error updating message: ${response['error']}");
} }
}); });
Message message = _messages.firstWhere((element) => element.id == editingMessageId!);
setState(() { setState(() {
message.content = content; message.content = content;
}); });
} }
if (noteMode) {
setState(() {
noteMode = false;
});
}
} }
void createThread () async { void createThread () async {
@@ -276,6 +308,18 @@ class _ChatScreenState extends State<ChatScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isWideScreen = MediaQuery.of(context).size.width > 600; 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( return Scaffold(
appBar: AppBar( appBar: AppBar(
@@ -297,6 +341,7 @@ class _ChatScreenState extends State<ChatScreen> {
if (entry is ThreadHeader) { if (entry is ThreadHeader) {
return _buildThreadHeader(context, entry); return _buildThreadHeader(context, entry);
} else if (entry is Message) { } else if (entry is Message) {
final isNote = entry is NoteMessage;
final message = entry as Message; final message = entry as Message;
return GestureDetector( return GestureDetector(
onLongPressStart: (details) { onLongPressStart: (details) {
@@ -318,7 +363,8 @@ class _ChatScreenState extends State<ChatScreen> {
padding: EdgeInsets.all(10), padding: EdgeInsets.all(10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: getMessageColor(context), color: getMessageColor(context),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(isNote ? 0 : 12),
border: isNote ? Border.all(color: Colors.white) : null,
), ),
child: SelectableText( child: SelectableText(
message.content, message.content,
@@ -343,8 +389,8 @@ class _ChatScreenState extends State<ChatScreen> {
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Column( child: Column(
children: [ children: [
if (editingMessageId!= null) ...[ if (editInfo != null) ...[
Text('Editing ${editingMessageId}'), Text(editInfo!),
], ],
Expanded( Expanded(
child: KeyboardListener( child: KeyboardListener(
@@ -380,6 +426,14 @@ class _ChatScreenState extends State<ChatScreen> {
}, },
child: Text('Start New Topic'), child: Text('Start New Topic'),
), ),
ElevatedButton(
onPressed: () {
setState(() {
noteMode = !noteMode;
});
},
child: Text(noteMode ? 'Switch to Message' : 'Make Note'),
),
if (editingMessageId!= null) ...[ if (editingMessageId!= null) ...[
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {