Markdown message (half) support

This commit is contained in:
Shivering Ocelot committed 2026-05-03 15:54:42 -05:00
1 parent e07f8ee05f
commit eb9a1ad9e8
9 files changed
+144 -89

No files matched your search

+2
View File
@@ -71,6 +71,8 @@ class ExpertAgent(Agent):
elif model_engine == "replicate":
if 'parameters' in db_model:
opts['parameters'] = db_model['parameters']
if 'format' in db_model:
opts['format'] = db_model['format']
response = await inference.replicate_completion(self.app, messages, model=db_model.value['model'], opts=opts)
+1 -1
View File
@@ -122,7 +122,7 @@ class ChatMixin:
sender=sender,
kind=kind or 'message',
subkind=subkind,
#format=format,
format=format,
content=initial_content,
thread_id=thread_id
)
+4 -1
View File
@@ -8,13 +8,14 @@ import temporary as tmp
MESSAGE_PAGE_SIZE = 256
class Message:
def __init__(self, id, content, sender, time=None, kind='message', subkind=None, thread_id=None):
def __init__(self, id, content, sender, time=None, kind='message', subkind=None, format=None, thread_id=None):
self.id = id
self.content = content
self.sender = sender
self.time = time
self.kind = kind
self.subkind = subkind
self.format = format
self.thread_id = thread_id
def from_db_record(record):
@@ -32,6 +33,8 @@ class Message:
"time": self.time,
"sender": self.sender,
"kind": self.kind,
"subkind": self.subkind,
"format": self.format,
"content": self.content,
"threadId": self.thread_id,
}
+10 -1
View File
@@ -31,7 +31,16 @@ async def init_database (data_directory):
# Checks whether an object is an instance of any database model
def is_instance (value):
isinstance(value, models.Model)
import temporary as tmp
if hasattr(value, 'id'):
if tmp.is_tmp_id(value.id):
return False
if isinstance(value, models.Model):
return True
print("NOT A DB INSTANCE??? " + repr(value))
return False
class User(models.Model):
id = fields.IntField(pk=True)
+3 -3
View File
@@ -33,8 +33,8 @@ def get_llama_cpp_model(app, model_file):
return last_model_instance
if last_model_instance is not None:
#last_model_instance = None
del last_model_instance
last_model_instance = None
#del last_model_instance
last_model_file = model_file
last_model_instance = Llama(
@@ -176,7 +176,7 @@ async def replicate_base_completion(app, model, input):
def run_replicate_inference_loop(replicate_client, model, input, content_stream):
print("START INFERENCE")
completion = replicate_client.run(model, input=input)
completion = replicate_client.run(model, input=input, stream=True)
for event in completion:
if event:
content_stream.feed(str(event))
+2 -2
View File
@@ -12,12 +12,12 @@ from stream import Stream
# relative to the module file???? wtf
frontend_path = '../../frontend/dist'
def start_server(app):
async def start_server(app):
nest_asyncio.apply()
server = Quart(__name__, static_folder=frontend_path)
server.secret_key = 'horde-secret'
auth_manager = QuartAuth(server, mode="brearer")
auth_manager = QuartAuth(server, mode="bearer")
stream_listeners = {}
+1
View File
@@ -9,6 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
"marked": "^18.0.2",
"vite-plugin-pwa": "^1.0.2",
"vue": "^3.5.17",
"vue-router": "^4.5.1"
+113
View File
@@ -0,0 +1,113 @@
<script setup>
import { marked } from "marked";
marked.use({
renderer: {
code (code, infostring, escaped) {
let langElem = infostring ?
`<div class="language">${infostring}</div>` : "";
return `<div class="code">
${langElem}
<div class="content">${code}</div>
</div>`;
}
}
});
const props = defineProps({
message: {
type: Object,
required: true
}
});
function chatEntryClass(msg) {
if (msg.type == 'note') {
return ['note'];
} else if (msg.type == 'message') {
return ['message', msg.sender == 'user' ? 'sent' : 'received'];
} else if (msg.type == 'log') {
return ['message', 'log'];
} else {
return ['info'];
}
}
</script>
<template>
<div :class="chatEntryClass(message)" @contextmenu.prevent="$emit('open-menu', message)">
<template v-if="message.type == 'message' || message.type == null">
<div v-if="message.sender != 'user'" class="sender">{{message.sender}}</div>
<div v-if="message.format == 'markdown'" v-html="marked.parse(message.content)" class="content"></div>
<div v-else class="content">{{message.content}}</div>
</template>
<template v-if="message.type == 'note'">
<div class="sender">{{message.sender}}</div>
<div class="content">{{message.content}}</div>
</template>
<template v-if="message.type == 'log'">
<div class="sender">Log ({{message.sender}})</div>
<div class="content">{{message.content}}</div>
</template>
<template v-if="message.type == 'thread-header'">
<div class="content">
Thread {{message.threadId}} ({{message.messageCount}} messages)
<a href="#" @click.prevent="$emit('toggleThread', message.threadId, !message.expanded)">{{message.expanded ? "Hide" : "Show"}}</a>
</div>
</template>
</div>
</template>
<style scoped>
.info {
align-self: center;
}
.message {
width: fit-content;
max-width: 50em;
padding: 10px;
border-radius: 8px;
background-color: var(--color-background-soft);
}
.note {
width: fit-content;
padding: 10px;
background-color: var(--color-background-soft);
border: 1px solid var(--color-border);
}
.message.sent {
align-self: end;
margin-left: 2em;
}
.message.received {
margin-right: 2em;
}
.message .content {
white-space: preserve;
width: fit-content;
}
.message ul, .message ol {
white-space: normal;
}
.message .sender,
.note .sender {
font-weight: bold;
font-size: 100%;
margin-top: -5px; /* half message padding */
}
.message.log .content {
font-family: monospace;
font-size: 80%;
}
</style>
+8 -81
View File
@@ -5,6 +5,7 @@
} from "vue";
import TitleBar from '../components/TitleBar.vue';
import Message from '../components/Message.vue';
import * as backend from '../backend';
import * as ui from '../ui';
@@ -401,18 +402,6 @@
});
}
function chatEntryClass (msg) {
if (msg.type == 'note') {
return ['note'];
} else if (msg.type == 'message') {
return ['message', msg.sender == 'user' ? 'sent' : 'received'];
} else if (msg.type == 'log') {
return ['message', 'log'];
} else {
return ['info'];
}
}
function inputKeyDown (event) {
if (event.key == "Enter" && event.ctrlKey) {
event.preventDefault();
@@ -433,31 +422,13 @@
</TitleBar>
<div class="messages-container" ref="chatContainerElem">
<div v-for="msg in chatEntries" :class="chatEntryClass(msg)" @contextmenu.prevent="chatEntryContextMenu(msg)">
<template v-if="msg.type == 'message' || msg.type == null">
<div v-if="msg.sender != 'user'" class="sender">{{msg.sender}}</div>
<div class="content">{{msg.content}}</div>
</template>
<template v-if="msg.type == 'note'">
<div class="sender">{{msg.sender}}</div>
<div class="content">{{msg.content}}</div>
</template>
<template v-if="msg.type == 'log'">
<div class="sender">Log ({{msg.sender}})</div>
<div class="content">{{msg.content}}</div>
</template>
<template v-if="msg.type == 'thread-header'">
<div class="content">
Thread {{msg.threadId}} ({{msg.messageCount}} messages)
<a hred="#" @click.prevent="expandThread(msg.threadId, !msg.expanded)">{{msg.expanded ? "Hide" : "Show"}}</a>
</div>
</template>
<!--message v-if="!msg.collapse" :value="msg.content" :format="msg.format"></message-->
</div>
<Message
v-for="msg in chatEntries"
:key="msg.id || msg.threadId"
:message="msg"
@open-menu="chatEntryContextMenu"
@toggle-thread="expandThread"
/>
</div>
<div class="input-container">
@@ -511,50 +482,6 @@
margin-bottom: 10px;
}
.info {
align-self: center;
}
.message {
width: fit-content;
max-width: 50em;
padding: 10px;
border-radius: 8px;
background-color: var(--color-background-soft);
}
.note {
width: fit-content;
padding: 10px;
background-color: var(--color-background-soft);
border: 1px solid var(--color-border);
}
.message.sent {
align-self: end;
margin-left: 2em;
}
.message.received {
margin-right: 2em;
}
.message .content {
white-space: preserve;
width: fit-content;
}
.message .sender,
.note .sender {
font-weight: bold;
font-size: 100%;
margin-top: -5px; /* half message padding */
}
.message.log .content {
font-family: monospace;
font-size: 80%;
}
.input-container {
padding: 10px;
background-color: var(--color-background-soft);