Files
gott/wwwroot/js/history.js
T
Meleki a14a991a0f feat: Nachrichten als gelöscht markieren
- DB: deleted_on TEXT Spalte in messages (Migration)
- Backend: ToggleMessageDelete(), includeDeleted-Parameter in allen History-Queries
- Neuer Endpoint: PUT /api/messages/{id}/delete
- Frontend: Trash-Button zwischen Reply- und Mark-Button
- Gelöschte Nachrichten: durchgestrichen + roter Rand (oder versteckt je nach Einstellung)
- Einstellungen: Toggle-Switch 'Gelöschte Nachrichten anzeigen'
- KI-Payload: gelöschte Nachrichten werden nicht mitgeschickt
2026-03-03 11:45:52 +01:00

406 lines
14 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, null, isDeletedVisible());
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, isDeletedVisible());
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 _mergeConsecutiveAssistantTexts(msgs) {
const result = [];
for (const msg of msgs) {
if (msg.type === 'text' && msg.role === 'assistant') {
const prev = result[result.length - 1];
if (prev && prev.type === 'text' && prev.role === 'assistant'
&& prev.iteration === msg.iteration) {
// Gleiche Iteration: nahtlos zusammenfügen (Web-Search-Fragmente etc.)
prev.content = (prev.content || '') + ' ' + (msg.content || '');
// IDs/Timestamps: letztes behalten (für Mark-Button etc.)
if (msg.id) prev.id = msg.id;
if (msg.createdAt) prev.createdAt = msg.createdAt;
if (msg.isMarked) prev.isMarked = true;
continue;
}
}
result.push(Object.assign({}, msg));
}
return result;
}
function _renderMessagesIntoFragment(fragment, msgs) {
msgs = _mergeConsecutiveAssistantTexts(msgs);
for (const msg of msgs) {
const div = document.createElement('div');
switch (msg.type) {
case 'text':
if (msg.role === 'user') {
div.className = 'msg user markdown' + (msg.isMarked ? ' marked' : '');
if (msg.deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
div.dataset.role = 'user';
div.dataset.timestamp = msg.createdAt || '';
if (msg.id) div.dataset.messageId = msg.id;
renderUserContent(div, msg.content);
_addReplyButton(div);
_addDeleteButton(div, msg.id, msg.deletedOn);
_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' : '');
if (msg.deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
div.dataset.role = 'assistant';
div.dataset.timestamp = msg.createdAt || '';
if (msg.id) div.dataset.messageId = msg.id;
div.innerHTML = renderMarkdown(msg.content);
parseFluentEmojis(div);
_addReplyButton(div);
_addDeleteButton(div, msg.id, msg.deletedOn);
_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) {
msgs = _mergeConsecutiveAssistantTexts(msgs);
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, msg.deletedOn);
if (msg.attachmentCount > 0) addAttachmentIndicator(msg.id, msg.attachmentCount, msg.attachmentNames);
if (addToConversation && !msg.deletedOn) messages.push({ role: 'user', content: msg.content });
} else if (msg.role === 'assistant') {
addAssistantBubble(msg.content, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn);
if (addToConversation && !msg.deletedOn) 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) {
const mergedMsgs = _mergeConsecutiveAssistantTexts(entry.messages);
for (const msg of mergedMsgs) {
const div = document.createElement('div');
switch (msg.type) {
case 'text':
if (msg.role === 'user') {
div.className = 'msg user markdown archived';
renderUserContent(div, msg.content);
} else {
div.className = 'msg assistant markdown archived';
div.innerHTML = renderMarkdown(msg.content);
parseFluentEmojis(div);
}
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, isDeletedVisible());
if (newMessages.length > 0) {
renderMessages(newMessages, true);
scrollDown();
}
} catch (e) {
console.error('History poll fehlgeschlagen:', e);
} finally {
_pollLock = false;
}
}