feat: message format, pretty markdown
This commit is contained in:
1 parent
3d8f77d154
commit
5c7a106413
7 files changed
+95
-15
No files matched your search
@@ -13,6 +13,7 @@ def message_json(msg):
|
|||||||
"sender": msg.sender,
|
"sender": msg.sender,
|
||||||
"kind": msg.kind,
|
"kind": msg.kind,
|
||||||
"subkind": msg.subkind,
|
"subkind": msg.subkind,
|
||||||
|
"format": msg.format,
|
||||||
"content": msg.content,
|
"content": msg.content,
|
||||||
"threadId": msg.thread_id,
|
"threadId": msg.thread_id,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ def register(server, app, auth_manager):
|
|||||||
elif action == "update-chat-message":
|
elif action == "update-chat-message":
|
||||||
message = await app.update_chat_message(
|
message = await app.update_chat_message(
|
||||||
message_id=body['messageId'],
|
message_id=body['messageId'],
|
||||||
content=body['content'],
|
content=body.get('content'),
|
||||||
kind=body.get('kind'),
|
kind=body.get('kind'),
|
||||||
subkind=body.get('subkind'),
|
subkind=body.get('subkind'),
|
||||||
format=body.get('format'),
|
format=body.get('format'),
|
||||||
|
|||||||
@@ -95,3 +95,13 @@ input:disabled, textarea:disabled, select:disabled {
|
|||||||
.with-sidebar-expanded .when-sidebar-collapsed {
|
.with-sidebar-expanded .when-sidebar-collapsed {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Content */
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin: 1em 0 0.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
p, ul, il {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
@@ -545,6 +545,33 @@ export class ChatSession {
|
|||||||
this.creatingThread.value = Boolean(fork);
|
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
|
// Adds a bulk of messages to the message list, in their correct
|
||||||
// chronological position and replacing duplicates
|
// chronological position and replacing duplicates
|
||||||
addMessageBulk (messages) {
|
addMessageBulk (messages) {
|
||||||
|
|||||||
@@ -4,13 +4,17 @@ import { marked } from "marked";
|
|||||||
import * as backend from '../backend';
|
import * as backend from '../backend';
|
||||||
marked.use({
|
marked.use({
|
||||||
renderer: {
|
renderer: {
|
||||||
code (code, infostring, escaped) {
|
code ({lang, text}) {
|
||||||
let langElem = infostring ?
|
let langElem = lang ?
|
||||||
`<div class="language">${infostring}</div>` : "";
|
`<div class="code-lang">${lang}</div>` : "";
|
||||||
|
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
const escaped = div.innerHTML;
|
||||||
|
|
||||||
return `<div class="code">
|
return `<div class="code">
|
||||||
${langElem}
|
${langElem}
|
||||||
<div class="content">${code}</div>
|
<div class="code-content">${escaped}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,7 +115,7 @@ watch(() => [props.message.format, props.message.content], ([newFormat, newConte
|
|||||||
<template v-if="message.type == 'message' || message.type == null">
|
<template v-if="message.type == 'message' || message.type == null">
|
||||||
<div v-if="message.sender != 'user'" class="sender">{{message.sender}}</div>
|
<div v-if="message.sender != 'user'" class="sender">{{message.sender}}</div>
|
||||||
|
|
||||||
<div v-if="format == 'markdown'" v-html="marked.parse(message.content)" class="content"></div>
|
<div v-if="format == 'markdown'" v-html="marked.parse(message.content)" class="md-content" style="white-space: collapse;"></div>
|
||||||
<div v-else class="content">{{content}}</div>
|
<div v-else class="content">{{content}}</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -189,14 +193,6 @@ watch(() => [props.message.format, props.message.content], ([newFormat, newConte
|
|||||||
margin-right: 2em;
|
margin-right: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message .content {
|
|
||||||
white-space: preserve;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message ul, .message ol {
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message .sender,
|
.message .sender,
|
||||||
.note .sender {
|
.note .sender {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -288,4 +284,38 @@ watch(() => [props.message.format, props.message.content], ([newFormat, newConte
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.md-content {
|
||||||
|
white-space: collapse;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 50em;
|
||||||
|
line-height: normal;
|
||||||
|
word-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Code blocks are generated at runtime, these styles cannot be scoped */
|
||||||
|
|
||||||
|
.code {
|
||||||
|
background-color: var(--color-surface-input);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
margin: .5em 0;
|
||||||
|
padding: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-lang {
|
||||||
|
padding-bottom: .5em;
|
||||||
|
margin-top: -.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-content {
|
||||||
|
white-space: pre;
|
||||||
|
font-family: monospace;
|
||||||
|
overflow-x: auto;
|
||||||
|
line-height: 1.6rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -23,8 +23,8 @@
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
:slotted(h1) {
|
:slotted(h1) {
|
||||||
font-size: 160%;
|
font-size: 160%;
|
||||||
font-weight: bold;
|
|
||||||
padding: 0 0.4em;
|
padding: 0 0.4em;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-header {
|
.main-header {
|
||||||
|
|||||||
@@ -130,6 +130,12 @@
|
|||||||
{value: 'pin', title: "Pin message"},
|
{value: 'pin', title: "Pin message"},
|
||||||
{value: 'edit', title: "Edit in-place"},
|
{value: 'edit', title: "Edit in-place"},
|
||||||
{value: 'edit-fork', title: "Edit in new thread"},
|
{value: 'edit-fork', title: "Edit in new thread"},
|
||||||
|
// ContextMenu.vue uses null for "dismissed", so the "None" entry can't
|
||||||
|
// carry a null value; "format:" (empty suffix) is the sentinel for "".
|
||||||
|
{title: "Set format", entries: [
|
||||||
|
{value: 'format:markdown', title: "Markdown"},
|
||||||
|
{value: 'format:', title: "None"},
|
||||||
|
]},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Long logs can be expanded/collapsed
|
// Long logs can be expanded/collapsed
|
||||||
@@ -164,6 +170,12 @@
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
default: {
|
||||||
|
if (selected?.startsWith('format:')) {
|
||||||
|
session.setMessageFormat(message, selected.slice('format:'.length));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user