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:
Meleki committed 2026-03-01 16:32:08 +01:00
1 parent 38bdb4408c
commit 575e5c1d44
7 files changed
+233 -18

No files matched your search

+20 -2
View File
@@ -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
});
+22
View File
@@ -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
View File
@@ -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();
}
+2
View File
@@ -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
View File
@@ -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;
+2
View File
@@ -239,6 +239,8 @@ registerPage('chat', {
messages.length = 0;
lastKnownMessageId = 0;
loadedArchiveCount = 0;
_oldestLoadedMessageId = 0;
_hasMoreOlderMessages = false;
isStreaming = false;
await loadHistory();
+26 -5
View File
@@ -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());
}