- 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
376 lines
13 KiB
JavaScript
376 lines
13 KiB
JavaScript
// ── 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 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) {
|
|
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) {
|
|
renderMessages(entry.messages, true);
|
|
if (entry.chain) addChainMetaBubble(entry.chain, false);
|
|
}
|
|
|
|
scrollDown();
|
|
setMeta('Bereit');
|
|
startHistoryPolling();
|
|
} catch (e) {
|
|
console.error('History laden fehlgeschlagen:', e);
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
switch (msg.type) {
|
|
case 'text':
|
|
if (msg.role === 'user') {
|
|
addUserBubble(msg.content, false, msg.createdAt, msg.id, msg.isMarked);
|
|
if (msg.attachmentCount > 0) addAttachmentIndicator(msg.id, msg.attachmentCount, msg.attachmentNames);
|
|
if (addToConversation) messages.push({ role: 'user', content: msg.content });
|
|
} else if (msg.role === 'assistant') {
|
|
addAssistantBubble(msg.content, msg.createdAt, msg.id, msg.isMarked);
|
|
if (addToConversation) messages.push({ role: 'assistant', content: msg.content });
|
|
}
|
|
break;
|
|
|
|
case 'tool_use':
|
|
if (isToolVisible(msg.toolName)) {
|
|
addToolBoxStatic(msg.toolName, msg.content, null, false);
|
|
}
|
|
break;
|
|
|
|
case 'tool_result':
|
|
// tool_result wird dem vorherigen tool_use-Box angehängt wenn möglich
|
|
appendToolResult(msg.content, msg.isError);
|
|
break;
|
|
|
|
case 'error':
|
|
addErrorBubble(msg.content);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Archive Loading ──
|
|
|
|
function getOrCreateArchiveContainer() {
|
|
let container = chatEl().querySelector('.archive-container');
|
|
if (!container) {
|
|
container = document.createElement('div');
|
|
container.className = 'archive-container';
|
|
chatEl().prepend(container);
|
|
}
|
|
return container;
|
|
}
|
|
|
|
function getOrCreateArchiveDivider() {
|
|
let divider = chatEl().querySelector('.archive-divider');
|
|
if (!divider) {
|
|
divider = document.createElement('div');
|
|
divider.className = 'msg system-info archive-divider';
|
|
divider.textContent = '── Aktuelle Konversation ──';
|
|
// Nach dem Container, vor den aktiven Messages
|
|
const container = getOrCreateArchiveContainer();
|
|
container.after(divider);
|
|
}
|
|
return divider;
|
|
}
|
|
|
|
async function loadLatestArchive() {
|
|
try {
|
|
const archives = await apiListArchives(getCurrentSessionId());
|
|
if (archives.length <= loadedArchiveCount) return;
|
|
|
|
const latest = archives[loadedArchiveCount];
|
|
loadedArchiveCount++;
|
|
|
|
removeArchiveLoadButton();
|
|
|
|
const data = await apiGetArchive(latest.archiveId);
|
|
const container = getOrCreateArchiveContainer();
|
|
getOrCreateArchiveDivider();
|
|
|
|
// Archiv-Block zusammenbauen
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
const sep = document.createElement('div');
|
|
sep.className = 'msg system-info';
|
|
sep.innerHTML = `<i class="fa fa-archive"></i> Archiv vom ${new Date(latest.createdAt).toLocaleString('de-AT')} (${latest.chainCount} Chains, ${latest.messageCount} Messages)`;
|
|
fragment.appendChild(sep);
|
|
|
|
for (const entry of data) {
|
|
for (const msg of entry.messages) {
|
|
const div = document.createElement('div');
|
|
switch (msg.type) {
|
|
case 'text':
|
|
if (msg.role === 'user') {
|
|
div.className = 'msg user archived';
|
|
renderUserContent(div, msg.content);
|
|
} else {
|
|
div.className = 'msg assistant markdown archived';
|
|
div.innerHTML = renderMarkdown(msg.content);
|
|
}
|
|
if (msg.role === 'user' && 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);
|
|
}
|
|
break;
|
|
case 'tool_use':
|
|
if (!isToolVisible(msg.toolName)) continue;
|
|
div.className = 'tool-box archived';
|
|
renderToolBoxContent(div, msg.toolName, msg.content, null, false);
|
|
break;
|
|
case 'tool_result': {
|
|
// An vorherigen tool-box anhängen
|
|
const prevBoxes = fragment.querySelectorAll ? 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);
|
|
lastToolBox.appendChild(rd);
|
|
}
|
|
continue; // kein separates div
|
|
}
|
|
case 'error':
|
|
div.className = 'msg system-info archived';
|
|
div.style.color = 'var(--dangerous)';
|
|
div.textContent = (msg.content || '').trim();
|
|
break;
|
|
default:
|
|
continue;
|
|
}
|
|
fragment.appendChild(div);
|
|
}
|
|
// Chain-Meta-Badges nach jeder Chain einfügen
|
|
if (entry.chain && isChainMetaVisible(entry.chain)) {
|
|
const badges = buildMetaBadges(entry.chain);
|
|
const metaDiv = document.createElement('div');
|
|
metaDiv.className = 'chain-meta archived';
|
|
metaDiv.innerHTML = badges.map(b => '<span class="chain-meta-badge">' + esc(b) + '</span>').join('');
|
|
fragment.appendChild(metaDiv);
|
|
}
|
|
}
|
|
|
|
// Älteres Archiv vor dem bisherigen Inhalt einfügen
|
|
container.prepend(fragment);
|
|
|
|
// Button ganz oben, falls es noch ältere Archive gibt
|
|
if (archives.length > loadedArchiveCount) {
|
|
showArchiveLoadButton();
|
|
}
|
|
|
|
} catch (e) {
|
|
console.error('Archiv laden fehlgeschlagen:', e);
|
|
}
|
|
}
|
|
|
|
// ── Polling ──
|
|
|
|
let _pollLock = false;
|
|
|
|
function startHistoryPolling(intervalMs = 3000) {
|
|
if (historyPollInterval) return;
|
|
historyPollInterval = setInterval(pollHistory, intervalMs);
|
|
}
|
|
|
|
function stopHistoryPolling() {
|
|
if (historyPollInterval) {
|
|
clearInterval(historyPollInterval);
|
|
historyPollInterval = null;
|
|
}
|
|
}
|
|
|
|
async function pollHistory(force = false) {
|
|
if (isStreaming && !force) return;
|
|
if (_pollLock) return;
|
|
_pollLock = true;
|
|
|
|
try {
|
|
const newMessages = await apiHistoryPoll(getCurrentSessionId(), lastKnownMessageId);
|
|
if (newMessages.length > 0) {
|
|
renderMessages(newMessages, true);
|
|
scrollDown();
|
|
}
|
|
} catch (e) {
|
|
console.error('History poll fehlgeschlagen:', e);
|
|
} finally {
|
|
_pollLock = false;
|
|
}
|
|
}
|