From 5c7a106413c98237b2a1ea226706977e5bc9d1d6 Mon Sep 17 00:00:00 2001 From: Shivering Ocelot Date: Sat, 10 Oct 2026 22:55:10 -0500 Subject: [PATCH] feat: message format, pretty markdown --- backend/src/app_mixins/chat.py | 1 + backend/src/server/websocket.py | 2 +- frontend/src/assets/main.css | 10 +++++ frontend/src/chat.js | 27 ++++++++++++++ frontend/src/components/Message.vue | 56 +++++++++++++++++++++------- frontend/src/components/TitleBar.vue | 2 +- frontend/src/views/Chat.vue | 12 ++++++ 7 files changed, 95 insertions(+), 15 deletions(-) diff --git a/backend/src/app_mixins/chat.py b/backend/src/app_mixins/chat.py index def0d48..c9f7fbe 100644 --- a/backend/src/app_mixins/chat.py +++ b/backend/src/app_mixins/chat.py @@ -13,6 +13,7 @@ def message_json(msg): "sender": msg.sender, "kind": msg.kind, "subkind": msg.subkind, + "format": msg.format, "content": msg.content, "threadId": msg.thread_id, } diff --git a/backend/src/server/websocket.py b/backend/src/server/websocket.py index d3e1d3f..0887453 100644 --- a/backend/src/server/websocket.py +++ b/backend/src/server/websocket.py @@ -276,7 +276,7 @@ def register(server, app, auth_manager): elif action == "update-chat-message": message = await app.update_chat_message( message_id=body['messageId'], - content=body['content'], + content=body.get('content'), kind=body.get('kind'), subkind=body.get('subkind'), format=body.get('format'), diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css index 248bfba..af0c2dc 100644 --- a/frontend/src/assets/main.css +++ b/frontend/src/assets/main.css @@ -94,4 +94,14 @@ input:disabled, textarea:disabled, select:disabled { .with-sidebar-expanded .when-sidebar-collapsed { display: none; +} + +/* Content */ +h1, h2, h3, h4, h5, h6 { + margin: 1em 0 0.5em; + font-weight: bold; +} + +p, ul, il { + margin: 0.5em 0; } \ No newline at end of file diff --git a/frontend/src/chat.js b/frontend/src/chat.js index a696d07..f3665e5 100644 --- a/frontend/src/chat.js +++ b/frontend/src/chat.js @@ -545,6 +545,33 @@ export class ChatSession { this.creatingThread.value = Boolean(fork); } + // Sets the render format of a message ("markdown", or "" for plain text). + // Omit `content` so the backend leaves the existing content untouched. + setMessageFormat (message, format) { + backend.sendAction("update-chat-message", { + messageId: message.id, format, + }).then(response => { + if (response && response.error) { + ui.alert("Error updating message format: " + response.error); + } + }); + + // No update event is broadcast (matching the content-edit path above), so + // patch the source message and rebuild the entries locally. Other sessions + // and stale views won't reflect this until reload — which is correct, since + // the format is persisted. If we ever broadcast edits, flip this to rely on + // the incoming update instead. + let thread = this.getThread(message.threadId); + let existing = thread.messages.find(msg => msg.id == message.id); + if (existing) { + existing.format = format; + } else { + console.error("Can't set format locally?", {messageId: message.id, thread}); + } + + this.computeChatEntries(); + } + // Adds a bulk of messages to the message list, in their correct // chronological position and replacing duplicates addMessageBulk (messages) { diff --git a/frontend/src/components/Message.vue b/frontend/src/components/Message.vue index 9511de7..683ce3f 100644 --- a/frontend/src/components/Message.vue +++ b/frontend/src/components/Message.vue @@ -4,13 +4,17 @@ import { marked } from "marked"; import * as backend from '../backend'; marked.use({ renderer: { - code (code, infostring, escaped) { - let langElem = infostring ? - `
${infostring}
` : ""; + code ({lang, text}) { + let langElem = lang ? + `
${lang}
` : ""; + + const div = document.createElement('div'); + div.textContent = text; + const escaped = div.innerHTML; return `
${langElem} -
${code}
+
${escaped}
`; } } @@ -111,7 +115,7 @@ watch(() => [props.message.format, props.message.content], ([newFormat, newConte @@ -189,14 +193,6 @@ watch(() => [props.message.format, props.message.content], ([newFormat, newConte margin-right: 2em; } -.message .content { - white-space: preserve; -} - -.message ul, .message ol { - white-space: normal; -} - .message .sender, .note .sender { font-weight: bold; @@ -288,4 +284,38 @@ watch(() => [props.message.format, props.message.content], ([newFormat, newConte font-style: normal; font-weight: bold; } + +.md-content { + white-space: collapse; + width: fit-content; + max-width: 50em; + line-height: normal; + word-wrap: anywhere; +} + + + + diff --git a/frontend/src/components/TitleBar.vue b/frontend/src/components/TitleBar.vue index d228da8..b6c56f6 100644 --- a/frontend/src/components/TitleBar.vue +++ b/frontend/src/components/TitleBar.vue @@ -23,8 +23,8 @@