feat: Ältere Nachrichten nachladen per Button (500er Batches)
- Backend: beforeId-Parameter + GetActiveMessagesBeforeId + GetActiveMessageCountBeforeId - Frontend: Button 'X ältere Nachrichten laden' statt Info-Text - Archiv-Button erscheint erst wenn alle älteren Nachrichten geladen - Scroll-Position bleibt beim Nachladen erhalten - Fix: wwwroot Quell-Dateien mit publish synchronisiert
This commit is contained in:
1 parent
38bdb4408c
commit
575e5c1d44
7 files changed
+233
-18
No files matched your search
@@ -34,9 +34,26 @@ public static class HistoryEndpoint
|
||||
}
|
||||
|
||||
const int messageLimit = 500;
|
||||
var beforeStr = ctx.Request.Query["beforeId"].FirstOrDefault();
|
||||
var beforeId = long.TryParse(beforeStr, out var b) ? b : 0;
|
||||
|
||||
var totalCount = await db.GetActiveMessageCount(sessionId);
|
||||
var messages = await db.GetLatestActiveMessages(sessionId, messageLimit);
|
||||
|
||||
List<MessageRecord> messages;
|
||||
if (beforeId > 0)
|
||||
{
|
||||
messages = await db.GetActiveMessagesBeforeId(sessionId, beforeId, messageLimit);
|
||||
}
|
||||
else
|
||||
{
|
||||
messages = await db.GetLatestActiveMessages(sessionId, messageLimit);
|
||||
}
|
||||
|
||||
// Zähle wie viele Nachrichten es VOR der ältesten geladenen noch gibt
|
||||
var oldestLoadedId = messages.Count > 0 ? messages[0].Id : 0;
|
||||
var remainingOlder = oldestLoadedId > 0
|
||||
? await db.GetActiveMessageCountBeforeId(sessionId, oldestLoadedId)
|
||||
: 0;
|
||||
|
||||
// Nach Chain gruppieren (Reihenfolge beibehalten)
|
||||
var chainIds = messages.Select(m => m.ChainId).Distinct().ToList();
|
||||
@@ -54,7 +71,8 @@ public static class HistoryEndpoint
|
||||
await ctx.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
entries = result,
|
||||
hasMore = totalCount > messageLimit,
|
||||
hasMore = remainingOlder > 0,
|
||||
remainingCount = remainingOlder,
|
||||
totalCount,
|
||||
loadedCount = messages.Count
|
||||
});
|
||||
|
||||
@@ -514,6 +514,28 @@ public class DatabaseService
|
||||
return messages.Reverse().ToList();
|
||||
}
|
||||
|
||||
public async Task<int> GetActiveMessageCountBeforeId(string sessionId, long beforeId)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
return await conn.ExecuteScalarAsync<int>(@"
|
||||
SELECT COUNT(*) FROM messages m
|
||||
JOIN chains c ON m.chain_id = c.chain_id
|
||||
WHERE m.id < @beforeId AND c.archive_id IS NULL AND c.session_id = @sessionId",
|
||||
new { sessionId, beforeId });
|
||||
}
|
||||
|
||||
public async Task<List<MessageRecord>> GetActiveMessagesBeforeId(string sessionId, long beforeId, int limit)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
var messages = await conn.QueryAsync<MessageRecord>(@"
|
||||
SELECT m.* FROM messages m
|
||||
JOIN chains c ON m.chain_id = c.chain_id
|
||||
WHERE m.id < @beforeId AND c.archive_id IS NULL AND c.session_id = @sessionId
|
||||
ORDER BY m.id DESC LIMIT @limit",
|
||||
new { sessionId, beforeId, limit });
|
||||
return messages.Reverse().ToList();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<MessageRecord>> GetActiveMessagesAfterId(string sessionId, long afterId, int limit = 200)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
|
||||
+4
-2
@@ -43,8 +43,10 @@ async function apiDeleteAgent(agentId) {
|
||||
return fetch(`/api/agents/${agentId}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
async function apiHistory(sessionId) {
|
||||
const resp = await fetch(`/api/history?sessionId=${sessionId}`);
|
||||
async function apiHistory(sessionId, beforeId) {
|
||||
let url = `/api/history?sessionId=${sessionId}`;
|
||||
if (beforeId) url += `&beforeId=${beforeId}`;
|
||||
const resp = await fetch(url);
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,8 @@ async function clearChat() {
|
||||
chatEl().innerHTML = '';
|
||||
lastKnownMessageId = 0;
|
||||
loadedArchiveCount = 0;
|
||||
_oldestLoadedMessageId = 0;
|
||||
_hasMoreOlderMessages = false;
|
||||
clearReply();
|
||||
|
||||
setMeta(`Archiviert: ${archive.chainCount} Chains, ${archive.messageCount} Messages`);
|
||||
|
||||
+157
-9
@@ -1,26 +1,30 @@
|
||||
// ── History Loading ──
|
||||
|
||||
let lastKnownMessageId = 0;
|
||||
let _oldestLoadedMessageId = 0;
|
||||
let _hasMoreOlderMessages = false;
|
||||
let historyPollInterval = null;
|
||||
let loadedArchiveCount = 0;
|
||||
|
||||
async function loadHistory() {
|
||||
try {
|
||||
const sid = getCurrentSessionId();
|
||||
const archives = await apiListArchives(sid);
|
||||
if (archives.length > 0) {
|
||||
showArchiveLoadButton();
|
||||
}
|
||||
|
||||
const data = await apiHistory(sid);
|
||||
const entries = data.entries || [];
|
||||
|
||||
// Älteste geladene Message-ID merken für Nachladen
|
||||
_hasMoreOlderMessages = data.hasMore;
|
||||
_oldestLoadedMessageId = _findOldestMessageId(entries);
|
||||
|
||||
if (data.hasMore) {
|
||||
const skipped = data.totalCount - data.loadedCount;
|
||||
const info = document.createElement('div');
|
||||
info.className = 'msg system-info';
|
||||
info.innerHTML = `<i class="fa fa-info-circle"></i> ${skipped} ältere Nachrichten nicht geladen`;
|
||||
chatEl().appendChild(info);
|
||||
showOlderMessagesButton(data.remainingCount);
|
||||
} else {
|
||||
// Keine älteren Nachrichten mehr → Archiv-Button anzeigen falls vorhanden
|
||||
const archives = await apiListArchives(sid);
|
||||
if (archives.length > 0) {
|
||||
showArchiveLoadButton();
|
||||
}
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
@@ -36,6 +40,150 @@ async function loadHistory() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOlderMessages() {
|
||||
if (!_hasMoreOlderMessages || !_oldestLoadedMessageId) return;
|
||||
|
||||
const btn = chatEl().querySelector('.older-messages-btn');
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fa fa-hourglass-half"></i> Wird geladen...';
|
||||
}
|
||||
|
||||
try {
|
||||
const sid = getCurrentSessionId();
|
||||
const data = await apiHistory(sid, _oldestLoadedMessageId);
|
||||
const entries = data.entries || [];
|
||||
|
||||
_hasMoreOlderMessages = data.hasMore;
|
||||
const newOldest = _findOldestMessageId(entries);
|
||||
if (newOldest > 0) _oldestLoadedMessageId = newOldest;
|
||||
|
||||
// Button entfernen
|
||||
removeOlderMessagesButton();
|
||||
|
||||
// Scroll-Position merken
|
||||
const chat = chatEl();
|
||||
const oldScrollHeight = chat.scrollHeight;
|
||||
|
||||
// Nachrichten oben einfügen (vor dem ersten .msg Element)
|
||||
const fragment = document.createDocumentFragment();
|
||||
for (const entry of entries) {
|
||||
_renderMessagesIntoFragment(fragment, entry.messages);
|
||||
if (entry.chain && isChainMetaVisible(entry.chain)) {
|
||||
const badges = buildMetaBadges(entry.chain);
|
||||
const metaDiv = document.createElement('div');
|
||||
metaDiv.className = 'chain-meta';
|
||||
metaDiv.innerHTML = badges.map(b => '<span class="chain-meta-badge">' + esc(b) + '</span>').join('');
|
||||
fragment.appendChild(metaDiv);
|
||||
}
|
||||
}
|
||||
|
||||
// Einfügen: vor dem ersten .msg oder .chain-meta Element
|
||||
const firstMsg = chat.querySelector('.msg, .chain-meta, .tool-box');
|
||||
if (firstMsg) {
|
||||
chat.insertBefore(fragment, firstMsg);
|
||||
} else {
|
||||
chat.appendChild(fragment);
|
||||
}
|
||||
|
||||
// Neuen Button oder Archiv-Button anzeigen
|
||||
if (data.hasMore) {
|
||||
showOlderMessagesButton(data.remainingCount);
|
||||
} else {
|
||||
const archives = await apiListArchives(sid);
|
||||
if (archives.length > 0) {
|
||||
showArchiveLoadButton();
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll-Position beibehalten
|
||||
const newScrollHeight = chat.scrollHeight;
|
||||
chat.scrollTop += (newScrollHeight - oldScrollHeight);
|
||||
|
||||
} catch (e) {
|
||||
console.error('Ältere Nachrichten laden fehlgeschlagen:', e);
|
||||
if (_hasMoreOlderMessages) {
|
||||
showOlderMessagesButton(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _findOldestMessageId(entries) {
|
||||
for (const entry of entries) {
|
||||
if (entry.messages && entry.messages.length > 0) {
|
||||
return entry.messages[0].id;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function _renderMessagesIntoFragment(fragment, msgs) {
|
||||
for (const msg of msgs) {
|
||||
const div = document.createElement('div');
|
||||
switch (msg.type) {
|
||||
case 'text':
|
||||
if (msg.role === 'user') {
|
||||
div.className = 'msg user' + (msg.isMarked ? ' marked' : '');
|
||||
div.dataset.role = 'user';
|
||||
div.dataset.timestamp = msg.createdAt || '';
|
||||
if (msg.id) div.dataset.messageId = msg.id;
|
||||
renderUserContent(div, msg.content);
|
||||
_addReplyButton(div);
|
||||
_addMarkButton(div, msg.id, msg.isMarked);
|
||||
fragment.appendChild(div);
|
||||
if (msg.attachmentCount > 0) {
|
||||
let names = [];
|
||||
try { if (msg.attachmentNames) names = JSON.parse(msg.attachmentNames); } catch {}
|
||||
const nameList = names.filter(n => n).join(', ');
|
||||
const ind = document.createElement('div');
|
||||
ind.className = 'attachment-indicator';
|
||||
ind.innerHTML = `<i class="fa fa-paperclip"></i> ${msg.attachmentCount} Datei${msg.attachmentCount > 1 ? 'en' : ''}` + (nameList ? `: ${esc(nameList)}` : '');
|
||||
ind.style.cursor = 'pointer';
|
||||
ind.title = 'Klicken zum Anzeigen';
|
||||
ind.addEventListener('click', () => loadAndShowAttachments(msg.id, ind));
|
||||
div.appendChild(ind);
|
||||
}
|
||||
} else if (msg.role === 'assistant') {
|
||||
div.className = 'msg assistant markdown' + (msg.isMarked ? ' marked' : '');
|
||||
div.dataset.role = 'assistant';
|
||||
div.dataset.timestamp = msg.createdAt || '';
|
||||
if (msg.id) div.dataset.messageId = msg.id;
|
||||
div.innerHTML = renderMarkdown(msg.content);
|
||||
_addReplyButton(div);
|
||||
_addMarkButton(div, msg.id, msg.isMarked);
|
||||
fragment.appendChild(div);
|
||||
}
|
||||
break;
|
||||
case 'tool_use':
|
||||
if (!isToolVisible(msg.toolName)) continue;
|
||||
div.className = 'tool-box';
|
||||
renderToolBoxContent(div, msg.toolName, msg.content, null, false);
|
||||
fragment.appendChild(div);
|
||||
break;
|
||||
case 'tool_result': {
|
||||
const prevBoxes = fragment.querySelectorAll('.tool-box');
|
||||
const lastToolBox = prevBoxes[prevBoxes.length - 1];
|
||||
if (lastToolBox && !lastToolBox.querySelector('.tool-result')) {
|
||||
const rd = document.createElement('div');
|
||||
rd.className = 'tool-result' + (msg.isError ? ' tool-error' : '');
|
||||
rd.textContent = truncate(msg.content || '', 300);
|
||||
rd.hidden = true;
|
||||
lastToolBox.appendChild(rd);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
case 'error':
|
||||
div.className = 'msg system-info';
|
||||
div.style.color = 'var(--dangerous)';
|
||||
div.textContent = (msg.content || '').trim();
|
||||
fragment.appendChild(div);
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderMessages(msgs, addToConversation) {
|
||||
for (const msg of msgs) {
|
||||
if (msg.id > lastKnownMessageId) lastKnownMessageId = msg.id;
|
||||
|
||||
@@ -239,6 +239,8 @@ registerPage('chat', {
|
||||
messages.length = 0;
|
||||
lastKnownMessageId = 0;
|
||||
loadedArchiveCount = 0;
|
||||
_oldestLoadedMessageId = 0;
|
||||
_hasMoreOlderMessages = false;
|
||||
isStreaming = false;
|
||||
|
||||
await loadHistory();
|
||||
|
||||
+26
-5
@@ -650,10 +650,26 @@ function setMetaHtml(html) {
|
||||
metaEl().innerHTML = html;
|
||||
}
|
||||
|
||||
// ── Older Messages Button ──
|
||||
|
||||
function showOlderMessagesButton(remainingCount) {
|
||||
removeOlderMessagesButton();
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'archive-load-btn older-messages-btn';
|
||||
btn.innerHTML = '<i class="fa fa-arrow-up"></i> ' + remainingCount + ' ältere Nachrichten laden';
|
||||
btn.addEventListener('click', () => loadOlderMessages());
|
||||
chatEl().prepend(btn);
|
||||
}
|
||||
|
||||
function removeOlderMessagesButton() {
|
||||
const btn = chatEl().querySelector('.older-messages-btn');
|
||||
if (btn) btn.remove();
|
||||
}
|
||||
|
||||
// ── Archive Button ──
|
||||
|
||||
function showArchiveLoadButton() {
|
||||
const existing = chatEl().querySelector('.archive-load-btn');
|
||||
const existing = chatEl().querySelector('.archive-load-btn:not(.older-messages-btn)');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const btn = document.createElement('button');
|
||||
@@ -661,11 +677,16 @@ function showArchiveLoadButton() {
|
||||
btn.innerHTML = '<i class="fa fa-archive"></i> Archivierte Nachrichten laden';
|
||||
btn.addEventListener('click', loadLatestArchive);
|
||||
|
||||
// Immer ganz oben im Chat
|
||||
chatEl().prepend(btn);
|
||||
// Immer ganz oben im Chat (aber nach dem older-messages-btn falls vorhanden)
|
||||
const olderBtn = chatEl().querySelector('.older-messages-btn');
|
||||
if (olderBtn) {
|
||||
olderBtn.after(btn);
|
||||
} else {
|
||||
chatEl().prepend(btn);
|
||||
}
|
||||
}
|
||||
|
||||
function removeArchiveLoadButton() {
|
||||
const btn = chatEl().querySelector('.archive-load-btn');
|
||||
if (btn) btn.remove();
|
||||
const btns = chatEl().querySelectorAll('.archive-load-btn:not(.older-messages-btn)');
|
||||
btns.forEach(b => b.remove());
|
||||
}
|
||||
Reference in new issue
Block a user