mirror of
https://github.com/perstarkse/minne.git
synced 2026-06-12 09:14:27 +02:00
fix: sorting of conv lists, streaming responses
This commit is contained in:
@@ -423,7 +423,7 @@ impl User {
|
|||||||
.client
|
.client
|
||||||
.query(
|
.query(
|
||||||
"SELECT * FROM type::table($table_name) WHERE user_id = $user_id
|
"SELECT * FROM type::table($table_name) WHERE user_id = $user_id
|
||||||
ORDER BY updated_at DESC",
|
ORDER BY created_at DESC",
|
||||||
)
|
)
|
||||||
.bind(("table_name", Conversation::table_name()))
|
.bind(("table_name", Conversation::table_name()))
|
||||||
.bind(("user_id", user_id.to_string()))
|
.bind(("user_id", user_id.to_string()))
|
||||||
@@ -789,7 +789,7 @@ mod tests {
|
|||||||
for i in 0..5 {
|
for i in 0..5 {
|
||||||
let mut conv = Conversation::new(user_id.to_string(), format!("Conv {}", i));
|
let mut conv = Conversation::new(user_id.to_string(), format!("Conv {}", i));
|
||||||
// Fake updated_at i minutes apart
|
// Fake updated_at i minutes apart
|
||||||
conv.updated_at = chrono::Utc::now() - chrono::Duration::minutes(i);
|
conv.created_at = chrono::Utc::now() - chrono::Duration::minutes(i);
|
||||||
db.store_item(conv.clone())
|
db.store_item(conv.clone())
|
||||||
.await
|
.await
|
||||||
.expect("Failed to store conversation");
|
.expect("Failed to store conversation");
|
||||||
@@ -806,13 +806,13 @@ mod tests {
|
|||||||
for window in retrieved.windows(2) {
|
for window in retrieved.windows(2) {
|
||||||
// Assert each earlier conversation has updated_at >= later conversation
|
// Assert each earlier conversation has updated_at >= later conversation
|
||||||
assert!(
|
assert!(
|
||||||
window[0].updated_at >= window[1].updated_at,
|
window[0].created_at >= window[1].created_at,
|
||||||
"Conversations not ordered descending by updated_at"
|
"Conversations not ordered descending by created_at"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// (Optional) Check first conversation title matches the most recently updated
|
// Check first conversation title matches the most recently updated
|
||||||
let most_recent = conversations.iter().max_by_key(|c| c.updated_at).unwrap();
|
let most_recent = conversations.iter().max_by_key(|c| c.created_at).unwrap();
|
||||||
assert_eq!(retrieved[0].id, most_recent.id);
|
assert_eq!(retrieved[0].id, most_recent.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
{% extends 'body_base.html' %}
|
{% extends 'body_base.html' %}
|
||||||
|
{% block title %} Minne Chat {% endblock %}
|
||||||
|
{% block head %}
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
|
<script src="/assets/htmx-ext-sse.js" defer></script>
|
||||||
|
{% endblock %}
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<div class="drawer xl:drawer-open h-[calc(100vh-65px)] overflow-auto">
|
<div class="drawer xl:drawer-open h-[calc(100vh-65px)] overflow-auto">
|
||||||
<input id="my-drawer-2" type="checkbox" class="drawer-toggle" />
|
<input id="my-drawer-2" type="checkbox" class="drawer-toggle" />
|
||||||
|
|
||||||
<!-- Drawer Content -->
|
<!-- Drawer Content -->
|
||||||
<div class="drawer-content flex justify-center ">
|
<div class="drawer-content flex justify-center ">
|
||||||
<main class="flex justify-center grow mt-2 sm:mt-4 gap-6 mb-10 max-w-3xl w-full absolute left-0 right-0 mx-auto">
|
<main class="flex justify-center grow mt-2 sm:mt-4 gap-6 mb-10 max-w-3xl w-full absolute left-0 right-0 mx-auto">
|
||||||
<div class="relative w-full">
|
<div class="relative w-full">
|
||||||
{% include "chat/history.html" %}
|
{% include "chat/history.html" %}
|
||||||
|
|
||||||
{% include "chat/new_message_form.html" %}
|
{% include "chat/new_message_form.html" %}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Drawer Sidebar -->
|
<!-- Drawer Sidebar -->
|
||||||
{% include "chat/drawer.html" %}
|
{% include "chat/drawer.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Custom styles to override DaisyUI defaults */
|
/* Custom styles to override DaisyUI defaults */
|
||||||
.drawer-content {
|
.drawer-content {
|
||||||
@@ -31,12 +32,109 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
@media (min-width: 1280px) {
|
||||||
|
|
||||||
/* xl breakpoint */
|
|
||||||
.drawer-open .drawer-content {
|
.drawer-open .drawer-content {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
/* Prevent content shift */
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hide-scrollbar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content p {
|
||||||
|
margin-bottom: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content ul,
|
||||||
|
.markdown-content ol {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
margin-bottom: 0.75em;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content li {
|
||||||
|
margin-bottom: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content pre {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 0.5em;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content code {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content {
|
||||||
|
line-height: 1.5;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 0.75em 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content th,
|
||||||
|
.markdown-content td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 6px 12px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content blockquote {
|
||||||
|
border-left: 4px solid #ddd;
|
||||||
|
padding-left: 10px;
|
||||||
|
margin: 0.5em 0 0.5em 0.5em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
margin: 0.75em 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<script>
|
||||||
|
marked.setOptions({
|
||||||
|
breaks: true, gfm: true, headerIds: false, mangle: false,
|
||||||
|
smartLists: true, smartypants: true, xhtml: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render static markdown (any .markdown-content[data-content])
|
||||||
|
function renderStaticMarkdown() {
|
||||||
|
document.querySelectorAll('.markdown-content[data-content]').forEach(el => {
|
||||||
|
const raw = el.getAttribute('data-content') || '';
|
||||||
|
el.innerHTML = marked.parse(raw);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollChatToBottom() {
|
||||||
|
const chatContainer = document.getElementById('chat_container');
|
||||||
|
if (chatContainer) chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function processChatUi() {
|
||||||
|
renderStaticMarkdown();
|
||||||
|
scrollChatToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', processChatUi);
|
||||||
|
document.body.addEventListener('htmx:afterSettle', processChatUi);
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
{% if message.role == "AI" %}
|
{% if message.role == "AI" %}
|
||||||
<div class="chat chat-start">
|
<div class="chat chat-start">
|
||||||
<div>
|
<div>
|
||||||
<div id="{{message.id}}" class="chat-bubble markdown-content">
|
<div id="{{message.id}}" class="chat-bubble markdown-content" data-content="{{message.content | escape}}">
|
||||||
{{ message.content|safe }}
|
{{ message.content | escape }}
|
||||||
</div>
|
</div>
|
||||||
{% if message.references %}
|
{% if message.references %}
|
||||||
{% include "chat/reference_list.html" %}
|
{% include "chat/reference_list.html" %}
|
||||||
@@ -13,128 +13,10 @@
|
|||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="chat chat-end">
|
<div class="chat chat-end">
|
||||||
<div id="{{message.id}}" class="chat-bubble markdown-content">
|
<div id="{{message.id}}" class="chat-bubble markdown-content" data-content="{{message.content | escape}}">
|
||||||
{{ message.content|safe }}
|
{{ message.content | escape }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
||||||
<script>
|
|
||||||
// Configure marked options
|
|
||||||
marked.setOptions({
|
|
||||||
breaks: true, // Critical for chat - preserves line breaks
|
|
||||||
gfm: true, // Enables GitHub Flavored Markdown features
|
|
||||||
headerIds: false, // Prevents ID collisions in dynamic chat content
|
|
||||||
mangle: false, // Safer for dynamic content
|
|
||||||
smartLists: true, // Better list rendering
|
|
||||||
smartypants: true, // Makes quotes and dashes look nicer
|
|
||||||
xhtml: false // No need for self-closing tags in most chat UIs
|
|
||||||
});
|
|
||||||
|
|
||||||
// Process all markdown content elements
|
|
||||||
function renderMarkdown() {
|
|
||||||
document.querySelectorAll('.markdown-content').forEach(el => {
|
|
||||||
const rawContent = el.getAttribute('data-content') || el.textContent;
|
|
||||||
el.innerHTML = marked.parse(rawContent);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add data attributes for raw content on server side
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
document.querySelectorAll('.markdown-content').forEach(el => {
|
|
||||||
el.setAttribute('data-content', el.textContent.trim());
|
|
||||||
});
|
|
||||||
renderMarkdown();
|
|
||||||
scrollChatToBottom();
|
|
||||||
});
|
|
||||||
|
|
||||||
function scrollChatToBottom() {
|
|
||||||
const chatContainer = document.getElementById('chat_container');
|
|
||||||
if (chatContainer) {
|
|
||||||
chatContainer.scrollTop = chatContainer.scrollHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.body.addEventListener('htmx:afterSwap', function (evt) {
|
|
||||||
scrollChatToBottom();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style>
|
|
||||||
.hide-scrollbar {
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hide-scrollbar::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content p {
|
|
||||||
margin-bottom: 0.75em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content p:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content ul,
|
|
||||||
.markdown-content ol {
|
|
||||||
margin-top: 0.5em;
|
|
||||||
margin-bottom: 0.75em;
|
|
||||||
padding-left: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content li {
|
|
||||||
margin-bottom: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content pre {
|
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
|
||||||
padding: 0.5em;
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content code {
|
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
|
||||||
padding: 0.2em 0.4em;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content {
|
|
||||||
line-height: 1.5;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Better table support */
|
|
||||||
.markdown-content table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin: 0.75em 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content th,
|
|
||||||
.markdown-content td {
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
padding: 6px 12px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Better blockquote for chat */
|
|
||||||
.markdown-content blockquote {
|
|
||||||
border-left: 4px solid #ddd;
|
|
||||||
padding-left: 10px;
|
|
||||||
margin: 0.5em 0 0.5em 0.5em;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Horizontal rule */
|
|
||||||
.markdown-content hr {
|
|
||||||
border: none;
|
|
||||||
border-top: 1px solid #ddd;
|
|
||||||
margin: 0.75em 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="chat chat-end">
|
<div class="chat chat-end">
|
||||||
<div class="chat-bubble markdown-content">
|
<div class="chat-bubble markdown-content" data-content="{{ user_message.content|escape }}">
|
||||||
{{ user_message.content }}
|
{{ user_message.content|escape }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat chat-start">
|
<div class="chat chat-start">
|
||||||
@@ -13,38 +13,27 @@
|
|||||||
<div sse-swap="references"></div>
|
<div sse-swap="references"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
marked.setOptions({
|
|
||||||
breaks: true, gfm: true, headerIds: false, mangle: false
|
|
||||||
});
|
|
||||||
// Buffer store for markdown, keyed by message id
|
|
||||||
window.markdownBuffer = window.markdownBuffer || {};
|
window.markdownBuffer = window.markdownBuffer || {};
|
||||||
document.body.addEventListener('htmx:sseBeforeMessage', function (e) {
|
document.body.addEventListener('htmx:sseBeforeMessage', function (e) {
|
||||||
const spinner = document.querySelector('.loading-id-{{user_message.id}}');
|
const msgId = '{{ user_message.id }}';
|
||||||
|
const spinner = document.querySelector('.loading-id-' + msgId);
|
||||||
if (spinner) spinner.style.display = 'none';
|
if (spinner) spinner.style.display = 'none';
|
||||||
|
const el = document.getElementById('ai-message-content-' + msgId);
|
||||||
if (e.detail.event === 'chat_message') {
|
if (e.detail.elt !== el) return;
|
||||||
const el = document.getElementById('ai-message-content-{{user_message.id}}');
|
e.preventDefault();
|
||||||
if (e.detail.elt !== el) return;
|
window.markdownBuffer[msgId] = (window.markdownBuffer[msgId] || '') + (e.detail.data || '');
|
||||||
e.preventDefault(); // Prevent htmx from swapping
|
el.innerHTML = marked.parse(window.markdownBuffer[msgId].replace(/\\n/g, '\n'));
|
||||||
// Use message id as buffer key
|
if (typeof scrollChatToBottom === "function") scrollChatToBottom();
|
||||||
const msgId = '{{user_message.id}}';
|
|
||||||
window.markdownBuffer[msgId] = (window.markdownBuffer[msgId] || '') + (e.detail.data || '');
|
|
||||||
// Render buffer (with newline fix) on *every* chunk
|
|
||||||
el.innerHTML = marked.parse(
|
|
||||||
window.markdownBuffer[msgId].replace(/\\n/g, '\n')
|
|
||||||
);
|
|
||||||
scrollChatToBottom();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
document.body.addEventListener('htmx:sseClose', function () {
|
document.body.addEventListener('htmx:sseClose', function () {
|
||||||
const el = document.getElementById('ai-message-content-{{user_message.id}}');
|
const msgId = '{{ user_message.id }}';
|
||||||
const msgId = '{{user_message.id}}';
|
const el = document.getElementById('ai-message-content-' + msgId);
|
||||||
if (el && window.markdownBuffer[msgId]) {
|
if (el && window.markdownBuffer[msgId]) {
|
||||||
el.innerHTML = marked.parse(
|
el.innerHTML = marked.parse(window.markdownBuffer[msgId].replace(/\\n/g, '\n'));
|
||||||
window.markdownBuffer[msgId].replace(/\\n/g, '\n')
|
delete window.markdownBuffer[msgId];
|
||||||
);
|
if (typeof scrollChatToBottom === "function") scrollChatToBottom();
|
||||||
delete window.markdownBuffer[msgId]; // Clean up in multichat
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
<!-- Preload critical assets -->
|
<!-- Preload critical assets -->
|
||||||
<link rel="preload" href="/assets/htmx.min.js" as="script">
|
<link rel="preload" href="/assets/htmx.min.js" as="script">
|
||||||
<link rel="preload" href="/assets/htmx-ext-sse.js" as="script">
|
|
||||||
<link rel="preload" href="/assets/style.css" as="style">
|
<link rel="preload" href="/assets/style.css" as="style">
|
||||||
|
|
||||||
<!-- Core styles -->
|
<!-- Core styles -->
|
||||||
@@ -16,7 +15,6 @@
|
|||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="/assets/htmx.min.js" defer></script>
|
<script src="/assets/htmx.min.js" defer></script>
|
||||||
<script src="/assets/htmx-ext-sse.js" defer></script>
|
|
||||||
<script src="/assets/theme-toggle.js" defer></script>
|
<script src="/assets/theme-toggle.js" defer></script>
|
||||||
<script src="/assets/toast.js" defer></script>
|
<script src="/assets/toast.js" defer></script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user